saml
Stateless, async-native SAML 2.0 toolkit with no libxml2 / xmlsec C build chain.
saml implements the SAML 2.0 protocol — Service Provider, Identity Provider, and proxy composition — in pure Rust. XML, XML-DSig, XML-Canonicalization, and XML-Encryption are reimplemented on top of quick-xml and the RustCrypto stack; there is no libxml2, libxmlsec1, libxslt, libclang, or openssl dependency. The HTTP backchannel is bring-your-own via the [HttpClient] trait (with an optional reqwest adapter).
Status
Pre-release: v0.1.0. The protocol surface described below is implemented and exercised against an interop corpus drawn from Okta, Microsoft Entra ID, Auth0, Google Workspace, OneLogin, Keycloak, ADFS, and Shibboleth fixtures. APIs and on-disk fixtures should be considered subject to change until v1.0. No claim of "production ready" or "battle-tested" is made yet.
Why this vs. samael
| Dimension | saml |
samael |
|---|---|---|
| Native deps for SAML protocol | None — Rust-only XML / DSig / C14N / XML-Enc | libxml2 + libxmlsec1 + libxslt + libclang + libtool + libiconv |
| HTTP backchannel | Bring-your-own via HttpClient; optional reqwest-client feature |
Bundled |
| Cross-compile (musl, distroless, arm64, wasm) | Clean on any rustc target for the protocol layer |
Requires native libs on every target |
cargo audit coverage |
Full for the protocol layer | Splits across cargo audit + OS packages |
| Stateless API | Yes — caller owns clock, persistence, replay store | Mixed |
| Role split | ServiceProvider / IdentityProvider / SpDescriptor / IdpDescriptor as distinct types |
Closer to one type per role |
| Proxy first-class | Yes — Proxy + stateless ProxyContext codec |
Compose sp + idp manually |
| Weak-algorithm quarantine | weak-algos feature, off by default |
RSA-PKCS1-v1.5 ships by default |
| XSW resistance | Structural: duplicate IDs rejected at parse, VerifiedSignature is the only path to validated payload |
Inherits xmlsec; configurable |
See docs/rfcs/RFC-001-architecture.md §11 for the full comparison.
Feature flags
| Flag | Default | Purpose |
|---|---|---|
reqwest-client |
yes | Optional ReqwestClient adapter for the HttpClient trait. |
rsa-sha |
yes | RSA-SHA256 / 384 / 512 signature algorithms. |
ecdsa-sha |
yes | ECDSA-SHA256 / 384 / 512 signature algorithms (P-256, P-384). |
xmlenc |
yes | XML Encryption (EncryptedAssertion, EncryptedID, AES-CBC / AES-GCM, RSA-OAEP). |
slo |
yes | Single Logout (Redirect / POST, with optional back-channel SOAP). |
metadata-emit |
yes | metadata_xml / metadata_xml_with_extras for SP and IdP. |
artifact-binding |
no | HTTP-Artifact binding (SOAP ArtifactResolve). Requires weak-algos for the SHA-1 SourceID. |
weak-algos |
no | SHA-1 digest, RSA-PKCS1-v1.5 key transport, DSA-SHA1. Off by default; opt in only for legacy peer interop. |
The protocol layer compiles for any target rustc supports, including wasm32-unknown-unknown with default-features = false.
Quick example — Service Provider
use ;
use ;
let sp = new?;
let idp = from_metadata_xml?;
let start = sp.start_login?;
match start.dispatch
let identity = sp.consume_response?;
The full SP, IdP, and proxy quickstarts live in src/lib.rs crate-level docs (visible on docs.rs).
End-to-end multi-IdP demo
A runnable Axum SP wired up to seven IdPs (Keycloak, Authentik, FusionAuth locally; Zitadel, Auth0, Descope, Asgardeo in the cloud) behind one /saml/acs lives under examples/demo/. See examples/demo/README.md for the setup steps, and examples/idps/ for the merged docker-compose stack for the three local IdPs.
Replay protection
SAML 2.0 Core §2.5.1.5 forbids re-consuming the same assertion within its validity window. The library exposes a ReplayCache trait and an in-memory default; pass an instance into ConsumeResponse::replay_cache to enable the check.
use ;
let replay_cache = default;
let identity = sp.consume_response?;
The cache is consulted AFTER signature verification and all other spec checks succeed, so forged or malformed responses never pollute the store. A duplicate assertion_id within the validity window surfaces as Error::AssertionReplay. Passing None keeps existing behavior — no check is performed, and the caller is responsible for deduping Identity::assertion_id against its own store.
For multi-instance deployments, implement ReplayCache against a shared backend (Redis, memcached, a SQL table with a unique constraint on (id, expires_at)) so a replay caught by one process is rejected by every process.
Implementation status
Implemented:
- Web-Browser-SSO profile (HTTP-Redirect, HTTP-POST) — SP and IdP sides.
- Single Logout (
slofeature) — Redirect and POST bindings, signed in both directions. - HTTP-Artifact binding (
artifact-bindingfeature) —ArtifactResolve/ArtifactResponseover SOAP. - XML-Encryption (
xmlencfeature) —EncryptedAssertion, AES-128/256-CBC, AES-128/256-GCM, RSA-OAEP-MGF1-SHA1/256/384/512 key transport. - XML-DSig — Exclusive and Inclusive C14N (with and without comments), enveloped-signature transform; multi-Reference signatures rejected by default; transform whitelist enforced.
- Metadata parse (
EntityDescriptorandEntitiesDescriptor) and signed-aggregate verification. - Metadata emit (
metadata-emitfeature) for SP and IdP descriptors. - Identity proxy composition with stateless
ProxyContext, opaque-handle Redirect codec, NameID transforms, and attribute release policies. - Pluggable signature verification via the
SignatureVerifiertrait (for HSM- or KMS-backed keys).
Out of scope for v0.1 (see docs/rfcs/RFC-001-architecture.md §12):
- ECP / PAOS profile (Enhanced Client or Proxy).
- Holder-of-Key subject confirmation — bearer only.
- SAML 1.x compatibility.
- Attribute Query profile.
- Name Identifier Management profile.
- Identity Discovery Service Protocol (IdP-disco).
- Metadata signing-key rotation policy (the library exposes the primitives; policy is the caller's).
- Asynchronous front-channel SLO chain orchestration across N downstream SPs.
RetrievalMethod,AgreementMethod(DH / ECDH) insideKeyInfo.
Security posture
- XSW resistance is structural. Duplicate
IDattributes are rejected at parse time; theReference URIresolves to a uniqueElementId; validated payload extraction is bound to theVerifiedSignaturehandle, not to a name lookup. There is no API path that returns a "validated" payload distinct from the signed payload. Seedocs/rfcs/RFC-002-xml-crypto-core.md§3. - Weak algorithms are feature-gated. SHA-1, RSA-PKCS1-v1.5 key transport, and DSA-SHA1 are unavailable unless
weak-algosis enabled — the dependency is visible incargo tree. Even when compiled in, the per-peerPeerCryptoPolicyallow-list still gates acceptance at validation time. - Hard-fail defaults for signature validity, audience restriction, ACS URL allow-listing, NameID scoping, destination matching, and the XML-DSig transform whitelist (XSLT, XPath, and base64 transforms are rejected).
- Caller-owned clock. Every method that compares against
xs:dateTimevalues takesnow: SystemTimeandclock_skew: Durationexplicitly. Multi-instance deployments avoid drift surprises; tests pass deterministic timestamps. - Replay protection is pluggable.
ConsumeResponse::replay_cacheaccepts anOption<&dyn ReplayCache>. The defaultInMemoryReplayCachecovers single-process deployments; multi-instance setups implement the trait against Redis or a SQL store. WhenNone, no replay check runs and the caller is expected to dedupeIdentity::assertion_idagainst their own store. See Replay protection below. - DTD, internal entities, and processing instructions are rejected at parse time, eliminating the XXE / billion-laughs class.
- Detached query-string signatures (HTTP-Redirect binding) go through a distinct verification entry point with the same
allowed_algorithmsdiscipline as XML-DSig. unsafe_codeis forbidden at the crate root.
See docs/rfcs/RFC-002-xml-crypto-core.md for the full crypto-layer threat model and docs/rfcs/RFC-001-architecture.md §2 for the operational principles.
Fuzzing
The fuzz/ workspace member ships three cargo-fuzz harnesses that exercise the bytes-from-the-wire boundaries of the crate:
| Target | What it drives | Why it matters |
|---|---|---|
fuzz_xml_parse |
The DOM-ish XML parser (xml::parse::Document::parse) |
Every inbound SAML message — Response, AuthnRequest, LogoutRequest, metadata — funnels through this code path before any signature or schema check runs. |
fuzz_c14n |
The parser plus the XML-C14N / Exclusive-C14N canonicalizer (one of the four variants is selected from the first input byte). | C14N output is what the signature is computed against; a divergence between signer and verifier here is a signature-bypass primitive. The fuzzer hammers the canonicalizer with adversarial well-formed inputs. |
fuzz_base64_response |
ServiceProvider::consume_response end-to-end (base64 → XML parse → schema check → DSig verify → assertion-level spec checks) against a fixed test SP / IdP fixture. |
Mirrors a hostile peer posting bytes to /acs. The fuzzer can't break RSA-SHA256, but it shakes out crashes, infinite loops, and quadratic blowups in the path before the signature check rejects the response. |
A small seed corpus per target lives under fuzz/corpus/<target>/, populated from fixtures borrowed from tests/corpus/ruby-saml/ and tests/corpus/python3-saml/ so each fuzzer starts from real-shaped input rather than random bytes.
Running
# One-time setup
# Build all harnesses (no execution)
# Run one harness until you Ctrl-C it
# Time-bounded smoke run (libfuzzer -runs is iterations, -max_total_time is seconds)
Reproducing a crash:
# libfuzzer drops crash inputs into fuzz/artifacts/<target>/.
The harnesses reach into pub mod __fuzz in src/lib.rs, which is gated on #[cfg(fuzzing)] and therefore invisible to regular cargo build / cargo doc / downstream callers — it exists solely to give the fuzz targets thin shims into otherwise pub(crate) parser + canonicalizer entry points.
Documentation
docs/rfcs/RFC-001-architecture.md— Design principles, role model, scope statement.docs/rfcs/RFC-002-xml-crypto-core.md— XML / DSig / C14N / XML-Enc layer.docs/rfcs/RFC-003-service-provider.md—ServiceProviderAPI.docs/rfcs/RFC-004-identity-provider.md—IdentityProviderAPI.docs/rfcs/RFC-005-proxy-composition.md— Proxy composition andProxyContext.docs/rfcs/RFC-006-metadata.md— Metadata parse and emit.docs/rfcs/RFC-007-single-logout.md— Single Logout.
Minimum Supported Rust Version
The MSRV is Rust 1.91.0 — the crate uses Duration::from_mins and Duration::from_hours, both stabilized in 1.91. CI runs the full test matrix against stable, beta, and 1.91.0. Bumping the MSRV is treated as a minor-version change and called out in the release notes.
License
Licensed under either of
- Apache License, Version 2.0
- MIT License
at your option.