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) #

stable0.8.62

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?) #

stable0.8.62
{
  {
    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?) #

stable0.8.62
{
  {
    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.