VS-17: full XML-DSig SAML signature verification #50

Merged
David merged 2 commits from feat/saml-signature-verification-vs-17 into main 2026-06-07 19:54:45 +02:00
Owner

VS-17: SAML signature verification (full XML-c14n)

Implements full inbound XML-DSig verification for signed SAML responses, replacing the previous whole-document digest shortcut in verify_signature.

What changed

The old verifier extracted the first ds:SignedInfo and digested the entire signature-stripped document. That does not match how Okta and Azure AD actually sign: they sign the Assertion and reference it by #id, so real responses would not verify. The new verifier:

  • Parses the ds:SignedInfo structure: CanonicalizationMethod, SignatureMethod, each ds:Reference (URI, transforms, DigestMethod, DigestValue) and any ec:InclusiveNamespaces PrefixList.
  • Enforces an algorithm policy: only exclusive c14n + RSA-SHA256 + SHA-256 are accepted; weaker or unknown algorithms are rejected as UnsupportedAlgorithm.
  • Canonicalizes the SignedInfo in document context and verifies ds:SignatureValue against the IdP certificate's RSA public key (or a directly supplied SPKI via the new verify_signature_with_pubkey).
  • Follows every ds:Reference: resolves the target element by ID (or the whole document for URI=""), applies the enveloped-signature transform and exclusive c14n, SHA-256s it, and compares the claimed DigestValue. A missing Reference, a bad digest, or a bad signature each yield a distinct error.

To canonicalize a referenced subtree correctly, saml_c14n now separates the in-scope namespace stack from the rendered-output stack, so an element lifted out of its document inherits its ancestors' namespace context and the visibly-utilized rule re-emits only what the subtree actually uses. New helpers: canonicalize_referenced_element (with enveloped-signature stripping) and canonicalize_signed_info_in_context.

Acceptance criteria

  • Known-good responses from Okta + Azure AD verify. Reconstructed wire-faithful Okta- and Azure-AD-shaped responses (the Azure case exercising an InclusiveNamespaces PrefixList and an inherited xmlns:ds on the Response root), signed with a freshly generated key, verify end to end through the production path.
  • Tampered responses are rejected with a clear error. Covered: tampered Assertion (DigestMismatch), tampered SignatureValue and wrong-key signature (SignatureNotVerified), downgraded algorithm (UnsupportedAlgorithm), reference-less SignedInfo (NoReferences).
  • Unit tests for the c14n module against the W3C test vectors. A battery of exclusive-c14n vectors whose expected bytes are the verbatim output of libxml2's xmllint --exc-c14n reference canonicalizer (the de-facto W3C reference), plus subtree-context tests for the new helpers.

Notes

  • Real Okta/Azure captured responses plus the IdP private keys cannot be committed, so the known-good tests reconstruct the exact wire shape those IdPs emit and sign it with a generated RSA key, driving the entire verification path.
  • Scope per the issue default: enveloped-signature transforms only.
  • cargo test -p meshcentral-auth (120 passing), cargo clippy, and cargo fmt --check are clean.

Closes VS-17 once merged.

🤖 Generated with Claude Code

