didbot_tls/resolver.rs
1//! The certificate as swappable state, not a listener to rebind.
2//!
3//! This server holds long-lived connections — `com.atproto.sync
4//! .subscribeRepos` is a firehose relays and indexers keep open for hours —
5//! so a renewal cannot mean tearing down the listener or the server to pick
6//! up a new certificate. rustls already has the seam for this:
7//! [`rustls::server::ResolvesServerCert`] is consulted once per handshake,
8//! not once per connection's lifetime, so a resolver that hands back a
9//! different [`CertifiedKey`] after a swap changes what *new* handshakes see
10//! without touching a byte of an established connection — the certificate
11//! only ever participates in the handshake that created the connection.
12//!
13//! One [`SwappableCert`] is built once, wrapped in the one
14//! `rustls::ServerConfig` this process builds, and lives for the process's
15//! whole life. Renewal calls [`SwappableCert::swap`]; nothing else about the
16//! listener or the server changes.
17//!
18//! # Why one `ArcSwap<CertifiedKey>`, not two fields
19//!
20//! A certificate and its private key are useless separately and dangerous if
21//! ever mismatched — a reader that saw a new cert paired with an old key (or
22//! vice versa) mid-update would hand out a `ServerHello` that fails on the
23//! client end at best and misissues at worst. [`CertifiedKey`] already
24//! bundles the chain, the signing key and the OCSP response as one value, so
25//! swapping *that* value under [`arc_swap::ArcSwap`] is one atomic pointer
26//! store: every reader sees either the whole old bundle or the whole new one,
27//! never a mix.
28
29use std::fmt;
30use std::sync::Arc;
31
32use arc_swap::ArcSwap;
33use rustls::server::{ClientHello, ResolvesServerCert};
34use rustls::sign::CertifiedKey;
35
36/// A certificate and key that can be swapped out from under live handshakes.
37///
38/// Implements [`ResolvesServerCert`] so it plugs directly into
39/// `ServerConfig::builder().with_cert_resolver(..)`.
40pub struct SwappableCert(ArcSwap<CertifiedKey>);
41
42impl SwappableCert {
43 /// Wraps `initial` as the certificate every handshake sees until the
44 /// first [`swap`](Self::swap).
45 pub fn new(initial: CertifiedKey) -> Arc<Self> {
46 Arc::new(Self(ArcSwap::new(Arc::new(initial))))
47 }
48
49 /// Atomically replaces the certificate every *subsequent* handshake will
50 /// see. Connections already established are unaffected: they never
51 /// consult this resolver again after their own handshake completed.
52 pub fn swap(&self, next: CertifiedKey) {
53 self.0.store(Arc::new(next));
54 }
55
56 /// The certificate current handshakes would see right now.
57 pub fn current(&self) -> Arc<CertifiedKey> {
58 self.0.load_full()
59 }
60}
61
62impl fmt::Debug for SwappableCert {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 // `CertifiedKey` does not implement `Debug`, and printing key
65 // material would be the wrong instinct anyway; say only that a
66 // certificate is loaded.
67 f.debug_struct("SwappableCert").finish_non_exhaustive()
68 }
69}
70
71impl ResolvesServerCert for SwappableCert {
72 fn resolve(&self, _client_hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
73 Some(self.current())
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80 use crate::cert::certified_key_from_pem;
81 use crate::test_support::self_signed_pem;
82
83 #[test]
84 fn resolve_reflects_the_latest_swap() {
85 let (cert_a, key_a) = self_signed_pem("a.example.invalid");
86 let (cert_b, key_b) = self_signed_pem("b.example.invalid");
87 let provider = rustls::crypto::ring::default_provider();
88
89 let ck_a = certified_key_from_pem(&cert_a, &key_a, &provider).unwrap();
90 let ck_b = certified_key_from_pem(&cert_b, &key_b, &provider).unwrap();
91
92 let swappable = SwappableCert::new(ck_a);
93 let before = swappable.current();
94
95 swappable.swap(ck_b);
96 let after = swappable.current();
97
98 // Different certificates, and the swap did not mutate the value the
99 // first reader is still holding.
100 assert_ne!(before.cert.first().unwrap(), after.cert.first().unwrap());
101 }
102}