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/// Builds a [`rustls::ClientConfig`] backed by a live SPIFFE `X509Source`.
12///
13/// The resulting client configuration:
14///
15/// * presents the current SPIFFE X.509 SVID as the client certificate
16/// * validates the server certificate chain against trust bundles from the Workload API
17/// * authorizes the server by SPIFFE ID (URI SAN)
18///
19/// The builder retains an `Arc<X509Source>`. When the underlying SVID or trust
20/// bundle is rotated by the SPIRE agent, **new TLS handshakes automatically use
21/// the updated material**.
22///
23/// ## Trust Domain Selection
24///
25/// The builder uses the bundle set from `X509Source`, which may contain bundles
26/// for multiple trust domains (when SPIFFE federation is configured). The verifier
27/// automatically selects the correct bundle based on the peer's SPIFFE ID—no
28/// manual configuration is required. You can optionally restrict which trust
29/// domains are accepted using [`Self::trust_domain_policy`].
30///
31/// ## Authorization
32///
33/// Server authorization is performed by invoking the provided [`Authorizer`] with
34/// the server's SPIFFE ID extracted from the certificate's URI SAN.
35///
36/// Use [`authorizer::any`] to disable authorization while retaining full TLS authentication.
37///
38/// # Examples
39///
40/// ```no_run
41/// use spiffe_rustls::{authorizer, mtls_client, AllowList};
42/// use std::collections::BTreeSet;
43///
44/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
45/// let source = spiffe::X509Source::new().await?;
46///
47/// // Pass string literals directly - exact() and trust_domains() will convert them
48/// let allowed_server_ids = [
49/// "spiffe://example.org/myservice",
50/// "spiffe://example.org/myservice2",
51/// ];
52///
53/// let mut allowed_trust_domains = BTreeSet::new();
54/// allowed_trust_domains.insert("example.org".try_into()?);
55///
56/// let client_config = mtls_client(source)
57/// .authorize(authorizer::exact(allowed_server_ids)?)
58/// .trust_domain_policy(AllowList(allowed_trust_domains))
59/// .build()?;
60/// # Ok(())
61/// # }
62/// ```
63pub struct ClientConfigBuilder {
64 source: Arc<X509Source>,
65 authorizer: Arc<dyn Authorizer>,
66 trust_domain_policy: TrustDomainPolicy,
67}
68
69impl std::fmt::Debug for ClientConfigBuilder {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 f.debug_struct("ClientConfigBuilder")
72 .field("source", &"<Arc<X509Source>>")
73 .field("authorizer", &"<Arc<dyn Authorizer>>")
74 .field("trust_domain_policy", &self.trust_domain_policy)
75 .finish()
76 }
77}
78
79impl ClientConfigBuilder {
80 /// Creates a new builder from an `X509Source`.
81 ///
82 /// Defaults:
83 /// - Authorization: accepts any SPIFFE ID (authentication only)
84 /// - Trust domain policy: `AnyInBundleSet` (uses all bundles from the Workload API)
85 pub fn new(source: X509Source) -> Self {
86 Self {
87 source: Arc::new(source),
88 authorizer: Arc::new(crate::authorizer::any()),
89 trust_domain_policy: TrustDomainPolicy::default(),
90 }
91 }
92
93 /// Sets the authorization policy for server SPIFFE IDs.
94 ///
95 /// Accepts any type that implements `Authorizer`, including closures.
96 ///
97 /// # Examples
98 ///
99 /// ```no_run
100 /// use spiffe_rustls::{authorizer, mtls_client};
101 ///
102 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
103 /// let source = spiffe::X509Source::new().await?;
104 ///
105 /// // Using a convenience constructor - pass string literals directly
106 /// let config = mtls_client(source.clone())
107 /// .authorize(authorizer::exact([
108 /// "spiffe://example.org/service",
109 /// "spiffe://example.org/service2",
110 /// ])?)
111 /// .build()?;
112 ///
113 /// // Using a closure
114 /// let config = mtls_client(source.clone())
115 /// .authorize(|id: &spiffe::SpiffeId| id.path().starts_with("/api/"))
116 /// .build()?;
117 ///
118 /// // Using the Any authorizer (default)
119 /// let config = mtls_client(source)
120 /// .authorize(authorizer::any())
121 /// .build()?;
122 /// # Ok(())
123 /// # }
124 /// ```
125 #[must_use]
126 pub fn authorize<A: Authorizer>(mut self, authorizer: A) -> Self {
127 self.authorizer = Arc::new(authorizer);
128 self
129 }
130
131 /// Sets the trust domain policy.
132 ///
133 /// Defaults to `AnyInBundleSet` (uses all bundles from the Workload API).
134 #[must_use]
135 pub fn trust_domain_policy(mut self, policy: TrustDomainPolicy) -> Self {
136 self.trust_domain_policy = policy;
137 self
138 }
139
140 /// Builds the `rustls::ClientConfig`.
141 ///
142 /// The returned configuration:
143 ///
144 /// * presents the current SPIFFE X.509 SVID as the client certificate
145 /// * validates the server certificate chain against trust bundles from the Workload API
146 /// * authorizes the server by SPIFFE ID (URI SAN)
147 ///
148 /// The configuration is backed by a live [`X509Source`]. When the underlying
149 /// SVID or trust bundle is rotated by the SPIRE agent, **new TLS handshakes
150 /// automatically use the updated material**.
151 ///
152 /// # Errors
153 ///
154 /// Returns an error if:
155 ///
156 /// * the Rustls crypto provider is not installed
157 /// * no current X.509 SVID is available from the `X509Source`
158 /// * building the underlying Rustls certificate verifier fails
159 pub fn build(self) -> Result<ClientConfig> {
160 crate::crypto::ensure_crypto_provider_installed();
161
162 let watcher = MaterialWatcher::spawn(self.source)?;
163
164 let resolver: Arc<dyn ResolvesClientCert> =
165 Arc::new(resolve_client::SpiffeClientCertResolver {
166 watcher: watcher.clone(),
167 });
168
169 let verifier = Arc::new(SpiffeServerCertVerifier::new(
170 Arc::new(watcher) as Arc<dyn crate::verifier::MaterialProvider>,
171 self.authorizer,
172 self.trust_domain_policy,
173 ));
174
175 let cfg = ClientConfig::builder()
176 .dangerous()
177 .with_custom_certificate_verifier(verifier)
178 .with_client_cert_resolver(resolver);
179
180 Ok(cfg)
181 }
182}
183
184mod resolve_client {
185 use crate::resolve::MaterialWatcher;
186 use rustls::client::ResolvesClientCert;
187 use rustls::sign::CertifiedKey;
188 use std::sync::Arc;
189
190 #[derive(Clone, Debug)]
191 pub(crate) struct SpiffeClientCertResolver {
192 pub watcher: MaterialWatcher,
193 }
194
195 impl ResolvesClientCert for SpiffeClientCertResolver {
196 fn resolve(
197 &self,
198 _acceptable_issuers: &[&[u8]],
199 _sigschemes: &[rustls::SignatureScheme],
200 ) -> Option<Arc<CertifiedKey>> {
201 Some(self.watcher.current().certified_key.clone())
202 }
203
204 fn has_certs(&self) -> bool {
205 true
206 }
207 }
208}