From 9a2514369453bb68633b86b67d467a6e9edc0642 Mon Sep 17 00:00:00 2001 From: kixelated Date: Mon, 16 Oct 2023 13:05:40 +0900 Subject: [PATCH] Support multiple TLS certificates. (#95) --- .github/workflows/pr.yml | 3 + Cargo.lock | 322 +++++++-------------------- moq-api/Cargo.toml | 1 - moq-pub/Cargo.toml | 2 - moq-relay/Cargo.toml | 5 +- moq-relay/src/config.rs | 14 +- moq-relay/src/main.rs | 61 ++--- moq-relay/src/{server.rs => quic.rs} | 77 +------ moq-relay/src/tls.rs | 152 +++++++++++++ moq-relay/src/web.rs | 44 ++++ 10 files changed, 325 insertions(+), 356 deletions(-) rename moq-relay/src/{server.rs => quic.rs} (53%) create mode 100644 moq-relay/src/tls.rs create mode 100644 moq-relay/src/web.rs diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 9ae79ed..8325078 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -23,3 +23,6 @@ jobs: - run: cargo test --verbose - run: cargo clippy --no-deps - run: cargo fmt --check + + # Check for unused dependencies + - uses: bnjbvr/cargo-machete@main diff --git a/Cargo.lock b/Cargo.lock index f0ca02a..37393a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -273,6 +273,26 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum-server" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447f28c85900215cc1bea282f32d4a2f22d55c5a300afdfbc661c8d6a632e063" +dependencies = [ + "arc-swap", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.69" @@ -306,15 +326,6 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - [[package]] name = "blocking" version = "1.3.1" @@ -458,15 +469,6 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" -[[package]] -name = "cpufeatures" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" -dependencies = [ - "libc", -] - [[package]] name = "crossbeam-utils" version = "0.8.16" @@ -476,32 +478,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "data-encoding" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - [[package]] name = "encoding_rs" version = "0.8.33" @@ -712,16 +688,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "getrandom" version = "0.2.10" @@ -782,30 +748,6 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" -[[package]] -name = "headers" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" -dependencies = [ - "base64", - "bytes", - "headers-core", - "http", - "httpdate", - "mime", - "sha1", -] - -[[package]] -name = "headers-core" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" -dependencies = [ - "http", -] - [[package]] name = "heck" version = "0.4.1" @@ -855,6 +797,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range-header" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" + [[package]] name = "httparse" version = "1.8.0" @@ -1065,16 +1013,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "mime_guess" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" -dependencies = [ - "mime", - "unicase", -] - [[package]] name = "miniz_oxide" version = "0.7.1" @@ -1110,7 +1048,6 @@ dependencies = [ "serde_json", "thiserror", "tokio", - "tower", "url", ] @@ -1127,10 +1064,8 @@ dependencies = [ "mp4", "quinn", "rfc6381-codec", - "ring", "rustls", "rustls-native-certs", - "rustls-pemfile", "serde_json", "tokio", "url", @@ -1142,6 +1077,8 @@ name = "moq-relay" version = "0.1.0" dependencies = [ "anyhow", + "axum", + "axum-server", "clap", "env_logger", "hex", @@ -1149,16 +1086,17 @@ dependencies = [ "moq-api", "moq-transport", "quinn", - "ring", + "ring 0.16.20", "rustls", "rustls-native-certs", "rustls-pemfile", "thiserror", "tokio", + "tower-http", "tracing", "tracing-subscriber", "url", - "warp", + "webpki", "webtransport-quinn", ] @@ -1204,24 +1142,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96a1fe2275b68991faded2c80aa4a33dba398b77d276038b8f50701a22e55918" -[[package]] -name = "multer" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" -dependencies = [ - "bytes", - "encoding_rs", - "futures-util", - "http", - "httparse", - "log", - "memchr", - "mime", - "spin 0.9.8", - "version_check", -] - [[package]] name = "native-tls" version = "0.2.11" @@ -1497,7 +1417,7 @@ checksum = "e13f81c9a9d574310b8351f8666f5a93ac3b0069c45c28ad52c10291389a7cf9" dependencies = [ "bytes", "rand", - "ring", + "ring 0.16.20", "rustc-hash", "rustls", "rustls-native-certs", @@ -1688,11 +1608,25 @@ dependencies = [ "libc", "once_cell", "spin 0.5.2", - "untrusted", + "untrusted 0.7.1", "web-sys", "winapi", ] +[[package]] +name = "ring" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babe80d5c16becf6594aa32ad2be8fe08498e7ae60b77de8df700e67f191d7e" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys", +] + [[package]] name = "roff" version = "0.2.1" @@ -1745,7 +1679,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" dependencies = [ "log", - "ring", + "ring 0.16.20", "rustls-webpki", "sct", ] @@ -1777,8 +1711,8 @@ version = "0.101.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] @@ -1802,12 +1736,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - [[package]] name = "scopeguard" version = "1.2.0" @@ -1820,8 +1748,8 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] @@ -1900,17 +1828,6 @@ dependencies = [ "serde", ] -[[package]] -name = "sha1" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "sha1_smol" version = "1.0.0" @@ -2154,29 +2071,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-stream" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" -dependencies = [ - "futures-util", - "log", - "tokio", - "tungstenite", -] - [[package]] name = "tokio-util" version = "0.7.8" @@ -2207,6 +2101,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" +dependencies = [ + "bitflags 2.4.0", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-range-header", + "pin-project-lite", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.2" @@ -2284,40 +2196,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" -[[package]] -name = "tungstenite" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" -dependencies = [ - "byteorder", - "bytes", - "data-encoding", - "http", - "httparse", - "log", - "rand", - "sha1", - "thiserror", - "url", - "utf-8", -] - -[[package]] -name = "typenum" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" - -[[package]] -name = "unicase" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] - [[package]] name = "unicode-bidi" version = "0.3.13" @@ -2345,6 +2223,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.4.1" @@ -2357,12 +2241,6 @@ dependencies = [ "serde", ] -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - [[package]] name = "utf8parse" version = "0.2.1" @@ -2387,12 +2265,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - [[package]] name = "waker-fn" version = "1.1.0" @@ -2408,38 +2280,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "warp" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e92e22e03ff1230c03a1a8ee37d2f89cd489e2e541b7550d6afad96faed169" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "headers", - "http", - "hyper", - "log", - "mime", - "mime_guess", - "multer", - "percent-encoding", - "pin-project", - "rustls-pemfile", - "scoped-tls", - "serde", - "serde_json", - "serde_urlencoded", - "tokio", - "tokio-rustls", - "tokio-stream", - "tokio-tungstenite", - "tokio-util", - "tower-service", - "tracing", -] - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2522,6 +2362,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" +dependencies = [ + "ring 0.17.3", + "untrusted 0.9.0", +] + [[package]] name = "webpki-roots" version = "0.25.2" diff --git a/moq-api/Cargo.toml b/moq-api/Cargo.toml index 282e82b..df1f7a8 100644 --- a/moq-api/Cargo.toml +++ b/moq-api/Cargo.toml @@ -19,7 +19,6 @@ categories = ["multimedia", "network-programming", "web-programming"] axum = "0.6" hyper = { version = "0.14", features = ["full"] } tokio = { version = "1", features = ["full"] } -tower = "0.4" # HTTP client reqwest = { version = "0.11", features = ["json", "rustls-tls"] } diff --git a/moq-pub/Cargo.toml b/moq-pub/Cargo.toml index c687236..b12c645 100644 --- a/moq-pub/Cargo.toml +++ b/moq-pub/Cargo.toml @@ -23,9 +23,7 @@ webtransport-quinn = "0.6" url = "2" # Crypto -ring = "0.16" rustls = "0.21" -rustls-pemfile = "1" rustls-native-certs = "0.6" # Async stuff diff --git a/moq-relay/Cargo.toml b/moq-relay/Cargo.toml index 63771aa..3fbfe97 100644 --- a/moq-relay/Cargo.toml +++ b/moq-relay/Cargo.toml @@ -26,13 +26,16 @@ ring = "0.16" rustls = "0.21" rustls-pemfile = "1" rustls-native-certs = "0.6" +webpki = "0.22" # Async stuff tokio = { version = "1", features = ["full"] } # Web server to serve the fingerprint -warp = { version = "0.3.6", features = ["tls"] } +axum = { version = "0.6", features = ["tokio"] } +axum-server = { version = "0.5", features = ["tls-rustls"] } hex = "0.4" +tower-http = { version = "0.4", features = ["cors"] } # Error handling anyhow = { version = "1", features = ["backtrace"] } diff --git a/moq-relay/src/config.rs b/moq-relay/src/config.rs index 7ca82aa..c70c663 100644 --- a/moq-relay/src/config.rs +++ b/moq-relay/src/config.rs @@ -10,13 +10,19 @@ pub struct Config { #[arg(long, default_value = "[::]:4443")] pub listen: net::SocketAddr, - /// Use the certificate file at this path + /// Use the certificates at this path, encoded as PEM. + /// + /// You can use this option multiple times for multiple certificates. + /// The first match for the provided SNI will be used, otherwise the last cert will be used. + /// You also need to provide the private key multiple times via `key``. #[arg(long)] - pub cert: path::PathBuf, + pub cert: Vec, - /// Use the private key at this path + /// Use the private key at this path, encoded as PEM. + /// + /// There must be a key for every certificate provided via `cert`. #[arg(long)] - pub key: path::PathBuf, + pub key: Vec, /// Listen on HTTPS and serve /fingerprint, for self-signed certificates #[arg(long, action)] diff --git a/moq-relay/src/main.rs b/moq-relay/src/main.rs index baa220b..1e13725 100644 --- a/moq-relay/src/main.rs +++ b/moq-relay/src/main.rs @@ -1,21 +1,21 @@ -use std::{fs, io, sync}; - use anyhow::Context; use clap::Parser; -use ring::digest::{digest, SHA256}; -use warp::Filter; mod config; mod error; mod origin; -mod server; +mod quic; mod session; +mod tls; +mod web; pub use config::*; pub use error::*; pub use origin::*; -pub use server::*; +pub use quic::*; pub use session::*; +pub use tls::*; +pub use web::*; #[tokio::main] async fn main() -> anyhow::Result<()> { @@ -28,47 +28,20 @@ async fn main() -> anyhow::Result<()> { tracing::subscriber::set_global_default(tracer).unwrap(); let config = Config::parse(); + let tls = Tls::load(&config)?; - // Create a server to actually serve the media - let server = Server::new(config.clone()).await.context("failed to create server")?; + // Create a QUIC server for media. + let quic = Quic::new(config.clone(), tls.clone()) + .await + .context("failed to create server")?; + + // Create the web server if the --fingerprint flag was set. + // This is currently only useful in local development so it's not enabled by default. + let web = config.fingerprint.then(|| Web::new(config, tls)); // Run all of the above tokio::select! { - res = server.run() => res.context("failed to run server"), - res = serve_http(config), if config.fingerprint => res.context("failed to run HTTP server"), + res = quic.serve() => res.context("failed to run server"), + res = web.unwrap().serve(), if web.is_some() => res.context("failed to run HTTP server"), } } - -// Run a HTTP server using Warp -// TODO remove this when Chrome adds support for self-signed certificates using WebTransport -async fn serve_http(config: Config) -> anyhow::Result<()> { - // Read the PEM certificate file - let crt = fs::File::open(&config.cert)?; - let mut crt = io::BufReader::new(crt); - - // Parse the DER certificate - let certs = rustls_pemfile::certs(&mut crt)?; - let cert = certs.first().expect("no certificate found"); - - // Compute the SHA-256 digest - let fingerprint = digest(&SHA256, cert.as_ref()); - let fingerprint = hex::encode(fingerprint.as_ref()); - let fingerprint = sync::Arc::new(fingerprint); - - let cors = warp::cors().allow_any_origin(); - - // What an annoyingly complicated way to serve a static String - // I spent a long time trying to find the exact way of cloning and dereferencing the Arc. - let routes = warp::path!("fingerprint") - .map(move || (*(fingerprint.clone())).clone()) - .with(cors); - - warp::serve(routes) - .tls() - .cert_path(config.cert) - .key_path(config.key) - .run(config.listen) - .await; - - Ok(()) -} diff --git a/moq-relay/src/server.rs b/moq-relay/src/quic.rs similarity index 53% rename from moq-relay/src/server.rs rename to moq-relay/src/quic.rs index 32cb63e..1cee51e 100644 --- a/moq-relay/src/server.rs +++ b/moq-relay/src/quic.rs @@ -1,17 +1,12 @@ -use std::{ - fs, - io::{self, Read}, - sync::Arc, - time, -}; +use std::{sync::Arc, time}; use anyhow::Context; use tokio::task::JoinSet; -use crate::{Config, Origin, Session}; +use crate::{Config, Origin, Session, Tls}; -pub struct Server { +pub struct Quic { quic: quinn::Endpoint, // The active connections. @@ -21,65 +16,11 @@ pub struct Server { origin: Origin, } -impl Server { - // Create a new server - pub async fn new(config: Config) -> anyhow::Result { - // Read the PEM certificate chain - let certs = fs::File::open(config.cert).context("failed to open cert file")?; - let mut certs = io::BufReader::new(certs); - - let certs: Vec = rustls_pemfile::certs(&mut certs)? - .into_iter() - .map(rustls::Certificate) - .collect(); - - anyhow::ensure!(!certs.is_empty(), "could not find certificate"); - - // Read the PEM private key - let mut keys = fs::File::open(config.key).context("failed to open key file")?; - - // Read the keys into a Vec so we can try parsing it twice. - let mut buf = Vec::new(); - keys.read_to_end(&mut buf)?; - - // Try to parse a PKCS#8 key - // -----BEGIN PRIVATE KEY----- - let mut keys = rustls_pemfile::pkcs8_private_keys(&mut io::Cursor::new(&buf))?; - - // Try again but with EC keys this time - // -----BEGIN EC PRIVATE KEY----- - if keys.is_empty() { - keys = rustls_pemfile::ec_private_keys(&mut io::Cursor::new(&buf))? - }; - - anyhow::ensure!(!keys.is_empty(), "could not find private key"); - anyhow::ensure!(keys.len() < 2, "expected a single key"); - - let key = rustls::PrivateKey(keys.remove(0)); - - // Set up a QUIC endpoint that can act as both a client and server. - - // Create a list of acceptable root certificates. - let mut client_roots = rustls::RootCertStore::empty(); - - // Add the platform's native root certificates. - for cert in rustls_native_certs::load_native_certs().context("could not load platform certs")? { - client_roots - .add(&rustls::Certificate(cert.0)) - .context("failed to add root cert")?; - } - - // For local development, we'll accept our own certificate. - let mut client_config = rustls::ClientConfig::builder() - .with_safe_defaults() - .with_root_certificates(client_roots) - .with_no_client_auth(); - - let mut server_config = rustls::ServerConfig::builder() - .with_safe_defaults() - .with_no_client_auth() - .with_single_cert(certs, key)?; - +impl Quic { + // Create a QUIC endpoint that can be used for both clients and servers. + pub async fn new(config: Config, tls: Tls) -> anyhow::Result { + let mut client_config = tls.client(); + let mut server_config = tls.server(); client_config.alpn_protocols = vec![webtransport_quinn::ALPN.to_vec()]; server_config.alpn_protocols = vec![webtransport_quinn::ALPN.to_vec()]; @@ -121,7 +62,7 @@ impl Server { Ok(Self { quic, origin, conns }) } - pub async fn run(mut self) -> anyhow::Result<()> { + pub async fn serve(mut self) -> anyhow::Result<()> { log::info!("listening on {}", self.quic.local_addr()?); loop { diff --git a/moq-relay/src/tls.rs b/moq-relay/src/tls.rs new file mode 100644 index 0000000..a39d7b6 --- /dev/null +++ b/moq-relay/src/tls.rs @@ -0,0 +1,152 @@ +use anyhow::Context; +use ring::digest::{digest, SHA256}; +use rustls::server::{ClientHello, ResolvesServerCert}; +use rustls::sign::CertifiedKey; +use rustls::{Certificate, PrivateKey, RootCertStore}; +use rustls::{ClientConfig, ServerConfig}; +use std::fs; +use std::io::{self, Cursor, Read}; +use std::path; +use std::sync::Arc; +use webpki::{DnsNameRef, EndEntityCert}; + +use crate::Config; + +#[derive(Clone)] +pub struct Tls { + // Support serving multiple certificates, choosing one that looks valid for the given SNI. + // We store the parsed certificate, and the certified cert/key that rustls expects + serve: Arc, + + // Accept any cert that is trusted by the system's native trust store. + accept: Arc, +} + +impl Tls { + pub fn load(config: &Config) -> anyhow::Result { + let mut serve = ServeCerts::default(); + + // Load the certificate and key files based on their index. + anyhow::ensure!(config.cert.len() == config.key.len(), "--cert and --key mismatch"); + for (chain, key) in config.cert.iter().zip(config.key.iter()) { + serve.load(chain, key)?; + } + + // Create a list of acceptable root certificates. + let mut roots = RootCertStore::empty(); + + // Add the platform's native root certificates. + for cert in rustls_native_certs::load_native_certs().context("could not load platform certs")? { + roots.add(&Certificate(cert.0)).context("failed to add root cert")?; + } + + let certs = Self { + serve: Arc::new(serve), + accept: Arc::new(roots), + }; + + Ok(certs) + } + + pub fn client(&self) -> ClientConfig { + rustls::ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(self.accept.clone()) + .with_no_client_auth() + } + + pub fn server(&self) -> ServerConfig { + rustls::ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_cert_resolver(self.serve.clone()) + } + + // Return the SHA256 fingerprint of our certificates. + pub fn fingerprints(&self) -> Vec { + self.serve.fingerprints() + } +} + +#[derive(Default)] +struct ServeCerts { + list: Vec>, +} + +impl ServeCerts { + // Load a certificate and cooresponding key from a file + pub fn load(&mut self, chain: &path::PathBuf, key: &path::PathBuf) -> anyhow::Result<()> { + // Read the PEM certificate chain + let chain = fs::File::open(chain).context("failed to open cert file")?; + let mut chain = io::BufReader::new(chain); + + let chain: Vec = rustls_pemfile::certs(&mut chain)? + .into_iter() + .map(Certificate) + .collect(); + + 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")?; + + // Read the keys into a Vec so we can parse it twice. + let mut buf = Vec::new(); + keys.read_to_end(&mut buf)?; + + // Try to parse a PKCS#8 key + // -----BEGIN PRIVATE KEY----- + let mut keys = rustls_pemfile::pkcs8_private_keys(&mut Cursor::new(&buf))?; + + // Try again but with EC keys this time + // -----BEGIN EC PRIVATE KEY----- + if keys.is_empty() { + keys = rustls_pemfile::ec_private_keys(&mut Cursor::new(&buf))? + }; + + anyhow::ensure!(!keys.is_empty(), "could not find private key"); + anyhow::ensure!(keys.len() < 2, "expected a single key"); + + let key = PrivateKey(keys.remove(0)); + let key = rustls::sign::any_supported_type(&key)?; + + let certified = Arc::new(CertifiedKey::new(chain, key)); + self.list.push(certified); + + Ok(()) + } + + // Return the SHA256 fingerprint of our certificates. + pub fn fingerprints(&self) -> Vec { + self.list + .iter() + .map(|ck| { + let fingerprint = digest(&SHA256, ck.cert[0].as_ref()); + let fingerprint = hex::encode(fingerprint.as_ref()); + fingerprint + }) + .collect() + } +} + +impl ResolvesServerCert for ServeCerts { + fn resolve(&self, client_hello: ClientHello<'_>) -> Option> { + if let Some(name) = client_hello.server_name() { + if let Ok(dns_name) = DnsNameRef::try_from_ascii_str(name) { + for ck in &self.list { + // TODO I gave up on caching the parsed result because of lifetime hell. + // If this shows up on benchmarks, somebody should fix it. + let leaf = ck.cert.first().expect("missing certificate"); + let parsed = EndEntityCert::try_from(leaf.0.as_ref()).expect("failed to parse certificate"); + + if parsed.verify_is_valid_for_dns_name(dns_name).is_ok() { + return Some(ck.clone()); + } + } + } + } + + // Default to the last certificate if we couldn't find one. + self.list.last().cloned() + } +} diff --git a/moq-relay/src/web.rs b/moq-relay/src/web.rs new file mode 100644 index 0000000..ce6c820 --- /dev/null +++ b/moq-relay/src/web.rs @@ -0,0 +1,44 @@ +use std::sync::Arc; + +use axum::{extract::State, http::Method, response::IntoResponse, routing::get, Router}; +use axum_server::{tls_rustls::RustlsAcceptor, Server}; +use tower_http::cors::{Any, CorsLayer}; + +use crate::{Config, Tls}; + +// Run a HTTP server using Axum +// TODO remove this when Chrome adds support for self-signed certificates using WebTransport +pub struct Web { + app: Router, + server: Server, +} + +impl Web { + pub fn new(config: Config, tls: Tls) -> Self { + // Get the first certificate's fingerprint. + // TODO serve all of them so we can support multiple signature algorithms. + let fingerprint = tls.fingerprints().first().expect("missing certificate").clone(); + + let mut tls_config = tls.server(); + tls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; + let tls_config = axum_server::tls_rustls::RustlsConfig::from_config(Arc::new(tls_config)); + + let app = Router::new() + .route("/fingerprint", get(serve_fingerprint)) + .layer(CorsLayer::new().allow_origin(Any).allow_methods([Method::GET])) + .with_state(fingerprint); + + let server = axum_server::bind_rustls(config.listen, tls_config); + + Self { app, server } + } + + pub async fn serve(self) -> anyhow::Result<()> { + self.server.serve(self.app.into_make_service()).await?; + Ok(()) + } +} + +async fn serve_fingerprint(State(fingerprint): State) -> impl IntoResponse { + fingerprint +}