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. Production
47/// deployments should usually configure a more specific authorizer.
48///
49/// # Examples
50///
51/// ```no_run
52/// use spiffe_rustls::{authorizer, mtls_client, AllowList};
53/// use std::collections::BTreeSet;
54///
55/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
56/// let source = spiffe::X509Source::new().await?;
57///
58/// // Pass string literals directly - exact() and trust_domains() will convert them
59/// let allowed_server_ids = [
60///     "spiffe://example.org/myservice",
61///     "spiffe://example.org/myservice2",
62/// ];
63///
64/// let mut allowed_trust_domains = BTreeSet::new();
65/// allowed_trust_domains.insert("example.org".try_into()?);
66///
67/// let client_config = mtls_client(source)
68///     .authorize(authorizer::exact(allowed_server_ids)?)
69///     .trust_domain_policy(AllowList(allowed_trust_domains))
70///     .build()?;
71/// # Ok(())
72/// # }
73/// ```
74pub struct ClientConfigBuilder {
75    source: Arc<X509Source>,
76    authorizer: Arc<dyn Authorizer>,
77    trust_domain_policy: TrustDomainPolicy,
78    alpn_protocols: Vec<Vec<u8>>,
79    config_customizer: Option<ClientConfigCustomizer>,
80}
81
82impl std::fmt::Debug for ClientConfigBuilder {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        f.debug_struct("ClientConfigBuilder")
85            .field("source", &"<Arc<X509Source>>")
86            .field("authorizer", &"<Arc<dyn Authorizer>>")
87            .field("trust_domain_policy", &self.trust_domain_policy)
88            .field("alpn_protocols", &self.alpn_protocols)
89            .field("config_customizer", &self.config_customizer.is_some())
90            .finish()
91    }
92}
93
94impl ClientConfigBuilder {
95    /// Creates a new builder from an `X509Source`.
96    ///
97    /// Defaults:
98    /// - Authorization: [`crate::authorizer::any`], which accepts any authenticated
99    ///   SPIFFE ID from any trust domain accepted by the configured trust-domain policy.
100    ///   By default, this means every trust domain in the source bundle set.
101    /// - Trust domain policy: [`TrustDomainPolicy::AnyInBundleSet`], which accepts any
102    ///   trust domain present in the source bundle set
103    /// - ALPN protocols: empty (no ALPN)
104    ///
105    /// Production deployments should usually configure a more specific authorizer.
106    /// Non-federated deployments should usually configure [`TrustDomainPolicy::LocalOnly`].
107    pub fn new(source: X509Source) -> Self {
108        Self {
109            source: Arc::new(source),
110            authorizer: Arc::new(crate::authorizer::any()),
111            trust_domain_policy: TrustDomainPolicy::default(),
112            alpn_protocols: Vec::new(),
113            config_customizer: None,
114        }
115    }
116
117    /// Sets the authorization policy for server SPIFFE IDs.
118    ///
119    /// Accepts any type that implements `Authorizer`, including closures.
120    ///
121    /// # Examples
122    ///
123    /// ```no_run
124    /// use spiffe_rustls::{authorizer, mtls_client};
125    ///
126    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
127    /// let source = spiffe::X509Source::new().await?;
128    ///
129    /// // Pass string literals directly
130    /// let config = mtls_client(source.clone())
131    ///     .authorize(authorizer::exact([
132    ///         "spiffe://example.org/service",
133    ///         "spiffe://example.org/service2",
134    ///     ])?)
135    ///     .build()?;
136    ///
137    /// // Using a closure
138    /// let config = mtls_client(source.clone())
139    ///     .authorize(|id: &spiffe::SpiffeId| id.path().starts_with("/api/"))
140    ///     .build()?;
141    ///
142    /// // Using the Any authorizer (default)
143    /// let config = mtls_client(source)
144    ///     .authorize(authorizer::any())
145    ///     .build()?;
146    /// # Ok(())
147    /// # }
148    /// ```
149    #[must_use]
150    pub fn authorize<A: Authorizer>(mut self, authorizer: A) -> Self {
151        self.authorizer = Arc::new(authorizer);
152        self
153    }
154
155    /// Sets the trust domain policy.
156    ///
157    /// Defaults to [`TrustDomainPolicy::AnyInBundleSet`], which accepts any trust
158    /// domain present in the source bundle set. For non-federated deployments,
159    /// prefer [`TrustDomainPolicy::LocalOnly`] to restrict verification to the local
160    /// trust domain.
161    #[must_use]
162    pub fn trust_domain_policy(mut self, policy: TrustDomainPolicy) -> Self {
163        self.trust_domain_policy = policy;
164        self
165    }
166
167    /// Sets the ALPN (Application-Layer Protocol Negotiation) protocols.
168    ///
169    /// The protocols are advertised during the TLS handshake. Common values:
170    /// - `b"h2"` for HTTP/2 (required for gRPC)
171    /// - `b"http/1.1"` for HTTP/1.1
172    ///
173    /// Protocols should be specified in order of preference (most preferred first).
174    ///
175    /// # Examples
176    ///
177    /// ```no_run
178    /// use spiffe_rustls::mtls_client;
179    ///
180    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
181    /// let source = spiffe::X509Source::new().await?;
182    /// let config = mtls_client(source)
183    ///     .with_alpn_protocols([b"h2"])
184    ///     .build()?;
185    /// # Ok(())
186    /// # }
187    /// ```
188    #[must_use]
189    pub fn with_alpn_protocols<I, P>(mut self, protocols: I) -> Self
190    where
191        I: IntoIterator<Item = P>,
192        P: AsRef<[u8]>,
193    {
194        self.alpn_protocols = protocols.into_iter().map(|p| p.as_ref().to_vec()).collect();
195        self
196    }
197
198    /// Applies a customizer function to the `ClientConfig` after it's built.
199    ///
200    /// This is an **advanced** API for configuration not directly exposed by the builder.
201    /// The customizer is called **last**, after all other builder settings (including
202    /// ALPN) have been applied, and gives direct access to the underlying `rustls`
203    /// configuration.
204    ///
205    /// **Warning:** Replacing the verifier or resolver disables SPIFFE authentication.
206    /// Do not use this hook for that purpose; build a custom `rustls` configuration instead.
207    ///
208    /// # Examples
209    ///
210    /// ```no_run
211    /// use spiffe_rustls::mtls_client;
212    ///
213    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
214    /// let source = spiffe::X509Source::new().await?;
215    /// let config = mtls_client(source)
216    ///     .with_config_customizer(|cfg| {
217    ///         // Example: adjust cipher suite preferences
218    ///     })
219    ///     .build()?;
220    /// # Ok(())
221    /// # }
222    /// ```
223    #[must_use]
224    pub fn with_config_customizer<F>(mut self, customizer: F) -> Self
225    where
226        F: FnOnce(&mut ClientConfig) + Send + 'static,
227    {
228        self.config_customizer = Some(Box::new(customizer));
229        self
230    }
231
232    /// Builds the `rustls::ClientConfig`.
233    ///
234    /// The returned configuration:
235    ///
236    /// * presents the current SPIFFE X.509 SVID as the client certificate
237    /// * validates the server certificate chain against trust bundles from the Workload API
238    /// * authorizes the server by SPIFFE ID (URI SAN)
239    ///
240    /// The configuration is backed by a live [`X509Source`]. When the underlying
241    /// SVID or trust bundle is rotated by the SPIRE agent, **new TLS handshakes
242    /// automatically use the updated material**.
243    ///
244    /// # Errors
245    ///
246    /// Returns an error if:
247    ///
248    /// * the Rustls crypto provider is not installed
249    /// * no current X.509 SVID is available from the `X509Source`
250    /// * building the underlying Rustls certificate verifier fails
251    pub fn build(self) -> Result<ClientConfig> {
252        crate::crypto::ensure_crypto_provider_installed();
253
254        let watcher = MaterialWatcher::spawn(self.source)?;
255
256        let resolver: Arc<dyn ResolvesClientCert> =
257            Arc::new(resolve_client::SpiffeClientCertResolver {
258                watcher: watcher.clone(),
259            });
260
261        let verifier = Arc::new(SpiffeServerCertVerifier::new(
262            Arc::new(watcher),
263            self.authorizer,
264            self.trust_domain_policy,
265        ));
266
267        let mut cfg = ClientConfig::builder()
268            .dangerous()
269            .with_custom_certificate_verifier(verifier)
270            .with_client_cert_resolver(resolver);
271
272        cfg.alpn_protocols = self.alpn_protocols;
273
274        // Apply customizer last
275        if let Some(customizer) = self.config_customizer {
276            customizer(&mut cfg);
277        }
278
279        Ok(cfg)
280    }
281}
282
283mod resolve_client {
284    use crate::resolve::MaterialWatcher;
285    use rustls::client::ResolvesClientCert;
286    use rustls::sign::CertifiedKey;
287    use std::sync::Arc;
288
289    #[derive(Clone, Debug)]
290    pub(crate) struct SpiffeClientCertResolver {
291        pub watcher: MaterialWatcher,
292    }
293
294    impl ResolvesClientCert for SpiffeClientCertResolver {
295        fn resolve(
296            &self,
297            _acceptable_issuers: &[&[u8]],
298            _sigschemes: &[rustls::SignatureScheme],
299        ) -> Option<Arc<CertifiedKey>> {
300            Some(Arc::clone(&self.watcher.current().certified_key))
301        }
302
303        fn has_certs(&self) -> bool {
304            true
305        }
306    }
307}