◐ Off-By-One · answer catalog

js-xml-dsig-signature-wrapping-c14n-confusion

2 answer(s)jsnode20jsnode20

js-xml-dsig-signature-wrapping-c14n-confusion

📦 Source in repository (JSON)

Answer 1

Done. The full solution is in ~/xmldsig/SOLUTION.md, with working code and a passing test suite. Summary below.

Root cause

XMLDSig signature wrapping exploits a split identity: the verifier proves a digest over whatever node Reference URI resolves to at verification time, while the application later calls getElementById again and reads a different node. The gap is opened by:

Canonicalization is where signed bytes are defined, so exc-C14N must render only visibly-utilized prefixes, sort namespace decls default-first then by prefix and attributes no-namespace-first then URI then local, and treat comments consistently. Comment truncation arises when C14N drops comments but the app parses around them.

Exact fix

verifier.js resolves IDs once, rejects ambiguity, and returns the verified node so the app never re-resolves:

  1. buildIdIndex — case-insensitive Id/ID/id/xml:id, rejects duplicate-id and ambiguous-id.
  2. Exactly one ds:Signature, and it must be inside the expected payload (ambiguous-signature, not-enveloped).
  3. Every Reference must be a same-document fragment resolving inside the payload subtree (outside-subtree), and at least one must bind expectedId (unbound).
  4. Applies declared transforms (enveloped-signature, exc-C14N/C14N ± comments, InclusiveNamespaces PrefixList) before hashing (bad-digest).
  5. Verifies the RSA/ECDSA SignedInfo signature (bad-signature).
  6. Refuses comments in signed content unless C14N is #WithComments (comment-truncation).

xml.js is a dependency-free parser + exc-C14N implementation covering namespace inheritance, xmlns="", the implicit xml prefix, attribute ordering, and escaping.

Verification

cd ~/xmldsig
npm test

Result: all pass — 7 canonicalization vectors, legit document accepted, and rejected: XSW1/XSW2/duplicate-ID → duplicate-id, XSW3 → not-enveloped, XSW4 → ambiguous-signature, XSW5 → outside-subtree, XSW6 → comment-truncation, tamper → bad-digest, wrong expected ID → unbound. The shipped on-disk corpus (test/corpus/, generated by test/emit.js) is re-verified by test/corpus.test.js.

ALL TESTS PASSED
CORPUS OK

Files: xml.js, verifier.js, SOLUTION.md, test/{gen,keys,emit,run,corpus.test,c14n.test}.js, test/corpus/*.xml.

Evidence & signatures

# Evidence
- Problem class: js-xml-dsig-signature-wrapping-c14n-confusion
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-29T16:44:55.368Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Build an XML Signature (XMLDSig) verifier in pure Node.js with no third-party dependencies that resists signature wrapping (XSW) attacks: it must bind the signature to the exact element the application later reads by ID, not merely to whatever node the Reference URI happens to resolve to. Implement exclusive canonicalization (exc-C14N) with correct namespace inheritance and attribute ordering, reject duplicated or ambiguous ID attributes, and refuse Reference URIs that resolve outside the signed subtree. Ship a test corpus of at least six known-bad documents (XSW1-6 variants, comment truncation, duplicate ID) that must all be rejected, plus a legitimate signed document that must verify.", "environment": "node20", "language": "js", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "js-xml-dsig-signature-wrapping-c14n-confusion", "provider": "openrouter", "solved_at": "2026-09-29T16:44:55.368Z", "version": "20"}

Answer 2

Done. The full solution is in ~/xmldsig/SOLUTION.md, with working code and a passing test suite. Summary below.

Root cause

XMLDSig signature wrapping exploits a split identity: the verifier proves a digest over whatever node Reference URI resolves to at verification time, while the application later calls getElementById again and reads a different node. The gap is opened by:

Canonicalization is where signed bytes are defined, so exc-C14N must render only visibly-utilized prefixes, sort namespace decls default-first then by prefix and attributes no-namespace-first then URI then local, and treat comments consistently. Comment truncation arises when C14N drops comments but the app parses around them.

Exact fix

verifier.js resolves IDs once, rejects ambiguity, and returns the verified node so the app never re-resolves:

  1. buildIdIndex — case-insensitive Id/ID/id/xml:id, rejects duplicate-id and ambiguous-id.
  2. Exactly one ds:Signature, and it must be inside the expected payload (ambiguous-signature, not-enveloped).
  3. Every Reference must be a same-document fragment resolving inside the payload subtree (outside-subtree), and at least one must bind expectedId (unbound).
  4. Applies declared transforms (enveloped-signature, exc-C14N/C14N ± comments, InclusiveNamespaces PrefixList) before hashing (bad-digest).
  5. Verifies the RSA/ECDSA SignedInfo signature (bad-signature).
  6. Refuses comments in signed content unless C14N is #WithComments (comment-truncation).

xml.js is a dependency-free parser + exc-C14N implementation covering namespace inheritance, xmlns="", the implicit xml prefix, attribute ordering, and escaping.

Verification

cd ~/xmldsig
npm test

Result: all pass — 7 canonicalization vectors, legit document accepted, and rejected: XSW1/XSW2/duplicate-ID → duplicate-id, XSW3 → not-enveloped, XSW4 → ambiguous-signature, XSW5 → outside-subtree, XSW6 → comment-truncation, tamper → bad-digest, wrong expected ID → unbound. The shipped on-disk corpus (test/corpus/, generated by test/emit.js) is re-verified by test/corpus.test.js.

ALL TESTS PASSED
CORPUS OK

Files: xml.js, verifier.js, SOLUTION.md, test/{gen,keys,emit,run,corpus.test,c14n.test}.js, test/corpus/*.xml.

Evidence & signatures

# Evidence
- Problem class: js-xml-dsig-signature-wrapping-c14n-confusion
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-29T16:44:55.368Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Build an XML Signature (XMLDSig) verifier in pure Node.js with no third-party dependencies that resists signature wrapping (XSW) attacks: it must bind the signature to the exact element the application later reads by ID, not merely to whatever node the Reference URI happens to resolve to. Implement exclusive canonicalization (exc-C14N) with correct namespace inheritance and attribute ordering, reject duplicated or ambiguous ID attributes, and refuse Reference URIs that resolve outside the signed subtree. Ship a test corpus of at least six known-bad documents (XSW1-6 variants, comment truncation, duplicate ID) that must all be rejected, plus a legitimate signed document that must verify.", "environment": "node20", "language": "js", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "js-xml-dsig-signature-wrapping-c14n-confusion", "provider": "openrouter", "solved_at": "2026-09-29T16:44:55.368Z", "version": "20"}
Generated from the verified corpus · MIT licensedBack to the catalog