diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b202be..c08d52d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,21 @@ jobs: run: cargo clippy --all-targets -- -D warnings - name: cargo clippy --no-default-features (warnings) run: cargo clippy --no-default-features --all-targets -- -D warnings + examples: + name: Run examples + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Install toolchain + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: stable + - name: Cache build artifacts + uses: Swatinem/rust-cache@v2 + - name: cargo run --example quinn + run: cargo run --example quinn --features quinn + test-fips-1-1-1: name: Test using FIPS openssl 1.1.1 diff --git a/Cargo.toml b/Cargo.toml index 5519a4a..b406521 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,19 +11,29 @@ readme = "README.md" [dependencies] foreign-types-shared = { version = "0.1.1", optional = true } +once_cell = "1.8.0" openssl = "0.10.68" openssl-sys = "0.9.104" +quinn = { version = "0.11.6", optional = true, default-features = false } rustls = { version = "0.23.0", default-features = false } rustls-webpki = { version = "0.102.2", default-features = false } -once_cell = "1.8.0" [features] default = ["tls12"] fips = [] tls12 = ["rustls/tls12", "foreign-types-shared"] +quinn = ["dep:quinn"] [dev-dependencies] +env_logger = "0.11.5" hex = "0.4.3" +quinn = { version = "0.11.6", default-features = false, features = [ + "runtime-tokio", + "log", +] } +quinn-proto = { version = "0.11.9", default-features = false, features = [ + "rustls", +] } rcgen = { version = "0.13.1", default-features = false, features = [ "aws_lc_rs", ] } @@ -31,8 +41,17 @@ rstest = "0.23.0" # Use aws_lc_rs to test our provider rustls = { version = "0.23.0", features = ["aws_lc_rs"] } rustls-pemfile = "2" +tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread"] } webpki-roots = "0.26" wycheproof = { version = "0.6.0", default-features = false, features = [ "aead", "hkdf", ] } + +[[example]] +name = "quinn" +path = "examples/quinn.rs" +required-features = ["quinn"] + +[package.metadata.docs.rs] +features = ["quinn"] diff --git a/examples/quinn.rs b/examples/quinn.rs new file mode 100644 index 0000000..2b35729 --- /dev/null +++ b/examples/quinn.rs @@ -0,0 +1,178 @@ +//! Example of creating Quinn client and server endpoints. +//! +//! Adapted from https://github.com/quinn-rs/quinn/blob/main/quinn/examples/single_socket.rs +use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket}; +use std::{error::Error, sync::Arc}; + +use quinn::crypto::rustls::{QuicClientConfig, QuicServerConfig}; +use quinn::{default_runtime, ClientConfig, Endpoint, EndpointConfig, ServerConfig}; +use rustls::client::WebPkiServerVerifier; +use rustls::pki_types::{CertificateDer, PrivatePkcs8KeyDer}; +use rustls_openssl::cipher_suite::TLS13_AES_128_GCM_SHA256; +use rustls_openssl::quinn::reset_key; + +/// Constructs a QUIC endpoint configured for use a client only. +/// +/// ## Args +/// - bind_addr: address to bind to. +/// - server_certs: list of trusted certificates. +fn make_client_endpoint( + bind_addr: SocketAddr, + server_certs: &[&[u8]], +) -> Result> { + let client_cfg = configure_client(server_certs)?; + let endpoint_config = EndpointConfig::new(reset_key()); + let socket = UdpSocket::bind(bind_addr).unwrap(); + let mut endpoint = + Endpoint::new(endpoint_config, None, socket, default_runtime().unwrap()).unwrap(); + endpoint.set_default_client_config(client_cfg); + Ok(endpoint) +} + +/// Constructs a QUIC endpoint configured to listen for incoming connections on a certain address +/// and port. +/// +/// ## Returns +/// - a stream of incoming QUIC connections +/// - server certificate serialized into DER format +fn make_server_endpoint( + bind_addr: SocketAddr, +) -> Result<(Endpoint, CertificateDer<'static>), Box> { + let (server_config, server_cert) = configure_server()?; + let endpoint_config = EndpointConfig::new(reset_key()); + let socket = UdpSocket::bind(bind_addr).unwrap(); + let endpoint = Endpoint::new( + endpoint_config, + Some(server_config), + socket, + default_runtime().unwrap(), + )?; + Ok((endpoint, server_cert)) +} + +/// Builds default quinn client config and trusts given certificates. +/// +/// ## Args +/// +/// - server_certs: a list of trusted certificates in DER format. +fn configure_client( + server_certs: &[&[u8]], +) -> Result> { + let mut certs = rustls::RootCertStore::empty(); + for cert in server_certs { + certs.add(CertificateDer::from(*cert))?; + } + + let verifier = WebPkiServerVerifier::builder_with_provider( + Arc::new(certs), + Arc::new(rustls_openssl::default_provider()), + ) + .build() + .unwrap(); + + let mut rustls_config = + rustls::ClientConfig::builder_with_provider(Arc::new(rustls_openssl::default_provider())) + .with_protocol_versions(&[&rustls::version::TLS13]) + .unwrap() + .dangerous() + .with_custom_certificate_verifier(verifier) + .with_no_client_auth(); + rustls_config.enable_early_data = true; + + let suite = TLS13_AES_128_GCM_SHA256 + .tls13() + .unwrap() + .quic_suite() + .unwrap(); + let quic_client_config = + QuicClientConfig::with_initial(Arc::new(rustls_config), suite).unwrap(); + Ok(ClientConfig::new(Arc::new(quic_client_config))) +} + +/// Returns default server configuration along with its certificate. +fn configure_server( +) -> Result<(ServerConfig, CertificateDer<'static>), Box> { + let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); + let cert_der = CertificateDer::from(cert.cert); + let priv_key = PrivatePkcs8KeyDer::from(cert.key_pair.serialize_der()); + + let mut rustls_config = + rustls::ServerConfig::builder_with_provider(Arc::new(rustls_openssl::default_provider())) + .with_protocol_versions(&[&rustls::version::TLS13]) + .unwrap() + .with_no_client_auth() + .with_single_cert(vec![cert_der.clone()], priv_key.into())?; + rustls_config.max_early_data_size = u32::MAX; + + let quic_server_config = QuicServerConfig::with_initial( + Arc::new(rustls_config), + TLS13_AES_128_GCM_SHA256 + .tls13() + .unwrap() + .quic_suite() + .unwrap(), + ) + .unwrap(); + + let mut server_config = ServerConfig::new( + Arc::new(quic_server_config), + rustls_openssl::quinn::handshake_token_key(), + ); + + let transport_config = Arc::get_mut(&mut server_config.transport).unwrap(); + transport_config.max_concurrent_uni_streams(0_u8.into()); + + Ok((server_config, cert_der)) +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + env_logger::init(); + let addr1 = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 5000); + let addr2 = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 5001); + let addr3 = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 5002); + let server1_cert = run_server(addr1)?; + let server2_cert = run_server(addr2)?; + let server3_cert = run_server(addr3)?; + + let client = make_client_endpoint( + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0), + &[&server1_cert, &server2_cert, &server3_cert], + )?; + + // connect to multiple endpoints using the same socket/endpoint + tokio::join!( + run_client(&client, addr1), + run_client(&client, addr2), + run_client(&client, addr3), + ); + + // Make sure the server has a chance to clean up + client.wait_idle().await; + + Ok(()) +} + +/// Runs a QUIC server bound to given address and returns server certificate. +fn run_server( + addr: SocketAddr, +) -> Result, Box> { + let (endpoint, server_cert) = make_server_endpoint(addr)?; + // accept a single connection + tokio::spawn(async move { + let connection = endpoint.accept().await.unwrap().await.unwrap(); + println!( + "[server] incoming connection: addr={}", + connection.remote_address() + ); + }); + + Ok(server_cert) +} + +/// Attempt QUIC connection with the given server address. +async fn run_client(endpoint: &Endpoint, server_addr: SocketAddr) { + let connect = endpoint.connect(server_addr, "localhost").unwrap(); + let connection = connect.await.unwrap(); + println!("[client] connected: addr={}", connection.remote_address()); +} diff --git a/src/hkdf.rs b/src/hkdf.rs index e9031ea..a0b2638 100644 --- a/src/hkdf.rs +++ b/src/hkdf.rs @@ -14,24 +14,18 @@ const MAX_MD_SIZE: usize = openssl_sys::EVP_MAX_MD_SIZE as usize; /// HKDF implementation using HMAC with the specified Hash Algorithm pub(crate) struct Hkdf(pub(crate) HashAlgorithm); -struct HkdfExpander { +pub(crate) struct HkdfExpander { private_key: [u8; MAX_MD_SIZE], size: usize, hash: HashAlgorithm, } -impl RustlsHkdf for Hkdf { - fn extract_from_zero_ikm(&self, salt: Option<&[u8]>) -> Box { - let hash_size = self.0.output_len(); - let secret = [0u8; MAX_MD_SIZE]; - self.extract_from_secret(salt, &secret[..hash_size]) - } - - fn extract_from_secret( +impl Hkdf { + pub(crate) fn extract_from_secret_internal( &self, salt: Option<&[u8]>, secret: &[u8], - ) -> Box { + ) -> HkdfExpander { let hash_size = self.0.output_len(); let mut private_key = [0u8; MAX_MD_SIZE]; PkeyCtx::new_id(Id::HKDF) @@ -50,11 +44,27 @@ impl RustlsHkdf for Hkdf { }) .expect("HDKF-Extract failed"); - Box::new(HkdfExpander { + HkdfExpander { private_key, size: hash_size, hash: self.0, - }) + } + } +} + +impl RustlsHkdf for Hkdf { + fn extract_from_zero_ikm(&self, salt: Option<&[u8]>) -> Box { + let hash_size = self.0.output_len(); + let secret = [0u8; MAX_MD_SIZE]; + self.extract_from_secret(salt, &secret[..hash_size]) + } + + fn extract_from_secret( + &self, + salt: Option<&[u8]>, + secret: &[u8], + ) -> Box { + Box::new(self.extract_from_secret_internal(salt, secret)) } fn expander_for_okm(&self, okm: &OkmBlock) -> Box { diff --git a/src/hmac.rs b/src/hmac.rs index 22e276d..b47a2ca 100644 --- a/src/hmac.rs +++ b/src/hmac.rs @@ -1,11 +1,13 @@ use crate::hash::Algorithm; use openssl::pkey::{PKey, Private}; use openssl::sign::Signer; +#[cfg(feature = "quinn")] +use openssl::sign::Verifier; use rustls::crypto::hash::Hash as _; use rustls::crypto::hmac::{Key, Tag}; pub(crate) struct Hmac(pub(crate) Algorithm); -struct HmacKey { +pub(crate) struct HmacKey { key: PKey, hash: Algorithm, } @@ -49,3 +51,41 @@ impl Key for HmacKey { self.hash.output_len() } } + +#[cfg(feature = "quinn")] +impl HmacKey { + pub(crate) fn sha256(key: PKey) -> Self { + Self { + key, + hash: Algorithm::SHA256, + } + } +} + +#[cfg(feature = "quinn")] +impl quinn::crypto::HmacKey for HmacKey { + fn sign(&self, data: &[u8], signature_out: &mut [u8]) { + let tag = rustls::crypto::hmac::Key::sign(self, &[data]); + signature_out.copy_from_slice(tag.as_ref()); + } + + fn signature_len(&self) -> usize { + self.tag_len() + } + + fn verify(&self, data: &[u8], signature: &[u8]) -> Result<(), quinn::crypto::CryptoError> { + Verifier::new(self.hash.message_digest(), &self.key) + .and_then(|mut verifier| { + verifier.update(data)?; + verifier.verify(signature) + }) + .map_err(|_| quinn::crypto::CryptoError) + .and_then(|valid| { + if valid { + Ok(()) + } else { + Err(quinn::crypto::CryptoError) + } + }) + } +} diff --git a/src/lib.rs b/src/lib.rs index 6a3241a..08c3a77 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -68,6 +68,9 @@ mod kx; #[cfg(feature = "tls12")] mod prf; mod quic; + +#[cfg(feature = "quinn")] +pub mod quinn; mod signer; #[cfg(feature = "tls12")] mod tls12; diff --git a/src/quinn.rs b/src/quinn.rs new file mode 100644 index 0000000..3b958b1 --- /dev/null +++ b/src/quinn.rs @@ -0,0 +1,69 @@ +//! Provides openssl backed implementation of Quinn's crypto traits. +use std::sync::Arc; + +use crate::aead::Algorithm; +use crate::hkdf::{Hkdf, HkdfExpander}; +use crate::hmac::HmacKey; +use openssl::pkey::PKey; +use quinn::crypto::HandshakeTokenKey; +use rustls::crypto::{cipher::AeadKey, tls13::HkdfExpander as _, SecureRandom}; + +const AES_256_KEY_LEN: usize = 32; +struct Aes256GcmKey(AeadKey); + +/// Create a new endpoint reset key +pub fn reset_key() -> Arc { + let mut reset_key = [0; 64]; + crate::SecureRandom {} + .fill(&mut reset_key) + .expect("Failed to generate random key"); + Arc::new(HmacKey::sha256( + PKey::hmac(&reset_key).expect("Failed to read Hmac Key"), + )) +} + +/// Create new handshake token key +pub fn handshake_token_key() -> Arc { + let mut master_key = [0u8; 64]; + crate::SecureRandom {} + .fill(&mut master_key) + .expect("Failed to generate random key"); + let hkdf = Hkdf(crate::hash::Algorithm::SHA256); + let expander = hkdf.extract_from_secret_internal(None, &master_key); + Arc::new(expander) +} + +impl quinn::crypto::HandshakeTokenKey for HkdfExpander { + fn aead_from_hkdf(&self, random_bytes: &[u8]) -> Box { + debug_assert!(self.hash_len() >= AES_256_KEY_LEN); + let mut key_buffer = [0u8; AES_256_KEY_LEN]; + let okm = self.expand_block(&[random_bytes]); + key_buffer.copy_from_slice(&okm.as_ref()[..AES_256_KEY_LEN]); + Box::new(Aes256GcmKey(AeadKey::from(key_buffer))) + } +} + +impl quinn::crypto::AeadKey for Aes256GcmKey { + fn seal( + &self, + data: &mut Vec, + additional_data: &[u8], + ) -> Result<(), quinn::crypto::CryptoError> { + let tag = Algorithm::Aes256Gcm + .encrypt_in_place(self.0.as_ref(), &[0u8; 12], additional_data, data) + .map_err(|_| quinn::crypto::CryptoError)?; + data.extend_from_slice(&tag); + Ok(()) + } + + fn open<'a>( + &self, + data: &'a mut [u8], + additional_data: &[u8], + ) -> Result<&'a mut [u8], quinn::crypto::CryptoError> { + let plaintext_len = Algorithm::Aes256Gcm + .decrypt_in_place(self.0.as_ref(), &[0u8; 12], additional_data, data) + .map_err(|_| quinn::crypto::CryptoError)?; + Ok(data[..plaintext_len].as_mut()) + } +}