From 01134c52682a23d95183116f37b5390d6e3b8ead Mon Sep 17 00:00:00 2001 From: Vanshdeep Date: Wed, 29 Jul 2020 15:05:56 +0200 Subject: [PATCH] [#783] Encrypt retry token --- quinn-proto/src/config.rs | 27 +++--- quinn-proto/src/crypto.rs | 25 ++++++ quinn-proto/src/crypto/ring.rs | 38 ++++++++- quinn-proto/src/crypto/rustls.rs | 3 +- quinn-proto/src/endpoint.rs | 7 +- quinn-proto/src/token.rs | 141 +++++++++++++++++++++++-------- 6 files changed, 189 insertions(+), 52 deletions(-) diff --git a/quinn-proto/src/config.rs b/quinn-proto/src/config.rs index d69b64656c..f628d3ef2a 100644 --- a/quinn-proto/src/config.rs +++ b/quinn-proto/src/config.rs @@ -8,7 +8,7 @@ use crate::crypto::types::{Certificate, CertificateChain, PrivateKey}; use crate::{ cid_generator::{ConnectionIdGenerator, RandomConnectionIdGenerator}, congestion, - crypto::{self, ClientConfig as _, HmacKey as _, ServerConfig as _}, + crypto::{self, ClientConfig as _, HandshakeTokenKey as _, HmacKey as _, ServerConfig as _}, VarInt, }; @@ -413,8 +413,9 @@ where /// Must be set to use TLS 1.3 only. pub crypto: S::ServerConfig, - /// Private key used to authenticate data included in handshake tokens. - pub(crate) token_key: Arc, + /// Used to generate one-time AEAD keys to protect handshake tokens + pub(crate) token_key: Arc, + /// Whether to require clients to prove ownership of an address before committing resources. /// /// Introduces an additional round-trip to the handshake to make denial of service attacks more difficult. @@ -438,13 +439,13 @@ impl ServerConfig where S: crypto::Session, { - /// Create a default config with a particular `token_key` - pub fn new(token_key: S::HmacKey) -> Self { + /// Create a default config with a particular `master_key` + pub fn new(prk: S::HandshakeTokenKey) -> Self { Self { transport: Arc::new(TransportConfig::default()), crypto: S::ServerConfig::new(), - token_key: Arc::new(token_key), + token_key: Arc::new(prk), use_stateless_retry: false, retry_token_lifetime: 15_000_000, @@ -455,8 +456,8 @@ where } /// Private key used to authenticate data included in handshake tokens. - pub fn token_key(&mut self, value: &[u8]) -> Result<&mut Self, ConfigError> { - self.token_key = Arc::new(S::HmacKey::new(value)?); + pub fn token_key(&mut self, master_key: &[u8]) -> Result<&mut Self, ConfigError> { + self.token_key = Arc::new(S::HandshakeTokenKey::from_secret(&master_key)); Ok(self) } @@ -529,12 +530,10 @@ where fn default() -> Self { let rng = &mut rand::thread_rng(); - let mut token_key = vec![0; S::HmacKey::KEY_LEN]; - rng.fill_bytes(&mut token_key); - Self::new( - S::HmacKey::new(&token_key) - .expect("HMAC key rejected random bytes; use ServerConfig::new instead"), - ) + let mut master_key = [0u8; 64]; + rng.fill_bytes(&mut master_key); + + Self::new(S::HandshakeTokenKey::from_secret(&master_key)) } } diff --git a/quinn-proto/src/crypto.rs b/quinn-proto/src/crypto.rs index 21c9c838ea..97b0db021d 100644 --- a/quinn-proto/src/crypto.rs +++ b/quinn-proto/src/crypto.rs @@ -38,6 +38,8 @@ pub trait Session: Send + Sized { type ClientConfig: ClientConfig; /// Type used to sign various values type HmacKey: HmacKey; + /// Key used to generate one-time-use handshake token keys + type HandshakeTokenKey: HandshakeTokenKey; /// Type of keys used to protect packet headers type HeaderKey: HeaderKey; /// Type used to represent packet protection keys @@ -210,3 +212,26 @@ pub trait HmacKey: Send + Sized + Sync { /// This error occurs if the requested output length is too large. #[derive(Debug, PartialEq, Eq)] pub struct ExportKeyingMaterialError; + +/// A pseudo random key for HKDF +pub trait HandshakeTokenKey: Send + Sized + Sync { + /// AEAD key type + type AeadKey: AeadKey; + + /// Derive AEAD using hkdf + fn aead_from_hkdf(&self, random_bytes: &[u8]) -> Self::AeadKey; + /// Method to build pseudo random key from existing bytes + fn from_secret(secret: &[u8]) -> Self; +} + +/// A key for sealing data with AEAD-based algorithms +pub trait AeadKey { + /// Length of AEAD Key + const KEY_LEN: usize; + + // fn from_hkdf(master_key: &impl PseudoRandomKey, random_bytes: &[u8]) -> Self; + /// Method for sealing message `data` + fn seal(&self, data: &mut Vec, additional_data: &[u8]) -> Result<(), ()>; + /// Method for opening a sealed message `data` + fn open<'a>(&self, data: &'a mut [u8], additional_data: &[u8]) -> Result<&'a mut [u8], ()>; +} diff --git a/quinn-proto/src/crypto/ring.rs b/quinn-proto/src/crypto/ring.rs index 07a8b1aada..00c695c125 100644 --- a/quinn-proto/src/crypto/ring.rs +++ b/quinn-proto/src/crypto/ring.rs @@ -1,4 +1,4 @@ -use ring::{aead, hmac}; +use ring::{aead, hkdf, hmac}; use crate::{ config::ConfigError, @@ -70,3 +70,39 @@ impl crypto::HmacKey for hmac::Key { hmac::verify(self, data, signature).map_err(|_| ()) } } + +impl crypto::HandshakeTokenKey for hkdf::Prk { + type AeadKey = ring::aead::LessSafeKey; + + fn aead_from_hkdf(&self, random_bytes: &[u8]) -> Self::AeadKey { + let mut key_buffer = [0u8; 32]; + let info = [random_bytes]; + let okm = self.expand(&info, hkdf::HKDF_SHA256).unwrap(); + + okm.fill(&mut key_buffer).unwrap(); + + let key = aead::UnboundKey::new(&aead::AES_256_GCM, &key_buffer).unwrap(); + Self::AeadKey::new(key) + } + + fn from_secret(bytes: &[u8]) -> Self { + hkdf::Salt::new(hkdf::HKDF_SHA256, &[]).extract(bytes) + } +} + +impl crypto::AeadKey for aead::LessSafeKey { + const KEY_LEN: usize = 32; + + fn seal(&self, data: &mut Vec, additional_data: &[u8]) -> Result<(), ()> { + let aad = ring::aead::Aad::from(additional_data); + let zero_nonce = ring::aead::Nonce::assume_unique_for_key([0u8; 12]); + self.seal_in_place_append_tag(zero_nonce, aad, data) + .map_err(|_| ()) + } + + fn open<'a>(&self, data: &'a mut [u8], additional_data: &[u8]) -> Result<&'a mut [u8], ()> { + let aad = ring::aead::Aad::from(additional_data); + let zero_nonce = ring::aead::Nonce::assume_unique_for_key([0u8; 12]); + self.open_in_place(zero_nonce, aad, data).map_err(|_| ()) + } +} diff --git a/quinn-proto/src/crypto/rustls.rs b/quinn-proto/src/crypto/rustls.rs index b7b8a27ec5..4061c71813 100644 --- a/quinn-proto/src/crypto/rustls.rs +++ b/quinn-proto/src/crypto/rustls.rs @@ -6,7 +6,7 @@ use std::{ }; use bytes::BytesMut; -use ring::{aead, aead::quic::HeaderProtectionKey, hmac}; +use ring::{aead, aead::quic::HeaderProtectionKey, hkdf, hmac}; pub use rustls::TLSError; use rustls::{ self, @@ -50,6 +50,7 @@ impl crypto::Session for TlsSession { type Identity = CertificateChain; type ClientConfig = Arc; type HmacKey = hmac::Key; + type HandshakeTokenKey = hkdf::Prk; type PacketKey = PacketKey; type HeaderKey = HeaderProtectionKey; type ServerConfig = Arc; diff --git a/quinn-proto/src/endpoint.rs b/quinn-proto/src/endpoint.rs index cfa57ec5b9..8bb4c755ce 100644 --- a/quinn-proto/src/endpoint.rs +++ b/quinn-proto/src/endpoint.rs @@ -548,16 +548,21 @@ where let (retry_src_cid, orig_dst_cid) = if server_config.use_stateless_retry { if token.is_empty() { // First Initial + let mut random_bytes = vec![0u8; RetryToken::RANDOM_BYTES_LEN]; + self.rng.fill_bytes(&mut random_bytes); + let token = RetryToken { orig_dst_cid: dst_cid, issued: SystemTime::now(), + random_bytes: &random_bytes, } .encode(&*server_config.token_key, &remote, &temp_loc_cid); - let mut buf = Vec::new(); + let header = Header::Retry { src_cid: temp_loc_cid, dst_cid: src_cid, }; + let mut buf = Vec::new(); let encode = header.encode(&mut buf); buf.put_slice(&token); buf.extend_from_slice(&S::retry_tag(&dst_cid, &buf)); diff --git a/quinn-proto/src/token.rs b/quinn-proto/src/token.rs index e6677473de..bc1f3574c4 100644 --- a/quinn-proto/src/token.rs +++ b/quinn-proto/src/token.rs @@ -8,7 +8,7 @@ use bytes::BufMut; use crate::{ coding::{BufExt, BufMutExt}, - crypto::HmacKey, + crypto::{AeadKey, HandshakeTokenKey, HmacKey}, shared::ConnectionId, RESET_TOKEN_SIZE, }; @@ -19,20 +19,27 @@ use crate::{ // - AEAD nonce is always set to 0 // in other words, for each ticket, use different key derived from random using HKDF -pub struct RetryToken { +pub struct RetryToken<'a> { /// The destination connection ID set in the very first packet from the client pub orig_dst_cid: ConnectionId, /// The time at which this token was issued pub issued: SystemTime, + /// Random bytes for deriving AEAD key + pub random_bytes: &'a [u8], } -impl RetryToken { +impl<'a> RetryToken<'a> { + const MAX_ADDITIONAL_DATA_SIZE: usize = 39; // max(ipv4, ipv6) + port + retry_src_cid + pub const RANDOM_BYTES_LEN: usize = 32; + pub fn encode( &self, - key: &impl HmacKey, + key: &impl HandshakeTokenKey, address: &SocketAddr, retry_src_cid: &ConnectionId, ) -> Vec { + let aead_key = key.aead_from_hkdf(self.random_bytes); + let mut buf = Vec::new(); self.orig_dst_cid.encode_long(&mut buf); @@ -43,46 +50,59 @@ impl RetryToken { .unwrap_or(0), ); - let signature_pos = buf.len(); + let mut additional_data = [0u8; Self::MAX_ADDITIONAL_DATA_SIZE]; + let mut cursor = &mut additional_data[..]; match address.ip() { - IpAddr::V4(x) => buf.put_slice(&x.octets()), - IpAddr::V6(x) => buf.put_slice(&x.octets()), + IpAddr::V4(x) => cursor.put_slice(&x.octets()), + IpAddr::V6(x) => cursor.put_slice(&x.octets()), } - buf.write(address.port()); - retry_src_cid.encode_long(&mut buf); - - let signature = key.sign(&buf); - // No reason to actually encode the IP in the token, since we always have the remote addr for an incoming packet. - buf.truncate(signature_pos); - buf.extend_from_slice(signature.as_ref()); - buf + cursor.write(address.port()); + retry_src_cid.encode_long(&mut cursor); + + let size = Self::MAX_ADDITIONAL_DATA_SIZE - cursor.len(); + aead_key.seal(&mut buf, &additional_data[..size]).unwrap(); + + let mut token = Vec::new(); + token.put_slice(self.random_bytes); + token.put_slice(&buf); + token } pub fn from_bytes( - key: &impl HmacKey, + key: &impl HandshakeTokenKey, address: &SocketAddr, retry_src_cid: &ConnectionId, - data: &[u8], + raw_token_bytes: &'a [u8], ) -> Result { - let mut reader = io::Cursor::new(data); + if raw_token_bytes.len() < Self::RANDOM_BYTES_LEN { + // Invalid length + return Err(()); + } - let orig_dst_cid = ConnectionId::decode_long(&mut reader).ok_or(())?; - let issued = UNIX_EPOCH + Duration::new(reader.get::().map_err(|_| ())?, 0); + let random_bytes = &raw_token_bytes[..Self::RANDOM_BYTES_LEN]; + let aead_key = key.aead_from_hkdf(random_bytes); + let mut sealed_token = raw_token_bytes[Self::RANDOM_BYTES_LEN..].to_vec(); - let signature_start = reader.position() as usize; - let mut buf = Vec::new(); - buf.put_slice(&data[0..signature_start]); + let mut additional_data = [0u8; Self::MAX_ADDITIONAL_DATA_SIZE]; + let mut cursor = &mut additional_data[..]; match address.ip() { - IpAddr::V4(x) => buf.put_slice(&x.octets()), - IpAddr::V6(x) => buf.put_slice(&x.octets()), + IpAddr::V4(x) => cursor.put_slice(&x.octets()), + IpAddr::V6(x) => cursor.put_slice(&x.octets()), } - buf.write(address.port()); - retry_src_cid.encode_long(&mut buf); + cursor.write(address.port()); + retry_src_cid.encode_long(&mut cursor); + + let size = Self::MAX_ADDITIONAL_DATA_SIZE - cursor.len(); + let data = aead_key.open(&mut sealed_token, &additional_data[..size])?; + let mut reader = io::Cursor::new(data); + + let orig_dst_cid = ConnectionId::decode_long(&mut reader).ok_or(())?; + let issued = UNIX_EPOCH + Duration::new(reader.get::().map_err(|_| ())?, 0); - key.verify(&buf, &data[signature_start..]).map_err(|_| ())?; Ok(Self { orig_dst_cid, issued, + random_bytes, }) } } @@ -141,17 +161,27 @@ mod test { fn token_sanity() { use super::*; use crate::cid_generator::{ConnectionIdGenerator, RandomConnectionIdGenerator}; - use crate::MAX_CID_SIZE; + use crate::{crypto, MAX_CID_SIZE}; + use rand::RngCore; - use ring::hmac; use std::{ net::Ipv6Addr, time::{Duration, UNIX_EPOCH}, }; - let mut key = [0; 64]; - rand::thread_rng().fill_bytes(&mut key); - let key = ::new(&key).unwrap(); + let rng = &mut rand::thread_rng(); + + let mut master_key = [0; 64]; + rng.fill_bytes(&mut master_key); + + let mut random_bytes = [0; 32]; + rng.fill_bytes(&mut random_bytes); + + let mut master_key = vec![0u8; 64]; + rng.fill_bytes(&mut master_key); + + let prk: ring::hkdf::Prk = crypto::HandshakeTokenKey::from_secret(&master_key); + let addr = SocketAddr::new(Ipv6Addr::LOCALHOST.into(), 4433); let (retry_src_cid, _) = RandomConnectionIdGenerator::new(MAX_CID_SIZE).generate_cid(); let token = RetryToken { @@ -159,11 +189,52 @@ mod test { .generate_cid() .0, issued: UNIX_EPOCH + Duration::new(42, 0), // Fractional seconds would be lost + random_bytes: &random_bytes, }; - let encoded = token.encode(&key, &addr, &retry_src_cid); - let decoded = RetryToken::from_bytes(&key, &addr, &retry_src_cid, &encoded) + let encoded = token.encode(&prk, &addr, &retry_src_cid); + + let decoded = RetryToken::from_bytes(&prk, &addr, &retry_src_cid, &encoded) .expect("token didn't validate"); assert_eq!(token.orig_dst_cid, decoded.orig_dst_cid); assert_eq!(token.issued, decoded.issued); } + + #[cfg(feature = "ring")] + #[test] + fn invalid_token_returns_err() { + use super::*; + use crate::cid_generator::{ConnectionIdGenerator, RandomConnectionIdGenerator}; + use crate::{crypto, MAX_CID_SIZE}; + use rand::RngCore; + use std::net::Ipv6Addr; + + let rng = &mut rand::thread_rng(); + + let mut master_key = [0; 64]; + rng.fill_bytes(&mut master_key); + + let mut random_bytes = [0; 32]; + rng.fill_bytes(&mut random_bytes); + + let prk: ring::hkdf::Prk = crypto::HandshakeTokenKey::from_secret(&master_key); + + let addr = SocketAddr::new(Ipv6Addr::LOCALHOST.into(), 4433); + let (retry_src_cid, _) = RandomConnectionIdGenerator::new(MAX_CID_SIZE).generate_cid(); + + let mut invalid_token = Vec::new(); + invalid_token.put_slice(&random_bytes); + + let mut random_data = [0; 32]; + rand::thread_rng().fill_bytes(&mut random_data); + invalid_token.put_slice(&random_data); + + // Assert: garbage sealed data with valid random bytes returns err + assert!(RetryToken::from_bytes(&prk, &addr, &retry_src_cid, &invalid_token).is_err()); + + let invalid_token = [0; 31]; + rand::thread_rng().fill_bytes(&mut random_bytes); + + // Assert: completely invalid retry token returns error + assert!(RetryToken::from_bytes(&prk, &addr, &retry_src_cid, &invalid_token).is_err()); + } }