rustls_spiffe/server_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 ServerConfig,
11 pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer},
12 server::WebPkiClientVerifier,
13};
14use rustls_config_stream::{ServerConfigStreamBuilder, ServerConfigStreamError};
15use spiffe::{TrustDomain, WorkloadApiClient, WorkloadApiError, X509Context};
16use tokio_stream::Stream;
17
18pub use rustls_config_stream::ServerConfigProvider;
19
20#[cfg(feature = "tracing")]
21use tracing::debug;
22
23use crate::TrustDomainStore;
24
25/// Builder for a [`SpiffeServerConfigStream`] that provides [`rustls::ServerConfig`]
26/// objects built w/ trust bundles and workload X509-SVID from SPIFFE.
27///
28/// The builder controls which SPIFFE trust domains are allowed to authenticate
29/// clients.
30pub struct SpiffeServerConfigStreamBuilder {
31 trust_domains: Vec<TrustDomain>,
32 client: Option<WorkloadApiClient>,
33}
34
35impl SpiffeServerConfigStreamBuilder {
36 /// Create a builder that can create [`SpiffeServerConfigStream`] objects
37 /// with the provided SPIFFE trust domains.
38 const fn new(trust_domains: Vec<TrustDomain>) -> Self {
39 Self {
40 trust_domains,
41 client: None,
42 }
43 }
44}
45impl ServerConfigStreamBuilder for SpiffeServerConfigStreamBuilder {
46 type ConfigStream = SpiffeServerConfigStream;
47
48 async fn build(&mut self) -> Result<Self::ConfigStream, ServerConfigStreamError> {
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| ServerConfigStreamError::StreamBuilderError(e.into()))?
55 };
56 Ok(SpiffeServerConfigStream {
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| ServerConfigStreamError::StreamError(e.into()))?,
63 )),
64 })
65 }
66}
67
68/// A stream that yields updated [`rustls::ServerConfig`] 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) client 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 [`ServerConfigStreamError::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 [`ServerConfigStreamError`]
83///
84/// # Usage
85///
86/// ```rust
87/// use rustls_spiffe::{SpiffeServerConfigStream, ServerConfigProvider};
88/// use tracing::warn;
89///
90/// async fn run() {
91/// let config_stream_builder =
92/// SpiffeServerConfigStream::builder(vec!["example.org".try_into().unwrap()]);
93/// let config_provider = ServerConfigProvider::start(config_stream_builder)
94/// .await
95/// .unwrap();
96/// let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
97/// .await
98/// .unwrap();
99/// loop {
100/// let (stream, _) = listener.accept().await.unwrap();
101///
102/// let acceptor =
103/// tokio_rustls::LazyConfigAcceptor::new(rustls::server::Acceptor::default(), stream);
104/// tokio::pin!(acceptor);
105///
106/// let config_provider = config_provider.clone();
107/// match acceptor.as_mut().await {
108/// Ok(start) => {
109/// tokio::spawn(async move {
110/// if !config_provider.stream_healthy() {
111/// warn!(
112/// "config provider does not have healthy stream; TLS config may be out of date"
113/// );
114/// }
115/// let config = config_provider.get_config();
116/// match start.into_stream(config).await {
117/// Ok(stream) => {
118/// // serve some app (e.g. hyper, tower, axum)
119/// todo!();
120/// }
121/// Err(err) => {
122/// // add error handling as-desired
123/// todo!()
124/// },
125/// }
126/// });
127/// }
128/// Err(err) => {
129/// // add error-handling as-desired
130/// todo!()
131/// },
132/// }
133/// }
134/// }
135/// ```
136pub struct SpiffeServerConfigStream {
137 inner:
138 Pin<Box<dyn Stream<Item = Result<X509Context, WorkloadApiError>> + Send + Sync + 'static>>,
139 trust_domains: Vec<TrustDomain>,
140}
141
142impl TrustDomainStore for SpiffeServerConfigStream {
143 fn get_trust_domains(&self) -> &Vec<TrustDomain> {
144 &self.trust_domains
145 }
146}
147
148impl SpiffeServerConfigStream {
149 /// Create a builder that can create [`SpiffeServerConfigStream`] objects
150 /// with the provided SPIFFE trust domains.
151 #[must_use]
152 pub const fn builder(trust_domains: Vec<TrustDomain>) -> SpiffeServerConfigStreamBuilder {
153 SpiffeServerConfigStreamBuilder::new(trust_domains)
154 }
155
156 fn build_server_config(
157 &self,
158 x509_context: &X509Context,
159 ) -> Result<Arc<ServerConfig>, ServerConfigStreamError> {
160 let roots = self.build_root_store(x509_context.bundle_set());
161 if roots.is_empty() {
162 return Err(ServerConfigStreamError::MissingRoots);
163 }
164 let verifier = WebPkiClientVerifier::builder(roots)
165 .build()
166 .map_err(ServerConfigStreamError::VerifierBuilderError)?;
167 let svid = x509_context
168 .default_svid()
169 .ok_or(ServerConfigStreamError::MissingCertifiedKey)?;
170
171 #[cfg(feature = "tracing")]
172 debug!(workload_identity = %svid.spiffe_id());
173
174 let config = ServerConfig::builder()
175 .with_client_cert_verifier(verifier)
176 .with_single_cert(
177 svid.cert_chain()
178 .iter()
179 .map(|c| CertificateDer::from(c.as_bytes().to_owned()))
180 .collect(),
181 PrivateKeyDer::from(PrivatePkcs8KeyDer::from(
182 svid.private_key().as_bytes().to_owned(),
183 )),
184 )
185 .map_err(ServerConfigStreamError::RustlsError)?;
186 Ok(Arc::from(config))
187 }
188}
189
190impl Stream for SpiffeServerConfigStream {
191 type Item = Result<Arc<ServerConfig>, ServerConfigStreamError>;
192
193 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
194 match self.inner.as_mut().poll_next(cx) {
195 Poll::Pending => Poll::Pending,
196 Poll::Ready(None) => Poll::Ready(None),
197 Poll::Ready(Some(Err(err))) => {
198 Poll::Ready(Some(Err(ServerConfigStreamError::StreamError(err.into()))))
199 }
200 Poll::Ready(Some(Ok(x509_context))) => match self.build_server_config(&x509_context) {
201 Ok(config) => Poll::Ready(Some(Ok(config))),
202 Err(err) => Poll::Ready(Some(Err(err))),
203 },
204 }
205 }
206}