spiffe-rustls 0.8.0

SPIFFE-based mutual TLS integration for rustls
Documentation
use crate::authorizer::Authorizer;
use crate::error::Result;
use crate::policy::TrustDomainPolicy;
use crate::resolve::MaterialWatcher;
use crate::verifier::SpiffeClientCertVerifier;
use rustls::server::ResolvesServerCert;
use rustls::ServerConfig;
use spiffe::X509Source;
use std::sync::Arc;

/// Function type for customizing a `ServerConfig`.
type ServerConfigCustomizer = Box<dyn FnOnce(&mut ServerConfig) + Send>;

/// Builds a [`rustls::ServerConfig`] backed by a live SPIFFE `X509Source`.
///
/// The resulting server configuration:
///
/// * presents the current SPIFFE X.509 SVID as the server certificate
/// * requires and validates client certificates (mTLS)
/// * authorizes the client by SPIFFE ID (URI SAN)
///
/// ## Trust Domain Selection
///
/// The builder uses the bundle set from `X509Source`, which may contain bundles
/// for multiple trust domains (when SPIFFE federation is configured). The verifier
/// automatically selects the correct bundle based on the peer's SPIFFE ID—no
/// manual configuration is required. You can optionally restrict which trust
/// domains are accepted using [`Self::trust_domain_policy`].
///
/// The default policy is [`TrustDomainPolicy::AnyInBundleSet`], which accepts any
/// trust domain present in the source bundle set. For non-federated deployments,
/// prefer [`TrustDomainPolicy::LocalOnly`] to restrict verification to the local
/// trust domain.
///
/// ## Authorization
///
/// Client authorization is performed by invoking the provided [`Authorizer`] with
/// the client's SPIFFE ID extracted from the certificate's URI SAN.
///
/// The default authorizer is [`crate::authorizer::any`]. It accepts any authenticated
/// SPIFFE ID from any trust domain accepted by the configured trust-domain policy.
/// By default, this means every trust domain in the source bundle set.
///
/// <div class="warning">
///
/// **Security:** with the default configuration, authentication validates the
/// client certificate against the trust bundle for the peer's SPIFFE ID trust
/// domain. Authorization uses [`crate::authorizer::any`] together with
/// [`TrustDomainPolicy::AnyInBundleSet`], so any client whose certificate
/// validates and whose SPIFFE ID is accepted by that policy is allowed.
/// Production deployments should normally configure a specific [`Authorizer`]
/// (via [`Self::authorize`]) and an appropriate [`TrustDomainPolicy`] (via
/// [`Self::trust_domain_policy`]); [`TrustDomainPolicy::LocalOnly`] is suitable
/// when federation is not required.
///
/// </div>
///
/// # Examples
///
/// ```no_run
/// use spiffe::{TrustDomain, X509Source};
/// use spiffe_rustls::{authorizer, mtls_server, LocalOnly};
///
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let source = X509Source::new().await?;
///
/// // Pass string literals directly - trust_domains() will convert them
/// let allowed_trust_domains = ["example.org"];
///
/// let local_trust_domain: TrustDomain = "example.org".try_into()?;
///
/// let server_config = mtls_server(source)
///     .authorize(authorizer::trust_domains(allowed_trust_domains)?)
///     .trust_domain_policy(LocalOnly(local_trust_domain))
///     .build()?;
/// # Ok(())
/// # }
/// ```
pub struct ServerConfigBuilder {
    source: Arc<X509Source>,
    authorizer: Arc<dyn Authorizer>,
    trust_domain_policy: TrustDomainPolicy,
    alpn_protocols: Vec<Vec<u8>>,
    config_customizer: Option<ServerConfigCustomizer>,
}

impl std::fmt::Debug for ServerConfigBuilder {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ServerConfigBuilder")
            .field("source", &"<Arc<X509Source>>")
            .field("authorizer", &"<Arc<dyn Authorizer>>")
            .field("trust_domain_policy", &self.trust_domain_policy)
            .field("alpn_protocols", &self.alpn_protocols)
            .field("config_customizer", &self.config_customizer.is_some())
            .finish()
    }
}

