Skip to content

[ENHANCEMENT]: createReferences emits an empty <Transforms></Transforms> block when ref.transforms is empty; the element is optional per XMLDSig and should be omitted #540

Description

@msheby

Is your feature request related to a problem? Please describe...

Summary

When a caller adds a Reference with an empty transforms array (addReference({ transforms: [], ... })), createReferences still emits an empty <Transforms></Transforms> block. XMLDSig defines <Transforms> as an optional child of <Reference> (minOccurs="0"), so the element should simply be omitted in that case. loadReference already treats a missing <Transforms> and an empty <Transforms> identically on the verify side, so this is a pure output cleanup with no round-trip impact.

Current behavior (v6.1.2)

src/signed-xml.ts in createReferences:

res += `<${prefix}Transforms>`;
for (const trans of ref.transforms || []) {
  // ... emit <Transform>
}
// ...
res +=
  `</${prefix}Transforms>` +
  `<${prefix}DigestMethod Algorithm="..." />` +
  ...

The open and close tags of <Transforms> sit outside the for loop, so an empty ref.transforms yields:

<Reference URI="#foo"><Transforms></Transforms><DigestMethod Algorithm="..."/><DigestValue>...</DigestValue></Reference>

Why the empty block is wrong

XMLDSig Core (W3C REC-xmldsig-core-20080610), section 4.3.3:

<complexType name="ReferenceType">
  <sequence>
    <element ref="ds:Transforms" minOccurs="0"/>
    <element ref="ds:DigestMethod"/>
    <element ref="ds:DigestValue"/>
  </sequence>
  ...
</complexType>

<Transforms> is optional. Section 4.3.3.4 describes it as "an ordered list of Transform elements" — an empty list is meaningless; the correct expression is to omit the element.

The empty block is legal XML but semantically noise: it declares "here are the transforms; there are none," when the spec-idiomatic way to say the same thing is to leave the element out entirely.

Why the fix is round-trip-safe

loadReference at src/signed-xml.ts:699 handles both cases identically today:

const transforms = [];
let inclusiveNamespacesPrefixList = [];
nodes = utils.findChildren(refNode, "Transforms");
if (nodes.length !== 0) {
  // ... parse transforms from the child element
}
// ...
if (transforms.length === 0 ||
    transforms[transforms.length - 1] === "http://www.w3.org/2000/09/xmldsig#enveloped-signature") {
  transforms.push("http://www.w3.org/TR/2001/REC-xml-c14n-20010315");
}
  • Missing <Transforms>nodes.length === 0 → skip parse → transforms = [] → auto-append C14N.
  • Empty <Transforms></Transforms>nodes.length === 1 → enter block, inner <Transform> loop yields nothing → transforms = [] → auto-append C14N.

Both paths converge on transforms = ['C14N']. Digest verification behaves identically. No existing signature is invalidated.

Motivating case

Some profiles require Reference elements to have no <Transforms> child. The current empty-block output means downstream code has to post-process the signed XML to strip <Transforms></Transforms> after computeSignature, and also has to modify the SignedInfo canonical form during signing so that the pre-computed SignatureValue matches the stripped output. Both dance steps go away if createReferences just omits the element when it's empty.

Describe teh solution you'd like...

Proposed change

Guard the <Transforms> emission on non-empty:

const transformList = ref.transforms || [];
if (transformList.length > 0) {
  res += `<${prefix}Transforms>`;
  for (const trans of transformList) {
    const transform = this.findCanonicalizationAlgorithm(trans);
    res += `<${prefix}Transform Algorithm="${transform.getAlgorithmName()}"`;
    if (utils.isArrayHasLength(ref.inclusiveNamespacesPrefixList)) {
      res += ">";
      res += `<InclusiveNamespaces PrefixList="${ref.inclusiveNamespacesPrefixList.join(" ")}" xmlns="${transform.getAlgorithmName()}"/>`;
      res += `</${prefix}Transform>`;
    } else {
      res += " />";
    }
  }
  res += `</${prefix}Transforms>`;
}

const canonXml = this.getCanonReferenceXml(doc, ref, node);
const digestAlgorithm = this.findHashAlgorithm(ref.digestAlgorithm);
res +=
  `<${prefix}DigestMethod Algorithm="${digestAlgorithm.getAlgorithmName()}" />` +
  `<${prefix}DigestValue>${digestAlgorithm.getHash(canonXml)}</${prefix}DigestValue>` +
  `</${prefix}Reference>`;

Roughly seven lines of diff. Behavior for non-empty transforms is byte-identical to today.

Compatibility

  • Existing consumers passing non-empty transforms: byte-identical output.
  • Existing consumers passing empty transforms: cleaner output, verification still succeeds via loadReference's existing fallback path.
  • No consumers rely on the empty block being emitted (there's nothing they could do with <Transforms></Transforms> that they couldn't do without it).

Describe the alternatives you've considered...

When canonicalizing SignedInfo, cloning SignedInfo without its <Transforms> children.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions