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}