-
Notifications
You must be signed in to change notification settings - Fork 136
Certificate reloading #774
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 12 commits
2391fc1
e029db0
4b56d72
0e566b9
7af7d3c
f2a2df6
ed8b5d1
3b4af62
5fa2e1a
4a2823f
d90aa81
6a0fee9
d6bbcb2
7280fb2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| use std::path::PathBuf; | ||
| use std::{net, sync::Arc, time::Duration}; | ||
| use std::{net, time::Duration}; | ||
|
|
||
| use crate::crypto; | ||
| use anyhow::Context; | ||
|
|
@@ -8,6 +8,9 @@ use rustls::server::{ClientHello, ResolvesServerCert}; | |
| use rustls::sign::CertifiedKey; | ||
| use std::fs; | ||
| use std::io::{self, Cursor, Read}; | ||
| use std::sync::{Arc, RwLock}; | ||
| #[cfg(unix)] | ||
| use tokio::signal::unix::{signal, SignalKind}; | ||
| use url::Url; | ||
| use web_transport_quinn::{http, ServerError}; | ||
|
|
||
|
|
@@ -81,7 +84,7 @@ impl ServerConfig { | |
| pub struct Server { | ||
| quic: quinn::Endpoint, | ||
| accept: FuturesUnordered<BoxFuture<'static, anyhow::Result<Request>>>, | ||
| fingerprints: Vec<String>, | ||
| certs: Arc<ServeCerts>, | ||
| } | ||
|
|
||
| impl Server { | ||
|
|
@@ -97,28 +100,37 @@ impl Server { | |
|
|
||
| let provider = crypto::provider(); | ||
|
|
||
| let mut serve = ServeCerts::new(provider.clone()); | ||
| let certs = ServeCerts::new(provider.clone()); | ||
|
|
||
| // Load the certificate and key files based on their index. | ||
| anyhow::ensure!( | ||
| config.tls.cert.len() == config.tls.key.len(), | ||
| "must provide both cert and key" | ||
| ); | ||
| certs.load_certs(&config.tls)?; | ||
|
|
||
| for (cert, key) in config.tls.cert.iter().zip(config.tls.key.iter()) { | ||
| serve.load(cert, key)?; | ||
| } | ||
| let certs = Arc::new(certs); | ||
|
|
||
| if !config.tls.generate.is_empty() { | ||
| serve.generate(&config.tls.generate)?; | ||
| } | ||
| #[cfg(unix)] | ||
| { | ||
| let certs = certs.clone(); | ||
| tokio::spawn(async move { | ||
| let tls_config = config.tls.clone(); | ||
|
|
||
| let fingerprints = serve.fingerprints(); | ||
| match signal(SignalKind::user_defined1()) { | ||
| Ok(mut signal) => loop { | ||
| if signal.recv().await.is_some() { | ||
| tracing::info!("reloading server certificates"); | ||
|
|
||
| if let Err(err) = certs.load_certs(&tls_config) { | ||
| tracing::warn!(%err, "failed to reload server certificates"); | ||
| } | ||
| } | ||
| }, | ||
| Err(err) => tracing::warn!(%err, "failed to setup server certificate reloading"), | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| let mut tls = rustls::ServerConfig::builder_with_provider(provider) | ||
| .with_protocol_versions(&[&rustls::version::TLS13])? | ||
| .with_no_client_auth() | ||
| .with_cert_resolver(Arc::new(serve)); | ||
| .with_cert_resolver(certs.clone()); | ||
|
|
||
| tls.alpn_protocols = vec![ | ||
| web_transport_quinn::ALPN.as_bytes().to_vec(), | ||
|
|
@@ -145,12 +157,13 @@ impl Server { | |
| Ok(Self { | ||
| quic: quic.clone(), | ||
| accept: Default::default(), | ||
| fingerprints, | ||
| certs, | ||
| }) | ||
| } | ||
|
|
||
| pub fn fingerprints(&self) -> &[String] { | ||
| &self.fingerprints | ||
| // Return the SHA256 fingerprints of all our certificates. | ||
| pub fn fingerprints(&self) -> Arc<RwLock<Vec<String>>> { | ||
| self.certs.fingerprints.clone() | ||
| } | ||
|
|
||
| /// Returns the next partially established QUIC or WebTransport session. | ||
|
|
@@ -301,21 +314,42 @@ impl QuicRequest { | |
|
|
||
| #[derive(Debug)] | ||
| struct ServeCerts { | ||
| certs: Vec<Arc<CertifiedKey>>, | ||
| certs: RwLock<Vec<Arc<CertifiedKey>>>, | ||
einrobin marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| fingerprints: Arc<RwLock<Vec<String>>>, | ||
| provider: crypto::Provider, | ||
| } | ||
|
|
||
| impl ServeCerts { | ||
| pub fn new(provider: crypto::Provider) -> Self { | ||
| Self { | ||
| certs: Vec::new(), | ||
| certs: RwLock::new(Vec::new()), | ||
| fingerprints: Arc::new(RwLock::new(Vec::new())), | ||
| provider, | ||
| } | ||
| } | ||
|
|
||
| // Load a certificate and corresponding key from a file | ||
| pub fn load(&mut self, chain: &PathBuf, key: &PathBuf) -> anyhow::Result<()> { | ||
| let chain = fs::File::open(chain).context("failed to open cert file")?; | ||
| pub fn load_certs(&self, config: &ServerTlsConfig) -> anyhow::Result<()> { | ||
| anyhow::ensure!(config.cert.len() == config.key.len(), "must provide both cert and key"); | ||
|
|
||
| let mut certs = Vec::new(); | ||
|
|
||
| // Load the certificate and key files based on their index. | ||
| for (cert, key) in config.cert.iter().zip(config.key.iter()) { | ||
| certs.push(Arc::new(self.load(cert, key)?)); | ||
| } | ||
|
|
||
| // Generate a new certificate if requested. | ||
| if !config.generate.is_empty() { | ||
| certs.push(Arc::new(self.generate(&config.generate)?)); | ||
| } | ||
|
|
||
| self.set_certs(certs); | ||
| Ok(()) | ||
| } | ||
|
|
||
| // Load a certificate and corresponding key from a file, but don't add it to the certs | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not? Why is this is
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was |
||
| fn load(&self, chain_path: &PathBuf, key_path: &PathBuf) -> anyhow::Result<CertifiedKey> { | ||
| let chain = fs::File::open(chain_path).context("failed to open cert file")?; | ||
| let mut chain = io::BufReader::new(chain); | ||
|
|
||
| let chain: Vec<CertificateDer> = rustls_pemfile::certs(&mut chain) | ||
|
|
@@ -325,7 +359,7 @@ impl ServeCerts { | |
| anyhow::ensure!(!chain.is_empty(), "could not find certificate"); | ||
|
|
||
| // Read the PEM private key | ||
| let mut keys = fs::File::open(key).context("failed to open key file")?; | ||
| let mut keys = fs::File::open(key_path).context("failed to open key file")?; | ||
|
|
||
| // Read the keys into a Vec so we can parse it twice. | ||
| let mut buf = Vec::new(); | ||
|
|
@@ -334,12 +368,18 @@ impl ServeCerts { | |
| let key = rustls_pemfile::private_key(&mut Cursor::new(&buf))?.context("missing private key")?; | ||
| let key = self.provider.key_provider.load_private_key(key)?; | ||
|
|
||
| self.certs.push(Arc::new(CertifiedKey::new(chain, key))); | ||
| let certified_key = CertifiedKey::new(chain, key); | ||
|
|
||
| Ok(()) | ||
| certified_key.keys_match().context(format!( | ||
| "private key {} doesn't match certificate {}", | ||
| key_path.display(), | ||
| chain_path.display() | ||
| ))?; | ||
|
|
||
| Ok(certified_key) | ||
| } | ||
|
|
||
| pub fn generate(&mut self, hostnames: &[String]) -> anyhow::Result<()> { | ||
| fn generate(&self, hostnames: &[String]) -> anyhow::Result<CertifiedKey> { | ||
| let key_pair = rcgen::KeyPair::generate()?; | ||
|
|
||
| let mut params = rcgen::CertificateParams::new(hostnames)?; | ||
|
|
@@ -358,28 +398,36 @@ impl ServeCerts { | |
| let key = self.provider.key_provider.load_private_key(key_der.into())?; | ||
|
|
||
| // Create a rustls::sign::CertifiedKey | ||
| self.certs.push(Arc::new(CertifiedKey::new(vec![cert.into()], key))); | ||
| Ok(CertifiedKey::new(vec![cert.into()], key)) | ||
| } | ||
|
|
||
| Ok(()) | ||
| // Replace the certificates | ||
| pub fn set_certs(&self, certs: Vec<Arc<CertifiedKey>>) { | ||
| *self.certs.write().expect("certs write lock poisened") = certs; | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| self.update_fingerprints(); | ||
| } | ||
|
|
||
| // Return the SHA256 fingerprints of all our certificates. | ||
| pub fn fingerprints(&self) -> Vec<String> { | ||
| self.certs | ||
| fn update_fingerprints(&self) { | ||
| let fingerprints = self | ||
| .certs | ||
| .read() | ||
| .unwrap() | ||
| .iter() | ||
| .map(|ck| { | ||
| let fingerprint = crate::crypto::sha256(&self.provider, ck.cert[0].as_ref()); | ||
| hex::encode(fingerprint) | ||
| }) | ||
| .collect() | ||
| .collect(); | ||
|
|
||
| *self.fingerprints.write().expect("fingerprints write lock poisened") = fingerprints; | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // Return the best certificate for the given ClientHello. | ||
| fn best_certificate(&self, client_hello: &ClientHello<'_>) -> Option<Arc<CertifiedKey>> { | ||
| let server_name = client_hello.server_name()?; | ||
| let dns_name = rustls::pki_types::ServerName::try_from(server_name).ok()?; | ||
|
|
||
| for ck in &self.certs { | ||
| for ck in self.certs.read().expect("certs read lock poisoned").iter() { | ||
| let leaf: webpki::EndEntityCert = ck | ||
| .end_entity_cert() | ||
| .expect("missing certificate") | ||
|
|
@@ -405,6 +453,6 @@ impl ResolvesServerCert for ServeCerts { | |
| // We do our best and return the first certificate. | ||
| tracing::warn!(server_name = ?client_hello.server_name(), "no SNI certificate found"); | ||
|
|
||
| self.certs.first().cloned() | ||
| self.certs.read().expect("certs read lock poisoned").first().cloned() | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.