Skip to content

Commit bb46490

Browse files
authored
feat(proto)!: Don't require a HKDF construction in HandshakeTokenKey (#480)
* Implement HandshakeTokenKeys directly using AES-GCM * Revert to old HDKF-based construction, but with changed trait * Cleanup * Clippy fix
1 parent 0185176 commit bb46490

File tree

5 files changed

+73
-83
lines changed

5 files changed

+73
-83
lines changed

noq-proto/src/config/mod.rs

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -400,18 +400,10 @@ impl ServerConfig {
400400
///
401401
/// Uses a randomized handshake token key.
402402
pub fn with_crypto(crypto: Arc<dyn crypto::ServerConfig>) -> Self {
403-
#[cfg(all(feature = "aws-lc-rs", not(feature = "ring")))]
404-
use aws_lc_rs::hkdf;
405-
use rand::RngCore;
406-
#[cfg(feature = "ring")]
407-
use ring::hkdf;
408-
409-
let rng = &mut rand::rng();
410-
let mut master_key = [0u8; 64];
411-
rng.fill_bytes(&mut master_key);
412-
let master_key = hkdf::Salt::new(hkdf::HKDF_SHA256, &[]).extract(&master_key);
403+
use crate::crypto::ring_like::RetryTokenKey;
413404

414-
Self::new(crypto, Arc::new(master_key))
405+
let retry_token_key = RetryTokenKey::new(&mut rand::rng());
406+
Self::new(crypto, Arc::new(retry_token_key))
415407
}
416408
}
417409

noq-proto/src/crypto.rs

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -204,22 +204,17 @@ pub trait HmacKey: Send + Sync {
204204
#[derive(Debug, PartialEq, Eq)]
205205
pub struct ExportKeyingMaterialError;
206206

207-
/// A pseudo random key for HKDF
207+
/// The trait for encrypting tokens that power retry packets and NEW_TOKEN frames.
208208
pub trait HandshakeTokenKey: Send + Sync {
209-
/// Derive AEAD using hkdf
210-
fn aead_from_hkdf(&self, random_bytes: &[u8]) -> Box<dyn AeadKey>;
211-
}
212-
213-
/// A key for sealing data with AEAD-based algorithms
214-
pub trait AeadKey {
215-
/// Method for sealing message `data`
216-
fn seal(&self, data: &mut Vec<u8>, additional_data: &[u8]) -> Result<(), CryptoError>;
217-
/// Method for opening a sealed message `data`
218-
fn open<'a>(
219-
&self,
220-
data: &'a mut [u8],
221-
additional_data: &[u8],
222-
) -> Result<&'a mut [u8], CryptoError>;
209+
/// Method for sealing a token in-place.
210+
///
211+
/// The nonce doesn't need to be attached to the ciphertext,
212+
/// but the authentication tag is expected to be appended to `data`.
213+
fn seal(&self, token_nonce: u128, data: &mut Vec<u8>) -> Result<(), CryptoError>;
214+
/// Method for opening a sealed message `data` in-place.
215+
///
216+
/// Returns the portion of `data` that contains the decrypted plaintext.
217+
fn open<'a>(&self, token_nonce: u128, data: &'a mut [u8]) -> Result<&'a [u8], CryptoError>;
223218
}
224219

225220
/// Generic crypto errors

noq-proto/src/crypto/ring_like.rs

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,34 +19,54 @@ impl crypto::HmacKey for hmac::Key {
1919
}
2020
}
2121

