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}