From 83b083c02a0f160d4b5274844506df5dd34b44a3 Mon Sep 17 00:00:00 2001 From: elagergren-spideroak <36516532+elagergren-spideroak@users.noreply.github.com> Date: Wed, 15 Jan 2025 08:05:07 -0800 Subject: [PATCH] crypto: use ChaCha8 instead of AES-CTR for `trng` CSPRNG (#45) Fixes #40 Signed-off-by: Eric Lagergren Co-authored-by: Jonathan Dygert --- Cargo.lock | 1 + crates/aranya-crypto-core/Cargo.toml | 3 + crates/aranya-crypto-core/src/csprng.rs | 350 ++++++++++++++---------- 3 files changed, 203 insertions(+), 151 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bbafa256..f77036fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -278,6 +278,7 @@ dependencies = [ "p384", "postcard", "rand", + "rand_chacha", "rand_core", "serde", "serde_json", diff --git a/crates/aranya-crypto-core/Cargo.toml b/crates/aranya-crypto-core/Cargo.toml index 38198d83..887fab4d 100644 --- a/crates/aranya-crypto-core/Cargo.toml +++ b/crates/aranya-crypto-core/Cargo.toml @@ -109,6 +109,7 @@ std = [ "postcard/use-std", "rand?/std", "rand?/std_rng", + "rand_chacha?/std", "rand_core/std", "serde/std", "serde_json?/std", @@ -133,6 +134,7 @@ test_util = [ trng = [ "dep:aes", "dep:lazy_static", + "dep:rand_chacha", "dep:spin", ] @@ -159,6 +161,7 @@ p384 = { version = "0.13", default-features = false, features = ["ecdh", "ecdsa" # Required by `aranya_crypto_derive::AlgId`. postcard = { workspace = true, default-features = false, features = ["heapless", "experimental-derive"] } rand = { workspace = true, default-features = false, optional = true } +rand_chacha = { version = "0.3", default-features = false, optional = true } # `rand_core` is required by the `rust` backend. rand_core = { workspace = true, default-features = false } # TODO(eric): Make this optional, it should only be needed by diff --git a/crates/aranya-crypto-core/src/csprng.rs b/crates/aranya-crypto-core/src/csprng.rs index ed2ead96..a51230a0 100644 --- a/crates/aranya-crypto-core/src/csprng.rs +++ b/crates/aranya-crypto-core/src/csprng.rs @@ -125,13 +125,16 @@ rand_int_impl!(i8 i16 i32 i64 i128 isize); #[cfg(feature = "trng")] pub(crate) mod trng { - use core::iter::{IntoIterator, Iterator}; - - use aes::{ - cipher::{BlockEncrypt, KeyInit}, - Aes256, + use core::{ + iter::{IntoIterator, Iterator}, + mem::MaybeUninit, + ptr, + sync::atomic::{self, Ordering}, }; + use cfg_if::cfg_if; + use rand_chacha::ChaCha8Rng; + use rand_core::{RngCore, SeedableRng}; use crate::{csprng::Csprng, kdf::Kdf, zeroize::ZeroizeOnDrop}; @@ -143,42 +146,56 @@ pub(crate) mod trng { } } - // If `std` is enabled, use a thread-local CSPRNG. + /// A thread-local (ish) CSPRNG. + #[derive(Clone)] + pub struct ThreadRng(inner::ThreadRng); + + /// Returns a thread-local (ish) CSPRNG. + pub fn thread_rng() -> ThreadRng { + ThreadRng(inner::thread_rng()) + } + + impl Csprng for ThreadRng { + fn fill_bytes(&mut self, dst: &mut [u8]) { + self.0.fill_bytes_and_reseed(dst); + } + } + + // If `std` is enabled, use a true thread-local CSPRNG. #[cfg(feature = "std")] mod inner { - use std::{cell::Cell, rc::Rc}; + use std::{cell::UnsafeCell, rc::Rc}; - use super::{random_key, AesCtrCsprng, Csprng, HkdfSha512, OsTrng}; + use super::{ChaCha8Csprng, HkdfSha512, OsTrng}; thread_local! { - static THREAD_RNG: Rc> = - Rc::new(Cell::new(random_key::<_, HkdfSha512>(OsTrng))); + static THREAD_RNG: Rc> = + Rc::new(UnsafeCell::new(ChaCha8Csprng::from_trng::<_, HkdfSha512>(OsTrng))); } - pub fn thread_rng() -> ThreadRng { - let key = THREAD_RNG.with(|t| t.clone()); - ThreadRng { key } + pub(super) fn thread_rng() -> ThreadRng { + let rng = THREAD_RNG.with(|t| t.clone()); + ThreadRng { rng } } // See https://github.com/rust-random/rand/blob/f3dd0b885c4597b9617ca79987a0dd899ab29fcb/src/rngs/thread.rs #[derive(Clone)] - pub struct ThreadRng { - key: Rc>, - } - - impl Csprng for ThreadRng { - fn fill_bytes(&mut self, dst: &mut [u8]) { - let key = self.key.get(); - let (mut rng, next) = AesCtrCsprng::new(key); - self.key.set(next); - rng.fill_bytes(dst) - } + pub(super) struct ThreadRng { + rng: Rc>, } - #[cfg(test)] - impl AsMut for ThreadRng { - fn as_mut(&mut self) -> &mut Self { - self + impl ThreadRng { + #[inline(always)] + pub(super) fn fill_bytes_and_reseed(&mut self, dst: &mut [u8]) { + // SAFETY: + // + // - `ThreadRng` is `!Sync`, so `self` can't be + // accessed concurrently. + // + // - `UnsafeCell::get` always returns a non-null + // pointer, so the dereference is safe. + let rng = unsafe { &mut *self.rng.get() }; + rng.fill_bytes_and_reseed(dst); } } } @@ -190,44 +207,29 @@ pub(crate) mod trng { use lazy_static::lazy_static; use spin::mutex::SpinMutex; - use super::{random_key, AesCtrCsprng, Csprng, HkdfSha512, OsTrng}; + use super::{ChaCha8Csprng, HkdfSha512, OsTrng}; lazy_static! { - static ref THREAD_RNG: SpinMutex<[u8; 32]> = - SpinMutex::new(random_key::<_, HkdfSha512>(OsTrng)); + static ref THREAD_RNG: SpinMutex = + SpinMutex::new(ChaCha8Csprng::from_trng::<_, HkdfSha512>(OsTrng)); } - fn next_rng() -> AesCtrCsprng { - let mut key = THREAD_RNG.lock(); - let (rng, next) = AesCtrCsprng::new(*key); - key.copy_from_slice(&next); - rng - } - - pub fn thread_rng() -> ThreadRng { + pub(super) fn thread_rng() -> ThreadRng { ThreadRng } - pub struct ThreadRng; - - impl Csprng for ThreadRng { - fn fill_bytes(&mut self, dst: &mut [u8]) { - next_rng().fill_bytes(dst) - } - } + #[derive(Clone)] + pub(super) struct ThreadRng; - #[cfg(test)] - impl AsMut for ThreadRng { - fn as_mut(&mut self) -> &mut Self { - self + impl ThreadRng { + #[inline(always)] + pub(super) fn fill_bytes_and_reseed(&mut self, dst: &mut [u8]) { + let mut rng = THREAD_RNG.lock(); + rng.fill_bytes_and_reseed(dst); } } } - pub(crate) use inner::thread_rng; - #[cfg(test)] - pub(crate) use inner::ThreadRng; - /// The system TRNG. struct OsTrng; @@ -245,7 +247,89 @@ pub(crate) mod trng { } } - fn random_key(trng: I) -> [u8; 32] + /// A ChaCha8 fast key erasure CSPRNG. + /// + /// The implementation is taken from + /// https://github.com/golang/go/blob/b50ccef67a5cd4a2919131cfeb6f3a21d6742385/src/crypto/internal/sysrand/rand_plan9.go + /// + /// For more information on "fast key erasure", see + /// . + #[derive(Clone)] + struct ChaCha8Csprng { + rng: ChaCha8Rng, + } + + impl ChaCha8Csprng { + /// Creates an ChaCha8 CSPRNG from `seed`. + #[inline(always)] + fn from_seed(seed: [u8; 32]) -> Self { + let rng = ChaCha8Rng::from_seed(seed); + Self { rng } + } + + /// Creates an ChaCha8 CSPRNG from a TRNG. + fn from_trng(trng: I) -> Self + where + I: IntoIterator, + K: Kdf, + { + let seed = random_seed::<_, K>(trng); + Self::from_seed(seed) + } + + /// Fills `dst` with cryptographically secure bytes, then + /// reseeds itself. + #[inline(always)] + fn fill_bytes_and_reseed(&mut self, dst: &mut [u8]) { + self.rng.fill_bytes(dst); + self.reseed(); + } + + /// Reseeds the CSPRNG. + #[inline(always)] + fn reseed(&mut self) { + let mut seed = [0; 32]; + self.rng.fill_bytes(&mut seed); + // NB: This uses a lot less stack space than + // *self = Self::from_seed(seed); + self.rng = ChaCha8Rng::from_seed(seed); + } + } + + impl ZeroizeOnDrop for ChaCha8Csprng {} + impl Drop for ChaCha8Csprng { + fn drop(&mut self) { + // Wipe the inner CSPRNG state. + let size = size_of_val(&self.rng); + let ptr = ptr::addr_of_mut!(self.rng).cast::>(); + for i in 0..size { + // SAFETY: this is safe because: + // - `ptr` points inside the allocated object + // because it's bounded to [0, size), which is + // as large as the object. + // - The computed offset cannot overflow `isize` + // (unless the size of the object is larger + // than `isize`, which is impossible). + // - The offset cannot wrap. + let ptr = unsafe { ptr.add(i) }; + // SAFETY: this is safe because: + // - `ptr` is valid for writes (see above). + // - `ptr` is is `MaybeUninit`, which has an + // alignment of 1, which is suitably aligned + // for all types. + unsafe { + ptr.write_volatile(MaybeUninit::zeroed()); + } + } + atomic::compiler_fence(Ordering::SeqCst); + } + } + + /// Expands random data from a TRNG into a uniformly random + /// seed. + /// + /// Used by [`ChaCha8Csprng`], but broken out for testing. + fn random_seed(trng: I) -> [u8; 32] where I: IntoIterator, K: Kdf, @@ -258,77 +342,19 @@ pub(crate) mod trng { let x = trng.next().expect("TRNG should not fail"); chunk.copy_from_slice(&x.to_le_bytes()); } - // A KDF is probably overkill here, but this - // method is only ever called once per RNG, so it - // doesn't hurt. + // A KDF is probably overkill here, but this method is + // only ever called once per CSRNG, so it doesn't hurt. let mut key = [0u8; 32]; - K::extract_and_expand(&mut key, &seed, &[], b"aes-ctr csprng for vxworks") + K::extract_and_expand(&mut key, &seed, &[], b"seed for chacha8 csprng") .expect("invalid KDF"); key } - /// A fast key erasure AES-CTR CSPRNG. - /// - /// Each instance is ephemeral; - /// - /// The implementation is taken from - /// https://github.com/golang/go/blob/e4aec1fa8a9c57672b783d16dd122cb4e6708089/src/crypto/rand/rand_plan9.go - /// - /// For more information, see - /// https://blog.cr.yp.to/20170723-random.html. - #[derive(ZeroizeOnDrop)] - struct AesCtrCsprng { - cipher: Aes256, - ctr: u64, - block: [u8; 16], - } - - impl AesCtrCsprng { - /// Creates an AES-CTR CSPRNG using `key` and returns it - /// as well as the next key. - #[inline(always)] - fn new(mut key: [u8; 32]) -> (Self, [u8; 32]) { - let mut ctr: u64 = 0; - let mut block = [0u8; 16]; - - let cipher = Aes256::new(&key.into()); - - // Erase the current key. - for chunk in key.chunks_exact_mut(16) { - cipher.encrypt_block_b2b(block.as_ref().into(), chunk.into()); - ctr = ctr.checked_add(1).expect("rng counter wrapped"); - block[..8].copy_from_slice(&ctr.to_le_bytes()) - } - - (Self { cipher, ctr, block }, key) - } - - /// Fills `dst` with cryptographically secure bytes. - #[inline(always)] - fn fill_bytes(&mut self, dst: &mut [u8]) { - // Read whole chunks - let mut dst = dst.chunks_exact_mut(16); - for chunk in dst.by_ref() { - self.cipher - .encrypt_block_b2b(self.block.as_ref().into(), chunk.into()); - self.ctr = self.ctr.checked_add(1).expect("rng counter wrapped"); - self.block[..8].copy_from_slice(&self.ctr.to_le_bytes()) - } - - // Read a partial chunk, if any. - let rem = dst.into_remainder(); - if !rem.is_empty() { - self.cipher.encrypt_block(self.block.as_mut().into()); - rem.copy_from_slice(&self.block[..rem.len()]); - } - } - } - #[cfg(test)] mod tests { use rand::{rngs::OsRng, RngCore}; - use super::{random_key, thread_rng, AesCtrCsprng, ThreadRng}; + use super::{random_seed, thread_rng, ChaCha8Csprng, ThreadRng}; use crate::{csprng::Csprng, kdf::Kdf}; #[no_mangle] @@ -336,64 +362,86 @@ pub(crate) mod trng { OsRng.next_u32() } + impl AsMut for ThreadRng { + fn as_mut(&mut self) -> &mut Self { + self + } + } + /// Test with BearSSL's HKDF. #[test] #[cfg(feature = "bearssl")] - fn test_aes_ctr_csprng_bearssl() { - test_aes_ctr_csprng::(); + fn test_random_seed_bearssl() { + test_random_seed::(); } /// Test our own HKDF. #[test] - fn test_aes_ctr_csprng_hkdf() { + fn test_random_seed_rust() { use crate::{hkdf::hkdf_impl, rust::Sha512}; hkdf_impl!(HkdfSha512, "HKDF-SHA512", Sha512); - test_aes_ctr_csprng::(); + test_random_seed::(); } - // Testing a RNG is a fraught endeavor. As such, we only - // implement a sanity test against a known-good - // implementation: https://go.dev/play/p/OAYa9kEqRHb - fn test_aes_ctr_csprng() { - let trng: &[u32] = &[ - 511020118, 3329505517, 3125191978, 2708588248, 2638371024, 1864699458, 1580599177, - 2931669449, 3911170326, 226101514, 3222450133, 2415624280, 3457417331, 2750971359, - 1283438866, 2735092416, 752222522, 531391756, 3105515119, 3499665662, 395492730, - 1606028116, 1422633577, 921326862, 40461932, 1750254861, 2210511461, 524576318, - 2841035765, 3036150926, 1117144028, 1942094251, 406390843, 1022411745, 2181488984, - 174429379, 196375134, 2128445749, 2226654310, 2876855800, 648736228, 4206437523, - 3780770807, 2337460207, 3038254605, 2284497048, 3691784102, 1444544244, 187268599, - 2171708536, 4093616657, 2773863083, 2520031184, 3369335287, 3730932382, 1377172275, - 2557866454, 2729367996, 2129009426, 2713073031, 352831220, 2298512516, 4277364210, - 23336659, 3536517015, 1423492831, 4031290816, 874352915, 2280206248, 2003567320, - 2965184223, 4045591871, 1214173797, 3231248046, 1756949802, 424814597, 1611041307, - 2304187543, 3013626048, 2083074060, + fn test_random_seed() { + // Generated by Go's `math/rand/v2.ChaCha8` RNG with + // a seed of "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456". + // + // See https://go.dev/play/p/NzDatQOf6O0 + const TRNG: [u32; 80] = [ + 1028003493, 2792012860, 1099769980, 2128370902, 756815533, 2414602873, 311122750, + 307405647, 2104290982, 530412394, 404676639, 182813750, 3425358440, 1260096186, + 2462344801, 399173164, 2135830142, 2699860934, 887799328, 1433459368, 289238002, + 1683313852, 3676428769, 1357185267, 3661058213, 1833683120, 3822579273, 2285597052, + 958698916, 2519770651, 1572529299, 2790931779, 420475008, 963064624, 1824154675, + 118275351, 4287391074, 1832189034, 50997640, 130225725, 1173499583, 610709929, + 2965402324, 1231825150, 2405225696, 3322754931, 3455205006, 3243476928, 234695516, + 93699511, 3838575301, 4027966375, 2597847841, 3510230663, 519341910, 571863882, + 3553626094, 3335867058, 1729293762, 1283510227, 1952190125, 1170477288, 2418110188, + 540190490, 4215328104, 1922401658, 3651883646, 2015091372, 1155297874, 1031749841, + 3836924763, 3524495878, 3395345112, 111962728, 2269910968, 1987501596, 841111076, + 328762168, 1383411217, 3898745338, ]; - let key = random_key::<_, K>(trng.iter().copied()); - let (mut rng, _) = AesCtrCsprng::new(key); - const WANT: &[u8] = &[ - 0x72, 0x36, 0x2b, 0x40, 0x54, 0xf6, 0x81, 0x15, 0xc5, 0x91, 0x3d, 0x58, 0x9f, 0xfd, - 0x19, 0x62, 0x13, 0x99, 0x65, 0x7, 0x53, 0xb1, 0x9c, 0xcc, 0x93, 0x86, 0x71, 0xd2, - 0x6, 0x8, 0xbf, 0x43, 0x40, 0xc2, 0xa7, 0xdf, 0xc3, 0x61, 0xfa, 0xaa, + let got = random_seed::<_, K>(TRNG); + const WANT: [u8; 32] = [ + 0xe, 0x6b, 0xc5, 0x9d, 0x68, 0x3e, 0x41, 0x16, 0x6b, 0x31, 0x76, 0x82, 0xe, 0xcb, + 0x7c, 0x30, 0x15, 0x6b, 0x72, 0x12, 0xda, 0x7d, 0x23, 0x94, 0x81, 0x5f, 0xe2, 0xc3, + 0xc3, 0x1f, 0x77, 0x2f, ]; - let mut got = [0u8; 16 * 2 + (16 / 2)]; - rng.fill_bytes(&mut got); assert_eq!(got, WANT); } + /// Test that reseeding `ChaCha8Csprng` changes its + /// state. + #[test] + fn test_chacha8csprng_reseed() { + const SEED: [u8; 32] = *b"ABCDEFGHIJKLMNOPQRSTUVWXYZ123456"; + let mut rng = ChaCha8Csprng::from_seed(SEED); + let old = rng.rng.get_seed(); + rng.reseed(); + let new = rng.rng.get_seed(); + assert_ne!(old, new); + } + + /// Sanity check that two [`ThreadRng`]s are different. #[test] fn test_thread_rng() { - fn get_bytes>(mut rng: R) -> [u8; 4096] { - let mut b = [0u8; 4096]; - rng.as_mut().fill_bytes(&mut b); + fn get_bytes(rng: &mut ThreadRng) -> [u8; 32] { + let mut b = [0; 32]; + rng.fill_bytes(&mut b); b } let mut rng = thread_rng(); assert_ne!(get_bytes(&mut rng), get_bytes(&mut rng)); - assert_ne!(get_bytes(thread_rng()), get_bytes(thread_rng())); - assert_ne!(get_bytes(thread_rng()), [0u8; 4096]) + assert_ne!(get_bytes(&mut thread_rng()), get_bytes(&mut thread_rng())); + assert_ne!(get_bytes(&mut thread_rng()), [0; 32]); + + let rng = thread_rng(); + let mut a = rng.clone(); + let mut b = thread_rng(); + assert_ne!(get_bytes(&mut a), get_bytes(&mut b)); } } }