Support multiple TLS certificates. (#95)

This commit is contained in:
kixelated 2023-10-16 13:05:40 +09:00 committed by GitHub
parent 1749989dc5
commit 9a25143694
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 325 additions and 356 deletions

View File

@ -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

322
Cargo.lock generated
View File

@ -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"

View File

@ -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"] }

View File

@ -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

View File

@ -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"] }

View File

@ -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<path::PathBuf>,
/// 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<path::PathBuf>,
/// Listen on HTTPS and serve /fingerprint, for self-signed certificates
#[arg(long, action)]

View File

@ -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(())
}

View File

@ -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<Self> {
// 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::Certificate> = 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<Self> {
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 {

152
moq-relay/src/tls.rs Normal file
View File

@ -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<ServeCerts>,
// Accept any cert that is trusted by the system's native trust store.
accept: Arc<RootCertStore>,
}
impl Tls {
pub fn load(config: &Config) -> anyhow::Result<Self> {
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<String> {
self.serve.fingerprints()
}
}
#[derive(Default)]
struct ServeCerts {
list: Vec<Arc<CertifiedKey>>,
}
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<Certificate> = 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<String> {
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<Arc<CertifiedKey>> {
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()
}
}

44
moq-relay/src/web.rs Normal file
View File

@ -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<RustlsAcceptor>,
}
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<String>) -> impl IntoResponse {
fingerprint
}