From cef114fc7ade1f0d5e7ddf2e94617c006c7f2195 Mon Sep 17 00:00:00 2001 From: Joe Birr-Pixton Date: Mon, 10 Feb 2025 14:01:19 +0000 Subject: [PATCH 1/5] Implement QUIC header protection algorithms --- graviola/src/lib.rs | 1 + graviola/src/mid/mod.rs | 1 + graviola/src/mid/quic.rs | 189 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 191 insertions(+) create mode 100644 graviola/src/mid/quic.rs diff --git a/graviola/src/lib.rs b/graviola/src/lib.rs index cb3ffd9c7..edb64c3fc 100644 --- a/graviola/src/lib.rs +++ b/graviola/src/lib.rs @@ -171,6 +171,7 @@ pub mod hashing { pub mod aead { pub use super::mid::aes_gcm::AesGcm; pub use super::mid::chacha20poly1305::ChaCha20Poly1305; + pub use super::mid::quic; pub use super::mid::xchacha20poly1305::XChaCha20Poly1305; } diff --git a/graviola/src/mid/mod.rs b/graviola/src/mid/mod.rs index 5ea2f0529..5ceceef45 100644 --- a/graviola/src/mid/mod.rs +++ b/graviola/src/mid/mod.rs @@ -8,6 +8,7 @@ pub(super) mod chacha20poly1305; pub(super) mod ed25519; pub(super) mod p256; pub(super) mod p384; +pub mod quic; pub(super) mod rng; pub(super) mod rsa_priv; pub(super) mod rsa_pub; diff --git a/graviola/src/mid/quic.rs b/graviola/src/mid/quic.rs new file mode 100644 index 000000000..cced75169 --- /dev/null +++ b/graviola/src/mid/quic.rs @@ -0,0 +1,189 @@ +// Written for Graviola by Joe Birr-Pixton, 2025. +// SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT-0 + +//! QUIC-specific cryptography. +//! +//! Do not use this except to implement QUIC. + +use crate::low::chacha20::ChaCha20; +use crate::low::zeroise; +use crate::low::AesKey; + +/// QUIC Header Protection, using AES-128 or AES-256. +/// +/// See RFC9001 section 5.4.3 +pub struct AesHeaderProtection(AesKey); + +impl AesHeaderProtection { + /// Create a new `AesHeaderProtection`, given a key of 128 or 256 bits. + pub fn new(key: &[u8]) -> Self { + Self(AesKey::new(key)) + } + + /// Encrypt `packet_0` and a prefix of `packet_number`. + /// + /// `sample` is the header protection sample. + /// + /// The number of bytes altered in `packet_number` is given by the bottom 2 bits + /// of `packet_0`. + pub fn encrypt_in_place(&self, sample: &[u8; 16], packet_0: &mut u8, packet_number: &mut [u8]) { + let mut mask = *sample; + self.0.encrypt_block(&mut mask); + let mask = mask[..5].try_into().unwrap_or_else(|_| unreachable!()); + + cipher_in_place::(mask, packet_0, packet_number); + } + + /// Decrypt `packet_0` and a prefix of `packet_number`. + /// + /// `sample` is the header protection sample. + /// + /// The number of bytes altered in `packet_number` is given by the bottom 2 bits + /// of `packet_0`. + pub fn decrypt_in_place(&self, sample: &[u8; 16], packet_0: &mut u8, packet_number: &mut [u8]) { + let mut mask = *sample; + self.0.encrypt_block(&mut mask); + let mask = mask[..5].try_into().unwrap_or_else(|_| unreachable!()); + + cipher_in_place::(mask, packet_0, packet_number); + } +} + +/// QUIC Header Protection, using ChaCha20. +/// +/// See RFC9001 section 5.4.4 +pub struct ChaCha20HeaderProtection([u8; 32]); + +impl ChaCha20HeaderProtection { + /// Create a new `ChaCha20HeaderProtection`, given a key of 256 bits. + pub fn new(key: [u8; 32]) -> Self { + Self(key) + } + /// Encrypt `packet_0` and a prefix of `packet_number`. + /// + /// `sample` is the header protection sample. + /// + /// The number of bytes altered in `packet_number` is given by the bottom 2 bits + /// of `packet_0`. + pub fn encrypt_in_place(&self, sample: &[u8; 16], packet_0: &mut u8, packet_number: &mut [u8]) { + let mut mask = [0u8; 5]; + ChaCha20::new(&self.0, sample).cipher(&mut mask); + cipher_in_place::(mask, packet_0, packet_number); + } + + /// Decrypt `packet_0` and a prefix of `packet_number`. + /// + /// `sample` is the header protection sample. + /// + /// The number of bytes altered in `packet_number` is given by the bottom 2 bits + /// of `packet_0`. + pub fn decrypt_in_place(&self, sample: &[u8; 16], packet_0: &mut u8, packet_number: &mut [u8]) { + let mut mask = [0u8; 5]; + ChaCha20::new(&self.0, sample).cipher(&mut mask); + cipher_in_place::(mask, packet_0, packet_number); + } +} + +impl Drop for ChaCha20HeaderProtection { + fn drop(&mut self) { + zeroise(&mut self.0); + } +} + +fn cipher_in_place(mask: [u8; 5], packet_0: &mut u8, packet_number: &mut [u8]) { + let (mask_0, mask) = mask.split_first().unwrap_or_else(|| unreachable!()); + + let mask_0 = match *packet_0 & HEADER_FORM_LONG == HEADER_FORM_LONG { + true => mask_0 & 0x0f, + false => mask_0 & 0x1f, + }; + + let pn_length = if ENC { + let len = (*packet_0 & 0x03) as usize + 1; + *packet_0 ^= mask_0; + len + } else { + *packet_0 ^= mask_0; + (*packet_0 & 0x03) as usize + 1 + }; + + for (pn, m) in packet_number.iter_mut().zip(mask.iter()).take(pn_length) { + *pn ^= m; + } +} + +const HEADER_FORM_LONG: u8 = 0x80u8; + +#[cfg(test)] +mod tests { + use super::*; + + // all known-answer tests from RFC9001 appendix A + + #[test] + fn client_initial() { + let k = AesHeaderProtection::new( + b"\x9f\x50\x44\x9e\x04\xa0\xe8\x10\x28\x3a\x1e\x99\x33\xad\xed\xd2", + ); + + let mut packet_0 = 0xc3; + let mut packet_number = [0x00, 0x00, 0x00, 0x02]; + let sample = &[ + 0xd1, 0xb1, 0xc9, 0x8d, 0xd7, 0x68, 0x9f, 0xb8, 0xec, 0x11, 0xd2, 0x42, 0xb1, 0x23, + 0xdc, 0x9b, + ]; + + k.encrypt_in_place(sample, &mut packet_0, &mut packet_number); + assert_eq!(packet_0, 0xc0); + assert_eq!(packet_number, [0x7b, 0x9a, 0xec, 0x34]); + + k.decrypt_in_place(sample, &mut packet_0, &mut packet_number); + assert_eq!(packet_0, 0xc3); + assert_eq!(packet_number, [0x00, 0x00, 0x00, 0x02]); + } + + #[test] + fn server_initial() { + let k = AesHeaderProtection::new( + b"\xc2\x06\xb8\xd9\xb9\xf0\xf3\x76\x44\x43\x0b\x49\x0e\xea\xa3\x14", + ); + + let mut packet_0 = 0xc1; + let mut packet_number = [0x00, 0x01]; + let sample = &[ + 0x2c, 0xd0, 0x99, 0x1c, 0xd2, 0x5b, 0x0a, 0xac, 0x40, 0x6a, 0x58, 0x16, 0xb6, 0x39, + 0x41, 0x00, + ]; + + k.encrypt_in_place(sample, &mut packet_0, &mut packet_number); + assert_eq!(packet_0, 0xcf); + assert_eq!(packet_number, [0xc0, 0xd9]); + + k.decrypt_in_place(sample, &mut packet_0, &mut packet_number); + assert_eq!(packet_0, 0xc1); + assert_eq!(packet_number, [0x00, 0x01]); + } + + #[test] + fn chacha20_short_header() { + let k = ChaCha20HeaderProtection::new( + *b"\x25\xa2\x82\xb9\xe8\x2f\x06\xf2\x1f\x48\x89\x17\xa4\xfc\x8f\x1b\ + \x73\x57\x36\x85\x60\x85\x97\xd0\xef\xcb\x07\x6b\x0a\xb7\xa7\xa4", + ); + + let mut packet_0 = 0x42; + let mut packet_number = [0x00, 0xbf, 0xf4]; + let sample = &[ + 0x5e, 0x5c, 0xd5, 0x5c, 0x41, 0xf6, 0x90, 0x80, 0x57, 0x5d, 0x79, 0x99, 0xc2, 0x5a, + 0x5b, 0xfb, + ]; + + k.encrypt_in_place(sample, &mut packet_0, &mut packet_number); + assert_eq!(packet_0, 0x4c); + assert_eq!(packet_number, [0xfe, 0x41, 0x89]); + + k.decrypt_in_place(sample, &mut packet_0, &mut packet_number); + assert_eq!(packet_0, 0x42); + assert_eq!(packet_number, [0x00, 0xbf, 0xf4]); + } +} From da5147352ea61b6f9484d5484aa8550dc1a007d4 Mon Sep 17 00:00:00 2001 From: Joe Birr-Pixton Date: Mon, 10 Feb 2025 15:57:22 +0000 Subject: [PATCH 2/5] Add QUIC support to TLS1.3 suites --- rustls-graviola/src/lib.rs | 1 + rustls-graviola/src/quic.rs | 132 ++++++++++++++++++++++++++++++++++ rustls-graviola/src/suites.rs | 8 +-- 3 files changed, 137 insertions(+), 4 deletions(-) create mode 100644 rustls-graviola/src/quic.rs diff --git a/rustls-graviola/src/lib.rs b/rustls-graviola/src/lib.rs index e8a59fe8a..6473f4382 100644 --- a/rustls-graviola/src/lib.rs +++ b/rustls-graviola/src/lib.rs @@ -18,6 +18,7 @@ use rustls::crypto::CryptoProvider; mod aead; mod hash; mod hmac; +mod quic; mod sign; mod verify; diff --git a/rustls-graviola/src/quic.rs b/rustls-graviola/src/quic.rs new file mode 100644 index 000000000..b695bf54d --- /dev/null +++ b/rustls-graviola/src/quic.rs @@ -0,0 +1,132 @@ +use rustls::crypto::cipher::{AeadKey, Iv, Nonce}; +use rustls::quic; +use rustls::Error; + +pub(crate) struct Aes128Gcm; + +impl quic::Algorithm for Aes128Gcm { + fn packet_key(&self, key: AeadKey, iv: Iv) -> Box { + Box::new(AesGcmPacketKey::new(key, iv)) + } + + fn header_protection_key(&self, key: AeadKey) -> Box { + Box::new(AesHeaderProtectionKey( + graviola::aead::quic::AesHeaderProtection::new(key.as_ref()), + )) + } + + fn aead_key_len(&self) -> usize { + 16 + } +} + +pub(crate) struct Aes256Gcm; + +impl quic::Algorithm for Aes256Gcm { + fn packet_key(&self, key: AeadKey, iv: Iv) -> Box { + Box::new(AesGcmPacketKey::new(key, iv)) + } + + fn header_protection_key(&self, key: AeadKey) -> Box { + Box::new(AesHeaderProtectionKey( + graviola::aead::quic::AesHeaderProtection::new(key.as_ref()), + )) + } + + fn aead_key_len(&self) -> usize { + 32 + } +} + +struct AesGcmPacketKey { + key: graviola::aead::AesGcm, + iv: Iv, +} + +impl AesGcmPacketKey { + fn new(key: AeadKey, iv: Iv) -> Self { + Self { + key: graviola::aead::AesGcm::new(key.as_ref()), + iv, + } + } +} + +impl quic::PacketKey for AesGcmPacketKey { + fn encrypt_in_place( + &self, + packet_number: u64, + header: &[u8], + payload: &mut [u8], + ) -> Result { + let mut tag = [0u8; 16]; + let nonce = Nonce::new(&self.iv, packet_number); + self.key.encrypt(&nonce.0, header, payload, &mut tag); + Ok(quic::Tag::from(&tag[..])) + } + + fn decrypt_in_place<'a>( + &self, + packet_number: u64, + header: &[u8], + payload: &'a mut [u8], + ) -> Result<&'a [u8], Error> { + let nonce = Nonce::new(&self.iv, packet_number); + + let (cipher, tag) = if payload.len() >= 16 { + payload.split_at_mut(payload.len() - 16) + } else { + return Err(Error::DecryptError); + }; + + self.key + .decrypt(&nonce.0, header, cipher, tag) + .map_err(|_| Error::DecryptError)?; + + Ok(cipher) + } + + fn tag_len(&self) -> usize { + 16 + } + + fn confidentiality_limit(&self) -> u64 { + // ref: + 1 << 23 + } + + fn integrity_limit(&self) -> u64 { + // ref: + 1 << 52 + } +} + +struct AesHeaderProtectionKey(graviola::aead::quic::AesHeaderProtection); + +impl quic::HeaderProtectionKey for AesHeaderProtectionKey { + fn encrypt_in_place( + &self, + sample: &[u8], + first: &mut u8, + packet_number: &mut [u8], + ) -> Result<(), Error> { + let sample16 = sample.try_into().map_err(|_| Error::EncryptError)?; + self.0.encrypt_in_place(sample16, first, packet_number); + Ok(()) + } + + fn decrypt_in_place( + &self, + sample: &[u8], + first: &mut u8, + packet_number: &mut [u8], + ) -> Result<(), Error> { + let sample16 = sample.try_into().map_err(|_| Error::EncryptError)?; + self.0.decrypt_in_place(sample16, first, packet_number); + Ok(()) + } + + fn sample_len(&self) -> usize { + 16 + } +} diff --git a/rustls-graviola/src/suites.rs b/rustls-graviola/src/suites.rs index f328f8d4e..6bf8c452c 100644 --- a/rustls-graviola/src/suites.rs +++ b/rustls-graviola/src/suites.rs @@ -5,7 +5,7 @@ use rustls::{ CipherSuite, SignatureScheme, SupportedCipherSuite, Tls12CipherSuite, Tls13CipherSuite, }; -use super::{aead, hash, hmac}; +use super::{aead, hash, hmac, quic}; /// All supported cipher suites, in priority order. pub static ALL_CIPHER_SUITES: &[SupportedCipherSuite] = &[ @@ -30,7 +30,7 @@ pub static TLS13_AES_256_GCM_SHA384: SupportedCipherSuite = }, hkdf_provider: &HkdfUsingHmac(&hmac::Sha384Hmac), aead_alg: &aead::TlsAesGcm(32), - quic: None, + quic: Some(&quic::Aes256Gcm), }); /// The TLS1.3 `TLS_AES_128_GCM_SHA256` cipher suite. @@ -43,7 +43,7 @@ pub static TLS13_AES_128_GCM_SHA256: SupportedCipherSuite = }, hkdf_provider: &HkdfUsingHmac(&hmac::Sha256Hmac), aead_alg: &aead::TlsAesGcm(16), - quic: None, + quic: Some(&quic::Aes128Gcm), }); /// The TLS1.3 `TLS_CHACHA20_POLY1305_SHA256` cipher suite. @@ -56,7 +56,7 @@ pub static TLS13_CHACHA20_POLY1305_SHA256: SupportedCipherSuite = }, hkdf_provider: &HkdfUsingHmac(&hmac::Sha256Hmac), aead_alg: &aead::Chacha20Poly1305, - quic: None, + quic: None, //Some(quic::CHACHA20_POLY1305), }); /// The TLS1.2 `TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256` cipher suite. From 353c52a3aeec90b9f33aec114bbdefbe6a7aaf62 Mon Sep 17 00:00:00 2001 From: Joe Birr-Pixton Date: Thu, 13 Feb 2025 18:02:34 +0000 Subject: [PATCH 3/5] tmp: quic examples borrowed from quinn repo --- Cargo.lock | 619 ++++++++++++++++++++++++ rustls-graviola/Cargo.toml | 12 + rustls-graviola/examples/quic-client.rs | 169 +++++++ rustls-graviola/examples/quic-server.rs | 269 ++++++++++ 4 files changed, 1069 insertions(+) create mode 100644 rustls-graviola/examples/quic-client.rs create mode 100644 rustls-graviola/examples/quic-server.rs diff --git a/Cargo.lock b/Cargo.lock index c9ef2bd33..f28627470 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -166,6 +166,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.8.0" @@ -322,6 +328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -330,8 +337,22 @@ version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ + "anstream", "anstyle", "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -616,6 +637,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + [[package]] name = "derive_arbitrary" version = "1.4.2" @@ -639,6 +669,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dunce" version = "1.0.5" @@ -799,6 +840,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + [[package]] name = "fs_extra" version = "1.3.0" @@ -862,8 +912,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -873,9 +925,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", + "wasm-bindgen", ] [[package]] @@ -1030,6 +1084,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.5.2" @@ -1179,6 +1239,108 @@ dependencies = [ "tracing", ] +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "2.12.1" @@ -1403,12 +1565,33 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + [[package]] name = "log" version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "memchr" version = "2.7.6" @@ -1450,6 +1633,15 @@ dependencies = [ "libc", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -1476,6 +1668,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + [[package]] name = "num-integer" version = "0.1.46" @@ -1506,6 +1704,15 @@ dependencies = [ "libm", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -1604,6 +1811,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec" +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1613,6 +1830,32 @@ dependencies = [ "base64ct", ] +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1718,6 +1961,21 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1792,6 +2050,61 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.42" @@ -1894,6 +2207,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + [[package]] name = "regex" version = "1.12.2" @@ -1968,6 +2294,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -2010,6 +2342,8 @@ dependencies = [ name = "rustls-graviola" version = "0.3.3" dependencies = [ + "anyhow", + "clap", "env_logger", "graviola", "http", @@ -2018,9 +2352,17 @@ dependencies = [ "hyper-rustls", "hyper-util", "libcrux-ml-kem", + "quinn", + "quinn-proto", + "rcgen", "rustls", + "rustls-pemfile", "tokio", "tokio-rustls", + "tracing", + "tracing-futures", + "tracing-subscriber", + "url", ] [[package]] @@ -2035,12 +2377,22 @@ dependencies = [ "security-framework", ] +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" dependencies = [ + "web-time", "zeroize", ] @@ -2205,6 +2557,15 @@ dependencies = [ "keccak", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -2259,6 +2620,12 @@ dependencies = [ "der", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "statrs" version = "0.18.0" @@ -2269,6 +2636,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -2286,6 +2659,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tempfile" version = "3.24.0" @@ -2299,6 +2683,78 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -2309,6 +2765,21 @@ dependencies = [ "serde_json", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.48.0" @@ -2370,6 +2841,7 @@ version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ + "log", "pin-project-lite", "tracing-core", ] @@ -2383,6 +2855,33 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "thread_local", + "time", + "tracing", + "tracing-core", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -2423,6 +2922,24 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -2550,6 +3067,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi-util" version = "0.1.11" @@ -2736,6 +3263,12 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + [[package]] name = "x25519-dalek" version = "2.0.1" @@ -2752,6 +3285,38 @@ dependencies = [ name = "x86-cpuid" version = "0.1.0" +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.30" @@ -2772,6 +3337,27 @@ dependencies = [ "syn", ] +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.2" @@ -2791,3 +3377,36 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/rustls-graviola/Cargo.toml b/rustls-graviola/Cargo.toml index 389c117f2..00497e7da 100644 --- a/rustls-graviola/Cargo.toml +++ b/rustls-graviola/Cargo.toml @@ -15,6 +15,8 @@ libcrux-ml-kem = { version = "0.0.6", default-features = false, features = ["mlk rustls = { version = "0.23.18", default-features = false, features = ["std", "tls12"] } [dev-dependencies] +anyhow = "1" +clap = { version = "4", features = ["derive"] } env_logger = "0.11" http = "1" http-body-util = "0.1" @@ -25,6 +27,16 @@ rustls = { version = "0.23.23", default-features = false, features = ["aws-lc-rs tokio = { version = "1.0", features = ["io-std", "macros", "net", "rt-multi-thread"] } tokio-rustls = { version = "0.26", default-features = false } +# quic stuff. TODO(matheus23): Look through this again +quinn = { version = "0.11", default-features = false, features = ["log", "runtime-tokio", "rustls"] } +quinn-proto = { version = "0.11", default-features = false, features = ["log", "rustls"] } +rcgen = "0.13" +rustls-pemfile = "2" +tracing = { version = "0.1", default-features = false, features = ["std"] } +tracing-futures = { version = "0.2.0", default-features = false, features = ["std-future"] } +tracing-subscriber = { version = "0.3", default-features = false, features = ["env-filter", "fmt", "ansi", "time", "local-time"] } +url = "2" + [features] default = ["libcrux-ml-kem"] libcrux-ml-kem = ["dep:libcrux-ml-kem"] diff --git a/rustls-graviola/examples/quic-client.rs b/rustls-graviola/examples/quic-client.rs new file mode 100644 index 000000000..f13a7d9e9 --- /dev/null +++ b/rustls-graviola/examples/quic-client.rs @@ -0,0 +1,169 @@ +//! This example demonstrates an HTTP client that requests files from a server. +//! +//! Checkout the `README.md` for guidance. + +use std::{ + fs, + io::{self, Write}, + net::{SocketAddr, ToSocketAddrs}, + path::PathBuf, + sync::Arc, + time::{Duration, Instant}, +}; + +use anyhow::{anyhow, Result}; +use clap::Parser; +use quinn_proto::crypto::rustls::QuicClientConfig; +use rustls::pki_types::CertificateDer; +use tracing::{error, info}; +use url::Url; + +/// HTTP/0.9 over QUIC client +#[derive(Parser, Debug)] +#[clap(name = "client")] +struct Opt { + /// Perform NSS-compatible TLS key logging to the file specified in `SSLKEYLOGFILE`. + #[clap(long = "keylog")] + keylog: bool, + + url: Url, + + /// Override hostname used for certificate verification + #[clap(long = "host")] + host: Option, + + /// Custom certificate authority to trust, in DER format + #[clap(long = "ca")] + ca: Option, + + /// Simulate NAT rebinding after connecting + #[clap(long = "rebind")] + rebind: bool, + + /// Address to bind on + #[clap(long = "bind", default_value = "[::]:0")] + bind: SocketAddr, +} + +fn main() { + rustls_graviola::default_provider().install_default().unwrap(); + tracing::subscriber::set_global_default( + tracing_subscriber::FmtSubscriber::builder() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .finish(), + ) + .unwrap(); + let opt = Opt::parse(); + let code = { + if let Err(e) = run(opt) { + eprintln!("ERROR: {e}"); + 1 + } else { + 0 + } + }; + ::std::process::exit(code); +} + +#[tokio::main] +async fn run(options: Opt) -> Result<()> { + let url = options.url; + let url_host = strip_ipv6_brackets(url.host_str().unwrap()); + let remote = (url_host, url.port().unwrap_or(4433)) + .to_socket_addrs()? + .next() + .ok_or_else(|| anyhow!("couldn't resolve to an address"))?; + + let mut roots = rustls::RootCertStore::empty(); + if let Some(ca_path) = options.ca { + roots.add(CertificateDer::from(fs::read(ca_path)?))?; + } else { + match fs::read("cert.der") { + Ok(cert) => { + roots.add(CertificateDer::from(cert))?; + } + Err(ref e) if e.kind() == io::ErrorKind::NotFound => { + info!("local server certificate not found"); + } + Err(e) => { + error!("failed to open local server certificate: {}", e); + } + } + } + let mut client_crypto = rustls::ClientConfig::builder() + .with_root_certificates(roots) + .with_no_client_auth(); + + client_crypto.alpn_protocols = ALPN_QUIC_HTTP.iter().map(|&x| x.into()).collect(); + if options.keylog { + client_crypto.key_log = Arc::new(rustls::KeyLogFile::new()); + } + + let client_config = + quinn::ClientConfig::new(Arc::new(QuicClientConfig::try_from(client_crypto)?)); + let mut endpoint = quinn::Endpoint::client(options.bind)?; + endpoint.set_default_client_config(client_config); + + let request = format!("GET {}\r\n", url.path()); + let start = Instant::now(); + let rebind = options.rebind; + let host = options.host.as_deref().unwrap_or(url_host); + + eprintln!("connecting to {host} at {remote}"); + let conn = endpoint + .connect(remote, host)? + .await + .map_err(|e| anyhow!("failed to connect: {}", e))?; + eprintln!("connected at {:?}", start.elapsed()); + let (mut send, mut recv) = conn + .open_bi() + .await + .map_err(|e| anyhow!("failed to open stream: {}", e))?; + if rebind { + let socket = std::net::UdpSocket::bind("[::]:0").unwrap(); + let addr = socket.local_addr().unwrap(); + eprintln!("rebinding to {addr}"); + endpoint.rebind(socket).expect("rebind failed"); + } + + send.write_all(request.as_bytes()) + .await + .map_err(|e| anyhow!("failed to send request: {}", e))?; + send.finish().unwrap(); + let response_start = Instant::now(); + eprintln!("request sent at {:?}", response_start - start); + let resp = recv + .read_to_end(usize::MAX) + .await + .map_err(|e| anyhow!("failed to read response: {}", e))?; + let duration = response_start.elapsed(); + eprintln!( + "response received in {:?} - {} KiB/s", + duration, + resp.len() as f32 / (duration_secs(&duration) * 1024.0) + ); + io::stdout().write_all(&resp).unwrap(); + io::stdout().flush().unwrap(); + conn.close(0u32.into(), b"done"); + + // Give the server a fair chance to receive the close packet + endpoint.wait_idle().await; + + Ok(()) +} + +fn strip_ipv6_brackets(host: &str) -> &str { + // An ipv6 url looks like eg https://[::1]:4433/Cargo.toml, wherein the host [::1] is the + // ipv6 address ::1 wrapped in brackets, per RFC 2732. This strips those. + if host.starts_with('[') && host.ends_with(']') { + &host[1..host.len() - 1] + } else { + host + } +} + +fn duration_secs(x: &Duration) -> f32 { + x.as_secs() as f32 + x.subsec_nanos() as f32 * 1e-9 +} + +const ALPN_QUIC_HTTP: &[&[u8]] = &[b"hq-29"]; diff --git a/rustls-graviola/examples/quic-server.rs b/rustls-graviola/examples/quic-server.rs new file mode 100644 index 000000000..6900d70f0 --- /dev/null +++ b/rustls-graviola/examples/quic-server.rs @@ -0,0 +1,269 @@ +//! This example demonstrates an HTTP server that serves files from a directory. +//! +//! Checkout the `README.md` for guidance. + +use std::{ + ascii, fs, io, + net::SocketAddr, + path::{self, Path, PathBuf}, + str, + sync::Arc, +}; + +use anyhow::{anyhow, bail, Context, Result}; +use clap::Parser; +use quinn_proto::crypto::rustls::QuicServerConfig; +use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}; +use tracing::{error, info, info_span}; +use tracing_futures::Instrument as _; + +#[derive(Parser, Debug)] +#[clap(name = "server")] +struct Opt { + /// file to log TLS keys to for debugging + #[clap(long = "keylog")] + keylog: bool, + /// directory to serve files from + root: PathBuf, + /// TLS private key in PEM format + #[clap(short = 'k', long = "key", requires = "cert")] + key: Option, + /// TLS certificate in PEM format + #[clap(short = 'c', long = "cert", requires = "key")] + cert: Option, + /// Enable stateless retries + #[clap(long = "stateless-retry")] + stateless_retry: bool, + /// Address to listen on + #[clap(long = "listen", default_value = "[::1]:4433")] + listen: SocketAddr, + /// Client address to block + #[clap(long = "block")] + block: Option, + /// Maximum number of concurrent connections to allow + #[clap(long = "connection-limit")] + connection_limit: Option, +} + +fn main() { + rustls_graviola::default_provider().install_default().unwrap(); + tracing::subscriber::set_global_default( + tracing_subscriber::FmtSubscriber::builder() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .finish(), + ) + .unwrap(); + let opt = Opt::parse(); + let code = { + if let Err(e) = run(opt) { + eprintln!("ERROR: {e}"); + 1 + } else { + 0 + } + }; + ::std::process::exit(code); +} + +#[tokio::main] +async fn run(options: Opt) -> Result<()> { + let (certs, key) = if let (Some(key_path), Some(cert_path)) = (&options.key, &options.cert) { + let key = fs::read(key_path).context("failed to read private key")?; + let key = if key_path.extension().is_some_and(|x| x == "der") { + PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key)) + } else { + rustls_pemfile::private_key(&mut &*key) + .context("malformed PKCS #1 private key")? + .ok_or_else(|| anyhow::Error::msg("no private keys found"))? + }; + let cert_chain = fs::read(cert_path).context("failed to read certificate chain")?; + let cert_chain = if cert_path.extension().is_some_and(|x| x == "der") { + vec![CertificateDer::from(cert_chain)] + } else { + rustls_pemfile::certs(&mut &*cert_chain) + .collect::>() + .context("invalid PEM-encoded certificate")? + }; + + (cert_chain, key) + } else { + let cert_path = "cert.der"; + let key_path = "key.der"; + let (cert, key) = match fs::read(&cert_path).and_then(|x| Ok((x, fs::read(&key_path)?))) { + Ok((cert, key)) => ( + CertificateDer::from(cert), + PrivateKeyDer::try_from(key).map_err(anyhow::Error::msg)?, + ), + Err(ref e) if e.kind() == io::ErrorKind::NotFound => { + info!("generating self-signed certificate"); + let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); + let key = PrivatePkcs8KeyDer::from(cert.key_pair.serialize_der()); + let cert = cert.cert.into(); + fs::write(&cert_path, &cert).context("failed to write certificate")?; + fs::write(&key_path, key.secret_pkcs8_der()) + .context("failed to write private key")?; + (cert, key.into()) + } + Err(e) => { + bail!("failed to read certificate: {}", e); + } + }; + + (vec![cert], key) + }; + + let mut server_crypto = rustls::ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(certs, key)?; + server_crypto.alpn_protocols = ALPN_QUIC_HTTP.iter().map(|&x| x.into()).collect(); + if options.keylog { + server_crypto.key_log = Arc::new(rustls::KeyLogFile::new()); + } + + let mut server_config = + quinn::ServerConfig::with_crypto(Arc::new(QuicServerConfig::try_from(server_crypto)?)); + let transport_config = Arc::get_mut(&mut server_config.transport).unwrap(); + transport_config.max_concurrent_uni_streams(0_u8.into()); + + let root = Arc::::from(options.root.clone()); + if !root.exists() { + bail!("root path does not exist"); + } + + let endpoint = quinn::Endpoint::server(server_config, options.listen)?; + eprintln!("listening on {}", endpoint.local_addr()?); + + while let Some(conn) = endpoint.accept().await { + if options + .connection_limit + .is_some_and(|n| endpoint.open_connections() >= n) + { + info!("refusing due to open connection limit"); + conn.refuse(); + } else if Some(conn.remote_address()) == options.block { + info!("refusing blocked client IP address"); + conn.refuse(); + } else if options.stateless_retry && !conn.remote_address_validated() { + info!("requiring connection to validate its address"); + conn.retry().unwrap(); + } else { + info!("accepting connection"); + let fut = handle_connection(root.clone(), conn); + tokio::spawn(async move { + if let Err(e) = fut.await { + error!("connection failed: {reason}", reason = e.to_string()) + } + }); + } + } + + Ok(()) +} + +async fn handle_connection(root: Arc, conn: quinn::Incoming) -> Result<()> { + let connection = conn.await?; + let span = info_span!( + "connection", + remote = %connection.remote_address(), + protocol = %connection + .handshake_data() + .unwrap() + .downcast::().unwrap() + .protocol + .map_or_else(|| "".into(), |x| String::from_utf8_lossy(&x).into_owned()) + ); + async { + info!("established"); + + // Each stream initiated by the client constitutes a new request. + loop { + let stream = connection.accept_bi().await; + let stream = match stream { + Err(quinn::ConnectionError::ApplicationClosed { .. }) => { + info!("connection closed"); + return Ok(()); + } + Err(e) => { + return Err(e); + } + Ok(s) => s, + }; + let fut = handle_request(root.clone(), stream); + tokio::spawn( + async move { + if let Err(e) = fut.await { + error!("failed: {reason}", reason = e.to_string()); + } + } + .instrument(info_span!("request")), + ); + } + } + .instrument(span) + .await?; + Ok(()) +} + +async fn handle_request( + root: Arc, + (mut send, mut recv): (quinn::SendStream, quinn::RecvStream), +) -> Result<()> { + let req = recv + .read_to_end(64 * 1024) + .await + .map_err(|e| anyhow!("failed reading request: {}", e))?; + let mut escaped = String::new(); + for &x in &req[..] { + let part = ascii::escape_default(x).collect::>(); + escaped.push_str(str::from_utf8(&part).unwrap()); + } + info!(content = %escaped); + // Execute the request + let resp = process_get(&root, &req).unwrap_or_else(|e| { + error!("failed: {}", e); + format!("failed to process request: {e}\n").into_bytes() + }); + // Write the response + send.write_all(&resp) + .await + .map_err(|e| anyhow!("failed to send response: {}", e))?; + // Gracefully terminate the stream + send.finish().unwrap(); + info!("complete"); + Ok(()) +} + +fn process_get(root: &Path, x: &[u8]) -> Result> { + if x.len() < 4 || &x[0..4] != b"GET " { + bail!("missing GET"); + } + if x[4..].len() < 2 || &x[x.len() - 2..] != b"\r\n" { + bail!("missing \\r\\n"); + } + let x = &x[4..x.len() - 2]; + let end = x.iter().position(|&c| c == b' ').unwrap_or(x.len()); + let path = str::from_utf8(&x[..end]).context("path is malformed UTF-8")?; + let path = Path::new(&path); + let mut real_path = PathBuf::from(root); + let mut components = path.components(); + match components.next() { + Some(path::Component::RootDir) => {} + _ => { + bail!("path must be absolute"); + } + } + for c in components { + match c { + path::Component::Normal(x) => { + real_path.push(x); + } + x => { + bail!("illegal component in path: {:?}", x); + } + } + } + let data = fs::read(&real_path).context("failed reading file")?; + Ok(data) +} + +const ALPN_QUIC_HTTP: &[&[u8]] = &[b"hq-29"]; From a6668168d0ee245534ad8c8a825994d68f7105e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Kr=C3=BCger?= Date: Fri, 13 Mar 2026 15:31:41 +0100 Subject: [PATCH 4/5] Implement QUIC multipath encryption functions --- rustls-graviola/src/quic.rs | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/rustls-graviola/src/quic.rs b/rustls-graviola/src/quic.rs index b695bf54d..328dc5ca3 100644 --- a/rustls-graviola/src/quic.rs +++ b/rustls-graviola/src/quic.rs @@ -1,6 +1,6 @@ +use rustls::Error; use rustls::crypto::cipher::{AeadKey, Iv, Nonce}; use rustls::quic; -use rustls::Error; pub(crate) struct Aes128Gcm; @@ -65,6 +65,19 @@ impl quic::PacketKey for AesGcmPacketKey { Ok(quic::Tag::from(&tag[..])) } + fn encrypt_in_place_for_path( + &self, + path_id: u32, + packet_number: u64, + header: &[u8], + payload: &mut [u8], + ) -> Result { + let mut tag = [0u8; 16]; + let nonce = Nonce::for_path(path_id, &self.iv, packet_number); + self.key.encrypt(&nonce.0, header, payload, &mut tag); + Ok(quic::Tag::from(&tag[..])) + } + fn decrypt_in_place<'a>( &self, packet_number: u64, @@ -86,6 +99,28 @@ impl quic::PacketKey for AesGcmPacketKey { Ok(cipher) } + fn decrypt_in_place_for_path<'a>( + &self, + path_id: u32, + packet_number: u64, + header: &[u8], + payload: &'a mut [u8], + ) -> Result<&'a [u8], Error> { + let nonce = Nonce::for_path(path_id, &self.iv, packet_number); + + let (cipher, tag) = if payload.len() >= 16 { + payload.split_at_mut(payload.len() - 16) + } else { + return Err(Error::DecryptError); + }; + + self.key + .decrypt(&nonce.0, header, cipher, tag) + .map_err(|_| Error::DecryptError)?; + + Ok(cipher) + } + fn tag_len(&self) -> usize { 16 } From d91f0ba584a4236b3917c75dd4d3fb6722095205 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Kr=C3=BCger?= Date: Fri, 13 Mar 2026 16:03:51 +0100 Subject: [PATCH 5/5] Implement the quic examples --- Cargo.lock | 198 +++++++++++++++++------- rustls-graviola/Cargo.toml | 3 +- rustls-graviola/examples/quic-client.rs | 52 ++++++- rustls-graviola/examples/quic-server.rs | 107 +++++++++++-- 4 files changed, 281 insertions(+), 79 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f28627470..5a415aee4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -448,6 +448,15 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -657,6 +666,29 @@ dependencies = [ "syn", ] +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "unicode-xid", +] + [[package]] name = "digest" version = "0.10.7" @@ -752,6 +784,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "enum-assoc" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed8956bd5c1f0415200516e78ff07ec9e16415ade83c056c230d7b7ea0d55b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "env_filter" version = "0.1.4" @@ -1320,6 +1363,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "identity-hash" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfdd7caa900436d8f13b2346fe10257e0c05c1f1f9e351f4f5d57c03bd5f45da" + [[package]] name = "idna" version = "1.1.0" @@ -1633,6 +1682,66 @@ dependencies = [ "libc", ] +[[package]] +name = "noq" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df966fb44ac763bc86da97fa6c811c54ae82ef656575949f93c6dae0c9f09bf" +dependencies = [ + "bytes", + "cfg_aliases", + "noq-proto", + "noq-udp", + "pin-project-lite", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "web-time", +] + +[[package]] +name = "noq-proto" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c61b72abd670eebc05b5cf720e077b04a3ef3354bc7bc19f1c3524cb424db7b" +dependencies = [ + "aes-gcm", + "bytes", + "derive_more", + "enum-assoc", + "getrandom 0.3.4", + "identity-hash", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "sorted-index-buffer", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "noq-udp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb9be4fedd6b98f3ba82ccd3506f4d0219fb723c3f97c67e12fe1494aa020e44" +dependencies = [ + "cfg_aliases", + "libc", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -2050,61 +2159,6 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" -dependencies = [ - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.2", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.60.2", -] - [[package]] name = "quote" version = "1.0.42" @@ -2352,8 +2406,7 @@ dependencies = [ "hyper-rustls", "hyper-util", "libcrux-ml-kem", - "quinn", - "quinn-proto", + "noq", "rcgen", "rustls", "rustls-pemfile", @@ -2604,6 +2657,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "sorted-index-buffer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea06cc588e43c632923a55450401b8f25e628131571d4e1baea1bdfdb2b5ed06" + [[package]] name = "spin" version = "0.9.8" @@ -2816,6 +2875,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + [[package]] name = "tokio-util" version = "0.7.17" @@ -2841,7 +2912,6 @@ version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ - "log", "pin-project-lite", "tracing-core", ] @@ -2906,6 +2976,18 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "universal-hash" version = "0.5.1" diff --git a/rustls-graviola/Cargo.toml b/rustls-graviola/Cargo.toml index 00497e7da..57cf36435 100644 --- a/rustls-graviola/Cargo.toml +++ b/rustls-graviola/Cargo.toml @@ -28,8 +28,7 @@ tokio = { version = "1.0", features = ["io-std", "macros", "net", "rt-multi-thre tokio-rustls = { version = "0.26", default-features = false } # quic stuff. TODO(matheus23): Look through this again -quinn = { version = "0.11", default-features = false, features = ["log", "runtime-tokio", "rustls"] } -quinn-proto = { version = "0.11", default-features = false, features = ["log", "rustls"] } +noq = { version = "0.17", default-features = false, features = ["runtime-tokio", "rustls"] } rcgen = "0.13" rustls-pemfile = "2" tracing = { version = "0.1", default-features = false, features = ["std"] } diff --git a/rustls-graviola/examples/quic-client.rs b/rustls-graviola/examples/quic-client.rs index f13a7d9e9..a872c4159 100644 --- a/rustls-graviola/examples/quic-client.rs +++ b/rustls-graviola/examples/quic-client.rs @@ -11,9 +11,9 @@ use std::{ time::{Duration, Instant}, }; -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use clap::Parser; -use quinn_proto::crypto::rustls::QuicClientConfig; +use noq::{EndpointConfig, Runtime, TokioRuntime, crypto::rustls::QuicClientConfig}; use rustls::pki_types::CertificateDer; use tracing::{error, info}; use url::Url; @@ -46,7 +46,6 @@ struct Opt { } fn main() { - rustls_graviola::default_provider().install_default().unwrap(); tracing::subscriber::set_global_default( tracing_subscriber::FmtSubscriber::builder() .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) @@ -67,6 +66,7 @@ fn main() { #[tokio::main] async fn run(options: Opt) -> Result<()> { + let provider = Arc::new(rustls_graviola::default_provider()); let url = options.url; let url_host = strip_ipv6_brackets(url.host_str().unwrap()); let remote = (url_host, url.port().unwrap_or(4433)) @@ -90,7 +90,8 @@ async fn run(options: Opt) -> Result<()> { } } } - let mut client_crypto = rustls::ClientConfig::builder() + let mut client_crypto = rustls::ClientConfig::builder_with_provider(provider.clone()) + .with_safe_default_protocol_versions()? .with_root_certificates(roots) .with_no_client_auth(); @@ -100,8 +101,14 @@ async fn run(options: Opt) -> Result<()> { } let client_config = - quinn::ClientConfig::new(Arc::new(QuicClientConfig::try_from(client_crypto)?)); - let mut endpoint = quinn::Endpoint::client(options.bind)?; + noq::ClientConfig::new(Arc::new(QuicClientConfig::try_from(client_crypto)?)); + let client_socket = std::net::UdpSocket::bind(options.bind)?; + let endpoint = noq::Endpoint::new_with_abstract_socket( + EndpointConfig::new(Arc::new(HmacKey::generate())), + None, + TokioRuntime.wrap_udp_socket(client_socket)?, + Arc::new(TokioRuntime), + )?; endpoint.set_default_client_config(client_config); let request = format!("GET {}\r\n", url.path()); @@ -167,3 +174,36 @@ fn duration_secs(x: &Duration) -> f32 { } const ALPN_QUIC_HTTP: &[&[u8]] = &[b"hq-29"]; + +struct HmacKey([u8; 32]); + +impl HmacKey { + pub fn generate() -> Self { + let mut key = [0u8; 32]; + graviola::random::fill(&mut key).unwrap(); + Self(key) + } +} + +impl noq::crypto::HmacKey for HmacKey { + fn sign(&self, data: &[u8], signature_out: &mut [u8]) { + let mut hmac = graviola::hashing::hmac::Hmac::::new(self.0); + hmac.update(data); + let out = hmac.finish(); + signature_out.copy_from_slice(out.as_ref()); + } + + fn signature_len(&self) -> usize { + graviola::hashing::sha2::Sha256Context::OUTPUT_SZ + } + + fn verify( + &self, + data: &[u8], + signature: &[u8], + ) -> std::result::Result<(), noq::crypto::CryptoError> { + let mut hmac = graviola::hashing::hmac::Hmac::::new(self.0); + hmac.update(data); + hmac.verify(signature).map_err(|_| noq::crypto::CryptoError) + } +} diff --git a/rustls-graviola/examples/quic-server.rs b/rustls-graviola/examples/quic-server.rs index 6900d70f0..c4340c070 100644 --- a/rustls-graviola/examples/quic-server.rs +++ b/rustls-graviola/examples/quic-server.rs @@ -10,9 +10,9 @@ use std::{ sync::Arc, }; -use anyhow::{anyhow, bail, Context, Result}; +use anyhow::{Context, Result, anyhow, bail}; use clap::Parser; -use quinn_proto::crypto::rustls::QuicServerConfig; +use noq::{Runtime, TokioRuntime, crypto::rustls::QuicServerConfig}; use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}; use tracing::{error, info, info_span}; use tracing_futures::Instrument as _; @@ -46,7 +46,6 @@ struct Opt { } fn main() { - rustls_graviola::default_provider().install_default().unwrap(); tracing::subscriber::set_global_default( tracing_subscriber::FmtSubscriber::builder() .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) @@ -112,16 +111,20 @@ async fn run(options: Opt) -> Result<()> { (vec![cert], key) }; - let mut server_crypto = rustls::ServerConfig::builder() - .with_no_client_auth() - .with_single_cert(certs, key)?; + let mut server_crypto = + rustls::ServerConfig::builder_with_provider(Arc::new(rustls_graviola::default_provider())) + .with_safe_default_protocol_versions()? + .with_no_client_auth() + .with_single_cert(certs, key)?; server_crypto.alpn_protocols = ALPN_QUIC_HTTP.iter().map(|&x| x.into()).collect(); if options.keylog { server_crypto.key_log = Arc::new(rustls::KeyLogFile::new()); } - let mut server_config = - quinn::ServerConfig::with_crypto(Arc::new(QuicServerConfig::try_from(server_crypto)?)); + let mut server_config = noq::ServerConfig::new( + Arc::new(QuicServerConfig::try_from(server_crypto)?), + Arc::new(TokenKey::generate()), + ); let transport_config = Arc::get_mut(&mut server_config.transport).unwrap(); transport_config.max_concurrent_uni_streams(0_u8.into()); @@ -130,7 +133,13 @@ async fn run(options: Opt) -> Result<()> { bail!("root path does not exist"); } - let endpoint = quinn::Endpoint::server(server_config, options.listen)?; + let socket = std::net::UdpSocket::bind(options.listen)?; + let endpoint = noq::Endpoint::new_with_abstract_socket( + noq::EndpointConfig::new(Arc::new(HmacKey::generate())), + Some(server_config), + TokioRuntime.wrap_udp_socket(socket)?, + Arc::new(TokioRuntime), + )?; eprintln!("listening on {}", endpoint.local_addr()?); while let Some(conn) = endpoint.accept().await { @@ -160,7 +169,7 @@ async fn run(options: Opt) -> Result<()> { Ok(()) } -async fn handle_connection(root: Arc, conn: quinn::Incoming) -> Result<()> { +async fn handle_connection(root: Arc, conn: noq::Incoming) -> Result<()> { let connection = conn.await?; let span = info_span!( "connection", @@ -168,7 +177,7 @@ async fn handle_connection(root: Arc, conn: quinn::Incoming) -> Result<()> protocol = %connection .handshake_data() .unwrap() - .downcast::().unwrap() + .downcast::().unwrap() .protocol .map_or_else(|| "".into(), |x| String::from_utf8_lossy(&x).into_owned()) ); @@ -179,7 +188,7 @@ async fn handle_connection(root: Arc, conn: quinn::Incoming) -> Result<()> loop { let stream = connection.accept_bi().await; let stream = match stream { - Err(quinn::ConnectionError::ApplicationClosed { .. }) => { + Err(noq::ConnectionError::ApplicationClosed { .. }) => { info!("connection closed"); return Ok(()); } @@ -206,7 +215,7 @@ async fn handle_connection(root: Arc, conn: quinn::Incoming) -> Result<()> async fn handle_request( root: Arc, - (mut send, mut recv): (quinn::SendStream, quinn::RecvStream), + (mut send, mut recv): (noq::SendStream, noq::RecvStream), ) -> Result<()> { let req = recv .read_to_end(64 * 1024) @@ -267,3 +276,75 @@ fn process_get(root: &Path, x: &[u8]) -> Result> { } const ALPN_QUIC_HTTP: &[&[u8]] = &[b"hq-29"]; + +struct TokenKey(graviola::aead::XChaCha20Poly1305); + +impl TokenKey { + pub fn generate() -> Self { + let mut key = [0u8; 32]; + graviola::random::fill(&mut key).unwrap(); + Self(graviola::aead::XChaCha20Poly1305::new(key)) + } +} + +impl noq::crypto::HandshakeTokenKey for TokenKey { + fn seal( + &self, + token_nonce: u128, + data: &mut Vec, + ) -> std::result::Result<(), noq::crypto::CryptoError> { + let mut nonce = [0u8; 24]; + nonce.copy_from_slice(&token_nonce.to_le_bytes()); // 16 bytes of random nonce. + data.extend(&[0u8; 16]); + let (to_cipher, tag) = data.split_last_chunk_mut::<16>().unwrap(); + self.0.encrypt(&nonce, &[], to_cipher, tag); + Ok(()) + } + + fn open<'a>( + &self, + token_nonce: u128, + data: &'a mut [u8], + ) -> std::result::Result<&'a [u8], noq::crypto::CryptoError> { + let mut nonce = [0u8; 24]; + nonce.copy_from_slice(&token_nonce.to_le_bytes()); // 16 bytes of random nonce. + let (to_decipher, tag) = data.split_last_chunk_mut::<16>().unwrap(); + self.0 + .decrypt(&nonce, &[], to_decipher, tag) + .map_err(|_| noq::crypto::CryptoError)?; + Ok(to_decipher) + } +} + +struct HmacKey([u8; 32]); + +impl HmacKey { + pub fn generate() -> Self { + let mut key = [0u8; 32]; + graviola::random::fill(&mut key).unwrap(); + Self(key) + } +} + +impl noq::crypto::HmacKey for HmacKey { + fn sign(&self, data: &[u8], signature_out: &mut [u8]) { + let mut hmac = graviola::hashing::hmac::Hmac::::new(self.0); + hmac.update(data); + let out = hmac.finish(); + signature_out.copy_from_slice(out.as_ref()); + } + + fn signature_len(&self) -> usize { + graviola::hashing::sha2::Sha256Context::OUTPUT_SZ + } + + fn verify( + &self, + data: &[u8], + signature: &[u8], + ) -> std::result::Result<(), noq::crypto::CryptoError> { + let mut hmac = graviola::hashing::hmac::Hmac::::new(self.0); + hmac.update(data); + hmac.verify(signature).map_err(|_| noq::crypto::CryptoError) + } +}