Skip to main content

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/// Function type for customizing a `ClientConfig`.
12type ClientConfigCustomizer = Box<dyn FnOnce(&mut ClientConfig) + Send>;
13
14/// Builds a [`rustls::ClientConfig`] backed by a live SPIFFE `X509Source`.
15///
16/// The resulting client configuration:
17///
18/// * presents the current SPIFFE X.509 SVID as the client certificate
19/// * validates the server certificate chain against trust bundles from the Workload API
20/// * authorizes the server by SPIFFE ID (URI SAN)
21///
22/// The builder retains an `Arc<X509Source>`. When the underlying SVID or trust
23/// bundle is rotated by the SPIRE agent, **new TLS handshakes automatically use
24/// the updated material**.
25///
26/// ## Trust Domain Selection
27///
28/// The builder uses the bundle set from `X509Source`, which may contain bundles
29/// for multiple trust domains (when SPIFFE federation is configured). The verifier
30/// automatically selects the correct bundle based on the peer's SPIFFE ID—no
31/// manual configuration is required. You can optionally restrict which trust
32/// domains are accepted using [`Self::trust_domain_policy`].
33///
34/// The default policy is [`TrustDomainPolicy::AnyInBundleSet`], which accepts any
35/// trust domain present in the source bundle set. For non-federated deployments,
36/// prefer [`TrustDomainPolicy::LocalOnly`] to restrict verification to the local
37/// trust domain.
38///
39/// ## Authorization
40///
41/// Server authorization is performed by invoking the provided [`Authorizer`] with
42/// the server's SPIFFE ID extracted from the certificate's URI SAN.
43///
44/// The default authorizer is [`crate::authorizer::any`]. It accepts any authenticated
45/// SPIFFE ID from any trust domain accepted by the configured trust-domain policy.
46/// By default, this means every trust domain in the source bundle set.
47///
48/// <div class="warning">
49///
50/// **Security:** with the default configuration, authentication validates the
51/// server certificate against the trust bundle for the peer's SPIFFE ID trust
52/// domain. Authorization uses [`crate::authorizer::any`] together with
53/// [`TrustDomainPolicy::AnyInBundleSet`], so any server whose certificate
54/// validates and whose SPIFFE ID is accepted by that policy is allowed.
55/// Production deployments should normally configure a specific [`Authorizer`]
56/// (via [`Self::authorize`]) and an appropriate [`TrustDomainPolicy`] (via
57/// [`Self::trust_domain_policy`]); [`TrustDomainPolicy::LocalOnly`] is suitable
58/// when federation is not required.
59///
60/// </div>
61///
62/// # Examples
63///
64/// ```no_run
65/// use spiffe_rustls::{authorizer, mtls_client, AllowList};
66/// use std::collections::BTreeSet;
67///
68/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
69/// let source = spiffe::X509Source::new().await?;
70///
71/// // Pass string literals directly - exact() and trust_domains() will convert them
72/// let allowed_server_ids = [
73///     "spiffe://example.org/myservice",
74///     "spiffe://example.org/myservice2",
75/// ];
76///
77/// let mut allowed_trust_domains = BTreeSet::new();
78/// allowed_trust_domains.insert("example.org".try_into()?);
79///
80/// let client_config = mtls_client(source)
81///     .authorize(authorizer::exact(allowed_server_ids)?)
82///     .trust_domain_policy(AllowList(allowed_trust_domains))
83///     .build()?;
84/// # Ok(())
85/// # }
86/// ```
87pub struct ClientConfigBuilder {
88    source: Arc<X509Source>,
89    authorizer: Arc<dyn Authorizer>,
90    trust_domain_policy: TrustDomainPolicy,
91    alpn_protocols: Vec<Vec<u8>>,
92    config_customizer: Option<ClientConfigCustomizer>,
93}
94
95impl std::fmt::Debug for ClientConfigBuilder {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        f.debug_struct("ClientConfigBuilder")
98            .field("source", &"<Arc<X509Source>>")
99            .field("authorizer", &"<Arc<dyn Authorizer>>")
100            .field("trust_domain_policy", &self.trust_domain_policy)
101            .field("alpn_protocols", &self.alpn_protocols)
102            .field("config_customizer", &self.config_customizer.is_some())
103            .finish()
104    }
105}
106
107impl ClientConfigBuilder {
108    /// Creates a new builder from an `X509Source`.
109    ///
110    /// Defaults:
111    /// - Authorization: [`crate::authorizer::any`], which accepts any authenticated
112    ///   SPIFFE ID from any trust domain accepted by the configured trust-domain policy.
113    ///   By default, this means every trust domain in the source bundle set.
114    /// - Trust domain policy: [`TrustDomainPolicy::AnyInBundleSet`], which accepts any
115    ///   trust domain present in the source bundle set
116    /// - ALPN protocols: empty (no ALPN)
117    ///
118    /// With the default configuration, authentication validates the peer
119    /// certificate against the configured trust bundle, and authorization uses
120    /// [`crate::authorizer::any`] with [`TrustDomainPolicy::AnyInBundleSet`].
121    /// Production deployments should normally configure a specific authorizer via
122    /// [`Self::authorize`] and an appropriate trust domain policy via
123    /// [`Self::trust_domain_policy`]; [`TrustDomainPolicy::LocalOnly`] is suitable
124    /// when federation is not required.
125    pub fn new(source: X509Source) -> Self {
126        Self {
127            source: Arc::new(source),
128            authorizer: Arc::new(crate::authorizer::any()),
129            trust_domain_policy: TrustDomainPolicy::default(),
130            alpn_protocols: Vec::new(),
131            config_customizer: None,
132        }
133    }
134
135    /// Sets the authorization policy for server SPIFFE IDs.
136    ///
137    /// Accepts any type that implements `Authorizer`, including closures.
138    ///
139    /// # Examples
140    ///
141    /// ```no_run
142    /// use spiffe_rustls::{authorizer, mtls_client};
143    ///
144    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
145    /// let source = spiffe::X509Source::new().await?;
146    ///
147    /// // Pass string literals directly
148    /// let config = mtls_client(source.clone())
149    ///     .authorize(authorizer::exact([
150    ///         "spiffe://example.org/service",
151    ///         "spiffe://example.org/service2",
152    ///     ])?)
153    ///     .build()?;
154    ///
155    /// // Using a closure
156    /// let config = mtls_client(source.clone())
157    ///     .authorize(|id: &spiffe::SpiffeId| id.path().starts_with("/api/"))
158    ///     .build()?;
159    ///
160    /// // Using the Any authorizer (default)
161    /// let config = mtls_client(source)
162    ///     .authorize(authorizer::any())
163    ///     .build()?;
164    /// # Ok(())
165    /// # }
166    /// ```
167    #[must_use]
168    pub fn authorize<A: Authorizer>(mut self, authorizer: A) -> Self {
169        self.authorizer = Arc::new(authorizer);
170        self
171    }
172
173    /// Sets the trust domain policy.
174    ///
175    /// Defaults to [`TrustDomainPolicy::AnyInBundleSet`], which accepts any trust
176    /// domain present in the source bundle set. For non-federated deployments,
177    /// prefer [`TrustDomainPolicy::LocalOnly`] to restrict verification to the local
178    /// trust domain.
179    #[must_use]
180    pub fn trust_domain_policy(mut self, policy: TrustDomainPolicy) -> Self {
181        self.trust_domain_policy = policy;
182        self
183    }
184
185    /// Sets the ALPN (Application-Layer Protocol Negotiation) protocols.
186    ///
187    /// The protocols are advertised during the TLS handshake. Common values:
188    /// - `b"h2"` for HTTP/2 (required for gRPC)
189    /// - `b"http/1.1"` for HTTP/1.1
190    ///
191    /// Protocols should be specified in order of preference (most preferred first).
192    ///
193    /// # Examples
194    ///
195    /// ```no_run
196    /// use spiffe_rustls::mtls_client;
197    ///
198    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
199    /// let source = spiffe::X509Source::new().await?;
200    /// let config = mtls_client(source)
201    ///     .with_alpn_protocols([b"h2"])
202    ///     .build()?;
203    /// # Ok(())
204    /// # }
205    /// ```
206    #[must_use]
207    pub fn with_alpn_protocols<I, P>(mut self, protocols: I) -> Self
208    where
209        I: IntoIterator<Item = P>,
210        P: AsRef<[u8]>,
211    {
212        self.alpn_protocols = protocols.into_iter().map(|p| p.as_ref().to_vec()).collect();
213        self
214    }
215
216    /// Applies a customizer function to the `ClientConfig` after it's built.
217    ///
218    /// This is an **advanced** API for configuration not directly exposed by the builder.
219    /// The customizer is called **last**, after all other builder settings (including
220    /// ALPN) have been applied, and gives direct access to the underlying `rustls`
221    /// configuration.
222    ///
223    /// **Warning:** Replacing the verifier or resolver disables SPIFFE authentication.
224    /// Do not use this hook for that purpose; build a custom `rustls` configuration instead.
225    ///
226    /// # Examples
227    ///
228    /// ```no_run
229    /// use spiffe_rustls::mtls_client;
230    ///
231    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
232    /// let source = spiffe::X509Source::new().await?;
233    /// let config = mtls_client(source)
234    ///     .with_config_customizer(|cfg| {
235    ///         // Example: adjust cipher suite preferences
236    ///     })
237    ///     .build()?;
238    /// # Ok(())
239    /// # }
240    /// ```
241    #[must_use]
242    pub fn with_config_customizer<F>(mut self, customizer: F) -> Self
243    where
244        F: FnOnce(&mut ClientConfig) + Send + 'static,
245    {
246        self.config_customizer = Some(Box::new(customizer));
247        self
248    }
249
250    /// Builds the `rustls::ClientConfig`.
251    ///
252    /// The returned configuration:
253    ///
254    /// * presents the current SPIFFE X.509 SVID as the client certificate
255    /// * validates the server certificate chain against trust bundles from the Workload API
256    /// * authorizes the server by SPIFFE ID (URI SAN)
257    ///
258    /// The configuration is backed by a live [`X509Source`]. When the underlying
259    /// SVID or trust bundle is rotated by the SPIRE agent, **new TLS handshakes
260    /// automatically use the updated material**.
261    ///
262    /// TLS session resumption is **disabled by default**. rustls does not
263    /// re-invoke the server certificate verifier on a resumed handshake, so
264    /// resumption would let a session outlive its peer's (deliberately short)
265    /// SVID expiry, and bypass a bundle or authorizer change (e.g.
266    /// defederation, a tightened allow list) until the cached session ages
267    /// out. SPIRE's own TLS server endpoint disables session tickets for the
268    /// same reason. If you understand this trade-off and want resumption for
269    /// its performance benefit, re-enable it via [`Self::with_config_customizer`]
270    /// by setting `cfg.resumption`.
271    ///
272    /// # Errors
273    ///
274    /// Returns an error if:
275    ///
276    /// * the Rustls crypto provider is not installed
277    /// * no current X.509 SVID is available from the `X509Source`
278    /// * building the underlying Rustls certificate verifier fails
279    pub fn build(self) -> Result<ClientConfig> {
280        crate::crypto::ensure_crypto_provider_installed();
281
282        let watcher = MaterialWatcher::spawn(self.source)?;
283
284        let resolver: Arc<dyn ResolvesClientCert> =
285            Arc::new(resolve_client::SpiffeClientCertResolver {
286                watcher: watcher.clone(),
287            });
288
289        let verifier = Arc::new(SpiffeServerCertVerifier::new(
290            Arc::new(watcher),
291            self.authorizer,
292            self.trust_domain_policy,
293        ));
294
295        let mut cfg = ClientConfig::builder()
296            .dangerous()
297            .with_custom_certificate_verifier(verifier)
298            .with_client_cert_resolver(resolver);
299
300        cfg.alpn_protocols = self.alpn_protocols;
301
302        // Disable session resumption by default: rustls does not re-run the
303        // server cert verifier on a resumed handshake, so a resumed session
304        // would silently bypass SVID expiry and bundle/authorizer changes.
305        // See the `build` doc for rationale and how to opt back in.
306        cfg.resumption = rustls::client::Resumption::disabled();
307
308        // Apply customizer last
309        if let Some(customizer) = self.config_customizer {
310            customizer(&mut cfg);
311        }
312
313        Ok(cfg)
314    }
315}
316
317mod resolve_client {
318    use crate::resolve::MaterialWatcher;
319    use rustls::client::ResolvesClientCert;
320    use rustls::sign::CertifiedKey;
321    use std::sync::Arc;
322
323    #[derive(Clone, Debug)]
324    pub(crate) struct SpiffeClientCertResolver {
325        pub watcher: MaterialWatcher,
326    }
327
328    impl ResolvesClientCert for SpiffeClientCertResolver {
329        fn resolve(
330            &self,
331            _acceptable_issuers: &[&[u8]],
332            _sigschemes: &[rustls::SignatureScheme],
333        ) -> Option<Arc<CertifiedKey>> {
334            Some(Arc::clone(&self.watcher.current().certified_key))
335        }
336
337        fn has_certs(&self) -> bool {
338            true
339        }
340    }
341}