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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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