Skip to main content

rustls_spiffe/
client_stream.rs

1// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
2
3use std::{
4    pin::Pin,
5    sync::Arc,
6    task::{Context, Poll},
7};
8
9use rustls::{
10    ClientConfig,
11    pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer},
12};
13use rustls_config_stream::{ClientConfigStreamBuilder, ClientConfigStreamError};
14use spiffe::{TrustDomain, WorkloadApiClient, WorkloadApiError, X509Context};
15use tokio_stream::Stream;
16
17pub use rustls_config_stream::ClientConfigProvider;
18
19#[cfg(feature = "tracing")]
20use tracing::debug;
21
22use crate::TrustDomainStore;
23
24/// Builder for a [`SpiffeClientConfigStream`] that provides [`rustls::ClientConfig`]
25/// objects built w/ trust bundles and workload X509-SVID from SPIFFE.
26///
27/// The builder controls which SPIFFE trust bundles are included in the
28/// internal [`rustls::RootCertStore`] used to build the [`ClientConfig`]
29pub struct SpiffeClientConfigStreamBuilder {
30    trust_domains: Vec<TrustDomain>,
31    client: Option<WorkloadApiClient>,
32}
33
34impl SpiffeClientConfigStreamBuilder {
35    /// Create a builder that can create [`SpiffeClientConfigStream`] objects
36    /// with the provided SPIFFE trust domains.
37    const fn new(trust_domains: Vec<TrustDomain>) -> Self {
38        Self {
39            trust_domains,
40            client: None,
41        }
42    }
43}
44
45impl ClientConfigStreamBuilder for SpiffeClientConfigStreamBuilder {
46    type ConfigStream = SpiffeClientConfigStream;
47
48    async fn build(&mut self) -> Result<Self::ConfigStream, ClientConfigStreamError> {
49        let client = if let Some(client) = &mut self.client {
50            client
51        } else {
52            &mut WorkloadApiClient::connect_env()
53                .await
54                .map_err(|e| ClientConfigStreamError::StreamBuilderError(e.into()))?
55        };
56        Ok(SpiffeClientConfigStream {
57            trust_domains: self.trust_domains.clone(),
58            inner: Pin::from(Box::from(
59                client
60                    .stream_x509_contexts()
61                    .await
62                    .map_err(|e| ClientConfigStreamError::StreamError(e.into()))?,
63            )),
64        })
65    }
66}
67
68/// A stream that yields updated [`rustls::ClientConfig`] values derived from the
69/// SPIFFE Workload API X509-SVID and Trust Bundles.
70///
71/// Each yielded config:
72/// * Uses the workload's default SVID (certificate chain + private key).
73/// * Requires (and verifies) server certificates whose trust anchors come from
74///   the configured SPIFFE trust domains.
75///
76/// # Behavior
77///
78/// * If the Workload API stream returns an error, this stream yields
79///   a [`ClientConfigStreamError::StreamError`] wrapping the original
80///   [`WorkloadApiError`].
81/// * If an update lacks roots/SVID or the verifier cannot be built, the error
82///   is returned on the stream as a [`ClientConfigStreamError`]
83pub struct SpiffeClientConfigStream {
84    inner:
85        Pin<Box<dyn Stream<Item = Result<X509Context, WorkloadApiError>> + Send + Sync + 'static>>,
86    trust_domains: Vec<TrustDomain>,
87}
88
89impl TrustDomainStore for SpiffeClientConfigStream {
90    fn get_trust_domains(&self) -> &Vec<TrustDomain> {
91        &self.trust_domains
92    }
93}
94
95impl SpiffeClientConfigStream {
96    /// Create a builder that can create [`SpiffeClientConfigStream`] objects
97    /// with the provided SPIFFE trust domains.
98    #[must_use]
99    pub const fn builder(trust_domains: Vec<TrustDomain>) -> SpiffeClientConfigStreamBuilder {
100        SpiffeClientConfigStreamBuilder::new(trust_domains)
101    }
102
103    fn build_client_config(
104        &self,
105        x509_context: &X509Context,
106    ) -> Result<Arc<ClientConfig>, ClientConfigStreamError> {
107        let roots = self.build_root_store(x509_context.bundle_set());
108        if roots.is_empty() {
109            return Err(ClientConfigStreamError::MissingRoots);
110        }
111        let svid = x509_context
112            .default_svid()
113            .ok_or(ClientConfigStreamError::MissingCertifiedKey)?;
114
115        #[cfg(feature = "tracing")]
116        debug!(workload_identity = %svid.spiffe_id());
117
118        let config = ClientConfig::builder()
119            .with_root_certificates(roots)
120            .with_client_auth_cert(
121                svid.cert_chain()
122                    .iter()
123                    .map(|c| CertificateDer::from(c.as_bytes().to_owned()))
124                    .collect(),
125                PrivateKeyDer::from(PrivatePkcs8KeyDer::from(
126                    svid.private_key().as_bytes().to_owned(),
127                )),
128            )
129            .map_err(ClientConfigStreamError::RustlsError)?;
130        Ok(Arc::from(config))
131    }
132}
133
134impl Stream for SpiffeClientConfigStream {
135    type Item = Result<Arc<ClientConfig>, ClientConfigStreamError>;
136
137    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
138        match self.inner.as_mut().poll_next(cx) {
139            Poll::Pending => Poll::Pending,
140            Poll::Ready(None) => Poll::Ready(None),
141            Poll::Ready(Some(Err(err))) => {
142                Poll::Ready(Some(Err(ClientConfigStreamError::StreamError(err.into()))))
143            }
144            Poll::Ready(Some(Ok(x509_context))) => match self.build_client_config(&x509_context) {
145                Ok(config) => Poll::Ready(Some(Ok(config))),
146                Err(err) => Poll::Ready(Some(Err(err))),
147            },
148        }
149    }
150}