From bd3686d44c74da777635caa2a7fe72963fd684b2 Mon Sep 17 00:00:00 2001 From: Lann Martin Date: Thu, 6 Aug 2026 06:57:35 -0400 Subject: [PATCH] Scrub header-protection key material on drop ChaChaHeaderProtectionKey held its raw 32-byte key as a plain array that survived its drop; the AES twin's expanded schedule and the transient per-mask chacha20 cipher states likewise dropped unscrubbed. The ChaCha key now rides zeroize::Zeroizing, and the aes and chacha20 dependencies enable their zeroize features, so the schedule and cipher states scrub themselves. zeroize was already in the lock. --- Cargo.lock | 3 +++ Cargo.toml | 5 +++-- rust/quinn/Cargo.toml | 1 + rust/quinn/src/packet.rs | 11 +++++++---- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0f8debb..05bdbb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,6 +30,7 @@ dependencies = [ "cipher", "cpubits", "cpufeatures 0.3.0", + "zeroize", ] [[package]] @@ -214,6 +215,7 @@ dependencies = [ "cipher", "cpufeatures 0.3.0", "rand_core", + "zeroize", ] [[package]] @@ -1531,6 +1533,7 @@ dependencies = [ "rustls-rustcrypto", "sha2 0.11.0", "subtle", + "zeroize", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 3d2190b..6fb490a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,12 +49,13 @@ wasip3 = "0.7" futures = { version = "0.3", default-features = false, features = ["std", "async-await"] } aead = { version = "0.6", default-features = false, features = ["alloc"] } -aes = "0.9" +aes = { version = "0.9", features = ["zeroize"] } aes-gcm = { version = "0.11", default-features = false, features = ["aes", "alloc"] } -chacha20 = "0.10" +chacha20 = { version = "0.10", features = ["zeroize"] } chacha20poly1305 = { version = "0.11", default-features = false, features = ["alloc"] } hkdf = "0.13" hmac = "0.13" sha2 = { version = "0.11", default-features = false } subtle = { version = "2", default-features = false } +zeroize = { version = "1", default-features = false } bytes = "1" diff --git a/rust/quinn/Cargo.toml b/rust/quinn/Cargo.toml index b771eae..3a424c8 100644 --- a/rust/quinn/Cargo.toml +++ b/rust/quinn/Cargo.toml @@ -22,4 +22,5 @@ hkdf = { workspace = true } hmac = { workspace = true } sha2 = { workspace = true } subtle = { workspace = true } +zeroize = { workspace = true } bytes = { workspace = true } diff --git a/rust/quinn/src/packet.rs b/rust/quinn/src/packet.rs index 9a7b5bb..58c60b5 100644 --- a/rust/quinn/src/packet.rs +++ b/rust/quinn/src/packet.rs @@ -62,7 +62,7 @@ impl quic::Algorithm for ChaCha20Poly1305Algorithm { fn header_protection_key(&self, key: AeadKey) -> Box { let key: [u8; 32] = key.as_ref().try_into().expect("invalid key length"); - Box::new(ChaChaHeaderProtectionKey(key)) + Box::new(ChaChaHeaderProtectionKey(zeroize::Zeroizing::new(key))) } fn aead_key_len(&self) -> usize { @@ -167,7 +167,10 @@ impl quic::HeaderProtectionKey for AesHeaderProtectionKey { } } -struct ChaChaHeaderProtectionKey([u8; 32]); +// The raw key scrubs itself on drop; the AES twin needs no wrapper — its +// expanded schedule zeroizes via the `aes` crate's `zeroize` feature, as +// do the transient `chacha20` cipher states built per mask. +struct ChaChaHeaderProtectionKey(zeroize::Zeroizing<[u8; 32]>); impl ChaChaHeaderProtectionKey { /// RFC 9001 §5.4.4: the sample's first 4 bytes are the block counter, @@ -179,7 +182,7 @@ impl ChaChaHeaderProtectionKey { .map_err(|_| Error::General("sample of invalid length".into()))?; let counter = u32::from_le_bytes(sample[..4].try_into().unwrap()); let nonce: [u8; 12] = sample[4..].try_into().unwrap(); - let mut cipher = chacha20::ChaCha20::new(&self.0.into(), &nonce.into()); + let mut cipher = chacha20::ChaCha20::new((&*self.0).into(), &nonce.into()); cipher.seek(u64::from(counter) * 64); let mut mask = [0u8; 5]; cipher.apply_keystream(&mut mask); @@ -295,7 +298,7 @@ mod tests { 0x5e, 0x5c, 0xd5, 0x5c, 0x41, 0xf6, 0x90, 0x80, 0x57, 0x5d, 0x79, 0x99, 0xc2, 0x5a, 0x5b, 0xfb, ]; - let key = ChaChaHeaderProtectionKey(hp_key); + let key = ChaChaHeaderProtectionKey(zeroize::Zeroizing::new(hp_key)); assert_eq!(key.mask(&sample).unwrap(), [0xae, 0xfe, 0xfe, 0x7d, 0x03]); }