Relyra.Security.XML.C14N (relyra v1.6.0)

Copy Markdown View Source

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/@PrefixList prefixes (and #default) are force-rendered, bypassing the visibly-utilized test (Pitfall 7) — wired via the :prefix_list option (transform-chain parsing lives in Relyra.Security.XML.C14N transform 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

opt()

@type opt() :: {:prefix_list, [String.t()]}

Options for serialize/2.

Functions

canonicalize_reference(referenced, transform_uris, signature_subtree, opts)

@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).

prefix_list_from_transforms(transforms)

@spec prefix_list_from_transforms(term()) :: [String.t()]

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(node, opts \\ [])

@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 from InclusiveNamespaces/@PrefixList. Listed prefixes are force-rendered on the apex element bypassing the visibly-utilized test (Pitfall 7). Defaults to [].

transform_uris(arg1)

@spec transform_uris(term()) :: [String.t()]

Read the ordered transform Algorithm URIs from a ds:Transforms parse-tree node (its ds:Transform children), in document order.