impl ServerConfigBuilder {
    /// Creates a new builder from an `X509Source`.
    ///
    /// Defaults:
    /// - Authorization: [`crate::authorizer::any`], which accepts any authenticated
    ///   SPIFFE ID from any trust domain accepted by the configured trust-domain policy.
    ///   By default, this means every trust domain in the source bundle set.
    /// - Trust domain policy: [`TrustDomainPolicy::AnyInBundleSet`], which accepts any
    ///   trust domain present in the source bundle set
    /// - ALPN protocols: empty (no ALPN)
    ///
    /// With the default configuration, authentication validates the peer
    /// certificate against the configured trust bundle, and authorization uses
    /// [`crate::authorizer::any`] with [`TrustDomainPolicy::AnyInBundleSet`].
    /// Production deployments should normally configure a specific authorizer via
    /// [`Self::authorize`] and an appropriate trust domain policy via
    /// [`Self::trust_domain_policy`]; [`TrustDomainPolicy::LocalOnly`] is suitable
    /// when federation is not required.
    pub fn new(source: X509Source) -> Self {
        Self {
            source: Arc::new(source),
            authorizer: Arc::new(crate::authorizer::any()),
            trust_domain_policy: TrustDomainPolicy::default(),
            alpn_protocols: Vec::new(),
            config_customizer: None,
        }
    }

    /// Sets the authorization policy for client SPIFFE IDs.
    ///
    /// Accepts any type that implements `Authorizer`, including closures.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use spiffe_rustls::{authorizer, mtls_server};
    ///
    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
    /// let source = spiffe::X509Source::new().await?;
    ///
    /// // Pass string literals directly
    /// let config = mtls_server(source.clone())
    ///     .authorize(authorizer::trust_domains([
    ///         "example.org",
    ///     ])?)
    ///     .build()?;
    ///
    /// // Using a closure
    /// let config = mtls_server(source.clone())
    ///     .authorize(|id: &spiffe::SpiffeId| id.path().starts_with("/api/"))
    ///     .build()?;
    ///
    /// // Using the Any authorizer (default)
    /// let config = mtls_server(source)
    ///     .authorize(authorizer::any())
    ///     .build()?;
    /// # Ok(())
    /// # }
    /// ```
    #[must_use]
    pub fn authorize<A: Authorizer>(mut self, authorizer: A) -> Self {
        self.authorizer = Arc::new(authorizer);
        self
    }

    /// Sets the trust domain policy.
    ///
    /// Defaults to [`TrustDomainPolicy::AnyInBundleSet`], which accepts any trust
    /// domain present in the source bundle set. For non-federated deployments,
    /// prefer [`TrustDomainPolicy::LocalOnly`] to restrict verification to the local
    /// trust domain.
    #[must_use]
    pub fn trust_domain_policy(mut self, policy: TrustDomainPolicy) -> Self {
        self.trust_domain_policy = policy;
        self
    }

    /// Sets the ALPN (Application-Layer Protocol Negotiation) protocols.
    ///
    /// The protocols are advertised during the TLS handshake. Common values:
    /// - `b"h2"` for HTTP/2 (required for gRPC)
    /// - `b"http/1.1"` for HTTP/1.1
    ///
    /// Protocols should be specified in order of preference (most preferred first).
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use spiffe_rustls::mtls_server;
    ///
    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
    /// let source = spiffe::X509Source::new().await?;
    /// let config = mtls_server(source)
    ///     .with_alpn_protocols([b"h2"])
    ///     .build()?;
    /// # Ok(())
    /// # }
    /// ```
    #[must_use]
    pub fn with_alpn_protocols<I, P>(mut self, protocols: I) -> Self
    where
        I: IntoIterator<Item = P>,
        P: AsRef<[u8]>,
    {
        self.alpn_protocols = protocols.into_iter().map(|p| p.as_ref().to_vec()).collect();
        self
    }

