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

Open

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

  1. added
    enhancementAdds a feature, option or export without breaking existing callers
    on Aug 10, 2026
  2. cjbarth commented on Aug 11, 2026

    @cjbarth
    Contributor

    PR welcome.

  3. msheby commented on Aug 11, 2026

    @msheby
    ContributorAuthor

    Under the XML Signature schema, the ReferenceType definition marks the Transforms element as optional (minOccurs="0"). However, its data type (TransformsType) is defined as a sequence containing one or more Transform elements (maxOccurs="unbounded" with a default minOccurs="1").

    An empty <Transforms></Transforms> is therefore schema-invalid by the W3C spec - not just a SMPTE concern. Omitting the element entirely when there are no transforms is the only conformant choice, because there's no way to serialize zero transforms within a TransformsType that satisfies the schema.

  4. added this to the v7.0 milestone on Sep 25, 2026
  5. cjbarth commented on Sep 28, 2026

    @cjbarth
    Contributor

    A correction to the premise. addReference() has refused an empty or omitted transforms since #410 (5.0.0), with transforms must contain at least one transform algorithm. I checked both cases on master at c13e30b. So an empty <Transforms></Transforms> can't be produced through the public API.

    That makes #542 more than an output change. To sign a reference without a <Transforms> element, it removes that check, makes Reference.transforms optional in types.ts, and has the signer fall back to C14N when there are no transforms. The type change is why it's on 7.0. The check was #410's deliberate choice that callers name their transforms, so undoing it needs its own decision, separate from the schema point about an empty <Transforms>.

  6. added a commit that references this issue on Oct 15, 2026
    86dc651
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementAdds a feature, option or export without breaking existing callerspr-welcome

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions