1use std::error::Error;
13use std::future::Future;
14use std::pin::Pin;
15use std::sync::{Arc, Once};
16use std::task::{Context, Poll};
17use std::time::Duration;
18
19use hyper_util::rt::TokioIo;
20use rustls::RootCertStore;
21use rustls::pki_types::pem::PemObject as _;
22use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
23use tokio::net::TcpStream;
24use tonic::codegen::http::Uri;
25use tonic::transport::{Channel, Endpoint};
26use x509_cert::Certificate;
27use x509_cert::der::Decode as _;
28use x509_cert::ext::pkix::{SubjectAltName, name::GeneralName};
29
30use crate::MtlsMaterial;
31
32type BoxError = Box<dyn Error + Send + Sync>;
33
34pub trait CertSource: Send + Sync + 'static {
41 fn current(&self) -> Option<MtlsMaterial>;
46}
47
48#[derive(Clone)]
52pub struct StaticCertSource(MtlsMaterial);
53
54impl StaticCertSource {
55 #[must_use]
57 pub fn new(material: MtlsMaterial) -> Self {
58 Self(material)
59 }
60}
61
62impl CertSource for StaticCertSource {
63 fn current(&self) -> Option<MtlsMaterial> {
64 Some(self.0.clone())
65 }
66}
67
68fn client_config(material: &MtlsMaterial) -> Result<rustls::ClientConfig, BoxError> {
72 let mut roots = RootCertStore::empty();
73 for ca in CertificateDer::pem_slice_iter(&material.trust_bundle_pem) {
74 roots.add(ca?)?;
75 }
76 if roots.is_empty() {
77 return Err("mTLS trust bundle holds no certificates".into());
78 }
79 let chain = CertificateDer::pem_slice_iter(&material.client_cert_chain_pem)
80 .collect::<Result<Vec<_>, _>>()?;
81 let key = PrivateKeyDer::from_pem_slice(&material.client_key_pem)?;
82
83 let mut config = rustls::ClientConfig::builder_with_provider(Arc::new(
84 rustls::crypto::aws_lc_rs::default_provider(),
85 ))
86 .with_safe_default_protocol_versions()?
87 .with_root_certificates(roots)
88 .with_client_auth_cert(chain, key)?;
89 config.alpn_protocols = vec![b"h2".to_vec()];
92 Ok(config)
93}
94
95#[derive(Clone)]
103pub(crate) struct RotatingConnector {
104 source: Arc<dyn CertSource>,
105 expected_id: Option<Arc<str>>,
106}
107
108fn require_spiffe_id(peer: Option<&[CertificateDer<'_>]>, expected: &str) -> Result<(), BoxError> {
110 let leaf = peer
111 .and_then(<[_]>::first)
112 .ok_or("collector presented no certificate")?;
113 let cert = Certificate::from_der(leaf.as_ref())
114 .map_err(|e| format!("collector certificate is not parseable: {e}"))?;
115 let mut found = Vec::new();
116 if let Some((_, san)) = cert
117 .tbs_certificate
118 .get::<SubjectAltName>()
119 .map_err(|e| format!("collector certificate has a malformed SAN: {e}"))?
120 {
121 for name in &san.0 {
122 if let GeneralName::UniformResourceIdentifier(uri) = name {
123 if uri.as_str() == expected {
124 return Ok(());
125 }
126 found.push(uri.as_str().to_owned());
127 }
128 }
129 }
130 Err(format!(
131 "collector identity mismatch: expected {expected}, certificate carries [{}]",
132 found.join(", ")
133 )
134 .into())
135}
136
137type TlsIo = TokioIo<tokio_rustls::client::TlsStream<TcpStream>>;
138
139impl RotatingConnector {
140 async fn connect(
141 source: Arc<dyn CertSource>,
142 expected_id: Option<Arc<str>>,
143 uri: Uri,
144 ) -> Result<TlsIo, BoxError> {
145 let host = uri.host().ok_or("OTLP endpoint has no host")?.to_owned();
146 let port = uri.port_u16().unwrap_or(443);
147 let material = source
148 .current()
149 .ok_or("mTLS material is not available yet")?;
150 let config = client_config(&material)?;
151 let tcp = TcpStream::connect((host.as_str(), port)).await?;
152 tcp.set_nodelay(true)?;
153 let name = ServerName::try_from(host)?;
154 let tls = tokio_rustls::TlsConnector::from(Arc::new(config))
155 .connect(name, tcp)
156 .await?;
157 match expected_id {
158 Some(id) => require_spiffe_id(tls.get_ref().1.peer_certificates(), &id)?,
159 None => {
160 static UNPINNED: Once = Once::new();
161 UNPINNED.call_once(|| {
162 tracing::warn!(
163 "OTLP collector identity is not pinned: its certificate is trusted on \
164 chain and DNS name only"
165 );
166 });
167 }
168 }
169 Ok(TokioIo::new(tls))
170 }
171}
172
173impl tower::Service<Uri> for RotatingConnector {
174 type Response = TlsIo;
175 type Error = BoxError;
176 type Future = Pin<Box<dyn Future<Output = Result<TlsIo, BoxError>> + Send>>;
177
178 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
179 Poll::Ready(Ok(()))
180 }
181
182 fn call(&mut self, uri: Uri) -> Self::Future {
183 Box::pin(Self::connect(
184 Arc::clone(&self.source),
185 self.expected_id.clone(),
186 uri,
187 ))
188 }
189}
190
191pub(crate) fn channel(
197 endpoint: &str,
198 source: Arc<dyn CertSource>,
199 timeout: Option<Duration>,
200 expected_id: Option<&str>,
201) -> Result<Channel, BoxError> {
202 let parsed: Uri = endpoint.parse()?;
203 let authority = parsed
204 .authority()
205 .ok_or("OTLP endpoint has no host")?
206 .as_str();
207 let mut ep = Endpoint::from_shared(format!("http://{authority}"))?
208 .connect_timeout(Duration::from_secs(10));
209 if let Some(t) = timeout {
210 ep = ep.timeout(t);
211 }
212 Ok(ep.connect_with_connector_lazy(RotatingConnector {
213 source,
214 expected_id: expected_id.map(Arc::from),
215 }))
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221 use std::sync::Mutex;
222 use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
223 use tokio::net::TcpListener;
224 use tower::Service as _;
225
226 const CA: &str = include_str!("../tests/fixtures/mtls/ca.pem");
230 const STRANGER_CA: &str = include_str!("../tests/fixtures/mtls/stranger-ca.pem");
231 const SERVER_CERT: &str = include_str!("../tests/fixtures/mtls/server.pem");
232 const SERVER_KEY: &str = include_str!("../tests/fixtures/mtls/server.key");
233 const CLIENT_ONE_CERT: &str = include_str!("../tests/fixtures/mtls/client-one.pem");
234 const CLIENT_ONE_KEY: &str = include_str!("../tests/fixtures/mtls/client-one.key");
235 const CLIENT_TWO_CERT: &str = include_str!("../tests/fixtures/mtls/client-two.pem");
236 const CLIENT_TWO_KEY: &str = include_str!("../tests/fixtures/mtls/client-two.key");
237
238 fn material(cert: &str, key: &str, bundle: &str) -> MtlsMaterial {
239 MtlsMaterial {
240 client_cert_chain_pem: cert.as_bytes().to_vec(),
241 client_key_pem: key.as_bytes().to_vec(),
242 trust_bundle_pem: bundle.as_bytes().to_vec(),
243 }
244 }
245
246 fn client_one() -> MtlsMaterial {
247 material(CLIENT_ONE_CERT, CLIENT_ONE_KEY, CA)
248 }
249
250 fn client_two() -> MtlsMaterial {
251 material(CLIENT_TWO_CERT, CLIENT_TWO_KEY, CA)
252 }
253
254 async fn server() -> (u16, Arc<Mutex<Vec<Vec<u8>>>>) {
257 server_with(SERVER_CERT, SERVER_KEY, CA).await
258 }
259
260 async fn server_with(
261 server_cert: &str,
262 server_key: &str,
263 client_ca: &str,
264 ) -> (u16, Arc<Mutex<Vec<Vec<u8>>>>) {
265 let mut roots = RootCertStore::empty();
266 roots
267 .add(CertificateDer::from_pem_slice(client_ca.as_bytes()).unwrap())
268 .unwrap();
269 let verifier = rustls::server::WebPkiClientVerifier::builder_with_provider(
270 Arc::new(roots),
271 Arc::new(rustls::crypto::aws_lc_rs::default_provider()),
272 )
273 .build()
274 .unwrap();
275 let mut config = rustls::ServerConfig::builder_with_provider(Arc::new(
276 rustls::crypto::aws_lc_rs::default_provider(),
277 ))
278 .with_safe_default_protocol_versions()
279 .unwrap()
280 .with_client_cert_verifier(verifier)
281 .with_single_cert(
282 CertificateDer::pem_slice_iter(server_cert.as_bytes())
283 .collect::<Result<Vec<_>, _>>()
284 .unwrap(),
285 PrivateKeyDer::from_pem_slice(server_key.as_bytes()).unwrap(),
286 )
287 .unwrap();
288 config.alpn_protocols = vec![b"h2".to_vec()];
289 let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(config));
290
291 let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
292 let port = listener.local_addr().unwrap().port();
293 let seen = Arc::new(Mutex::new(Vec::new()));
294 let seen_task = Arc::clone(&seen);
295 tokio::spawn(async move {
296 loop {
297 let (tcp, _) = listener.accept().await.unwrap();
298 let acceptor = acceptor.clone();
299 let seen = Arc::clone(&seen_task);
300 tokio::spawn(async move {
301 if let Ok(mut tls) = acceptor.accept(tcp).await {
302 let leaf = tls
303 .get_ref()
304 .1
305 .peer_certificates()
306 .and_then(|c| c.first())
307 .map(|c| c.as_ref().to_vec());
308 if let Some(leaf) = leaf {
309 seen.lock().unwrap().push(leaf);
310 }
311 let mut b = [0_u8; 1];
312 let _ = tls.read(&mut b).await;
313 let _ = tls.shutdown().await;
314 }
315 });
316 }
317 });
318 (port, seen)
319 }
320
321 struct Swappable(Mutex<Option<MtlsMaterial>>);
322 impl CertSource for Swappable {
323 fn current(&self) -> Option<MtlsMaterial> {
324 self.0.lock().unwrap().clone()
325 }
326 }
327
328 fn uri(port: u16) -> Uri {
329 format!("http://localhost:{port}").parse().unwrap()
330 }
331
332 fn pem_to_der(pem: &[u8]) -> Vec<u8> {
333 CertificateDer::from_pem_slice(pem).unwrap().to_vec()
334 }
335
336 #[tokio::test]
337 async fn each_new_connection_presents_the_material_current_at_that_moment() {
338 let (port, seen) = server().await;
339 let source = Arc::new(Swappable(Mutex::new(Some(client_one()))));
340 let mut connector = RotatingConnector {
341 source: Arc::clone(&source) as Arc<dyn CertSource>,
342 expected_id: None,
343 };
344
345 std::future::poll_fn(|cx| connector.poll_ready(cx))
346 .await
347 .expect("connector is always ready");
348 drop(connector.call(uri(port)).await.expect("first connection"));
349
350 *source.0.lock().unwrap() = Some(client_two());
351 drop(connector.call(uri(port)).await.expect("rotated connection"));
352
353 tokio::time::timeout(Duration::from_secs(5), async {
354 while seen.lock().unwrap().len() < 2 {
355 tokio::time::sleep(Duration::from_millis(10)).await;
356 }
357 })
358 .await
359 .expect("server saw both clients");
360 let seen = seen.lock().unwrap();
361 assert_eq!(seen[0], pem_to_der(CLIENT_ONE_CERT.as_bytes()));
362 assert_eq!(seen[1], pem_to_der(CLIENT_TWO_CERT.as_bytes()));
363 }
364
365 #[tokio::test]
366 async fn a_source_not_ready_at_first_connect_fails_that_attempt_then_connects_once_ready() {
367 let (port, seen) = server().await;
368 let source = Arc::new(Swappable(Mutex::new(None)));
369 let mut connector = RotatingConnector {
370 source: Arc::clone(&source) as Arc<dyn CertSource>,
371 expected_id: None,
372 };
373
374 let err = connector
375 .call(uri(port))
376 .await
377 .err()
378 .expect("an unready source must fail the attempt");
379 assert!(err.to_string().contains("not available yet"), "{err}");
380 assert!(seen.lock().unwrap().is_empty());
381
382 *source.0.lock().unwrap() = Some(client_one());
383 drop(
384 connector
385 .call(uri(port))
386 .await
387 .expect("connects once ready"),
388 );
389 }
390
391 #[tokio::test]
392 async fn a_bundle_of_several_certificates_trusts_each_of_them() {
393 let (port, _seen) = server().await;
394 let bundle = format!("{STRANGER_CA}{CA}");
397 let mut connector = RotatingConnector {
398 source: Arc::new(StaticCertSource::new(material(
399 CLIENT_ONE_CERT,
400 CLIENT_ONE_KEY,
401 &bundle,
402 ))),
403 expected_id: None,
404 };
405 drop(
406 connector
407 .call(uri(port))
408 .await
409 .expect("second anchor trusted"),
410 );
411 }
412
413 #[tokio::test]
414 async fn a_server_the_bundle_does_not_vouch_for_is_refused() {
415 let (port, _seen) = server().await;
416 let mut connector = RotatingConnector {
417 source: Arc::new(StaticCertSource::new(material(
418 CLIENT_ONE_CERT,
419 CLIENT_ONE_KEY,
420 STRANGER_CA,
421 ))),
422 expected_id: None,
423 };
424 assert!(connector.call(uri(port)).await.is_err());
425 }
426
427 #[test]
428 fn malformed_material_is_an_error_not_a_panic() {
429 let bad = MtlsMaterial {
430 client_cert_chain_pem: b"x".to_vec(),
431 client_key_pem: b"x".to_vec(),
432 trust_bundle_pem: Vec::new(),
433 };
434 assert!(
435 client_config(&bad)
436 .unwrap_err()
437 .to_string()
438 .contains("no certificates")
439 );
440 }
441
442 #[test]
443 fn static_source_returns_its_material() {
444 let m = client_one();
445 let got = StaticCertSource::new(m.clone()).current().unwrap();
446 assert_eq!(got.client_cert_chain_pem, m.client_cert_chain_pem);
447 }
448
449 const PINNED_CA: &str = include_str!("../tests/fixtures/mtls/pinned/ca.pem");
454 const PINNED_CLIENT_CERT: &str = include_str!("../tests/fixtures/mtls/pinned/client.pem");
455 const PINNED_CLIENT_KEY: &str = include_str!("../tests/fixtures/mtls/pinned/client.key");
456
457 struct Pki {
458 ca: String,
459 server_cert: String,
460 server_key: String,
461 client: MtlsMaterial,
462 }
463
464 fn pki(server_uri: Option<&str>) -> Pki {
465 let (server_cert, server_key) = match server_uri {
466 Some(COLLECTOR_ID) => (
467 include_str!("../tests/fixtures/mtls/pinned/server-match.pem"),
468 include_str!("../tests/fixtures/mtls/pinned/server-match.key"),
469 ),
470 Some(_) => (
471 include_str!("../tests/fixtures/mtls/pinned/server-other.pem"),
472 include_str!("../tests/fixtures/mtls/pinned/server-other.key"),
473 ),
474 None => (
475 include_str!("../tests/fixtures/mtls/pinned/server-nouri.pem"),
476 include_str!("../tests/fixtures/mtls/pinned/server-nouri.key"),
477 ),
478 };
479 Pki {
480 ca: PINNED_CA.to_owned(),
481 server_cert: server_cert.to_owned(),
482 server_key: server_key.to_owned(),
483 client: material(PINNED_CLIENT_CERT, PINNED_CLIENT_KEY, PINNED_CA),
484 }
485 }
486
487 const COLLECTOR_ID: &str = "spiffe://brefwiz.e2e/otel-collector";
488
489 fn pinned(pki: &Pki, expected: Option<&str>) -> RotatingConnector {
490 RotatingConnector {
491 source: Arc::new(StaticCertSource::new(pki.client.clone())),
492 expected_id: expected.map(Arc::from),
493 }
494 }
495
496 #[tokio::test]
497 async fn a_collector_carrying_the_expected_spiffe_id_is_accepted() {
498 let pki = pki(Some(COLLECTOR_ID));
499 let (port, _seen) = server_with(&pki.server_cert, &pki.server_key, &pki.ca).await;
500 drop(
501 pinned(&pki, Some(COLLECTOR_ID))
502 .call(uri(port))
503 .await
504 .expect("matching identity"),
505 );
506 }
507
508 #[tokio::test]
509 async fn a_collector_with_another_spiffe_id_is_refused_though_chain_and_dns_hold() {
510 let pki = pki(Some("spiffe://brefwiz.e2e/impostor"));
511 let (port, _seen) = server_with(&pki.server_cert, &pki.server_key, &pki.ca).await;
512 drop(
515 pinned(&pki, None)
516 .call(uri(port))
517 .await
518 .expect("chain and DNS name are valid"),
519 );
520 let err = pinned(&pki, Some(COLLECTOR_ID))
521 .call(uri(port))
522 .await
523 .err()
524 .expect("identity mismatch must be refused");
525 assert!(err.to_string().contains("identity mismatch"), "{err}");
526 assert!(err.to_string().contains("impostor"), "{err}");
527 }
528
529 #[tokio::test]
530 async fn a_collector_with_no_spiffe_id_is_refused_when_one_is_expected() {
531 let pki = pki(None);
532 let (port, _seen) = server_with(&pki.server_cert, &pki.server_key, &pki.ca).await;
533 let err = pinned(&pki, Some(COLLECTOR_ID))
534 .call(uri(port))
535 .await
536 .err()
537 .expect("a certificate without the identity must be refused");
538 assert!(err.to_string().contains("identity mismatch"), "{err}");
539 }
540
541 #[tokio::test]
542 async fn without_an_expected_id_chain_and_dns_name_are_enough() {
543 let pki = pki(None);
544 let (port, _seen) = server_with(&pki.server_cert, &pki.server_key, &pki.ca).await;
545 drop(
546 pinned(&pki, None)
547 .call(uri(port))
548 .await
549 .expect("legacy path is unchanged"),
550 );
551 }
552
553 #[tokio::test]
554 async fn channel_rejects_an_endpoint_without_a_host() {
555 let source: Arc<dyn CertSource> = Arc::new(Swappable(Mutex::new(None)));
556 assert!(channel("not a uri", Arc::clone(&source), None, None).is_err());
557 assert!(
558 channel(
559 "https://collector.example:4320",
560 source,
561 Some(Duration::from_secs(3)),
562 None,
563 )
564 .is_ok()
565 );
566 }
567}