saml
The SAML 2.0 service-provider verify core: XML-DSig verification and assertion validation as a pure codec, the enterprise counterpart to cx-stdlib/oidc. verify takes the raw XML octets and a sequence of crypto public-key elements and returns THE VERIFIED SUBTREE — the Response when it is signed, else the one signed Assertion — never a boolean beside a document; assertion checks the Response envelope (Status, Issuer, InResponseTo, Destination) and selects the single Assertion within the verified subtree; validate checks the assertion's own Issuer, Conditions and bearer SubjectConfirmation against opts.now; attributes and name-id read the assertion the way canonicalization does, every text node concatenated and comments elided, which is what defeats the comment-truncation class. Every signature-wrapping family is a distinct, named refusal, and the corpus that pins them was written before the verifier. The module reads XML itself, canonicalizes exclusively, refuses SHA-1 by default and every transform beyond enveloped-signature plus exclusive C14N, never consults KeyInfo for key SELECTION, holds no state, and reads no clock. SHA-1 has one per-deployment escape (RULED: 1410-a): opts.allow-sha1 accepts xmldsig#rsa-sha1 and xmldsig#sha1 for an ADFS tenant that still emits them, and writes a line naming the document and the algorithm at EVERY use. The reader is BOUNDED and the deployment sets the bounds (RULED: 1410-c): opts.max-bytes, max-depth, max-attributes and max-text-bytes, each enforced before the allocation it governs and each refused CXER5414 naming the bound, because an ACS endpoint is unauthenticated by construction. KeyInfo IS read for the CXER5402 message — the key hint the document carried beside the thumbprints of the keys supplied — which is the rotation an operator otherwise diffs by hand. Beside the verify core it exchanges METADATA (RULED: 1402-a): metadata reads one md:EntityDescriptor into the [entity …] a deployment configures from — each role's endpoints, NameID formats, and keys with the validity windows crypto:certificate-validity reports — keys answers a role's public-key elements for a use with SAML's own rule that a use-less KeyDescriptor is both, and sp-metadata emits our own descriptor in schema order and byte-deterministically. Metadata is read by this module's reader for the same reason assertions are, metadata signatures are not verified, and a certificate's expiry is checked at import against opts.now and never inside verify.
saml:canonicalize
[$saml:canonicalize] -> string — Exclusive C14N octets of a document, or of the element opts.id names, with an optional InclusiveNamespaces prefix-list; comments are omitted, whitespace is kept.
[?lib 'cx-stdlib/saml']
[?lib 'cx-stdlib/hash']
[?lib 'cx-stdlib/bytes']
[$hash:sha256-hex [$bytes:from-string-utf8 [$saml:canonicalize '<r><!--c-->t<!-- d --></r>' {}]]]
'42fa3c712204457ddc3f4fb6f3cb104ac966e72631fcc8b0e8b3c0d8c0555737'
saml:verify
[$saml:verify] -> element — Verify every XML-DSig signature in the document and return the verified subtree itself; the position, reference, transforms, algorithms and digest each refuse with their own code, and a CXER5402 names the document's ds:KeyInfo key hint beside the supplied keys' thumbprints so a rotation reads off the message. The reader is bounded by opts.max-bytes / max-depth / max-attributes / max-text-bytes (CXER5414), and opts.allow-sha1 accepts rsa-sha1 and sha1 for one deployment, logged at every use (RULED: 1410-a, 1410-c). With opts.decryption-keys it also decrypts EncryptedAssertion, EncryptedID and EncryptedAttribute in place — signatures over the ciphertext first, the decrypted assertion's own signature after — and without them an encrypted document is CXER5413 rather than a misleading CXER5401 (RULED: 1400-a, 1401-a).
[?lib 'cx-stdlib/saml']
[?lib 'cx-stdlib/crypto']
[?let [= $keys ([$crypto:jwks-parse "{\"keys\":[{\"kty\":\"RSA\",\"kid\":\"idp-1\",\"n\":\"kf8lgTRcwnfJ4NygnL3wp-LlDrclZzumTy98C88jtBCz4abn91HFYjSCDsLZVh8sgVRMv9DFDzFNaV_szCoL-rksw-VWCVCAwjaOpmH6tB2FFqnHp6pCVNQxQjVwqZLEefogKaPxFVsgtpipDwWMLdzGbM3fWk_dSXqP6-Prf6JRv-9Oak1iqPscuy5Yauija_3J5I7lycKED4PF-q1CxSQcnZVPvoBvmcaIvG_3B434aBUK2zGIlRQBV8oCo9qPGxvRuFgSYq4QOuTAKOFOCtnhz3YQI-3Lo6Bo1A2OG3qDsmSeDGrMEmmMCJF4h9uHR_brT9vKnK_TrGA7o_5y9w\",\"e\":\"AQAB\"}]}"])]
[= $v [$saml:verify '<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_a1" Version="2.0" IssueInstant="2026-09-04T12:00:00Z"><saml:Issuer>https://idp.example</saml:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:CanonicalizationMethod><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"></ds:SignatureMethod><ds:Reference URI="#_a1"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></ds:Transform><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"></ds:DigestMethod><ds:DigestValue>XzVFyM3MF7YTejwNV+WGoOjoerg7GuFG24vhCG5vn08=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>KOmpiuVLShZBA3VTPY9vvLBecvB5oxpkQAXcGwh1nv6JeWeaieFs7/BIrKk9tO98vbNylkU5cSFw2vsSLD4GGo5gxDe/lnLbvsbpuHAcz827VO+FW5TnppOH05Usq5P3AgF961SXovOJwFXiNpaJisoqScZ8xiWHb72scoOk0WpgwMftT7gRif4cV/+gO/KXqKpkZJOMsuHmFMInuR/uaihfc3ND+vbhfuaQKuiAKgw7t79aSfwFtGlcTnUOGi37sdnR2dfI7S/ZFMmcEIttHRzpoHSi/uY2k8JD0DN5Acd4egRGGvkx4reZ5UWi+NPViiIFOABBX8Gips5BY9WtFg==</ds:SignatureValue></ds:Signature><saml:Subject><saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">alice@example.com</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml:SubjectConfirmationData NotOnOrAfter="2026-09-04T12:05:00Z" Recipient="https://sp.example/acs" InResponseTo="_req1"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore="2026-09-04T11:55:00Z" NotOnOrAfter="2026-09-04T12:05:00Z"><saml:AudienceRestriction><saml:Audience>https://sp.example</saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AuthnStatement AuthnInstant="2026-09-04T12:00:00Z" SessionIndex="_s1"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement><saml:AttributeStatement><saml:Attribute Name="email"><saml:AttributeValue>alice@example.com</saml:AttributeValue></saml:Attribute><saml:Attribute Name="groups"><saml:AttributeValue>staff</saml:AttributeValue><saml:AttributeValue>admins</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion>' $keys {}]]
[= $a [$saml:assertion $v {}]]
([$name $v], $a@ID)]
('saml:Assertion', _a1)
saml:assertion
[$saml:assertion] -> element — Check the Response envelope — Status, Issuer, InResponseTo, Destination — and select the single Assertion within the verified subtree; refuses to pick among several. Under opts.strict a supplied option the signature placement cannot honour is refused CXER5408 naming why, rather than silently ignored — destination against a verified subtree that is an Assertion, because Destination lives only on the Response and this one is unsigned.
[?lib 'cx-stdlib/saml']
[?lib 'cx-stdlib/crypto']
[?let [= $keys ([$crypto:jwks-parse "{\"keys\":[{\"kty\":\"RSA\",\"kid\":\"idp-1\",\"n\":\"kf8lgTRcwnfJ4NygnL3wp-LlDrclZzumTy98C88jtBCz4abn91HFYjSCDsLZVh8sgVRMv9DFDzFNaV_szCoL-rksw-VWCVCAwjaOpmH6tB2FFqnHp6pCVNQxQjVwqZLEefogKaPxFVsgtpipDwWMLdzGbM3fWk_dSXqP6-Prf6JRv-9Oak1iqPscuy5Yauija_3J5I7lycKED4PF-q1CxSQcnZVPvoBvmcaIvG_3B434aBUK2zGIlRQBV8oCo9qPGxvRuFgSYq4QOuTAKOFOCtnhz3YQI-3Lo6Bo1A2OG3qDsmSeDGrMEmmMCJF4h9uHR_brT9vKnK_TrGA7o_5y9w\",\"e\":\"AQAB\"}]}"])]
[= $v [$saml:verify '<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_r1" Version="2.0" IssueInstant="2026-09-04T12:00:00Z" Destination="https://sp.example/acs" InResponseTo="_req1"><saml:Issuer>https://idp.example</saml:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:CanonicalizationMethod><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"></ds:SignatureMethod><ds:Reference URI="#_r1"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></ds:Transform><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"></ds:DigestMethod><ds:DigestValue>Ye5UqF2CGadTJtz3rwkbkAXgv78tgqhYQ4IKr1TLNHY=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>VqjNyp88yxj/1JW35iO56RxO9zR+qI8dt/E/KCiYsGToWDSvE0JKgqlYGg2IVQ1WwGTSUEs5bvXZClZZwiDfP8FBIQCdvIf5cSCt7PpOtINwH+0kFrQEuPYGfrJHJM4MQQGjPXUmDG5w4Gko6ywns2f6npLyil+hGbkptZ0UpZJUJ6vpAGhgv3LklIgZtn+pG/Wmpe8+14n1hwbEQryQocFZbiKX+6bSs5w/0Sy+HIMyvVPlZUXi4RzmYjJEqXxwPcFUsyfvZijdyTJ+Ytg2H3tZto61GNw6V37qXEJVm4GKiNqT4RdTQgl5Aq37fK2QdkFm9Cy/AmK6LHN5D4y79g==</ds:SignatureValue></ds:Signature><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status><saml:Assertion ID="_a1" Version="2.0" IssueInstant="2026-09-04T12:00:00Z"><saml:Issuer>https://idp.example</saml:Issuer><saml:Subject><saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">alice@example.com</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml:SubjectConfirmationData NotOnOrAfter="2026-09-04T12:05:00Z" Recipient="https://sp.example/acs" InResponseTo="_req1"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore="2026-09-04T11:55:00Z" NotOnOrAfter="2026-09-04T12:05:00Z"><saml:AudienceRestriction><saml:Audience>https://sp.example</saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AuthnStatement AuthnInstant="2026-09-04T12:00:00Z" SessionIndex="_s1"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement><saml:AttributeStatement><saml:Attribute Name="email"><saml:AttributeValue>alice@example.com</saml:AttributeValue></saml:Attribute><saml:Attribute Name="groups"><saml:AttributeValue>staff</saml:AttributeValue><saml:AttributeValue>admins</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion></samlp:Response>' $keys {}]]
[= $a [$saml:assertion $v {issuer: "https://idp.example", in-response-to: "_req1", destination: "https://sp.example/acs"}]]
$a@ID]
_a1
saml:validate
[$saml:validate] -> element — Check the assertion's Issuer, Conditions (NotBefore, NotOnOrAfter, AudienceRestriction) and bearer SubjectConfirmationData against opts.now with opts.leeway; returns the validated assertion. Conditions is read against an ALLOW-LIST first (SAML Core §2.5.1.1 rule 3: a condition the relying party does not understand makes the assertion Indeterminate, which MUST be rejected) — AudienceRestriction, OneTimeUse and ProxyRestriction are the recognized children and NotBefore/NotOnOrAfter the recognized attributes, and anything else, an abstract <saml:Condition xsi:type=…> included, is CXER5406 naming it. OneTimeUse and ProxyRestriction evaluate VALID: the first constrains the relying party's caching and the second a relying party that re-issues, which this one never does. Under opts.strict the ABSENCES refuse too: no Conditions/@NotOnOrAfter is CXER5406, and a bearer SubjectConfirmation with no SubjectConfirmationData, a SubjectConfirmationData with no Recipient, or one carrying an InResponseTo when opts.idp-initiated is set are each CXER5407.
[?lib 'cx-stdlib/saml']
[?lib 'cx-stdlib/crypto']
[?lib 'cx-stdlib/time']
[?let [= $keys ([$crypto:jwks-parse "{\"keys\":[{\"kty\":\"RSA\",\"kid\":\"idp-1\",\"n\":\"kf8lgTRcwnfJ4NygnL3wp-LlDrclZzumTy98C88jtBCz4abn91HFYjSCDsLZVh8sgVRMv9DFDzFNaV_szCoL-rksw-VWCVCAwjaOpmH6tB2FFqnHp6pCVNQxQjVwqZLEefogKaPxFVsgtpipDwWMLdzGbM3fWk_dSXqP6-Prf6JRv-9Oak1iqPscuy5Yauija_3J5I7lycKED4PF-q1CxSQcnZVPvoBvmcaIvG_3B434aBUK2zGIlRQBV8oCo9qPGxvRuFgSYq4QOuTAKOFOCtnhz3YQI-3Lo6Bo1A2OG3qDsmSeDGrMEmmMCJF4h9uHR_brT9vKnK_TrGA7o_5y9w\",\"e\":\"AQAB\"}]}"])]
[= $v [$saml:verify '<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_r1" Version="2.0" IssueInstant="2026-09-04T12:00:00Z" Destination="https://sp.example/acs" InResponseTo="_req1"><saml:Issuer>https://idp.example</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status><saml:Assertion ID="_a1" Version="2.0" IssueInstant="2026-09-04T12:00:00Z"><saml:Issuer>https://idp.example</saml:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:CanonicalizationMethod><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"></ds:SignatureMethod><ds:Reference URI="#_a1"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></ds:Transform><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"></ds:DigestMethod><ds:DigestValue>XzVFyM3MF7YTejwNV+WGoOjoerg7GuFG24vhCG5vn08=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>KOmpiuVLShZBA3VTPY9vvLBecvB5oxpkQAXcGwh1nv6JeWeaieFs7/BIrKk9tO98vbNylkU5cSFw2vsSLD4GGo5gxDe/lnLbvsbpuHAcz827VO+FW5TnppOH05Usq5P3AgF961SXovOJwFXiNpaJisoqScZ8xiWHb72scoOk0WpgwMftT7gRif4cV/+gO/KXqKpkZJOMsuHmFMInuR/uaihfc3ND+vbhfuaQKuiAKgw7t79aSfwFtGlcTnUOGi37sdnR2dfI7S/ZFMmcEIttHRzpoHSi/uY2k8JD0DN5Acd4egRGGvkx4reZ5UWi+NPViiIFOABBX8Gips5BY9WtFg==</ds:SignatureValue></ds:Signature><saml:Subject><saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">alice@example.com</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml:SubjectConfirmationData NotOnOrAfter="2026-09-04T12:05:00Z" Recipient="https://sp.example/acs" InResponseTo="_req1"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore="2026-09-04T11:55:00Z" NotOnOrAfter="2026-09-04T12:05:00Z"><saml:AudienceRestriction><saml:Audience>https://sp.example</saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AuthnStatement AuthnInstant="2026-09-04T12:00:00Z" SessionIndex="_s1"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement><saml:AttributeStatement><saml:Attribute Name="email"><saml:AttributeValue>alice@example.com</saml:AttributeValue></saml:Attribute><saml:Attribute Name="groups"><saml:AttributeValue>staff</saml:AttributeValue><saml:AttributeValue>admins</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion></samlp:Response>' $keys {}]]
[= $a [$saml:assertion $v {}]]
[= $ok [$saml:validate $a {now: [$time:datetime 2026 9 4 12 0 0], audience: "https://sp.example", issuer: "https://idp.example", recipient: "https://sp.example/acs", in-response-to: "_req1"}]]
([$name $ok], $ok@ID)]
('saml:Assertion', _a1)
saml:attributes
[$saml:attributes] -> map — The AttributeStatement as a map from Name to the SEQUENCE of its values — always a sequence, even for one value.
[?lib 'cx-stdlib/saml']
[?lib 'cx-stdlib/crypto']
[?lib 'cx-stdlib/map']
[?let [= $keys ([$crypto:jwks-parse "{\"keys\":[{\"kty\":\"RSA\",\"kid\":\"idp-1\",\"n\":\"kf8lgTRcwnfJ4NygnL3wp-LlDrclZzumTy98C88jtBCz4abn91HFYjSCDsLZVh8sgVRMv9DFDzFNaV_szCoL-rksw-VWCVCAwjaOpmH6tB2FFqnHp6pCVNQxQjVwqZLEefogKaPxFVsgtpipDwWMLdzGbM3fWk_dSXqP6-Prf6JRv-9Oak1iqPscuy5Yauija_3J5I7lycKED4PF-q1CxSQcnZVPvoBvmcaIvG_3B434aBUK2zGIlRQBV8oCo9qPGxvRuFgSYq4QOuTAKOFOCtnhz3YQI-3Lo6Bo1A2OG3qDsmSeDGrMEmmMCJF4h9uHR_brT9vKnK_TrGA7o_5y9w\",\"e\":\"AQAB\"}]}"])]
[= $v [$saml:verify '<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_r1" Version="2.0" IssueInstant="2026-09-04T12:00:00Z" Destination="https://sp.example/acs" InResponseTo="_req1"><saml:Issuer>https://idp.example</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status><saml:Assertion ID="_a1" Version="2.0" IssueInstant="2026-09-04T12:00:00Z"><saml:Issuer>https://idp.example</saml:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:CanonicalizationMethod><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"></ds:SignatureMethod><ds:Reference URI="#_a1"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></ds:Transform><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"></ds:DigestMethod><ds:DigestValue>XzVFyM3MF7YTejwNV+WGoOjoerg7GuFG24vhCG5vn08=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>KOmpiuVLShZBA3VTPY9vvLBecvB5oxpkQAXcGwh1nv6JeWeaieFs7/BIrKk9tO98vbNylkU5cSFw2vsSLD4GGo5gxDe/lnLbvsbpuHAcz827VO+FW5TnppOH05Usq5P3AgF961SXovOJwFXiNpaJisoqScZ8xiWHb72scoOk0WpgwMftT7gRif4cV/+gO/KXqKpkZJOMsuHmFMInuR/uaihfc3ND+vbhfuaQKuiAKgw7t79aSfwFtGlcTnUOGi37sdnR2dfI7S/ZFMmcEIttHRzpoHSi/uY2k8JD0DN5Acd4egRGGvkx4reZ5UWi+NPViiIFOABBX8Gips5BY9WtFg==</ds:SignatureValue></ds:Signature><saml:Subject><saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">alice@example.com</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml:SubjectConfirmationData NotOnOrAfter="2026-09-04T12:05:00Z" Recipient="https://sp.example/acs" InResponseTo="_req1"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore="2026-09-04T11:55:00Z" NotOnOrAfter="2026-09-04T12:05:00Z"><saml:AudienceRestriction><saml:Audience>https://sp.example</saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AuthnStatement AuthnInstant="2026-09-04T12:00:00Z" SessionIndex="_s1"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement><saml:AttributeStatement><saml:Attribute Name="email"><saml:AttributeValue>alice@example.com</saml:AttributeValue></saml:Attribute><saml:Attribute Name="groups"><saml:AttributeValue>staff</saml:AttributeValue><saml:AttributeValue>admins</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion></samlp:Response>' $keys {}]]
[= $a [$saml:assertion $v {}]]
[= $attrs [$saml:attributes $a {}]]
([$map:get $attrs "groups"], [$map:get $attrs "email"], [$count [$map:get $attrs "email"]])]
((staff, admins), ('alice@example.com'), 1)
saml:name-id
[$saml:name-id] -> map — The Subject's NameID as {value, format, …qualifiers}, read as the concatenation of every text node with comments elided — never the first text node.
[?lib 'cx-stdlib/saml']
[?lib 'cx-stdlib/crypto']
[?lib 'cx-stdlib/map']
[?let [= $keys ([$crypto:jwks-parse "{\"keys\":[{\"kty\":\"RSA\",\"kid\":\"idp-1\",\"n\":\"kf8lgTRcwnfJ4NygnL3wp-LlDrclZzumTy98C88jtBCz4abn91HFYjSCDsLZVh8sgVRMv9DFDzFNaV_szCoL-rksw-VWCVCAwjaOpmH6tB2FFqnHp6pCVNQxQjVwqZLEefogKaPxFVsgtpipDwWMLdzGbM3fWk_dSXqP6-Prf6JRv-9Oak1iqPscuy5Yauija_3J5I7lycKED4PF-q1CxSQcnZVPvoBvmcaIvG_3B434aBUK2zGIlRQBV8oCo9qPGxvRuFgSYq4QOuTAKOFOCtnhz3YQI-3Lo6Bo1A2OG3qDsmSeDGrMEmmMCJF4h9uHR_brT9vKnK_TrGA7o_5y9w\",\"e\":\"AQAB\"}]}"])]
[= $v [$saml:verify '<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_r1" Version="2.0" IssueInstant="2026-09-04T12:00:00Z" Destination="https://sp.example/acs" InResponseTo="_req1"><saml:Issuer>https://idp.example</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status><saml:Assertion ID="_a1" Version="2.0" IssueInstant="2026-09-04T12:00:00Z"><saml:Issuer>https://idp.example</saml:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:CanonicalizationMethod><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"></ds:SignatureMethod><ds:Reference URI="#_a1"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></ds:Transform><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"></ds:DigestMethod><ds:DigestValue>XzVFyM3MF7YTejwNV+WGoOjoerg7GuFG24vhCG5vn08=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>KOmpiuVLShZBA3VTPY9vvLBecvB5oxpkQAXcGwh1nv6JeWeaieFs7/BIrKk9tO98vbNylkU5cSFw2vsSLD4GGo5gxDe/lnLbvsbpuHAcz827VO+FW5TnppOH05Usq5P3AgF961SXovOJwFXiNpaJisoqScZ8xiWHb72scoOk0WpgwMftT7gRif4cV/+gO/KXqKpkZJOMsuHmFMInuR/uaihfc3ND+vbhfuaQKuiAKgw7t79aSfwFtGlcTnUOGi37sdnR2dfI7S/ZFMmcEIttHRzpoHSi/uY2k8JD0DN5Acd4egRGGvkx4reZ5UWi+NPViiIFOABBX8Gips5BY9WtFg==</ds:SignatureValue></ds:Signature><saml:Subject><saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">alice@example.com</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml:SubjectConfirmationData NotOnOrAfter="2026-09-04T12:05:00Z" Recipient="https://sp.example/acs" InResponseTo="_req1"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore="2026-09-04T11:55:00Z" NotOnOrAfter="2026-09-04T12:05:00Z"><saml:AudienceRestriction><saml:Audience>https://sp.example</saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AuthnStatement AuthnInstant="2026-09-04T12:00:00Z" SessionIndex="_s1"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement><saml:AttributeStatement><saml:Attribute Name="email"><saml:AttributeValue>alice@example.com</saml:AttributeValue></saml:Attribute><saml:Attribute Name="groups"><saml:AttributeValue>staff</saml:AttributeValue><saml:AttributeValue>admins</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion></samlp:Response>' $keys {}]]
[= $a [$saml:assertion $v {}]]
[= $n [$saml:name-id $a {}]]
([$map:get $n "value"], [$map:get $n "format"])]
('alice@example.com', 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress')
saml:claims
[$saml:claims] -> element — Project a verified assertion onto the [claims …] value crypto:jwt-verify yields — iss/sub/aud/exp/nbf/iat and jti from @ID, the AuthnStatement as auth_time/acr, one_time_use when Conditions carries OneTimeUse, plus attribute-derived claims named by opts.attrs — so a SAML login reaches the same session establishment a JWT does. exp is the EARLIER of Conditions/@NotOnOrAfter and AuthnStatement/@SessionNotOnOrAfter, so the identity provider's own session bound is enforced by the expiry session already derives from exp. auth_time, acr and one_time_use ride in the [payload] child, where crypto:jwt-verify's own attribute rule puts everything outside iss/sub/aud/jti/exp/nbf/iat.
[?lib 'cx-stdlib/saml']
[?lib 'cx-stdlib/crypto']
[?let [= $keys ([$crypto:jwks-parse "{\"keys\":[{\"kty\":\"RSA\",\"kid\":\"idp-1\",\"n\":\"kf8lgTRcwnfJ4NygnL3wp-LlDrclZzumTy98C88jtBCz4abn91HFYjSCDsLZVh8sgVRMv9DFDzFNaV_szCoL-rksw-VWCVCAwjaOpmH6tB2FFqnHp6pCVNQxQjVwqZLEefogKaPxFVsgtpipDwWMLdzGbM3fWk_dSXqP6-Prf6JRv-9Oak1iqPscuy5Yauija_3J5I7lycKED4PF-q1CxSQcnZVPvoBvmcaIvG_3B434aBUK2zGIlRQBV8oCo9qPGxvRuFgSYq4QOuTAKOFOCtnhz3YQI-3Lo6Bo1A2OG3qDsmSeDGrMEmmMCJF4h9uHR_brT9vKnK_TrGA7o_5y9w\",\"e\":\"AQAB\"}]}"])]
[= $v [$saml:verify '<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_r1" Version="2.0" IssueInstant="2026-09-04T12:00:00Z" Destination="https://sp.example/acs" InResponseTo="_req1"><saml:Issuer>https://idp.example</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status><saml:Assertion ID="_a1" Version="2.0" IssueInstant="2026-09-04T12:00:00Z"><saml:Issuer>https://idp.example</saml:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:CanonicalizationMethod><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"></ds:SignatureMethod><ds:Reference URI="#_a1"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></ds:Transform><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"></ds:DigestMethod><ds:DigestValue>XzVFyM3MF7YTejwNV+WGoOjoerg7GuFG24vhCG5vn08=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>KOmpiuVLShZBA3VTPY9vvLBecvB5oxpkQAXcGwh1nv6JeWeaieFs7/BIrKk9tO98vbNylkU5cSFw2vsSLD4GGo5gxDe/lnLbvsbpuHAcz827VO+FW5TnppOH05Usq5P3AgF961SXovOJwFXiNpaJisoqScZ8xiWHb72scoOk0WpgwMftT7gRif4cV/+gO/KXqKpkZJOMsuHmFMInuR/uaihfc3ND+vbhfuaQKuiAKgw7t79aSfwFtGlcTnUOGi37sdnR2dfI7S/ZFMmcEIttHRzpoHSi/uY2k8JD0DN5Acd4egRGGvkx4reZ5UWi+NPViiIFOABBX8Gips5BY9WtFg==</ds:SignatureValue></ds:Signature><saml:Subject><saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">alice@example.com</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml:SubjectConfirmationData NotOnOrAfter="2026-09-04T12:05:00Z" Recipient="https://sp.example/acs" InResponseTo="_req1"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore="2026-09-04T11:55:00Z" NotOnOrAfter="2026-09-04T12:05:00Z"><saml:AudienceRestriction><saml:Audience>https://sp.example</saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AuthnStatement AuthnInstant="2026-09-04T12:00:00Z" SessionIndex="_s1"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement><saml:AttributeStatement><saml:Attribute Name="email"><saml:AttributeValue>alice@example.com</saml:AttributeValue></saml:Attribute><saml:Attribute Name="groups"><saml:AttributeValue>staff</saml:AttributeValue><saml:AttributeValue>admins</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion></samlp:Response>' $keys {}]]
[= $a [$saml:assertion $v {}]]
[= $c [$saml:claims $a {attrs: {groups: "groups"}}]]
([$crypto:claim $c "sub"], [$crypto:claim $c "groups"], [$crypto:claim $c "exp"], $c@iss)]
('alice@example.com', (staff, admins), 1788523500, 'https://idp.example')
saml:metadata
[$saml:metadata] -> element — Read one md:EntityDescriptor into the [entity …] a deployment configures from — each role's endpoints, NameID formats, and keys with the validity windows crypto:certificate-validity reports. Read with this module's own reader, so a DOCTYPE or a duplicate attribute is CXER5400 here too; opts.now, when given, refuses a signing certificate outside its window.
[?lib 'cx-stdlib/saml' as=saml]
[?let [= $e [$saml:metadata '<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="https://idp.example"><md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"><md:KeyDescriptor use="signing"><ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIIBhjCCAS2gAwIBAgIUeiYVce7ieSYJfQHqrDFWv4cv8AwwCgYIKoZIzj0EAwIwGTEXMBUGA1UEAwwOaWRwLWVjLmV4YW1wbGUwHhcNMjYwOTA1MTQyNTM3WhcNMzYwOTAyMTQyNTM3WjAZMRcwFQYDVQQDDA5pZHAtZWMuZXhhbXBsZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABFBopgvqME1BHXMxZD8KIpAaNI1vv5F1BnkXFDeyIud11EsfmqTgJJ2CDHNnQLdNDRvnqaa12nWLEhcgnXi/iH6jUzBRMB0GA1UdDgQWBBSrcc4H0O84aTC8mlGdcebpdAc6gzAfBgNVHSMEGDAWgBSrcc4H0O84aTC8mlGdcebpdAc6gzAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0cAMEQCIFoZwyPzqZ138Bvmm+fxVi2nlgKQ4xyMu7rm9UDtqDykAiASj4+iWZ8I6/MFyROL4HYosee6Fc3nlksTIIzB/Ae/zQ==</ds:X509Certificate></ds:X509Data></ds:KeyInfo></md:KeyDescriptor><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://idp.example/sso"/><md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat></md:IDPSSODescriptor></md:EntityDescriptor>' {}]]
[= $i [$first $e/*]]
($e@entity-id, ([?for [in $c $i/*] [yield [$name $c]]]),
[?for [in $c $i/*] [where [= [$name $c] 'key']] [yield [key use=$c@use not-after=$c@not-after]]])]
('https://idp.example', (sso, name-id-format, key), [key use=signing not-after=2036-09-02T14:25:37Z])
saml:keys
[$saml:keys] -> [sequence element] — The role's crypto public-key elements for a use, as the sequence verify takes — signing and encryption each include a KeyDescriptor that carries no use at all, because SAML Metadata §2.4.1.1 makes that one BOTH.
[?lib 'cx-stdlib/saml' as=saml]
[?let [= $e [$saml:metadata '<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="https://idp.example"><md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"><md:KeyDescriptor use="signing"><ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIIBhjCCAS2gAwIBAgIUeiYVce7ieSYJfQHqrDFWv4cv8AwwCgYIKoZIzj0EAwIwGTEXMBUGA1UEAwwOaWRwLWVjLmV4YW1wbGUwHhcNMjYwOTA1MTQyNTM3WhcNMzYwOTAyMTQyNTM3WjAZMRcwFQYDVQQDDA5pZHAtZWMuZXhhbXBsZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABFBopgvqME1BHXMxZD8KIpAaNI1vv5F1BnkXFDeyIud11EsfmqTgJJ2CDHNnQLdNDRvnqaa12nWLEhcgnXi/iH6jUzBRMB0GA1UdDgQWBBSrcc4H0O84aTC8mlGdcebpdAc6gzAfBgNVHSMEGDAWgBSrcc4H0O84aTC8mlGdcebpdAc6gzAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0cAMEQCIFoZwyPzqZ138Bvmm+fxVi2nlgKQ4xyMu7rm9UDtqDykAiASj4+iWZ8I6/MFyROL4HYosee6Fc3nlksTIIzB/Ae/zQ==</ds:X509Certificate></ds:X509Data></ds:KeyInfo></md:KeyDescriptor><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://idp.example/sso"/><md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat></md:IDPSSODescriptor></md:EntityDescriptor>' {}]]
[= $k [$saml:keys $e 'signing']]
[?for [in $x $k] [yield [key kind=[$name $x] crv=$x@crv]]]]
[key kind=ec-public-key crv=P-256]
saml:sp-metadata
[$saml:sp-metadata] -> string — Emit our own md:EntityDescriptor as XML, in schema order (KeyDescriptor, SingleLogoutService, NameIDFormat, AssertionConsumerService) and byte-deterministically, with short binding names expanded to their URNs and PEM armor stripped from certificates. opts.id is emitted as the descriptor's xs:ID and opts.sign signs the document through crypto:xml-sign, the signature as the first child (RULED: 1394-b); opts.sign with no opts.id is CXER5408, because the Reference is URI='#<ID>' and a descriptor with no ID has nothing for it to name. Two [signing-certificate] children are both published with use='signing' — the rotation window — and the document is signed with the first, the active one.
[?lib 'cx-stdlib/saml' as=saml]
[$saml:sp-metadata [sp entity-id="https://rp.example" [acs binding=HTTP-POST location="https://rp.example/saml/acs" index=0 default=true]] {}]
'<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="https://rp.example">\n <md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">\n <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://rp.example/saml/acs" index="0" isDefault="true"/>\n </md:SPSSODescriptor>\n</md:EntityDescriptor>'
saml:acs-decode
[$saml:acs-decode] -> map — The application/x-www-form-urlencoded body an Assertion Consumer Service receives, read into {response, relay-state}: SAMLResponse base64-decoded under the reader's byte bound — checked from the ENCODED length, before the allocation — and RelayState carried back as an OPAQUE STRING under the binding specification's 80-byte cap. RelayState is a KEY into the deployment's pending-request table and never a URL, which is why no wiring of this verb can produce an open redirect. A missing or duplicated SAMLResponse, or one that is not base64, is CXER5415.
[?lib 'cx-stdlib/saml' as=saml]
[?lib 'cx-stdlib/map' as=map]
[?lib 'cx-stdlib/strings' as=strings]
[?let [= $d [$saml:acs-decode 'SAMLResponse=PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJfcjEiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDI2LTA5LTA0VDEyOjAwOjAwWiIgRGVzdGluYXRpb249Imh0dHBzOi8vc3AuZXhhbXBsZS9hY3MiIEluUmVzcG9uc2VUbz0iX3JlcTEiPjxzYW1sOklzc3Vlcj5odHRwczovL2lkcC5leGFtcGxlPC9zYW1sOklzc3Vlcj48c2FtbHA6U3RhdHVzPjxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWxwOlN0YXR1cz48c2FtbDpBc3NlcnRpb24gSUQ9Il9hMSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMjYtMDktMDRUMTI6MDA6MDBaIj48c2FtbDpJc3N1ZXI%2BaHR0cHM6Ly9pZHAuZXhhbXBsZTwvc2FtbDpJc3N1ZXI%2BPGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI%2BPGRzOlNpZ25lZEluZm8%2BPGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiPjwvZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZD48ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxkc2lnLW1vcmUjcnNhLXNoYTI1NiI%2BPC9kczpTaWduYXR1cmVNZXRob2Q%2BPGRzOlJlZmVyZW5jZSBVUkk9IiNfYTEiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSI%2BPC9kczpUcmFuc2Zvcm0%2BPGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyI%2BPC9kczpUcmFuc2Zvcm0%2BPC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGEyNTYiPjwvZHM6RGlnZXN0TWV0aG9kPjxkczpEaWdlc3RWYWx1ZT5YelZGeU0zTUY3WVRlandOVitXR29Pam9lcmc3R3VGRzI0dmhDRzV2bjA4PTwvZHM6RGlnZXN0VmFsdWU%2BPC9kczpSZWZlcmVuY2U%2BPC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5LT21waXVWTFNoWkJBM1ZUUFk5dnZMQmVjdkI1b3hwa1FBWGNHd2gxbnY2SmVXZWFpZUZzNy9CSXJLazl0Tzk4dmJOeWxrVTVjU0Z3MnZzU0xENEdHbzVneERlL2xuTGJ2c2JwdUhBY3o4MjdWTytGVzVUbnBwT0gwNVVzcTVQM0FnRjk2MVNYb3ZPSndGWGlOcGFKaXNvcVNjWjh4aVdIYjcyc2NvT2swV3Bnd01mdFQ3Z1JpZjRjVi8rZ08vS1hxS3BrWkpPTXN1SG1GTUludVIvdWFpaGZjM05EK3ZiaGZ1YVFLdWlBS2d3N3Q3OWFTZndGdEdsY1RuVU9HaTM3c2RuUjJkZkk3Uy9aRk1tY0VJdHRIUnpwb0hTaS91WTJrOEpEMERONUFjZDRlZ1JHR3ZreDRyZVo1VVdpK05QVmlpSUZPQUJCWDhHaXBzNUJZOVd0Rmc9PTwvZHM6U2lnbmF0dXJlVmFsdWU%2BPC9kczpTaWduYXR1cmU%2BPHNhbWw6U3ViamVjdD48c2FtbDpOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPmFsaWNlQGV4YW1wbGUuY29tPC9zYW1sOk5hbWVJRD48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI%2BPHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDI2LTA5LTA0VDEyOjA1OjAwWiIgUmVjaXBpZW50PSJodHRwczovL3NwLmV4YW1wbGUvYWNzIiBJblJlc3BvbnNlVG89Il9yZXExIi8%2BPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24%2BPC9zYW1sOlN1YmplY3Q%2BPHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMjYtMDktMDRUMTE6NTU6MDBaIiBOb3RPbk9yQWZ0ZXI9IjIwMjYtMDktMDRUMTI6MDU6MDBaIj48c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjxzYW1sOkF1ZGllbmNlPmh0dHBzOi8vc3AuZXhhbXBsZTwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMjYtMDktMDRUMTI6MDA6MDBaIiBTZXNzaW9uSW5kZXg9Il9zMSI%2BPHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkUHJvdGVjdGVkVHJhbnNwb3J0PC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ%2BPC9zYW1sOkF1dGhuU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iZW1haWwiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlPmFsaWNlQGV4YW1wbGUuY29tPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU%2BPHNhbWw6QXR0cmlidXRlIE5hbWU9Imdyb3VwcyI%2BPHNhbWw6QXR0cmlidXRlVmFsdWU%2Bc3RhZmY8L3NhbWw6QXR0cmlidXRlVmFsdWU%2BPHNhbWw6QXR0cmlidXRlVmFsdWU%2BYWRtaW5zPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU%2BPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U%2B&RelayState=a1b2c3d4e5f6' {}]]
([$eq [$map:get $d "relay-state"] 'a1b2c3d4e5f6'],
[$strings:starts-with [$map:get $d "response"] '<samlp:Response '],
[$strings:length [$map:get $d "response"]])]
(true, true, 2769)
saml:redirect-encode
[$saml:redirect-encode] -> string — A REQUEST or a LOGOUT message into the HTTP-Redirect query string: raw DEFLATE, base64, percent-encoded, with the parameter chosen by the MESSAGE — SAMLRequest for an AuthnRequest or a LogoutRequest, SAMLResponse for a LogoutResponse. An assertion-bearing samlp:Response is refused CXER5415: Redirect signs the query string rather than the document, and POST is its only binding.
[?lib 'cx-stdlib/saml' as=saml]
[?lib 'cx-stdlib/map' as=map]
[?let [= $q [$saml:redirect-encode '<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_req1" Version="2.0" IssueInstant="2026-09-04T12:00:00Z" Destination="https://idp.example/sso" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="https://rp.example/saml/acs"><saml:Issuer>https://rp.example</saml:Issuer><samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" AllowCreate="true"/></samlp:AuthnRequest>' {relay-state: 'a1b2c3d4e5f6'}]]
[= $d [$saml:redirect-decode $q {}]]
([$eq [$map:get $d "param"] 'SAMLRequest'], [$eq [$map:get $d "relay-state"] 'a1b2c3d4e5f6'],
[$eq [$map:get $d "message"] '<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_req1" Version="2.0" IssueInstant="2026-09-04T12:00:00Z" Destination="https://idp.example/sso" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="https://rp.example/saml/acs"><saml:Issuer>https://rp.example</saml:Issuer><samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" AllowCreate="true"/></samlp:AuthnRequest>'])]
(true, true, true)
saml:redirect-decode
[$saml:redirect-decode] -> map — The other direction, into {param, message, relay-state}. Bounded by opts.max-bytes (1 MiB by default) on the INFLATED length, because a compression ratio is not a size the sender gets to choose; bad base64, a stream that is not raw DEFLATE, trailing bytes, both parameters at once, neither, and a Response arriving this way are each CXER5415.
[?lib 'cx-stdlib/saml' as=saml]
[?lib 'cx-stdlib/map' as=map]
[?let [= $d [$saml:redirect-decode 'SAMLRequest=fZJbS8MwFMe%2FSsh713aIYGgHdUMsTC1r9cEXie3RBXLpzkl1fntj56UwHOQh%2FC%2BcXy4ZSaN7UQx%2BazewG4A82xttSYxGzge0wklSJKw0QMK3oi5u1mI%2BS0SPzrvWaT6pnG5IIkCvnOWsXOX8CWGXcvYASEHLeYgEg2iA0pKX1gcpmZ9HyUWUnDXpXCRJWI%2BcrQKnstKPra33PYk4Vl0%2Fg700vYaYyHFWffNdKtsp%2B3oa7fkQInHdNFVU3dUNZ8UP7tJZGgxgDfimWrjfrP%2FG4mRquIBYtsQX2ddWjEfBxXEyi6d%2BdniE28BUriqnVfvBrhwa6f9HTmfpqKguehmjAoxUuug6BKKArrV7XyJIDzn3OEBgio%2Fn%2FIrTH7D4BA%3D%3D&RelayState=a1b2c3d4e5f6' {}]]
([$eq [$map:get $d "param"] 'SAMLRequest'], [$eq [$map:get $d "relay-state"] 'a1b2c3d4e5f6'],
[$eq [$map:get $d "message"] '<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_req1" Version="2.0" IssueInstant="2026-09-04T12:00:00Z" Destination="https://idp.example/sso" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="https://rp.example/saml/acs"><saml:Issuer>https://rp.example</saml:Issuer><samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" AllowCreate="true"></samlp:NameIDPolicy></samlp:AuthnRequest>'])]
(true, true, true)
saml:authn-request
[$saml:authn-request] -> string — An AuthnRequest in schema order, taking the AssertionConsumerServiceURL from the same [sp …] sp-metadata reads so one URL is configured once. Deterministic — and therefore byte-pinnable — given opts.id and opts.issue-instant, both REQUIRED because the verb is pure: it reads no clock and draws no randomness. opts.sign names the SP signing key and crypto:xml-sign writes the enveloped ds:Signature after Issuer (RULED: 1394-b); unsigned is legal and is what ADFS, Entra and Okta accept, but an [sp authn-requests-signed=true] with no opts.sign is CXER5417, because that flag is what this SP publishes to the IdP. An id that is not an XML NCName, or an [sp] with no HTTP-POST [acs], is CXER5408.
[?lib 'cx-stdlib/saml' as=saml]
[?lib 'cx-stdlib/time' as=time]
[$saml:authn-request
[sp entity-id="https://rp.example"
[acs binding=HTTP-POST location="https://rp.example/saml/acs" index=0 default=true]]
{id: '_req1', issue-instant: [$time:datetime 2026 9 4 12 0 0],
destination: 'https://idp.example/sso',
name-id-format: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
allow-create: true}]
'<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_req1" Version="2.0" IssueInstant="2026-09-04T12:00:00Z" Destination="https://idp.example/sso" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="https://rp.example/saml/acs"><saml:Issuer>https://rp.example</saml:Issuer><samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" AllowCreate="true"/></samlp:AuthnRequest>'
saml:logout-request
[$saml:logout-request] -> string — A SIGNED LogoutRequest in schema order — Issuer, the signature, NameID, SessionIndex. opts.sign names the SP signing key and crypto:xml-sign writes the enveloped ds:Signature (saml.md §3.3, RULED: 1394-b); without it the verb is CXER5417, because an unsigned LogoutRequest is a forced logout of any user by anyone who learns a NameID and logout-parse refuses one. SessionIndex is what makes the logout single-SESSION rather than a logout of every session that NameID ever had.
[?lib 'cx-stdlib/saml' as=saml]
[?lib 'cx-stdlib/bytes' as=bytes]
[?lib 'cx-stdlib/map' as=map]
[?lib 'cx-stdlib/time' as=time]
[?let [= $msg [$saml:logout-request [sp entity-id="https://rp.example"]
{id: '_slo1', issue-instant: [$time:datetime 2026 9 4 12 0 0],
destination: 'https://idp.example/slo',
name-id: 'alice@example.com',
name-id-format: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
session-index: '_s1',
sign: [ec-private-key crv="P-256" [d [$bytes:from-hex "0bd5ce1d4dd3f581558f42c420be72b9247d1d9a780bf3079ad18557abe6e98f"]]]}]]
[= $back [$saml:logout-parse $msg ([ec-public-key crv="P-256" [x [$bytes:from-hex "e016ea4fdb3d5d5ceecee126633cbf6f52661359f05eef034bd2140f36ac0f28"]] [y [$bytes:from-hex "27a9f49e325cbdcefb594e57ce6cc6980d0db1360402625f8fa0cb247f8e7e2e"]]]) {}]]
([$eq [$map:get $back "kind"] 'LogoutRequest'],
[$eq [$map:get $back "name-id"] 'alice@example.com'],
[$count [$map:get $back "session-index"]])]
(true, true, 1)
saml:logout-response
[$saml:logout-response] -> string — The SIGNED answer to a LogoutRequest: InResponseTo carries the request's ID and opts.status defaults to Success. opts.sign is required for the same reason it is on logout-request — an unsigned LogoutResponse tells a peer a logout succeeded that need never have happened — and without it the verb is CXER5417.
[?lib 'cx-stdlib/saml' as=saml]
[?lib 'cx-stdlib/bytes' as=bytes]
[?lib 'cx-stdlib/map' as=map]
[?lib 'cx-stdlib/time' as=time]
[?let [= $msg [$saml:logout-response [sp entity-id="https://rp.example"]
{id: '_slor1', issue-instant: [$time:datetime 2026 9 4 12 0 0],
in-response-to: '_lo1', destination: 'https://idp.example/slo',
sign: [ec-private-key crv="P-256" [d [$bytes:from-hex "0bd5ce1d4dd3f581558f42c420be72b9247d1d9a780bf3079ad18557abe6e98f"]]]}]]
[= $back [$saml:logout-parse $msg ([ec-public-key crv="P-256" [x [$bytes:from-hex "e016ea4fdb3d5d5ceecee126633cbf6f52661359f05eef034bd2140f36ac0f28"]] [y [$bytes:from-hex "27a9f49e325cbdcefb594e57ce6cc6980d0db1360402625f8fa0cb247f8e7e2e"]]]) {in-response-to: '_lo1'}]]
([$eq [$map:get $back "kind"] 'LogoutResponse'],
[$eq [$map:get $back "in-response-to"] '_lo1'],
[$eq [$map:get $back "status"] 'urn:oasis:names:tc:SAML:2.0:status:Success'])]
(true, true, true)
saml:logout-parse
[$saml:logout-parse] -> map — An incoming LogoutRequest or LogoutResponse, read into {kind, id, issue-instant, issuer, destination, …} — name-id / name-id-format / session-index (always a sequence) for a request, in-response-to / status for a response. The message MUST be signed and the signature is checked by the same code verify uses, so its refusals keep their own codes; CXER5416 is what is specific to logout — not a logout message, unsigned, or answering a request opts.in-response-to says this caller never sent.
[?lib 'cx-stdlib/saml']
[?lib 'cx-stdlib/crypto']
[?lib 'cx-stdlib/map']
[?let [= $keys ([$crypto:jwks-parse "{\"keys\":[{\"kty\":\"RSA\",\"kid\":\"idp-1\",\"n\":\"kf8lgTRcwnfJ4NygnL3wp-LlDrclZzumTy98C88jtBCz4abn91HFYjSCDsLZVh8sgVRMv9DFDzFNaV_szCoL-rksw-VWCVCAwjaOpmH6tB2FFqnHp6pCVNQxQjVwqZLEefogKaPxFVsgtpipDwWMLdzGbM3fWk_dSXqP6-Prf6JRv-9Oak1iqPscuy5Yauija_3J5I7lycKED4PF-q1CxSQcnZVPvoBvmcaIvG_3B434aBUK2zGIlRQBV8oCo9qPGxvRuFgSYq4QOuTAKOFOCtnhz3YQI-3Lo6Bo1A2OG3qDsmSeDGrMEmmMCJF4h9uHR_brT9vKnK_TrGA7o_5y9w\",\"e\":\"AQAB\"}]}"])]
[= $m [$saml:logout-parse '<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_lo1" Version="2.0" IssueInstant="2026-09-04T12:00:00Z" Destination="https://sp.example/saml/slo"><saml:Issuer>https://idp.example</saml:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:CanonicalizationMethod><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"></ds:SignatureMethod><ds:Reference URI="#_lo1"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></ds:Transform><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"></ds:DigestMethod><ds:DigestValue>epAJtp3iPUQdHNaGXsPYHOib77ga33MsvF7NSTHNzDo=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>A9P8WY9su0bqoRljWux3VKcQ3genTWPdUwPbUWliE/k6rc8l8uUo1zko/rqzPn/oQY9BJtyJIsYQF/nj0ejxH7uPrAjYTmIMHi6Ph+0MRYlX8j/aSsw9oyJNFcLwxCo2znmUOmhYgRAB+Y9d0hhSI4ofFiYbFiS+QiAX/tWNfL3R/Ym9iY7lwayWiwFnvtLDH6WOo+Zfq6Zjhtai/LHd4wJ2HqCvRJQfwhaChB2dYP3X2M2HuSEclT8jbAB6F8Onj21x/53ldjV3ZZrmahEp+I6n7uepSahGF3wQSDlQnKqwkEbsEOgJDH4tYOei0ZtiC84X1IlcuxQuDJ4iYMtJzA==</ds:SignatureValue></ds:Signature><saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">alice@example.com</saml:NameID><samlp:SessionIndex>_s1</samlp:SessionIndex></samlp:LogoutRequest>' $keys {}]]
([$eq [$map:get $m "kind"] 'LogoutRequest'], [$eq [$map:get $m "name-id"] 'alice@example.com'],
[$count [$map:get $m "session-index"]], [$eq [$map:get $m "issuer"] 'https://idp.example'])]
(true, true, 1, true)