22-
impl crypto::HandshakeTokenKey for hkdf::Prk {
23-
fn aead_from_hkdf(&self, random_bytes: &[u8]) -> Box<dyn crypto::AeadKey> {
24-
let mut key_buffer = [0u8; 32];
25-
let info = [random_bytes];
26-
let okm = self.expand(&info, hkdf::HKDF_SHA256).unwrap();
22+
/// Implements retry token encryption using HKDF followed by AES-256-GCM.
23+
///
24+
/// This construction was originally defined in https://github.com/quinn-rs/quinn/issues/783
25+
/// The goal is to produce tokens that look random to clients, but contain decryptable
26+
/// information for the server.
27+
///
28+
/// The problem is the 12-byte nonce in AES-GCM: I suspect the original authors of this
29+
/// code didn't like the fact that it limits you to ~2^32 safe encryptions before you're getting
30+
/// into "dangerous" nonce-reuse territory with randomly-generated nonces.
31+
/// You can't use sequence numbers either, though, because that doesn't look random to
32+
/// clients and leaks information.
33+
pub(crate) struct RetryTokenKey(hkdf::Prk);
34+
35+
impl RetryTokenKey {
36+
pub(crate) fn new(rng: &mut impl rand::Rng) -> Self {
37+
let mut master_key = [0u8; 64];
38+
rng.fill_bytes(&mut master_key);
39+
let master_key = hkdf::Salt::new(hkdf::HKDF_SHA256, &[]).extract(&master_key);
40+
Self(master_key)
41+
}
42+
43+
fn derive_aead(&self, token_nonce: u128) -> aead::LessSafeKey {
44+
let nonce_bytes = token_nonce.to_le_bytes();
45+
let info = &[&nonce_bytes[..]];
46+
let okm = self.0.expand(info, hkdf::HKDF_SHA256).unwrap();
2747

48+
let mut key_buffer = [0u8; 32];
2849
okm.fill(&mut key_buffer).unwrap();
2950

3051
let key = aead::UnboundKey::new(&aead::AES_256_GCM, &key_buffer).unwrap();
31-
Box::new(aead::LessSafeKey::new(key))
52+
aead::LessSafeKey::new(key)
3253
}
3354
}
3455

35-
impl crypto::AeadKey for aead::LessSafeKey {
36-
fn seal(&self, data: &mut Vec<u8>, additional_data: &[u8]) -> Result<(), CryptoError> {
37-
let aad = aead::Aad::from(additional_data);
38-
let zero_nonce = aead::Nonce::assume_unique_for_key([0u8; 12]);
39-
Ok(self.seal_in_place_append_tag(zero_nonce, aad, data)?)
40-
}
41-
42-
fn open<'a>(
43-
&self,
44-
data: &'a mut [u8],
45-
additional_data: &[u8],
46-
) -> Result<&'a mut [u8], CryptoError> {
47-
let aad = aead::Aad::from(additional_data);
48-
let zero_nonce = aead::Nonce::assume_unique_for_key([0u8; 12]);
49-
Ok(self.open_in_place(zero_nonce, aad, data)?)
56+
impl crypto::HandshakeTokenKey for RetryTokenKey {
57+
fn seal(&self, token_nonce: u128, data: &mut Vec<u8>) -> Result<(), CryptoError> {
58+
let aead_key = self.derive_aead(token_nonce);
59+
let nonce = aead::Nonce::assume_unique_for_key([0u8; 12]); // See docs for RetryTokenKey
60+
let aad = aead::Aad::empty();
61+
aead_key.seal_in_place_append_tag(nonce, aad, data)?;
62+
Ok(())
63+
}
64+
65+
fn open<'a>(&self, token_nonce: u128, data: &'a mut [u8]) -> Result<&'a [u8], CryptoError> {
66+
let aead_key = self.derive_aead(token_nonce);
67+
let aad = aead::Aad::empty();
68+
let nonce = aead::Nonce::assume_unique_for_key([0u8; 12]); // See docs for RetryTokenKey
69+
Ok(aead_key.open_in_place(nonce, aad, data)?)
5070
}
5171
}
5272

