//! Pure-Rust SAML 2.0 Service Provider (v0.5.1). //! //! This module implements the SP half of a SAML Web-Browser-SSO profile: //! //! * [`metadata`] — parse the IdP's `EntityDescriptor` (SSO URLs + signing //! certificates) and build *our* SP metadata for the IdP admin to import. //! * [`authn_request`] — build an `AuthnRequest` and encode it for the //! HTTP-Redirect binding. //! * [`response`] — decode a `SAMLResponse`, **verify its XML signature** //! against the IdP's pinned certificate (via the pure-Rust `bergshamra` //! crate — no OpenSSL/libxml2/xmlsec, so the static musl binary stays //! C-free), then enforce the SP-side semantic checks (Status, Destination, //! Audience, time bounds) that are where SAML SPs actually get attacked. //! //! Stateful checks (replay of assertion IDs, correlating `InResponseTo` //! against requests *we* issued, gating IdP-initiated login) live in the //! HTTP layer — [`response::consume`] is deliberately stateless and returns //! the IDs the caller needs to perform them. //! //! Access model: any assertion the IdP authenticates and we cryptographically //! verify yields an operator [`VerifiedPrincipal`]. OpenPXE is single-tier — //! there is no per-user role table — and the local admin account remains a //! guaranteed fallback owner regardless of SSO state. pub mod authn_request; pub mod metadata; pub mod response; pub use authn_request::AuthnRequest; pub use metadata::IdpMetadata; pub use response::{VerifiedPrincipal, VerifiedResponse}; use thiserror::Error; /// Default clock-skew tolerance applied to assertion time bounds. SAML IdPs /// and SPs rarely have perfectly synced clocks; 60s matches common practice /// (Shibboleth/FleetDM defaults are in this ballpark). pub const DEFAULT_CLOCK_SKEW_SECS: i64 = 60; /// Runtime SP parameters, derived from [`crate::SsoConfig`] + the advertised /// public base URL by the HTTP layer. #[derive(Debug, Clone)] pub struct SpParams { /// Our SP Entity ID (the `` we send and the `Audience` we require /// in responses). Defaults to the public base URL when the operator left /// the Entity ID field blank. pub entity_id: String, /// The Assertion Consumer Service URL the IdP POSTs the response to — /// `/api/sso/acs`. pub acs_url: String, } /// Everything that can go wrong consuming a SAML response. Kept coarse on /// purpose: the HTTP layer logs the detail and shows the operator a generic /// "SSO sign-in failed" — we never leak which specific check tripped to the /// browser, since that aids an attacker probing the SP. #[derive(Debug, Error)] pub enum SamlError { #[error("SAML XML parse error: {0}")] Xml(String), #[error("IdP metadata is missing a required element: {0}")] Metadata(String), #[error("no usable IdP signing certificate in metadata")] NoSigningCert, #[error("signature verification failed: {0}")] Signature(String), #[error("the signature does not cover the assertion we read")] SignatureScope, #[error("SAML response status was not Success: {0}")] Status(String), #[error("response is missing a required element: {0}")] MissingElement(String), #[error("encrypted assertions are not supported in this release")] EncryptedAssertionUnsupported, #[error("expected exactly one assertion, found {0}")] AssertionCount(usize), #[error("issuer mismatch: response was not issued by the configured IdP")] IssuerMismatch, #[error("audience mismatch: assertion is not addressed to this service provider")] AudienceMismatch, #[error("response destination does not match our ACS URL")] DestinationMismatch, #[error("assertion is expired or not yet valid")] TimeBounds, #[error("invalid SAML timestamp: {0}")] Timestamp(String), #[error("base64 decode failed: {0}")] Base64(String), } #[cfg(test)] mod tests;