XML Exclusive Canonicalization
XML signatures cover canonicalized bytes, not the source XML. Two structurally-equivalent XML documents (different attribute ordering, different namespace prefixes, different whitespace) must produce the same canonical bytes; otherwise an attacker could swap a benign signed assertion for a malicious one whose parsed tree is identical but whose serialized bytes differ.
This module implements the SAML/XMLDSig-relevant subset of RFC 3741 Exclusive XML Canonicalization 1.0 plus xml-exc-c14n#WithComments (controlled via opts).
What's covered (the v1-defensible SAML/SP subset):
- UTF-8 output with no BOM - Element + attribute serialization with &, <, >, ", \r, \t, \n proper escaping per §1.3.2 - Attribute ordering: namespace declarations first (alphabetical by namespace prefix, xmlns before xmlns:foo); regular attributes second (by namespace URI, then local name) - Exclusive namespace propagation: only the namespace prefixes *visibly used* by the canonicalized subtree are emitted - Empty elements expanded ( → ) - Whitespace normalization in attribute values - Comments suppressed by default; withComments: true keeps them per xml-exc-c14n#WithComments
What's NOT covered (deferred — open conditions on first operator demand or live SAML interop need):
- InclusiveNamespaces PrefixList (the Transform parameter — we always operate in the strict exclusive mode without an inclusive list). - Inherited XML namespace propagation for xml:lang, xml:space, xml:base past the canonicalization boundary.
Surface:
b.xmlC14n.canonicalize(xmlString | parsedTree, opts?) → Buffer of canonicalized UTF-8 bytes b.xmlC14n.canonicalizeElementById(xmlString, id, opts?) → Buffer of c14n'd bytes for the element whose ID=" attribute matches (used by XMLDSig Reference resolution) b.xmlC14n.parse(xmlString) → DOM tree (used by SAML)
b.xmlC14n.parse(xml) #
Lightweight DOM parser: produces a simple node tree with { type, name, attrs, children, parent }. Node types: "element" / "text" / "comment". The parser is strict about what it refuses (DOCTYPE, ENTITY, malformed entity references); XML c14n is a security primitive and an over-permissive parser undermines the canonicalization guarantees downstream. Operators rarely call this directly — canonicalize and canonicalizeElementById accept either a string OR a parsed node, so the parsed-tree path is exposed mainly for the SAML primitive's signature-element lookup and operator-side custom traversal.
var tree = b.xmlC14n.parse(" ");
tree.type; // → "element"
tree.name; // → "root"
tree.children[0].name;// → "child"
b.xmlC14n.canonicalize(input, opts?) #
{
{
withComments?: boolean, // default false (per xml-exc-c14n)
}
}
Produce the RFC 3741 Exclusive XML Canonicalization 1.0 byte sequence for an XML document or a parsed DOM node. Returns a Buffer of UTF-8 bytes.
var c = b.xmlC14n.canonicalize(" ");
// → Buffer< >
b.xmlC14n.canonicalizeElementById(xml, id, opts?) #
{
{
attrName?: string, // default "ID"
withComments?: boolean,
}
}
Find the element whose ID (or operator-specified attribute name) matches the supplied id, then return its canonical-form bytes. Throws if zero or more than one element matches — this single- match invariant is the core defense against XML signature-wrapping attacks where an attacker injects a sibling assertion with the same ID hoping the verifier picks the wrong one.
var bytes = b.xmlC14n.canonicalizeElementById(
"payload ",
"sig"
);
// → Buffer<payload>
Last updated 2026-08-08T16:39:49.652Z by seeder.