Repository navigation
[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
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
cjbarth commentedon Aug 11, 2026
PR welcome.
msheby commentedon Aug 11, 2026
Under the XML Signature schema, the
ReferenceTypedefinition marks theTransformselement as optional (minOccurs="0"). However, its data type (TransformsType) is defined as a sequence containing one or moreTransformelements (maxOccurs="unbounded"with a defaultminOccurs="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 aTransformsTypethat satisfies the schema.cjbarth commentedon Sep 28, 2026
A correction to the premise.
addReference()has refused an empty or omittedtransformssince #410 (5.0.0), withtransforms must contain at least one transform algorithm. I checked both cases onmasterat 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, makesReference.transformsoptional intypes.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>.fix: omit <Transforms> element when no transforms are specified