spiffe_rustls/client.rs
1use crate::error::Result;
2use crate::resolve::MaterialWatcher;
3use crate::types::{authorize_any, AuthorizeSpiffeId};
4use crate::verifier::SpiffeServerCertVerifier;
5use rustls::client::ResolvesClientCert;
6use rustls::ClientConfig;
7use spiffe::{TrustDomain, X509Source};
8use std::sync::Arc;
9
10/// Configuration options for [`ClientConfigBuilder`].
11///
12/// These options control trust bundle selection and server authorization.
13#[derive(Clone)]
14pub struct ClientConfigOptions {
15 /// Trust domain whose bundle is used as the verification root set.
16 pub trust_domain: TrustDomain,
17
18 /// Authorization hook invoked with the server SPIFFE ID.
19 ///
20 /// The hook receives the SPIFFE ID extracted from the server certificate’s
21 /// URI SAN and must return `true` to allow the connection.
22 pub authorize_server: AuthorizeSpiffeId,
23}
24
25impl std::fmt::Debug for ClientConfigOptions {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 f.debug_struct("ClientConfigOptions")
28 .field("trust_domain", &self.trust_domain)
29 .field("authorize_server", &"<authorize_fn>")
30 .finish()
31 }
32}
33
34impl ClientConfigOptions {
35 /// Creates options that authenticate the server but allow any SPIFFE ID.
36 ///
37 /// This disables authorization while retaining full TLS authentication.
38 /// Use only if authorization is performed at another layer.
39 pub fn allow_any(trust_domain: TrustDomain) -> Self {
40 Self {
41 trust_domain,
42 authorize_server: authorize_any(),
43 }
44 }
45}
46
47/// Builds a [`rustls::ClientConfig`] backed by a live SPIFFE `X509Source`.
48///
49/// The resulting client configuration:
50///
51/// * presents the current SPIFFE X.509 SVID as the client certificate
52/// * validates the server certificate chain against the trust domain bundle
53/// * authorizes the server by SPIFFE ID (URI SAN)
54///
55/// The builder retains an `Arc<X509Source>`. When the underlying SVID or trust
56/// bundle is rotated by the SPIRE agent, **new TLS handshakes automatically use
57/// the updated material**.
58///
59/// ## Authorization
60///
61/// Server authorization is performed by invoking the provided
62/// [`AuthorizeSpiffeId`] hook with the server’s SPIFFE ID extracted from the
63/// certificate’s URI SAN.
64///
65/// Use [`ClientConfigOptions::allow_any`] to disable authorization while
66/// retaining full TLS authentication.
67#[derive(Debug)]
68pub struct ClientConfigBuilder {
69 source: Arc<X509Source>,
70 opts: ClientConfigOptions,
71}
72
73impl ClientConfigBuilder {
74 /// Creates a new builder from an `X509Source` and options.
75 pub fn new(source: Arc<X509Source>, opts: ClientConfigOptions) -> Self {
76 Self { source, opts }
77 }
78
79 /// Builds the `rustls::ClientConfig`.
80 ///
81 /// The returned configuration:
82 ///
83 /// * presents the current SPIFFE X.509 SVID as the client certificate
84 /// * validates the server certificate chain against the configured trust domain
85 /// * authorizes the server by SPIFFE ID (URI SAN)
86 ///
87 /// The configuration is backed by a live [`X509Source`]. When the underlying
88 /// SVID or trust bundle is rotated by the SPIRE agent, **new TLS handshakes
89 /// automatically use the updated material**.
90 ///
91 /// # Errors
92 ///
93 /// Returns an error if:
94 ///
95 /// * the Rustls crypto provider is not installed
96 /// * no current X.509 SVID is available from the `X509Source`
97 /// * the trust bundle for the configured trust domain is missing
98 /// * building the underlying Rustls certificate verifier fails
99 pub fn build(self) -> Result<ClientConfig> {
100 crate::crypto::ensure_crypto_provider_installed();
101
102 let watcher = MaterialWatcher::new(self.source, self.opts.trust_domain)?;
103
104 let resolver: Arc<dyn ResolvesClientCert> =
105 Arc::new(resolve_client::SpiffeClientCertResolver {
106 watcher: watcher.clone(),
107 });
108
109 let verifier = Arc::new(SpiffeServerCertVerifier::from_watcher(
110 watcher.clone(),
111 self.opts.authorize_server,
112 ));
113
114 let cfg = ClientConfig::builder()
115 .dangerous()
116 .with_custom_certificate_verifier(verifier)
117 .with_client_cert_resolver(resolver);
118
119 Ok(cfg)
120 }
121}
122
123mod resolve_client {
124 use crate::resolve::MaterialWatcher;
125 use rustls::client::ResolvesClientCert;
126 use rustls::sign::CertifiedKey;
127 use std::sync::Arc;
128
129 #[derive(Clone, Debug)]
130 pub(crate) struct SpiffeClientCertResolver {
131 pub watcher: MaterialWatcher,
132 }
133
134 impl ResolvesClientCert for SpiffeClientCertResolver {
135 fn resolve(
136 &self,
137 _acceptable_issuers: &[&[u8]],
138 _sigschemes: &[rustls::SignatureScheme],
139 ) -> Option<Arc<CertifiedKey>> {
140 Some(self.watcher.current().certified_key.clone())
141 }
142
143 fn has_certs(&self) -> bool {
144 true
145 }
146 }
147}