## VS-17: SAML signature verification (full XML-c14n) Implements full inbound XML-DSig verification for signed SAML responses, replacing the previous whole-document digest shortcut in `verify_signature`. ### What changed The old verifier extracted the first `ds:SignedInfo` and digested the entire signature-stripped document. That does not match how Okta and Azure AD actually sign: they sign the Assertion and reference it by `#id`, so real responses would not verify. The new verifier: - Parses the `ds:SignedInfo` structure: CanonicalizationMethod, SignatureMethod, each `ds:Reference` (URI, transforms, DigestMethod, DigestValue) and any `ec:InclusiveNamespaces` PrefixList. - Enforces an algorithm policy: only exclusive c14n + RSA-SHA256 + SHA-256 are accepted; weaker or unknown algorithms are rejected as `UnsupportedAlgorithm`. - Canonicalizes the SignedInfo in document context and verifies `ds:SignatureValue` against the IdP certificate's RSA public key (or a directly supplied SPKI via the new `verify_signature_with_pubkey`). - Follows every `ds:Reference`: resolves the target element by ID (or the whole document for `URI=""`), applies the enveloped-signature transform and exclusive c14n, SHA-256s it, and compares the claimed DigestValue. A missing Reference, a bad digest, or a bad signature each yield a distinct error. To canonicalize a referenced subtree correctly, `saml_c14n` now separates the in-scope namespace stack from the rendered-output stack, so an element lifted out of its document inherits its ancestors' namespace context and the visibly-utilized rule re-emits only what the subtree actually uses. New helpers: `canonicalize_referenced_element` (with enveloped-signature stripping) and `canonicalize_signed_info_in_context`. ### Acceptance criteria - [x] Known-good responses from Okta + Azure AD verify. Reconstructed wire-faithful Okta- and Azure-AD-shaped responses (the Azure case exercising an `InclusiveNamespaces` PrefixList and an inherited `xmlns:ds` on the Response root), signed with a freshly generated key, verify end to end through the production path. - [x] Tampered responses are rejected with a clear error. Covered: tampered Assertion (`DigestMismatch`), tampered SignatureValue and wrong-key signature (`SignatureNotVerified`), downgraded algorithm (`UnsupportedAlgorithm`), reference-less SignedInfo (`NoReferences`). - [x] Unit tests for the c14n module against the W3C test vectors. A battery of exclusive-c14n vectors whose expected bytes are the verbatim output of libxml2's `xmllint --exc-c14n` reference canonicalizer (the de-facto W3C reference), plus subtree-context tests for the new helpers. ### Notes - Real Okta/Azure captured responses plus the IdP private keys cannot be committed, so the known-good tests reconstruct the exact wire shape those IdPs emit and sign it with a generated RSA key, driving the entire verification path. - Scope per the issue default: enveloped-signature transforms only. - `cargo test -p meshcentral-auth` (120 passing), `cargo clippy`, and `cargo fmt --check` are clean. Closes VS-17 once merged. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(auth): full XML-DSig SAML signature verification
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 1m34s
0986f90b56
Replace the whole-document digest shortcut in `verify_signature` with a faithful inbound XML-DSig verifier that follows each `ds:Reference` to the element it covers, matching how Okta and Azure AD actually sign responses (a signed Assertion referenced by `#id` with enveloped-signature + exclusive-c14n transforms).

The verifier parses the `ds:SignedInfo` structure (algorithms, References, transforms, InclusiveNamespaces prefix lists), canonicalizes the SignedInfo in document context, verifies `ds:SignatureValue` against the IdP cert's RSA public key, then for each Reference resolves the target by ID, applies the enveloped-signature transform and exclusive c14n, and compares the SHA-256 digest. Only exclusive c14n + RSA-SHA256 + SHA-256 are accepted; weaker or unknown algorithms, a missing Reference, a bad digest, or a bad signature are each rejected with a distinct error.

To canonicalize a referenced subtree correctly, `saml_c14n` now decouples the in-scope namespace stack from the rendered-output stack so a lifted-out element inherits its ancestors' namespace context (the visibly-utilized rule then re-emits only what the subtree uses). New helpers `canonicalize_referenced_element` (with enveloped-signature stripping) and `canonicalize_signed_info_in_context` build on this.

Tests: known-good Okta- and Azure-AD-shaped responses (the latter exercising an InclusiveNamespaces PrefixList and an inherited `xmlns:ds`) verify end to end; tampered assertions, tampered signatures, wrong-key signatures, downgraded algorithms, and reference-less SignedInfo are all rejected. The c14n module gains a battery of exclusive-c14n vectors whose expected bytes are the verbatim output of libxml2's `xmllint --exc-c14n` reference canonicalizer.

#VS-17

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merge branch 'main' into feat/saml-signature-verification-vs-17
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
Check / fmt + clippy + build + tests (pull_request) Successful in 2m12s
6758163338
David merged commit 5ef237ba84 into main 2026-06-07 19:54:45 +02:00
David deleted branch feat/saml-signature-verification-vs-17 2026-06-07 19:54:45 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/vervain-server!50
No description provided.