diff --git a/Cargo.lock b/Cargo.lock index c10500f..547b2d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -401,9 +401,9 @@ dependencies = [ [[package]] name = "asn1-rs" -version = "0.5.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" +checksum = "22ad1373757efa0f70ec53939aabc7152e1591cb485208052993070ac8d2429d" dependencies = [ "asn1-rs-derive", "asn1-rs-impl", @@ -417,25 +417,25 @@ dependencies = [ [[package]] name = "asn1-rs-derive" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" +checksum = "7378575ff571966e99a744addeff0bff98b8ada0dedf1956d59e634db95eaac1" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.48", "synstructure", ] [[package]] name = "asn1-rs-impl" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.48", ] [[package]] @@ -916,6 +916,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.6.0" @@ -3043,9 +3049,9 @@ dependencies = [ [[package]] name = "der-parser" -version = "8.2.0" +version = "9.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" dependencies = [ "asn1-rs", "displaydoc", @@ -5647,9 +5653,9 @@ dependencies = [ [[package]] name = "oid-registry" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" +checksum = "1c958dd45046245b9c3c2547369bb634eb461670b2e7e0de552905801a648d1d" dependencies = [ "asn1-rs", ] @@ -5833,11 +5839,11 @@ checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" [[package]] name = "pem" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8fcc794035347fb64beda2d3b462595dd2753e3f268d89c5aae77e8cf2c310" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "serde", ] @@ -6347,12 +6353,13 @@ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" [[package]] name = "rcgen" -version = "0.12.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d918c80c5a4c7560db726763020bd16db179e4d5b828078842274a443addb5d" +checksum = "54077e1872c46788540de1ea3d7f4ccb1983d12f9aa909b234468676c1a36779" dependencies = [ "pem", "ring 0.17.7", + "rustls-pki-types", "time 0.3.31", "yasna", ] @@ -6690,7 +6697,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" dependencies = [ "openssl-probe", - "rustls-pemfile", + "rustls-pemfile 1.0.4", "schannel", "security-framework", ] @@ -6704,11 +6711,21 @@ dependencies = [ "base64 0.21.7", ] +[[package]] +name = "rustls-pemfile" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +dependencies = [ + "base64 0.22.1", + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" -version = "1.3.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "048a63e5b3ac996d78d402940b5fa47973d2d080c6c6fffa1d0f19c4445310b7" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" @@ -7456,14 +7473,13 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "synstructure" -version = "0.12.6" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", - "unicode-xid", + "syn 2.0.48", ] [[package]] @@ -8821,17 +8837,19 @@ dependencies = [ [[package]] name = "wtransport" -version = "0.1.11" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "295c6d8c425e11f178e35ed1442eb77c1ffba98248728fecc96fb9c5a707e7f3" +checksum = "3367f124e6272c52b83a8e7c9aaea274782b3f32a8997ed0b33776b540a9a1d2" dependencies = [ "bytes", + "pem", "quinn", "rcgen", - "ring 0.17.7", "rustls 0.21.10", "rustls-native-certs", - "rustls-pemfile", + "rustls-pemfile 2.1.2", + "rustls-pki-types", + "sha2 0.10.8", "socket2 0.5.5", "thiserror", "time 0.3.31", @@ -8844,9 +8862,9 @@ dependencies = [ [[package]] name = "wtransport-proto" -version = "0.1.11" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d19ee96cc428b718c1557e327c47016e12147adf773e050ae86384198699f57" +checksum = "78ac9905446911e950a38598640b63c4a6b401e704b6273935dff33f560977c6" dependencies = [ "httlib-huffman", "octets", @@ -8888,9 +8906,9 @@ checksum = "e63e71c4b8bd9ffec2c963173a4dc4cbde9ee96961d4fcb4429db9929b606c34" [[package]] name = "x509-parser" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7069fba5b66b9193bd2c5d3d4ff12b839118f6bcbef5328efafafb5395cf63da" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" dependencies = [ "asn1-rs", "data-encoding", diff --git a/Cargo.toml b/Cargo.toml index b4e22bb..ec50765 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,7 +73,7 @@ tracing = "0.1.40" tracing-subscriber = "0.3.18" url = "2.5.0" uuid = "1.7.0" -wtransport = "0.1.11" +wtransport = "0.1.13" [workspace.dependencies.derive_more] version = "0.99" diff --git a/crates/replicate/server/src/chad/certificate.rs b/crates/replicate/server/src/chad/certificate.rs new file mode 100644 index 0000000..1b49cfd --- /dev/null +++ b/crates/replicate/server/src/chad/certificate.rs @@ -0,0 +1,73 @@ +use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD}; + +/// Newtype on [`wtransport::Identity`]. +pub(super) struct Certificate { + pub(super) cert: wtransport::Identity, + pub(super) base64_hash: String, +} + +impl Certificate { + pub fn new(cert: wtransport::Identity) -> Self { + let cert_hash = cert + .certificate_chain() + .as_slice() + .last() + .expect("should be at least one cert") + .hash(); + let base64_hash = BASE64_URL_SAFE_NO_PAD.encode(cert_hash.as_ref()); + Self { cert, base64_hash } + } + + pub fn self_signed( + subject_alt_names: I, + ) -> Result + where + I: Iterator, + S: AsRef, + { + wtransport::Identity::self_signed(subject_alt_names).map(Self::new) + } +} + +impl Clone for Certificate { + fn clone(&self) -> Self { + Self { + cert: self.cert.clone_identity(), + base64_hash: self.base64_hash.clone(), + } + } +} + +impl From for Certificate { + fn from(value: wtransport::Identity) -> Self { + Self::new(value) + } +} + +impl std::fmt::Debug for Certificate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple(std::any::type_name::()) + .field(&self.cert.certificate_chain()) + .finish() + } +} + +impl std::fmt::Display for Certificate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_list() + .entries( + self.cert + .certificate_chain() + .as_slice() + .iter() + .map(|c| c.hash().fmt(wtransport::tls::Sha256DigestFmt::DottedHex)), + ) + .finish() + } +} + +impl AsRef for Certificate { + fn as_ref(&self) -> &wtransport::Identity { + &self.cert + } +} diff --git a/crates/replicate/server/src/chad/mod.rs b/crates/replicate/server/src/chad/mod.rs index dfd8499..b2086cb 100644 --- a/crates/replicate/server/src/chad/mod.rs +++ b/crates/replicate/server/src/chad/mod.rs @@ -1,23 +1,25 @@ //! WebTransport server, i.e. "chad" transport. +mod certificate; + use std::{ num::Wrapping, sync::{Arc, RwLock}, time::Duration, }; -use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD}; use color_eyre::{eyre::Context, Result}; use eyre::{bail, ensure}; -use futures::{sink::SinkExt, stream::StreamExt}; +use futures::{sink::SinkExt, stream::StreamExt, FutureExt as _}; use replicate_common::{ messages::manager::{Clientbound as Cb, Serverbound as Sb}, InstanceId, }; -use tracing::{error, info, info_span, Instrument}; +use tracing::{error, info, info_span, instrument, Instrument}; use url::Url; use wtransport::{endpoint::IncomingSession, ServerConfig}; +use self::certificate::Certificate; use crate::{instance::InstanceManager, Args}; type Server = wtransport::Endpoint; @@ -29,13 +31,14 @@ pub async fn launch_webtransport_server( args: Args, _im: Arc, ) -> Result<()> { - let cert = Certificate::new(wtransport::Certificate::self_signed( - args.subject_alt_names.iter(), - )); + let cert = Certificate::new( + wtransport::Identity::self_signed(args.subject_alt_names.iter()) + .wrap_err("failed to create self signed certificate")?, + ); let server = Server::server( ServerConfig::builder() .with_bind_default(args.port.unwrap_or(0)) - .with_certificate(cert.cert.clone()) + .with_identity(cert.as_ref()) .build(), ) .wrap_err("failed to create wtransport server")?; @@ -75,37 +78,44 @@ pub async fn launch_webtransport_server( } }; + let _: ((), ()) = tokio::try_join! { + accept_fut.map(|()| Ok(())), + cert_refresh_task(&server,svr_ctx.clone(), args.port), + }?; + + Ok(()) +} + +#[instrument(name = "cert refresh task", skip(server))] +async fn cert_refresh_task( + server: &Server, + svr_ctx: ServerCtx, + port: Option, +) -> Result<()> { let mut interval = tokio::time::interval(CERT_REFRESH_INTERVAL); - let refresh_cert_fut = async { - // tick it once to clear the initial tick + // tick it once to clear the initial tick + interval.tick().await; + loop { interval.tick().await; - loop { - interval.tick().await; - info!("refreshing certs"); - let mut svr_ctx_l = svr_ctx.0.write().expect("server context poisoned"); - svr_ctx_l.cert = - wtransport::Certificate::self_signed(svr_ctx_l.san.iter()).into(); - - #[allow(clippy::question_mark)] // false positive - if let Err(err) = server - .reload_config( - ServerConfig::builder() - .with_bind_default(args.port.unwrap_or(0)) - .with_certificate(svr_ctx_l.cert.cert.clone()) - .build(), - false, - ) - .wrap_err("failed to reload server config") - { - return Err(err); - } - info!("new server url:\n{}", server_url(&svr_ctx_l)); + info!("refreshing certs"); + let mut svr_ctx_l = svr_ctx.0.write().expect("server context poisoned"); + svr_ctx_l.cert = Certificate::self_signed(svr_ctx_l.san.iter()) + .expect("already validated the SAN, so this should never panic"); + + #[allow(clippy::question_mark)] // false positive + if let Err(err) = server + .reload_config( + ServerConfig::builder() + .with_bind_default(port.unwrap_or(0)) + .with_identity(svr_ctx_l.cert.as_ref()) + .build(), + false, + ) + .wrap_err("failed to reload server config") + { + return Err(err); } - } - .instrument(info_span!("cert refresh task")); - tokio::select! { - _ = accept_fut => unreachable!(), - result = refresh_cert_fut => result?, + info!("new server url:\n{}", server_url(&svr_ctx_l)); } } @@ -162,7 +172,7 @@ async fn handle_connection( let domain_name = svr_ctx_l.san.first().expect("should have domain name"); let port = svr_ctx_l.port; - let hash = &svr_ctx_l.cert.base64; + let hash = &svr_ctx_l.cert.base64_hash; // TODO: Actually manipulate the instance manager. Url::parse(&format!("https://{domain_name}:{port}/{id}/#{hash}")) .expect("invalid url") @@ -181,7 +191,7 @@ async fn handle_connection( } fn server_url(svr_ctx: &ServerCtxInner) -> String { - let encoded_cert_hash = &svr_ctx.cert.base64; + let encoded_cert_hash = &svr_ctx.cert.base64_hash; let subject_alt_name = svr_ctx.san.first().expect("should have at least 1 SAN"); let port = svr_ctx.port; format!("https://{subject_alt_name}:{port}/#{encoded_cert_hash}") @@ -204,54 +214,6 @@ impl ServerCtx { } } -/// Newtype on [`wtransport::Certificate`]. -#[derive(Clone)] -struct Certificate { - cert: wtransport::Certificate, - base64: String, -} - -impl Certificate { - fn new(cert: wtransport::Certificate) -> Self { - let cert_hash = cert.hashes().pop().expect("should be at least one hash"); - let base64 = BASE64_URL_SAFE_NO_PAD.encode(cert_hash.as_ref()); - Self { cert, base64 } - } -} - -impl From for Certificate { - fn from(value: wtransport::Certificate) -> Self { - Self::new(value) - } -} - -impl std::fmt::Debug for Certificate { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple(std::any::type_name::()) - .field(&self.cert.certificates()) - .finish() - } -} - -impl std::fmt::Display for Certificate { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_list() - .entries( - self.cert - .hashes() - .into_iter() - .map(|h| h.fmt(wtransport::tls::Sha256DigestFmt::DottedHex)), - ) - .finish() - } -} - -impl AsRef for Certificate { - fn as_ref(&self) -> &wtransport::Certificate { - &self.cert - } -} - #[cfg(test)] mod test { use std::time::Duration;