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