Summary
Algorithm identifiers read out of a signature are matched against our registries (CanonicalizationAlgorithms, SignatureAlgorithms, HashAlgorithms) by exact string equality. We never apply the xsd:anyURI whitespace-collapse that the XML-DSIG schema calls for, so a signature whose Algorithm attribute was line-wrapped when it was signed — as it will be if the document is pretty-printed before signing — fails to verify with a misleading "is not supported" error naming an algorithm we very much do support.
XML attribute-value normalization (XML 1.0 §3.3.3) replaces a literal newline inside an attribute value with a space but does not trim or collapse it. So Algorithm="http://...c14n-20010315\n " reaches us as the URI followed by eleven spaces, and the lookup misses.
This surfaces as an error message where the offending whitespace is essentially invisible, which makes it very hard for a user to diagnose. It is almost certainly what some of the reporters in #143 were actually hitting.
Why this is correctness, not leniency
The XML Signature schema declares Algorithm as xsd:anyURI, and xsd:anyURI has a fixed whiteSpace="collapse" facet (XML Schema Part 2 §4.3.6). A line-wrapped Algorithm is therefore well-formed and schema-valid, and its value is the collapsed URI. When we report 'http://...c14n-20010315 ' is not supported, we are misreading a valid value, not rejecting a malformed one. The fix applies the schema's rule exactly: only XML whitespace, so NBSP is not stripped, and internal runs collapse to one space rather than disappearing. Nothing becomes an alias for a registered URI unless the schema already says it is that URI.
It is not a MUST we were violating, though. XML-DSig 1.1 §7.1 notes that Signature is only laxly schema-valid, so a verifier may never see schema normalization. It therefore tells signers that a signature is only verifiable by other implementations if "attribute value white space be normalized". Signers that emit wrapped Algorithm attributes are outside that constraint, and rejecting them was permitted. Collapsing is the more faithful reading of the normative schema, and it removes an error that names an algorithm we support.
Reproduction
Save as repro.js in the repo root and run npm run build && node repro.js. It signs a document with non-exclusive c14n, then re-verifies it four more times, each time line-wrapping exactly one Algorithm attribute and changing nothing else.
const fs = require("fs");
const xpath = require("xpath");
const { DOMParser } = require("@xmldom/xmldom");
const { SignedXml } = require("./lib/index.js");
const C14N = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315";
const ENVELOPED = "http://www.w3.org/2000/09/xmldsig#enveloped-signature";
// 1. Produce a valid signature using non-exclusive c14n.
const signer = new SignedXml({
privateKey: fs.readFileSync("./test/static/client.pem"),
canonicalizationAlgorithm: C14N,
signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
});
signer.addReference({
xpath: "//*[local-name(.)='book']",
transforms: [ENVELOPED, C14N],
digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1",
});
signer.computeSignature("<library><book Id='b1'><title>Harry Potter</title></book></library>");
const signedXml = signer.getSignedXml();
// 2. Verify it, optionally after line-wrapping one Algorithm attribute the way a
// pretty-printer would. Nothing else about the document changes.
function verify(label, xmlString) {
const doc = new DOMParser().parseFromString(xmlString, "text/xml");
const signatureNode = xpath.select1("//*[local-name(.)='Signature']", doc);
const sig = new SignedXml({ publicCert: fs.readFileSync("./test/static/client_public.pem") });
try {
sig.loadSignature(signatureNode);
console.log(`${label} -> checkSignature: ${sig.checkSignature(xmlString)}`);
} catch (err) {
console.log(`${label} -> THREW: ${err.message}`);
}
}
const wrap = (needle) => signedXml.replace(needle, needle.replace(/"\/>$/, '\n "/>'));
verify("as signed ", signedXml);
verify("CanonicalizationMethod ws", wrap(`<CanonicalizationMethod Algorithm="${C14N}"/>`));
verify("Transform ws ", wrap(`<Transform Algorithm="${C14N}"/>`));
verify("SignatureMethod ws ", wrap('<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>'));
verify("DigestMethod ws ", wrap('<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>'));
Actual output (v6.0.0, master @ 8353bab)
as signed -> checkSignature: true
CanonicalizationMethod ws -> THREW: canonicalization algorithm 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315 ' is not supported
Transform ws -> THREW: canonicalization algorithm 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315 ' is not supported
SignatureMethod ws -> THREW: signature algorithm 'http://www.w3.org/2000/09/xmldsig#rsa-sha1 ' is not supported
DigestMethod ws -> THREW: hash algorithm 'http://www.w3.org/2000/09/xmldsig#sha1 ' is not supported
Expected output
Only the unmodified signature verifies. The repro wraps each Algorithm after signing, and those attributes are inside SignedInfo, so the other four must still fail. They should fail as a signature mismatch, not with a misleading is not supported:
as signed -> checkSignature: true
CanonicalizationMethod ws -> THREW: invalid signature: the signature value … is incorrect
Transform ws -> THREW: invalid signature: the signature value … is incorrect
SignatureMethod ws -> THREW: invalid signature: the signature value … is incorrect
DigestMethod ws -> THREW: invalid signature: the signature value … is incorrect
A line-wrapped Algorithm should verify when the wrapping was present at signing time (pretty-print, then sign).
Affected read sites
All four are in src/signed-xml.ts, and all take the raw .value with no normalization:
| Element |
Location |
CanonicalizationMethod/@Algorithm |
signed-xml.ts:633 (loadSignature) |
SignatureMethod/@Algorithm |
signed-xml.ts:642 (loadSignature) |
DigestMethod/@Algorithm |
signed-xml.ts:721 (loadReference) |
Transform/@Algorithm |
signed-xml.ts:748 (loadReference) |
Suggested fix
Normalize at read time, not inside the three find*Algorithm helpers.
The helpers are the tempting place to put a .trim(), but that would leave this.canonicalizationAlgorithm holding the polluted string, and it is compared by equality elsewhere. In particular, the exclusive-c14n guard in loadSignature (signed-xml.ts:658-666) does an === against the two REC-xml-c14n-20010315 URIs to decide whether it can canonicalize SignedInfo in isolation. With a whitespace-laden value the guard doesn't recognize inclusive c14n, so the raw value goes on to getCanonXml. That is why the CanonicalizationMethod ws case above throws from inside loadSignature rather than from anywhere the user would think to look. It fails closed: it throws, and accepts nothing it shouldn't. Normalizing on the way in fixes the guard and the lookup together.
Collapse per xsd:anyURI — trim, and collapse internal whitespace runs to a single space — rather than a bare .trim(). A small shared helper in src/utils.ts applied at all four sites keeps this consistent.
Security considerations
This is not a security fix: today these signatures are rejected, which fails closed.
Collapsing would weaken what we reject. Documents we refuse today would verify, and anything downstream that relies on that refusal would lose it. For example, a consumer that reads Algorithm from the DOM itself and bans an algorithm by exact string (say, rsa-sha1) would miss a wrapped value that we would then accept. That is a new disagreement between our reading of the document and the consumer's, and the consumer had no say in it.
XML-DSig 1.1 §7.1 already puts the burden of normalizing attribute whitespace on signers, and no signer that emits wrapped Algorithm attributes has been reported. The case for widening what we accept is therefore weak.
Suggested test coverage
- Each affected element (four total) covered by a signature that was signed over a line-wrapped
Algorithm attribute, asserting it verifies. Ideally this comes from a fixture generated outside xml-crypto, so a canonicalization bug shared by our signer and verifier can't hide.
- A line-wrapped
Algorithm added after signing still fails verification.
- Leading whitespace and internal whitespace runs, not just trailing.
- A negative test confirming a genuinely unregistered URI still throws
is not supported, so the fix does not degenerate into fuzzy matching.
Notes
Split out of #143. The original complaint there — that http://www.w3.org/TR/2001/REC-xml-c14n-20010315 is unregistered — was fixed by #116 and shipped in v0.9.0 (2017-02-26), and non-exclusive c14n round-trips correctly on master today. This whitespace handling is the one live defect that still produces that exact error message for a supported URI.
Summary
Algorithm identifiers read out of a signature are matched against our registries (
CanonicalizationAlgorithms,SignatureAlgorithms,HashAlgorithms) by exact string equality. We never apply thexsd:anyURIwhitespace-collapse that the XML-DSIG schema calls for, so a signature whoseAlgorithmattribute was line-wrapped when it was signed — as it will be if the document is pretty-printed before signing — fails to verify with a misleading "is not supported" error naming an algorithm we very much do support.XML attribute-value normalization (XML 1.0 §3.3.3) replaces a literal newline inside an attribute value with a space but does not trim or collapse it. So
Algorithm="http://...c14n-20010315\n "reaches us as the URI followed by eleven spaces, and the lookup misses.This surfaces as an error message where the offending whitespace is essentially invisible, which makes it very hard for a user to diagnose. It is almost certainly what some of the reporters in #143 were actually hitting.
Why this is correctness, not leniency
The XML Signature schema declares
Algorithmasxsd:anyURI, andxsd:anyURIhas a fixedwhiteSpace="collapse"facet (XML Schema Part 2 §4.3.6). A line-wrappedAlgorithmis therefore well-formed and schema-valid, and its value is the collapsed URI. When we report'http://...c14n-20010315 ' is not supported, we are misreading a valid value, not rejecting a malformed one. The fix applies the schema's rule exactly: only XML whitespace, so NBSP is not stripped, and internal runs collapse to one space rather than disappearing. Nothing becomes an alias for a registered URI unless the schema already says it is that URI.It is not a MUST we were violating, though. XML-DSig 1.1 §7.1 notes that
Signatureis only laxly schema-valid, so a verifier may never see schema normalization. It therefore tells signers that a signature is only verifiable by other implementations if "attribute value white space be normalized". Signers that emit wrappedAlgorithmattributes are outside that constraint, and rejecting them was permitted. Collapsing is the more faithful reading of the normative schema, and it removes an error that names an algorithm we support.Reproduction
Save as
repro.jsin the repo root and runnpm run build && node repro.js. It signs a document with non-exclusive c14n, then re-verifies it four more times, each time line-wrapping exactly oneAlgorithmattribute and changing nothing else.Actual output (v6.0.0,
master@ 8353bab)Expected output
Only the unmodified signature verifies. The repro wraps each
Algorithmafter signing, and those attributes are insideSignedInfo, so the other four must still fail. They should fail as a signature mismatch, not with a misleadingis not supported:A line-wrapped
Algorithmshould verify when the wrapping was present at signing time (pretty-print, then sign).Affected read sites
All four are in
src/signed-xml.ts, and all take the raw.valuewith no normalization:CanonicalizationMethod/@Algorithmsigned-xml.ts:633(loadSignature)SignatureMethod/@Algorithmsigned-xml.ts:642(loadSignature)DigestMethod/@Algorithmsigned-xml.ts:721(loadReference)Transform/@Algorithmsigned-xml.ts:748(loadReference)Suggested fix
Normalize at read time, not inside the three
find*Algorithmhelpers.The helpers are the tempting place to put a
.trim(), but that would leavethis.canonicalizationAlgorithmholding the polluted string, and it is compared by equality elsewhere. In particular, the exclusive-c14n guard inloadSignature(signed-xml.ts:658-666) does an===against the twoREC-xml-c14n-20010315URIs to decide whether it can canonicalizeSignedInfoin isolation. With a whitespace-laden value the guard doesn't recognize inclusive c14n, so the raw value goes on togetCanonXml. That is why theCanonicalizationMethod wscase above throws from insideloadSignaturerather than from anywhere the user would think to look. It fails closed: it throws, and accepts nothing it shouldn't. Normalizing on the way in fixes the guard and the lookup together.Collapse per
xsd:anyURI— trim, and collapse internal whitespace runs to a single space — rather than a bare.trim(). A small shared helper insrc/utils.tsapplied at all four sites keeps this consistent.Security considerations
This is not a security fix: today these signatures are rejected, which fails closed.
Collapsing would weaken what we reject. Documents we refuse today would verify, and anything downstream that relies on that refusal would lose it. For example, a consumer that reads
Algorithmfrom the DOM itself and bans an algorithm by exact string (say,rsa-sha1) would miss a wrapped value that we would then accept. That is a new disagreement between our reading of the document and the consumer's, and the consumer had no say in it.XML-DSig 1.1 §7.1 already puts the burden of normalizing attribute whitespace on signers, and no signer that emits wrapped
Algorithmattributes has been reported. The case for widening what we accept is therefore weak.Suggested test coverage
Algorithmattribute, asserting it verifies. Ideally this comes from a fixture generated outside xml-crypto, so a canonicalization bug shared by our signer and verifier can't hide.Algorithmadded after signing still fails verification.is not supported, so the fix does not degenerate into fuzzy matching.Notes
Split out of #143. The original complaint there — that
http://www.w3.org/TR/2001/REC-xml-c14n-20010315is unregistered — was fixed by #116 and shipped in v0.9.0 (2017-02-26), and non-exclusive c14n round-trips correctly onmastertoday. This whitespace handling is the one live defect that still produces that exact error message for a supported URI.