    /// Applies a customizer function to the `ServerConfig` after it's built.
    ///
    /// This is an **advanced** API for configuration not directly exposed by the builder.
    /// The customizer is called **last**, after all other builder settings (including
    /// ALPN) have been applied, and gives direct access to the underlying `rustls`
    /// configuration.
    ///
    /// **Warning:** Replacing the verifier or resolver disables SPIFFE authentication.
    /// Do not use this hook for that purpose; build a custom `rustls` configuration instead.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use spiffe_rustls::mtls_server;
    ///
    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
    /// let source = spiffe::X509Source::new().await?;
    /// let config = mtls_server(source)
    ///     .with_config_customizer(|cfg| {
    ///         // Example: adjust cipher suite preferences
    ///     })
    ///     .build()?;
    /// # Ok(())
    /// # }
    /// ```
    #[must_use]
    pub fn with_config_customizer<F>(mut self, customizer: F) -> Self
    where
        F: FnOnce(&mut ServerConfig) + Send + 'static,
    {
        self.config_customizer = Some(Box::new(customizer));
        self
    }

    /// Builds the `rustls::ServerConfig`.
    ///
    /// TLS session resumption (stateful session caching and TLS 1.3 session
    /// tickets) is **disabled by default**. rustls does not re-invoke the
    /// client certificate verifier on a resumed handshake, so resumption would
    /// let a session outlive its SVID's (deliberately short) expiry, and
    /// bypass a bundle or authorizer change (e.g. defederation, a tightened
    /// allow list) until the cached session/ticket ages out. SPIRE's own TLS
    /// server endpoint disables session tickets for the same reason. If you
    /// understand this trade-off and want resumption for its performance
    /// benefit, re-enable it via [`Self::with_config_customizer`] by setting
    /// `cfg.session_storage` and `cfg.send_tls13_tickets`.
    ///
    /// # Errors
    ///
    /// Returns an error if:
    ///
    /// * the SPIFFE `X509Source` does not currently have an SVID,
    /// * rustls crypto providers are not installed,
    /// * or the material watcher cannot be initialized.
    pub fn build(self) -> Result<ServerConfig> {
        crate::crypto::ensure_crypto_provider_installed();

        let watcher = MaterialWatcher::spawn(self.source)?;

        let resolver: Arc<dyn ResolvesServerCert> =
            Arc::new(resolve_server::SpiffeServerCertResolver {
                watcher: watcher.clone(),
            });

        let verifier = Arc::new(SpiffeClientCertVerifier::new(
            Arc::new(watcher),
            self.authorizer,
            self.trust_domain_policy,
        ));

        let mut cfg = ServerConfig::builder()
            .with_client_cert_verifier(verifier)
            .with_cert_resolver(resolver);

        cfg.alpn_protocols = self.alpn_protocols;

        // Disable session resumption by default: rustls does not re-run the
        // client cert verifier on a resumed handshake, so a resumed session
        // would silently bypass SVID expiry and bundle/authorizer changes.
        // See the `build` doc for rationale and how to opt back in.
        // `session_storage` covers TLS 1.2 and stateful TLS 1.3 resumption;
        // `send_tls13_tickets = 0` stops issuing TLS 1.3 tickets so clients
        // cannot resume statelessly either.
        cfg.session_storage = Arc::new(rustls::server::NoServerSessionStorage {});
        cfg.send_tls13_tickets = 0;

        // Apply customizer last
        if let Some(customizer) = self.config_customizer {
            customizer(&mut cfg);
        }

        Ok(cfg)
    }
}

mod resolve_server {
    use crate::resolve::MaterialWatcher;
    use rustls::server::ResolvesServerCert;
    use rustls::sign::CertifiedKey;
    use std::sync::Arc;

    #[derive(Clone, Debug)]
    pub(crate) struct SpiffeServerCertResolver {
        pub watcher: MaterialWatcher,
    }

    impl ResolvesServerCert for SpiffeServerCertResolver {
        fn resolve(
            &self,
            _client_hello: rustls::server::ClientHello<'_>,
        ) -> Option<Arc<CertifiedKey>> {
            Some(Arc::clone(&self.watcher.current().certified_key))
        }
    }
}