Hand-rolled Exclusive XML Canonicalization 1.0 (no-comments)
(http://www.w3.org/2001/10/xml-exc-c14n#) engine over the
Relyra.Security.XML.SaxyTree.Node parse-tree shape.
This is the only net-new algorithm in Phase 28 and has NO in-repo analog.
Hand-rolling is mandated by ADR-0001 / decision D-05: no correct exclusive-C14N
BEAM library exists (esaml/xmerl_c14n is inclusive-only, xmerl-DOM-based,
and carries CVE-2026-28809 XXE). It serializes a tree node to byte-exact
canonical UTF-8 — the precondition for Phase 29's :public_key.verify and
DigestValue recompute. A single byte-divergence here silently defeats the
downstream digest check and re-opens the confirmed SAML auth-bypass class, so
byte-equality against an independent golden oracle is proven in Plan 04.
Correctness surface (decision D-06)
- Visibly-utilized namespace rendering against a rendered-namespaces stack distinct from the in-scope stack (no over-rendering — Pitfall 1).
- Default-namespace handling: an unprefixed element visibly utilizes the
default namespace; a prefixed element does not.
xmlns=""undeclaration is rendered only when needed to clear an inherited default in output (Pitfall 2). InclusiveNamespaces/@PrefixListprefixes (and#default) are force-rendered, bypassing the visibly-utilized test (Pitfall 7) — wired via the:prefix_listoption (transform-chain parsing lives inRelyra.Security.XML.C14Ntransform helpers).- Sort order: namespace nodes before attribute nodes; namespaces sorted by
local name (default ns
""sorts least); attributes sorted by RESOLVED namespace-URI then local name (no-namespace attributes sort first — Pitfall 8). - Two escaping functions with the EXACT W3C char sets — text content
escapes
&<>and#xD; attribute values escape&<"and#x9/#xA/#xD. - Empty elements expanded to start+end tag pairs (
<a></a>, never<a/>). - No trailing newline — output starts with
<and ends with>(Pitfall 4); output is a UTF-8 binary (Pitfall 6).
Fail-closed (Pitfall 9): an incomplete / non-bindable node returns
{:error, %Relyra.Error{type: :canonicalization_failed}}. Only the existing
:canonicalization_failed atom is reused — no new error atom is invented.
The line-ending normalization layer (XML 1.0 §2.11) is applied at tree-build
time by Relyra.Security.XML.SaxyTree; this engine does NOT re-trim or strip
(the legacy String.trim/1 from normalize_signed_xml/1 is deliberately
dropped — it violates byte-exact C14N).
Summary
Types
Options for serialize/2.
Functions
Apply a signed Reference's transform chain to referenced_node and serialize
the result to canonical exclusive-C14N 1.0 bytes.
Read InclusiveNamespaces/@PrefixList from a ds:Transforms parse-tree node,
returning the whitespace-separated prefixes (e.g. ["ec", "saml"]), or []
when no InclusiveNamespaces element / PrefixList attribute is present.
Serialize a Relyra.Security.XML.SaxyTree.Node subtree to canonical
exclusive-C14N 1.0 (no-comments) UTF-8 bytes.
Read the ordered transform Algorithm URIs from a ds:Transforms parse-tree
node (its ds:Transform children), in document order.
Types
@type opt() :: {:prefix_list, [String.t()]}
Options for serialize/2.
Functions
@spec canonicalize_reference( term(), [String.t()], Relyra.Security.XML.SaxyTree.Node.t() | nil, [opt()] ) :: {:ok, binary()} | {:error, Relyra.Error.t()}
Apply a signed Reference's transform chain to referenced_node and serialize
the result to canonical exclusive-C14N 1.0 bytes.
transform_uris is the ordered list of transform Algorithm URIs (see
transform_uris/1). Only the enveloped-signature
(http://www.w3.org/2000/09/xmldsig#enveloped-signature) and exclusive-C14N (http://www.w3.org/2001/10/xml-exc-c14n#) transforms are
accepted; ANY other URI (XSLT, XPath/xmldsig-filter2, inclusive C14N) is
rejected fail-closed with :canonicalization_failed (threat T-28-05).
When the chain includes the enveloped-signature transform, the SPECIFIC
signature_subtree node (the ds:Signature containing the Reference being
processed) is pruned from referenced_node — and only that node; an unrelated
sibling ds:Signature survives (anti-XSW, D-10). Pass nil when no
enveloped-signature transform applies.
opts accepts :prefix_list (typically from prefix_list_from_transforms/1),
which is force-rendered per serialize/2.
Returns {:ok, binary()} or
{:error, %Relyra.Error{type: :canonicalization_failed}} (fail-closed for a
non-Node referenced value or a rejected transform).
Read InclusiveNamespaces/@PrefixList from a ds:Transforms parse-tree node,
returning the whitespace-separated prefixes (e.g. ["ec", "saml"]), or []
when no InclusiveNamespaces element / PrefixList attribute is present.
@spec serialize(term(), [opt()]) :: {:ok, binary()} | {:error, Relyra.Error.t()}
Serialize a Relyra.Security.XML.SaxyTree.Node subtree to canonical
exclusive-C14N 1.0 (no-comments) UTF-8 bytes.
Returns {:ok, binary()} of canonical bytes (starts with <, ends with >,
no trailing newline), or {:error, %Relyra.Error{type: :canonicalization_failed}}
for an incomplete / non-bindable node (fail-closed, Pitfall 9).
Options
:prefix_list— a list of namespace prefixes (and/or the literal"#default") drawn fromInclusiveNamespaces/@PrefixList. Listed prefixes are force-rendered on the apex element bypassing the visibly-utilized test (Pitfall 7). Defaults to[].
Read the ordered transform Algorithm URIs from a ds:Transforms parse-tree
node (its ds:Transform children), in document order.