Relyra.TestSupport.XmldsigSigner (relyra v1.6.0)

Copy Markdown View Source

Phase 29 (D-11) — a minimal, genuine XMLDSig signer for tests.

This module produces a SAML <Response> carrying a real ds:DigestValue and a real ds:SignatureValue over the contained <Assertion>. It exists so the suite can assert the ONE input that must remain {:ok} after Phase 29 wires cryptographic verification into Relyra.Security.Signature: a legitimately signed Response (the positive control, ROADMAP success #3). Every structure-only "signature" the verifier now rejects has a genuine counterpart here.

Anti-divergent-signer guarantee (D-12)

The signer canonicalizes with the SAME C14N engine the verifier uses — it parses the final Response with Relyra.Security.XML.SaxyTree, locates the exact <Assertion> / <SignedInfo> tree nodes the verifier will bind, and canonicalizes THEM via Relyra.Security.XML.PureBeam.canonicalize/1 (referenced element → DigestValue) and Relyra.Security.XML.C14N.serialize/1 (SignedInfo → signed bytes). Because the digest and the signature are computed over the bytes produced by parsing the emitted XML, the signer can never canonicalize differently from the verifier — a divergent second signer would make the positive control pass for the wrong reason (T-29-15).

Keypair reuse (D-11)

The RSA-2048 key is Relyra.TestSupport.FakeIdP.keypair() — there is NO second :public_key.generate_key call in this module. The self-signed cert PEM the verifier trusts is derived from that same key. Phase 30 PROMOTES this module into FakeIdP (it fills the empty SignedInfo that FakeIdP.response_xml emits today); keeping the byte-alignment guarantee above is the whole point of the promotion.

Prod guard (T-29-18)

Mirrors FakeIdP's @prod_build + ensure_not_prod!/0 discipline so the signing code never compiles/runs in :prod.

Summary

Types

Result of signed_response/1: the genuinely-signed Response XML plus the cert chain (one self-signed PEM) the verifier must be configured with to accept it.

Functions

The self-signed certificate PEM (a single-element cert chain) the verifier must trust to accept this signer's Responses. Derived from FakeIdP.keypair().

Genuinely sign an EXISTING SAML <Response> XML in place, preserving its exact element shape.

Build a genuinely-signed SAML <Response> and the matching cert chain.

Types

signed()

@type signed() :: %{response_xml: String.t(), cert_chain: [String.t()]}

Result of signed_response/1: the genuinely-signed Response XML plus the cert chain (one self-signed PEM) the verifier must be configured with to accept it.

Functions

self_signed_cert_pem()

@spec self_signed_cert_pem() :: String.t()

The self-signed certificate PEM (a single-element cert chain) the verifier must trust to accept this signer's Responses. Derived from FakeIdP.keypair().

Exposed so a test can assert against the cert directly, or pass a DIFFERENT cert (a throwaway keypair) to drive the wrong-key :invalid_signature negative.

sign_response(response_xml)

@spec sign_response(String.t()) :: signed()

Genuinely sign an EXISTING SAML <Response> XML in place, preserving its exact element shape.

The input response_xml must contain an <Assertion ID="..."> (the referenced element) and a <Signature> whose <SignedInfo> carries a <Reference URI="#assertion_id"> with a <DigestMethod> but no <DigestValue>/<SignatureValue> yet (the structure-only shape the suite already builds). This function:

  1. parses the input with the verifier's parser,
  2. computes the genuine DigestValue over the canonicalized referenced <Assertion> (the SAME engine the verifier uses),
  3. injects <DigestValue>…</DigestValue> into the Reference,
  4. re-parses, canonicalizes the <SignedInfo> (SAME engine), signs it with FakeIdP.keypair(), and
  5. injects <SignatureValue>…</SignatureValue> after </SignedInfo>.

Returns %{response_xml: signed_xml, cert_chain: [pem]}.

Use this to re-point structure-only fixtures (whose declared outcome fires AT or AFTER the crypto step — time conditions, replay, success) at a genuine signature WITHOUT changing the fixture's assertion shape (so the downstream stage still sees exactly the fields it asserts on).

Requires the referenced <Assertion> to carry the ID matching the Reference's URI. Raises if the structure cannot be located (a malformed test fixture should fail loudly, not sign silently).

signed_response(opts \\ [])

@spec signed_response(keyword()) :: signed()

Build a genuinely-signed SAML <Response> and the matching cert chain.

Returns %{response_xml: binary(), cert_chain: [pem]}. Feed response_xml through Relyra.consume_response/3 (or ValidationPipeline.run/4) with cert_chain threaded onto the connection (:idp_certificates / :cert_chain) or the :cert_chain opt, and the login verifies {:ok} for the RIGHT reason (a real RSA signature + a real digest over the canonicalized assertion).

Options

  • :issuer, :destination, :recipient, :audience, :in_response_to, :name_id, :assertion_id, :status, :not_before, :not_on_or_after, :subject_confirmation_not_on_or_after — protocol field overrides so the same genuine signer can stand in for the fixed-shape structure-only Responses the triaged tests previously fed.
  • :tamper_name_id — when set to a string, the emitted XML's <NameID> text is rewritten to that value AFTER signing (the signature/digest are NOT recomputed). The result is a Response whose SignatureValue is well-formed but whose Reference digest no longer matches the (tampered) content — the :digest_mismatch negative control (T-29-17). Defaults to nil (no tamper).