noq-proto/src/crypto/rustls.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ use std::{any::Any, io, str, sync::Arc};
33
use aes_gcm::{KeyInit, aead::AeadMutInPlace};
44
use bytes::BytesMut;
55
pub use rustls::Error;
6-
#[cfg(feature = "__rustls-post-quantum-test")]
7-
use rustls::NamedGroup;
86
use rustls::{
97
self, CipherSuite,
108
pki_types::{CertificateDer, ServerName},
@@ -268,7 +266,7 @@ pub struct HandshakeData {
268266
pub server_name: Option<String>,
269267
/// The key exchange group negotiated with the peer
270268
#[cfg(feature = "__rustls-post-quantum-test")]
271-
pub negotiated_key_exchange_group: NamedGroup,
269+
pub negotiated_key_exchange_group: rustls::NamedGroup,
272270
}
273271

274272
/// A QUIC-compatible TLS client configuration

noq-proto/src/token.rs

Lines changed: 18 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use std::{
22
fmt,
3-
mem::size_of,
43
net::{IpAddr, SocketAddr},
54
};
65

@@ -236,8 +235,7 @@ impl Token {
236235
}
237236

238237
// Encrypt
239-
let aead_key = key.aead_from_hkdf(&self.nonce.to_le_bytes());
240-
aead_key.seal(&mut buf, &[]).unwrap();
238+
key.seal(self.nonce, &mut buf).unwrap();
241239
buf.extend(&self.nonce.to_le_bytes());
242240

243241
buf
@@ -247,30 +245,27 @@ impl Token {
247245
fn decode(key: &dyn HandshakeTokenKey, raw_token_bytes: &[u8]) -> Option<Self> {
248246
// Decrypt
249247

250-
let nonce_slice_start = raw_token_bytes.len().checked_sub(size_of::<u128>())?;
251-
let (sealed_token, nonce_bytes) = raw_token_bytes.split_at_checked(nonce_slice_start)?;
248+
let (sealed_token, nonce_bytes) = raw_token_bytes.split_last_chunk()?;
252249

253-
let nonce = u128::from_le_bytes(nonce_bytes.try_into().unwrap());
250+
let nonce = u128::from_le_bytes(*nonce_bytes);
254251

255-
let aead_key = key.aead_from_hkdf(nonce_bytes);
256252
let mut sealed_token = sealed_token.to_vec();
257-
let data = aead_key.open(&mut sealed_token, &[]).ok()?;
253+
let mut data = key.open(nonce, &mut sealed_token).ok()?;
258254

259255
// Decode payload
260-
let mut reader = &data[..];
261-
let payload = match TokenType::from_byte((&mut reader).get::<u8>().ok()?)? {
256+
let payload = match TokenType::from_byte((&mut data).get::<u8>().ok()?)? {
262257
TokenType::Retry => TokenPayload::Retry {
263-
address: decode_addr(&mut reader)?,
264-
orig_dst_cid: ConnectionId::decode_long(&mut reader)?,
265-
issued: decode_unix_secs(&mut reader)?,
258+
address: decode_addr(&mut data)?,
259+
orig_dst_cid: ConnectionId::decode_long(&mut data)?,
260+
issued: decode_unix_secs(&mut data)?,
266261
},
267262
TokenType::Validation => TokenPayload::Validation {
268-
ip: decode_ip(&mut reader)?,
269-
issued: decode_unix_secs(&mut reader)?,
263+
ip: decode_ip(&mut data)?,
264+
issued: decode_unix_secs(&mut data)?,
270265
},
271266
};
272267

273-
if !reader.is_empty() {
268+
if !data.is_empty() {
274269
// Consider extra bytes a decoding error (it may be from an incompatible endpoint)
275270
return None;
276271
}
@@ -408,21 +403,17 @@ impl fmt::Display for ResetToken {
408403

409404
#[cfg(all(test, any(feature = "aws-lc-rs", feature = "ring")))]
410405
mod test {
406+
use crate::crypto::ring_like::RetryTokenKey;
407+
411408
use super::*;
412-
#[cfg(all(feature = "aws-lc-rs", not(feature = "ring")))]
413-
use aws_lc_rs::hkdf;
414409
use rand::prelude::*;
415-
#[cfg(feature = "ring")]
416-
use ring::hkdf;
417410

418411
fn token_round_trip(payload: TokenPayload) -> TokenPayload {
419412
let rng = &mut rand::rng();
420413
let token = Token::new(payload, rng);
421-
let mut master_key = [0; 64];
422-
rng.fill_bytes(&mut master_key);
423-
let prk = hkdf::Salt::new(hkdf::HKDF_SHA256, &[]).extract(&master_key);
424-
let encoded = token.encode(&prk);
425-
let decoded = Token::decode(&prk, &encoded).expect("token didn't decrypt / decode");
414+
let master_key = RetryTokenKey::new(rng);
415+
let encoded = token.encode(&master_key);
416+
let decoded = Token::decode(&master_key, &encoded).expect("token didn't decrypt / decode");
426417
assert_eq!(token.nonce, decoded.nonce);
427418
decoded.payload
428419
}
@@ -485,14 +476,8 @@ mod test {
485476
#[test]
486477
fn invalid_token_returns_err() {
487478
use super::*;
488-
use rand::RngCore;
489-
490-
let rng = &mut rand::rng();
491-
492-
let mut master_key = [0; 64];
493-
rng.fill_bytes(&mut master_key);
494479

495-
let prk = hkdf::Salt::new(hkdf::HKDF_SHA256, &[]).extract(&master_key);
480+
let master_key = RetryTokenKey::new(&mut rand::rng());
496481

497482
let mut invalid_token = Vec::new();
498483

@@ -501,6 +486,6 @@ mod test {
501486
invalid_token.put_slice(&random_data);
502487

503488
// Assert: garbage sealed data returns err
504-
assert!(Token::decode(&prk, &invalid_token).is_none());
489+
assert!(Token::decode(&master_key, &invalid_token).is_none());
505490
}
506491
}

0 commit comments

Comments
 (0)