spiffe_rustls/
client.rs

1use crate::authorizer::Authorizer;
2use crate::error::Result;
3use crate::policy::TrustDomainPolicy;
4use crate::resolve::MaterialWatcher;
5use crate::verifier::SpiffeServerCertVerifier;
6use rustls::client::ResolvesClientCert;
7use rustls::ClientConfig;
8use spiffe::X509Source;
9use std::sync::Arc;
10
11/// Builds a [`rustls::ClientConfig`] backed by a live SPIFFE `X509Source`.
12///
13/// The resulting client configuration:
14///
15/// * presents the current SPIFFE X.509 SVID as the client certificate
16/// * validates the server certificate chain against trust bundles from the Workload API
17/// * authorizes the server by SPIFFE ID (URI SAN)
18///
19/// The builder retains an `Arc<X509Source>`. When the underlying SVID or trust
20/// bundle is rotated by the SPIRE agent, **new TLS handshakes automatically use
21/// the updated material**.
22///
23/// ## Trust Domain Selection
24///
25/// The builder uses the bundle set from `X509Source`, which may contain bundles
26/// for multiple trust domains (when SPIFFE federation is configured). The verifier
27/// automatically selects the correct bundle based on the peer's SPIFFE ID—no
28/// manual configuration is required. You can optionally restrict which trust
29/// domains are accepted using [`Self::trust_domain_policy`].
30///
31/// ## Authorization
32///
33/// Server authorization is performed by invoking the provided [`Authorizer`] with
34/// the server's SPIFFE ID extracted from the certificate's URI SAN.
35///
36/// Use [`authorizer::any`] to disable authorization while retaining full TLS authentication.
37///
38/// # Examples
39///
40/// ```no_run
41/// use spiffe_rustls::{authorizer, mtls_client, AllowList};
42/// use std::collections::BTreeSet;
43///
44/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
45/// let source = spiffe::X509Source::new().await?;
46///
47/// // Pass string literals directly - exact() and trust_domains() will convert them
48/// let allowed_server_ids = [
49///     "spiffe://example.org/myservice",
50///     "spiffe://example.org/myservice2",
51/// ];
52///
53/// let mut allowed_trust_domains = BTreeSet::new();
54/// allowed_trust_domains.insert("example.org".try_into()?);
55///
56/// let client_config = mtls_client(source)
57///     .authorize(authorizer::exact(allowed_server_ids)?)
58///     .trust_domain_policy(AllowList(allowed_trust_domains))
59///     .build()?;
60/// # Ok(())
61/// # }
62/// ```
63pub struct ClientConfigBuilder {
64    source: Arc<X509Source>,
65    authorizer: Arc<dyn Authorizer>,
66    trust_domain_policy: TrustDomainPolicy,
67}
68
69impl std::fmt::Debug for ClientConfigBuilder {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        f.debug_struct("ClientConfigBuilder")
72            .field("source", &"<Arc<X509Source>>")
73            .field("authorizer", &"<Arc<dyn Authorizer>>")
74            .field("trust_domain_policy", &self.trust_domain_policy)
75            .finish()
76    }
77}
78
79impl ClientConfigBuilder {
80    /// Creates a new builder from an `X509Source`.
81    ///
82    /// Defaults:
83    /// - Authorization: accepts any SPIFFE ID (authentication only)
84    /// - Trust domain policy: `AnyInBundleSet` (uses all bundles from the Workload API)
85    pub fn new(source: X509Source) -> Self {
86        Self {
87            source: Arc::new(source),
88            authorizer: Arc::new(crate::authorizer::any()),
89            trust_domain_policy: TrustDomainPolicy::default(),
90        }
91    }
92
93    /// Sets the authorization policy for server SPIFFE IDs.
94    ///
95    /// Accepts any type that implements `Authorizer`, including closures.
96    ///
97    /// # Examples
98    ///
99    /// ```no_run
100    /// use spiffe_rustls::{authorizer, mtls_client};
101    ///
102    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
103    /// let source = spiffe::X509Source::new().await?;
104    ///
105    /// // Using a convenience constructor - pass string literals directly
106    /// let config = mtls_client(source.clone())
107    ///     .authorize(authorizer::exact([
108    ///         "spiffe://example.org/service",
109    ///         "spiffe://example.org/service2",
110    ///     ])?)
111    ///     .build()?;
112    ///
113    /// // Using a closure
114    /// let config = mtls_client(source.clone())
115    ///     .authorize(|id: &spiffe::SpiffeId| id.path().starts_with("/api/"))
116    ///     .build()?;
117    ///
118    /// // Using the Any authorizer (default)
119    /// let config = mtls_client(source)
120    ///     .authorize(authorizer::any())
121    ///     .build()?;
122    /// # Ok(())
123    /// # }
124    /// ```
125    #[must_use]
126    pub fn authorize<A: Authorizer>(mut self, authorizer: A) -> Self {
127        self.authorizer = Arc::new(authorizer);
128        self
129    }
130
131    /// Sets the trust domain policy.
132    ///
133    /// Defaults to `AnyInBundleSet` (uses all bundles from the Workload API).
134    #[must_use]
135    pub fn trust_domain_policy(mut self, policy: TrustDomainPolicy) -> Self {
136        self.trust_domain_policy = policy;
137        self
138    }
139
140    /// Builds the `rustls::ClientConfig`.
141    ///
142    /// The returned configuration:
143    ///
144    /// * presents the current SPIFFE X.509 SVID as the client certificate
145    /// * validates the server certificate chain against trust bundles from the Workload API
146    /// * authorizes the server by SPIFFE ID (URI SAN)
147    ///
148    /// The configuration is backed by a live [`X509Source`]. When the underlying
149    /// SVID or trust bundle is rotated by the SPIRE agent, **new TLS handshakes
150    /// automatically use the updated material**.
151    ///
152    /// # Errors
153    ///
154    /// Returns an error if:
155    ///
156    /// * the Rustls crypto provider is not installed
157    /// * no current X.509 SVID is available from the `X509Source`
158    /// * building the underlying Rustls certificate verifier fails
159    pub fn build(self) -> Result<ClientConfig> {
160        crate::crypto::ensure_crypto_provider_installed();
161
162        let watcher = MaterialWatcher::spawn(self.source)?;
163
164        let resolver: Arc<dyn ResolvesClientCert> =
165            Arc::new(resolve_client::SpiffeClientCertResolver {
166                watcher: watcher.clone(),
167            });
168
169        let verifier = Arc::new(SpiffeServerCertVerifier::new(
170            Arc::new(watcher) as Arc<dyn crate::verifier::MaterialProvider>,
171            self.authorizer,
172            self.trust_domain_policy,
173        ));
174
175        let cfg = ClientConfig::builder()
176            .dangerous()
177            .with_custom_certificate_verifier(verifier)
178            .with_client_cert_resolver(resolver);
179
180        Ok(cfg)
181    }
182}
183
184mod resolve_client {
185    use crate::resolve::MaterialWatcher;
186    use rustls::client::ResolvesClientCert;
187    use rustls::sign::CertifiedKey;
188    use std::sync::Arc;
189
190    #[derive(Clone, Debug)]
191    pub(crate) struct SpiffeClientCertResolver {
192        pub watcher: MaterialWatcher,
193    }
194
195    impl ResolvesClientCert for SpiffeClientCertResolver {
196        fn resolve(
197            &self,
198            _acceptable_issuers: &[&[u8]],
199            _sigschemes: &[rustls::SignatureScheme],
200        ) -> Option<Arc<CertifiedKey>> {
201            Some(self.watcher.current().certified_key.clone())
202        }
203
204        fn has_certs(&self) -> bool {
205            true
206        }
207    }
208}