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.
Is your feature request related to a problem? Please describe...
Summary
When a caller adds a Reference with an empty transforms array (
addReference({ transforms: [], ... })),createReferencesstill 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.loadReferencealready 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.tsincreateReferences:The open and close tags of
<Transforms>sit outside theforloop, so an emptyref.transformsyields:Why the empty block is wrong
XMLDSig Core (W3C REC-xmldsig-core-20080610), section 4.3.3:
<Transforms>is optional. Section 4.3.3.4 describes it as "an ordered list ofTransformelements" — 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
loadReferenceatsrc/signed-xml.ts:699handles both cases identically today:<Transforms>→nodes.length === 0→ skip parse →transforms = []→ auto-append C14N.<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>aftercomputeSignature, and also has to modify the SignedInfo canonical form during signing so that the pre-computedSignatureValuematches the stripped output. Both dance steps go away ifcreateReferencesjust omits the element when it's empty.Describe teh solution you'd like...
Proposed change
Guard the
<Transforms>emission on non-empty:Roughly seven lines of diff. Behavior for non-empty transforms is byte-identical to today.
Compatibility
loadReference's existing fallback path.<Transforms></Transforms>that they couldn't do without it).Describe the alternatives you've considered...
When canonicalizing
SignedInfo, cloningSignedInfowithout its<Transforms>children.