diff --git a/alpha_0.1.2_release_notes.md b/alpha_0.1.2_release_notes.md index f60dbb3..cad6e6a 100644 --- a/alpha_0.1.2_release_notes.md +++ b/alpha_0.1.2_release_notes.md @@ -2,36 +2,13 @@ [remove this section before publication] -* ML-DSA & ML-KEM - * Check the crate release checklist and run claude against the style guide (maybe Francis could cross-check me) - * Run Crucible testing - * Add factories for ML-DSA and ML-KEM (if we are keeping factories, see below) - * After merging the Signer/Verifier, Encrypter/Decrypter split, check if the keygen_from_rng() is still on the right - trait. -* Split the Signature trait into a Signer and a Verifier so that, for example, we can implement the verifier for MTC in - a different struct from the signer; or so that you can get FIPS compliance on old algorithms that are currently only - FIPS-allowed for verification of existing signatures but not for creation of new ones. -* Check out Megan's email May 13 about KeyMaterial: "I was wondering if there might be scope for a closure based - approach that could guarantee encapsulation of the state change from safe to hazardous back to safe again." -* Go back to previous algs and apply memory optimization tricks like internal functions. And add a docs section "Memory - Usage" that measures with valgrind. -* Ensure that all crates have `#![forbid(missing_docs)]` -* Apply Secret trait consistently across the library --> study the `Zeroize` trait in RustCrypto -* Change all "[u8;0]" to "[]" throughout the code and docs ... or better yet, change the APIs to take an Option<> -* Change all `-> Vec` to `-> [u8; CONST_LEN]`, and the `output: &mut [u8]` to `output: &mut [u8; CONST_LEN]` where - appropriate. -* Probably it makes sense to leave Hex and Base64 as requiring std; ... or maybe add a no_std version that uses - fixed-sized blocks? -* Create a cargo feature #[cfg(feature='rng')] and put it around things like keygen that takes an rng so that the build - dependency on bouncycastle_rng is optional. -* Factories ... Are they worth it? Michael Richardson says Very Yes. If we are keeping them, then we need a serious - re-engineering of them because I really dislike that currently they make it hard for the underlying primitive to have - static one-shot APIs. * Deal with as many of the inline TODOs as possible * Close all open github issues and document them in this file. +* Clean up `cargo doc --all` warnings +* On release/0.1.2alpha, get Claude to make a cosmetic change to the docs for all crates to use [`Secret`] instead + of [Secret] because this renders better. * After everything is merged, circle back to crucible, and make sure that the harness still works -* Search for all the uses of .unwrap() in non-test code and replace each with either a comment or an expect with a - meaningful error string. +* Delete this file and stuff it into the Description of a github Release. # 0.1.2 Features / Changelog @@ -47,16 +24,47 @@ potentially across versions of the library. The intended use case is if you are processing a large input that depends on one or more network round-trips and you wish to suspend to a cache and potentially transfer to a new host while waiting for network IO. +* dyn RNG: anywhere that consumes randomness (such as keygen and non-deterministic sign / encaps functions) can now be + handed an instance of an object that impl's `bouncycastle-core::traits::RNG`. +* Rework of the Secret system for protecting secret data against leakage via returning to the memory pool unzeroized, + or being logged in debug messages, stack traces, and crash dumps. Now properly uses `core::mem::write_volatile` to + prevent + the compiler from eliding writes on drop, and introduced a new type system `Secret` that is used across the library + to give more fine-grained control over which objects (and which fields within objects) get this extra protection. + Bonus: this is a public type that you can use to protect your application data as well! ## Minor features / bug fixes -* All public `*_out(.., out: &mut [u8])` functions now begin by zeroizing the entire output buffer with `.fill(0)`, - preventing exposure of stale data in oversized output buffers or on early error returns. +Trait system: + +* Split the Signature trait into a Signer and a Verifier trait. This is for two reasons: 1) some of the future signature + algorithms (like hash-based signatures) the verifier code is substantially lighter than the signer code, or we may not + even want to implement a signer in software, and 2) NIST likes to soft-deprecate algorithms by disallowing generation + of new signatures, but still allowing verification of existing signatures. +* Added traits for symmetric ciphers in the block cipher, stream cipher, and AEAD families. We don't have any of these + algorithms implemented yet, but they're coming! + +The KeyMaterial object: + * Reworked the way KeyMaterial hazardous operations work; instead of a stateful .allow_hazardous_operations() / .drop_hazardous_operations(), it now uses a closure-based do_hazardous_operations(). Github issue #39. * Renamed KeyMaterial::KeyType's and deleted KeyMaterial::concatenate in order to give a better intuition and FIPS-alignment. -* Removed the dependence on nightly / experimental compiler features; the library now buildds on stable. -* Github issues resolved: - * #6: https://github.com/bcgit/bc-rust/issues/6, thanks to Q. T. Felix (github: @Quant-TheodoreFelix) - * #10: https://github.com/bcgit/bc-rust/issues/10, thanks to Nicola Tuveri (github: @romen) \ No newline at end of file +* Tightened up the entropy-tracking behaviour of the KeyMaterial object, thanks to Q. T. Felix (github: + @Quant-TheodoreFelix, github issue #6) + +Docs: + +* All crypto algorithm crates now have Memory Usage docs that list the stack memory usage of the implementation. +* All crypto algorithm crates now have `#![forbid(missing_docs)]` to ensure that they have a fully-documented public + API. + +* Other miscellaneous Github issues resolved: + * #10: https://github.com/bcgit/bc-rust/issues/10, thanks to Nicola Tuveri (github: @romen) + * #18: All public `*_out(.., out: &mut [u8])` functions now begin by zeroizing the entire provided output buffer + with `.fill(0)`, + preventing exposure of stale data in oversized output buffers or on early error returns. Thanks to Q. T. Felix ( + github: @Quant-TheodoreFelix) + * #27: "SHAKE absorb-after-squeeze": clarified and hardened the behaviour of SHAKE with respect to absorbing more + input after having been squeezed. + * #28: Removed the dependence on nightly / experimental compiler features; the library now builds on stable. diff --git a/crypto/core/src/key_material.rs b/crypto/core/src/key_material.rs index 50dbcb2..514b50f 100644 --- a/crypto/core/src/key_material.rs +++ b/crypto/core/src/key_material.rs @@ -51,8 +51,8 @@ //! See [do_hazardous_operations] for documentation and sample code. use crate::errors::{KeyMaterialError, SuspendableError}; -use crate::traits::{RNG, Secret, SecurityStrength}; -use bouncycastle_utils::{ct, min}; +use crate::traits::{RNG, SecurityStrength}; +use bouncycastle_utils::{ct, min, secret::Secret}; use core::cmp::{Ordering, PartialOrd}; use core::fmt; @@ -215,15 +215,13 @@ pub trait KeyMaterialTrait: KeyMaterialInternalTrait { /// The capacity of the internal buffer can be set at compile-time via the param. #[derive(Clone)] pub struct KeyMaterial { - buf: [u8; KEY_LEN], - key_len: usize, + buf: Secret<[u8; KEY_LEN]>, + key_len: Secret, key_type: KeyType, security_strength: SecurityStrength, allow_hazardous_operations: bool, } -impl Secret for KeyMaterial {} - // The explicit `#[repr(u8)]` discriminants are the stable on-the-wire encoding used by // `SerializableState` implementations (see the `TryFrom` impl below). Pin each value to its // variant name: reordering variants is fine, but never reuse or renumber an existing discriminant, @@ -287,8 +285,8 @@ impl KeyMaterial { /// If you want a properly populated instance, use [KeyMaterial::from_rng]. pub fn new() -> Self { Self { - buf: [0u8; KEY_LEN], - key_len: 0, + buf: Secret::new(), + key_len: Secret::new(), key_type: KeyType::Zeroized, security_strength: SecurityStrength::None, allow_hazardous_operations: false, @@ -305,7 +303,7 @@ impl KeyMaterial { Ok(()) })?; - key.key_len = KEY_LEN; + *key.key_len = KEY_LEN; key.key_type = KeyType::CryptographicRandom; key.security_strength = rng.security_strength(); Ok(key) @@ -347,14 +345,11 @@ impl KeyMaterial { return Err(KeyMaterialError::InputDataLongerThanKeyCapacity); } - let mut key = Self { - buf: [0u8; KEY_LEN], - key_len: other.key_len(), - key_type: other.key_type(), - security_strength: SecurityStrength::None, - allow_hazardous_operations: false, - }; + let mut key = Self::new(); key.buf[..other.key_len()].copy_from_slice(other.ref_to_bytes()); + *key.key_len = other.key_len(); + key.key_type = other.key_type(); + key.security_strength = other.security_strength(); Ok(key) } } @@ -378,7 +373,7 @@ impl KeyMaterialTrait for KeyMaterial { }; self.buf[..source.len()].copy_from_slice(source); - self.key_len = source.len(); + *self.key_len = source.len(); self.key_type = new_key_type; do_hazardous_operations(self, |s| { @@ -399,14 +394,14 @@ impl KeyMaterialTrait for KeyMaterial { } fn ref_to_bytes(&self) -> &[u8] { - &self.buf[..self.key_len] + &self.buf[..*self.key_len] } fn ref_to_bytes_mut(&mut self) -> Result<&mut [u8], KeyMaterialError> { if !self.allow_hazardous_operations { return Err(KeyMaterialError::HazardousOperationNotPermitted); } - Ok(&mut self.buf) + Ok(self.buf.as_mut()) } fn capacity(&self) -> usize { @@ -414,7 +409,7 @@ impl KeyMaterialTrait for KeyMaterial { } fn key_len(&self) -> usize { - self.key_len + *self.key_len } fn set_key_len(&mut self, key_len: usize) -> Result<(), KeyMaterialError> { @@ -423,7 +418,7 @@ impl KeyMaterialTrait for KeyMaterial { } // are we extending the key length, or truncating? - if key_len <= self.key_len { + if key_len <= *self.key_len { // truncation is always allowed (not hazardous) self.security_strength = @@ -433,14 +428,14 @@ impl KeyMaterialTrait for KeyMaterial { self.key_type = KeyType::Zeroized; } - self.key_len = key_len; + *self.key_len = key_len; Ok(()) } else { if !self.allow_hazardous_operations { return Err(KeyMaterialError::HazardousOperationNotPermitted); } - self.key_len = key_len; + *self.key_len = key_len; Ok(()) } } @@ -557,8 +552,8 @@ impl KeyMaterialTrait for KeyMaterial { } fn zeroize(&mut self) { - self.buf.fill(0u8); - self.key_len = 0; + self.buf.zeroize(); + self.key_len.zeroize(); self.key_type = KeyType::Zeroized; } @@ -579,7 +574,7 @@ impl PartialEq for KeyMaterial { if self.key_len != other.key_len { return false; } - ct::ct_eq_bytes(&self.buf[..self.key_len], &other.buf[..self.key_len]) + ct::ct_eq_bytes(&self.buf[..*self.key_len], &other.buf[..*self.key_len]) } } impl Eq for KeyMaterial {} @@ -618,10 +613,11 @@ impl PartialOrd for KeyType { /// Block accidental logging of the internal key material buffer. impl fmt::Display for KeyMaterial { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // deref the key_len explicitly so that Secret doesn't render it as "" write!( f, - "KeyMaterial {{ len: {}, key_type: {:?}, security_strength: {:?} }}", - self.key_len, self.key_type, self.security_strength + "KeyMaterial<{}>{{ len: {}, key_type: {:?}, security_strength: {:?} }}", + KEY_LEN, *self.key_len, self.key_type, self.security_strength ) } } @@ -629,21 +625,15 @@ impl fmt::Display for KeyMaterial { /// Block accidental logging of the internal key material buffer. impl fmt::Debug for KeyMaterial { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // deref the key_len explicitly so that Secret doesn't render it as "" write!( f, - "KeyMaterial {{ len: {}, key_type: {:?}, security_strength: {:?} }}", - self.key_len, self.key_type, self.security_strength + "KeyMaterial<{}>{{ len: {}, key_type: {:?}, security_strength: {:?} }}", + KEY_LEN, *self.key_len, self.key_type, self.security_strength ) } } -/// Zeroize the key material on drop. -impl Drop for KeyMaterial { - fn drop(&mut self) { - self.zeroize() - } -} - /* Hazardous Operations Runner */ /// Internal-use trait holding the low-level hazardous-operations guard toggle. diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index a19c3bd..4086dd1 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -36,9 +36,7 @@ pub trait AlgorithmOID { /// ciphertexts that are incompatible with other implementations as ciphers in more complex modes, such /// as AEADs or stream ciphers may need to stick extra data either at the beginning or end of the ciphertext. /// See the documentation of the underlying implementation for more details. -pub trait SymmetricCipher: - Algorithm + Secret -{ +pub trait SymmetricCipher: Algorithm { #[cfg(feature = "std")] /// A one-shot API to encrypt some plaintext with the given key. /// This function returns the ciphertext as a Vec, and therefore is only available when compiling with std. @@ -522,7 +520,7 @@ pub trait KEMPublicKey: } /// A private key for a KEM algorithm, often denoted "sk" (for "secret key"). -pub trait KEMPrivateKey: PartialEq + Eq + Clone + Secret + Sized { +pub trait KEMPrivateKey: PartialEq + Eq + Clone + Sized { /// Write it out to bytes in its standard encoding. fn encode(&self) -> [u8; SK_LEN]; /// Write it out to bytes in its standard encoding. @@ -768,14 +766,6 @@ pub trait RNG { fn security_strength(&self) -> SecurityStrength; } -/// A trait that forces an object to implement a zeroizing Drop() as well as Debug and Display that -/// will not log the sensitive contents, even in error or crash-dump scenarios. -// Since rust auto-implements Drop, there's a lint that explicitly bounding on Drop is useless. -// I disagree because I want to force things that are secrets to manually implement Drop that zeroizes the data. -// So I'm turning off this lint. -#[allow(drop_bounds)] -pub trait Secret: Drop + Debug + Display {} - /// Allows a stateful object to suspend its operation by serializing its state into a byte array ///so that it can be resumed later, potentially from a different host. /// @@ -925,9 +915,7 @@ pub trait SignaturePublicKey: } /// A private key for a signature algorithm, often denoted "sk" (for "secret key"). -pub trait SignaturePrivateKey: - PartialEq + Eq + Clone + Secret + Sized -{ +pub trait SignaturePrivateKey: PartialEq + Eq + Clone + Sized { /// Write it out to bytes in its standard encoding. fn encode(&self) -> [u8; SK_LEN]; /// Write it out to bytes in its standard encoding. diff --git a/crypto/core/tests/key_material_tests.rs b/crypto/core/tests/key_material_tests.rs index 0dfcf96..4246752 100644 --- a/crypto/core/tests/key_material_tests.rs +++ b/crypto/core/tests/key_material_tests.rs @@ -172,6 +172,33 @@ mod test_key_material { assert_eq!(key.unwrap().security_strength(), SecurityStrength::None); } + #[test] + fn from_keymaterial() { + let key1 = KeyMaterial256::from_bytes_as_type(&DUMMY_KEY[..32], KeyType::MACKey).unwrap(); + assert_eq!(key1.key_type(), KeyType::MACKey); + assert_eq!(key1.security_strength(), SecurityStrength::_256bit); + + // success case: same size using default From impl; only works if the sizes are the same (ie the compiler knows that they are the same type. + let key2 = KeyMaterial256::from(key1.clone()); + assert_eq!(key1.key_len(), key2.key_len()); + assert_eq!(key1.key_type(), key2.key_type()); + assert_eq!(key1.security_strength(), key2.security_strength()); + assert_eq!(key1, key2); + + // success case: same size + let key2 = KeyMaterial256::from_key(&key1).unwrap(); + assert_eq!(key1.key_len(), key2.key_len()); + assert_eq!(key1.key_type(), key2.key_type()); + assert_eq!(key1, key2); + + // success case: bigger + let key2 = KeyMaterial512::from_key(&key1).unwrap(); + assert_eq!(key1.key_len(), key2.key_len()); + assert_eq!(key1.key_type(), key2.key_type()); + assert_eq!(key1.security_strength(), key2.security_strength()); + assert_eq!(key1.ref_to_bytes(), &key2.ref_to_bytes()[..key1.key_len()]); + } + #[test] fn new_from_rng() { use bouncycastle_rng as rng; @@ -441,46 +468,36 @@ mod test_key_material { #[test] /// impl Display for KeyMaterial to not print the key data. fn test_display() { - let key = KeyMaterial256::from_bytes_as_type(&DUMMY_KEY[..32], KeyType::MACKey).unwrap(); - // println!("{:?}", key); + let key256 = KeyMaterial256::from_bytes_as_type(&DUMMY_KEY[..32], KeyType::MACKey).unwrap(); + // println!("{:?}", key256); // test fmt assert_eq!( - format!("{}", key), - "KeyMaterial { len: 32, key_type: MACKey, security_strength: _256bit }" + format!("{}", key256), + "KeyMaterial<32>{ len: 32, key_type: MACKey, security_strength: _256bit }" ); // test debug assert_eq!( - format!("{:?}", key), - "KeyMaterial { len: 32, key_type: MACKey, security_strength: _256bit }" + format!("{:?}", key256), + "KeyMaterial<32>{ len: 32, key_type: MACKey, security_strength: _256bit }" ); - } - #[test] - fn from_keym() { - let key1 = KeyMaterial256::from_bytes_as_type(&DUMMY_KEY[..32], KeyType::MACKey).unwrap(); - assert_eq!(key1.key_type(), KeyType::MACKey); - assert_eq!(key1.security_strength(), SecurityStrength::_256bit); + // and an underfull one of a different size. - // success case: same size using default From impl; only works if the sizes are the same (ie the compiler knows that they are the same type. - let key2 = KeyMaterial256::from(key1.clone()); - assert_eq!(key1.key_len(), key2.key_len()); - assert_eq!(key1.key_type(), key2.key_type()); - assert_eq!(key1.security_strength(), key2.security_strength()); - assert_eq!(key1, key2); + let key512 = KeyMaterial512::from_key(&key256).unwrap(); - // success case: same size - let key2 = KeyMaterial256::from_key(&key1).unwrap(); - assert_eq!(key1.key_len(), key2.key_len()); - assert_eq!(key1.key_type(), key2.key_type()); - assert_eq!(key1, key2); + // test fmt + assert_eq!( + format!("{}", key512), + "KeyMaterial<64>{ len: 32, key_type: MACKey, security_strength: _256bit }" + ); - // success case: bigger - let key2 = KeyMaterial512::from_key(&key1).unwrap(); - assert_eq!(key1.key_len(), key2.key_len()); - assert_eq!(key1.key_type(), key2.key_type()); - assert_eq!(key1.ref_to_bytes(), &key2.ref_to_bytes()[..key1.key_len()]); + // test debug + assert_eq!( + format!("{:?}", key512), + "KeyMaterial<64>{ len: 32, key_type: MACKey, security_strength: _256bit }" + ); } #[test] diff --git a/crypto/hmac/src/lib.rs b/crypto/hmac/src/lib.rs index acd55c3..9ee4076 100644 --- a/crypto/hmac/src/lib.rs +++ b/crypto/hmac/src/lib.rs @@ -22,7 +22,7 @@ //! //! # Examples //! -//! Instantation of an HMAC object is straightforward: +//! Instantiation of an HMAC object is straightforward: //! //! ``` //! use bouncycastle_hmac::HMAC_SHA256; @@ -186,15 +186,14 @@ use bouncycastle_core::errors::{KeyMaterialError, MACError, RNGError, SuspendableError}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::traits::{ - Algorithm, AlgorithmOID, Hash, MAC, RNG, Secret, SecurityStrength, Suspendable, - SuspendableKeyed, + Algorithm, AlgorithmOID, Hash, MAC, RNG, SecurityStrength, Suspendable, SuspendableKeyed, }; use bouncycastle_rng::{HashDRBG_SHA256, HashDRBG_SHA512}; use bouncycastle_sha2::{ SHA224, SHA256, SHA384, SHA512, SUSPENDED_SHA256_STATE_LEN, SUSPENDED_SHA512_STATE_LEN, }; use bouncycastle_sha3::{SHA3_224, SHA3_256, SHA3_384, SHA3_512, SUSPENDED_SHA3_STATE_LEN}; -use bouncycastle_utils::ct; +use bouncycastle_utils::{ct, secret::Secret}; use core::fmt::{Debug, Display, Formatter}; /*** String constants ***/ @@ -342,18 +341,8 @@ pub struct HMAC Secret for HMAC {} - -impl Drop for HMAC { - fn drop(&mut self) { - self.key.fill(0); - self.key_len = 0; - } + key: Secret<[u8; KEY_BUF_LEN]>, + key_len: Secret, // Doing it this way to avoid needing a vec, so that this can be made no_std friendly. } impl Debug for HMAC { @@ -390,7 +379,7 @@ impl HMAC { // TODO: make this no_std-friendly let mut padded = vec![0u8; self.hasher.block_bitlen() / 8]; - padded[..self.key_len].copy_from_slice(&self.key[..self.key_len]); + padded[..*self.key_len].copy_from_slice(&self.key[..*self.key_len]); // XXX: easier way to xor over Vec? for entry in &mut padded { @@ -410,15 +399,15 @@ impl HMAC { if key_bytes.len() > self.hasher.block_bitlen() / 8 { // then we have to pre-hash it -- use a new instance of the hasher rather than the internal one HASH::default().hash_out(key_bytes, &mut self.key[..self.hasher.output_len()]); - self.key_len = self.hasher.output_len(); + *self.key_len = self.hasher.output_len(); } else { self.key[..key_bytes.len()].copy_from_slice(key_bytes); - self.key_len = key_bytes.len(); + *self.key_len = key_bytes.len(); } // Just as a sanity-check. assert!( - self.key_len <= KEY_BUF_LEN, + *self.key_len <= KEY_BUF_LEN, "Fatal error: Key length exceeds HMAC internal buffer length" ); } @@ -491,13 +480,13 @@ impl HMAC { impl MAC for HMAC { fn new(key: &impl KeyMaterialTrait) -> Result { - let mut hmac = Self { hasher: HASH::default(), key: [0u8; KEY_BUF_LEN], key_len: 0 }; + let mut hmac = Self { hasher: HASH::default(), key: Secret::new(), key_len: Secret::new() }; hmac.init(key, false)?; Ok(hmac) } fn new_allow_weak_key(key: &impl KeyMaterialTrait) -> Result { - let mut hmac = Self { hasher: HASH::default(), key: [0u8; KEY_BUF_LEN], key_len: 0 }; + let mut hmac = Self { hasher: HASH::default(), key: Secret::new(), key_len: Secret::new() }; hmac.init(key, true)?; Ok(hmac) } @@ -616,7 +605,7 @@ impl< // Re-load the key material exactly as `new()` did (pre-hashing an over-length key), but do // NOT re-absorb `K ⊕ ipad` — the deserialized hasher already contains it. The key is only // needed for the outer `K ⊕ opad` step at finalization. - let mut hmac = HMAC { hasher, key: [0u8; KEY_BUF_LEN], key_len: 0 }; + let mut hmac = HMAC { hasher, key: Secret::new(), key_len: Secret::new() }; hmac.load_key_material(key.ref_to_bytes()); Ok(hmac) diff --git a/crypto/mldsa-lowmemory/src/aux_functions.rs b/crypto/mldsa-lowmemory/src/aux_functions.rs index 19782da..ff3273d 100644 --- a/crypto/mldsa-lowmemory/src/aux_functions.rs +++ b/crypto/mldsa-lowmemory/src/aux_functions.rs @@ -239,11 +239,9 @@ pub(crate) fn simple_bit_unpack_t1(v: &[u8; POLY_T1PACKED_LEN]) -> Polynomial { // the hope here is that the compiler will aggressively inline this function, // and optimize away the branching. #[inline(always)] -pub(crate) fn bit_unpack_eta(v: &[u8]) -> Polynomial { +pub(crate) fn bit_unpack_eta_out(v: &[u8], w: &mut Polynomial) { debug_assert_eq!(v.len(), bitlen_eta(ETA)); - let mut w = Polynomial::new(); - match ETA { // MLDSA44 and MLDSA87 2 => { @@ -281,8 +279,6 @@ pub(crate) fn bit_unpack_eta(v: &[u8]) -> Polynomial { } _ => panic!("Invalid eta value"), } - - w } /// A variant of Algorithm 19 BitUnpack specific to a=𝛾1 − 1, b=𝛾1 diff --git a/crypto/mldsa-lowmemory/src/lib.rs b/crypto/mldsa-lowmemory/src/lib.rs index d1e7a8b..32ad8f4 100644 --- a/crypto/mldsa-lowmemory/src/lib.rs +++ b/crypto/mldsa-lowmemory/src/lib.rs @@ -178,7 +178,8 @@ //! And that's the basic usage! There are lots more bells-and-whistles in the form of exposed algorithm //! parameters, streaming APIs and other goodies that you can find by poking around this documentation. //! -//! # Security +//! # 🚨 Security 🚨 +//! //! All functionality exposed by this crate is considered secure to use. //! In other words, this crate does not contain any "hazmat" except for the obvous points about //! handling your private keys properly: if you post your private key to github, or you generate diff --git a/crypto/mldsa-lowmemory/src/low_memory_helpers.rs b/crypto/mldsa-lowmemory/src/low_memory_helpers.rs index d9e52f1..81505ef 100644 --- a/crypto/mldsa-lowmemory/src/low_memory_helpers.rs +++ b/crypto/mldsa-lowmemory/src/low_memory_helpers.rs @@ -3,11 +3,12 @@ //! what it needs in pieces, which generally means handling the matrices and vectors row-wise or entry-wise. use crate::aux_functions::{ - bit_unpack_eta, bitlen_eta, expand_mask_poly, rej_ntt_poly, unpack_z_row, + bit_unpack_eta_out, bitlen_eta, expand_mask_poly, rej_ntt_poly, unpack_z_row, }; use crate::mldsa::d; use crate::polynomial::Polynomial; use bouncycastle_core::errors::SignatureError; +use bouncycastle_utils::secret::Secret; #[inline(always)] pub(crate) fn expandA_elem(rho: &[u8; 32], i: usize, j: usize) -> Polynomial { @@ -161,6 +162,15 @@ pub(crate) fn compute_ct0_component( if ct0.check_norm::() { None } else { Some(ct0) } } -pub(crate) fn s_unpack(s_packed: &[u8], idx: usize) -> Polynomial { - bit_unpack_eta::(&s_packed[idx * bitlen_eta(eta)..(idx + 1) * bitlen_eta(eta)]) +/// Unpack a single s value from the packed representation. +pub(crate) fn s_unpack( + s_packed: &Secret<[u8; S_PACKED_LEN]>, + idx: usize, +) -> Polynomial { + let mut s = Polynomial::new(); + bit_unpack_eta_out::( + &s_packed[idx * bitlen_eta(eta)..(idx + 1) * bitlen_eta(eta)], + &mut s, + ); + s } diff --git a/crypto/mldsa-lowmemory/src/mldsa.rs b/crypto/mldsa-lowmemory/src/mldsa.rs index 454665a..725fabb 100644 --- a/crypto/mldsa-lowmemory/src/mldsa.rs +++ b/crypto/mldsa-lowmemory/src/mldsa.rs @@ -413,6 +413,7 @@ use crate::hash_mldsa; use bouncycastle_core::key_material::KeyMaterial256; #[allow(unused_imports)] use bouncycastle_core::traits::{PHSignatureVerifier, PHSigner}; +use bouncycastle_utils::secret::Secret; /*** Constants ***/ /// @@ -1087,8 +1088,8 @@ impl< // We'll uncompress them as-needed, and only one polynomial at a time. // You can avoid storing these in memory, but then all the sites where they are used // will require calls to sk.compute_s1_row() and sk.compute_s2_row(), which are fairly expensive. - let s1_packed: [u8; S1_PACKED_LEN] = sk.compute_s1_packed(); - let s2_packed: [u8; S2_PACKED_LEN] = sk.compute_s2_packed(); + let s1_packed: Secret<[u8; S1_PACKED_LEN]> = sk.compute_s1_packed(); + let s2_packed: Secret<[u8; S2_PACKED_LEN]> = sk.compute_s2_packed(); // 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀 ′, 64) // skip: mu has already been provided @@ -1165,7 +1166,7 @@ impl< // weirdly, in perf testing, this actually caused memory usage to go by a small amount; // maybe because re-computing the intermediates adds more to the widest point of the alg? // &sk.compute_s1_row(col), - &s_unpack::(&s1_packed, col), + &s_unpack::(&s1_packed, col), &rho_p_p, &c_hat, kappa, @@ -1196,7 +1197,7 @@ impl< // [Optimization Note]: // This is one of the places that a row of s1 can be re-computed instead of unpacked from the compressed form. // &sk.compute_s2_row(row), - &s_unpack::(&s2_packed, row), + &s_unpack::(&s2_packed, row), &w, &c_hat, ) { diff --git a/crypto/mldsa-lowmemory/src/mldsa_keys.rs b/crypto/mldsa-lowmemory/src/mldsa_keys.rs index f23a46c..734df32 100644 --- a/crypto/mldsa-lowmemory/src/mldsa_keys.rs +++ b/crypto/mldsa-lowmemory/src/mldsa_keys.rs @@ -23,12 +23,11 @@ use crate::{ML_DSA_44_NAME, ML_DSA_65_NAME, ML_DSA_87_NAME}; use bouncycastle_core::errors::SignatureError; use bouncycastle_core::key_material; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; -use bouncycastle_core::traits::{ - Secret, SecurityStrength, SignaturePrivateKey, SignaturePublicKey, XOF, -}; +use bouncycastle_core::traits::{SecurityStrength, SignaturePrivateKey, SignaturePublicKey, XOF}; +use bouncycastle_utils::secret::Secret; use core::fmt; use core::fmt::{Debug, Display, Formatter}; - +use core::ops::DerefMut; // imports just for docs #[allow(unused_imports)] use crate::mldsa::MLDSATrait; @@ -309,76 +308,13 @@ pub struct MLDSASeedPrivateKey< const SK_LEN: usize, const FULL_SK_LEN: usize, > { + // note: KeyMaterial is inherently Secret seed: KeyMaterial<32>, + // public seed rho does not need to be secret rho: [u8; 32], - rho_prime: [u8; 64], - K: [u8; 32], -} - -impl< - const LAMBDA: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const eta: usize, - const S1_PACKED_LEN: usize, - const S2_PACKED_LEN: usize, - const T1_PACKED_LEN: usize, - const SK_LEN: usize, - const PK_LEN: usize, - const FULL_SK_LEN: usize, -> Drop - for MLDSASeedPrivateKey< - LAMBDA, - GAMMA2, - k, - l, - eta, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - PK_LEN, - SK_LEN, - FULL_SK_LEN, - > -{ - fn drop(&mut self) { - // seed is a KeyMaterialSized which will zeroize itself - self.rho.fill(0u8); - self.rho_prime.fill(0u8); - self.K.fill(0u8); - } + rho_prime: Secret<[u8; 64]>, + K: Secret<[u8; 32]>, } - -impl< - const LAMBDA: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const eta: usize, - const S1_PACKED_LEN: usize, - const S2_PACKED_LEN: usize, - const T1_PACKED_LEN: usize, - const PK_LEN: usize, - const SK_LEN: usize, - const FULL_SK_LEN: usize, -> Secret - for MLDSASeedPrivateKey< - LAMBDA, - GAMMA2, - k, - l, - eta, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - PK_LEN, - SK_LEN, - FULL_SK_LEN, - > -{ -} - impl< const LAMBDA: i32, const GAMMA2: i32, @@ -499,16 +435,19 @@ impl< } let (rho, rho_prime, K) = Self::compute_rhos_and_K(&seed); + Ok(Self { seed: seed.clone(), rho, rho_prime, K }) } - fn compute_rhos_and_K(seed: &KeyMaterial<32>) -> ([u8; 32], [u8; 64], [u8; 32]) { + fn compute_rhos_and_K( + seed: &KeyMaterial<32>, + ) -> ([u8; 32], Secret<[u8; 64]>, Secret<[u8; 32]>) { // derive sk.K // Alg 6; 1: (rho, rho_prime, K) <- H(𝜉||IntegerToBytes(𝑘, 1)||IntegerToBytes(ℓ, 1), 128) // ▷ expand seed - let mut rho: [u8; 32] = [0u8; 32]; - let mut rho_prime: [u8; 64] = [0u8; 64]; - let mut K: [u8; 32] = [0u8; 32]; + let mut rho = [0u8; 32]; + let mut rho_prime: Secret<[u8; 64]> = Secret::new(); + let mut K: Secret<[u8; 32]> = Secret::new(); let mut h = H::default(); h.absorb(seed.ref_to_bytes()).expect("absorb before squeeze is infallible"); @@ -516,21 +455,26 @@ impl< h.absorb(&(l as u8).to_le_bytes()).expect("absorb before squeeze is infallible"); let bytes_written = h.squeeze_out(&mut rho); debug_assert_eq!(bytes_written, 32); - let bytes_written = h.squeeze_out(&mut rho_prime); + let bytes_written = h.squeeze_out(rho_prime.deref_mut()); debug_assert_eq!(bytes_written, 64); - let bytes_written = h.squeeze_out(&mut K); + let bytes_written = h.squeeze_out(K.deref_mut()); debug_assert_eq!(bytes_written, 32); (rho, rho_prime, K) } - fn compute_t_row(&self, idx: usize, s1_packed: &[u8], s2_packed: &[u8]) -> Polynomial { + fn compute_t_row( + &self, + idx: usize, + s1_packed: &Secret<[u8; S1_PACKED_LEN]>, + s2_packed: &Secret<[u8; S2_PACKED_LEN]>, + ) -> Polynomial { debug_assert!(idx < k); // [Optimization Note]: // This is one of the places that a row of s1 can be re-computed instead of expanded from the compressed form. // let mut s1 = self.compute_s1_row(0); - let mut s1_hat_i = s_unpack::(s1_packed, 0); + let mut s1_hat_i = s_unpack::(s1_packed, 0); s1_hat_i.ntt(); let mut t_i = { @@ -541,7 +485,7 @@ impl< // [Optimization Note]: // This is one of the places that a row of s1 can be re-computed instead of expanded from the compressed form. // s1 = self.compute_s1_row(col); - let mut s1_hat = s_unpack::(s1_packed, col); + let mut s1_hat = s_unpack::(s1_packed, col); s1_hat.ntt(); let mut A_elem = expandA_elem(&self.rho, idx, col); A_elem.multiply_ntt(&s1_hat); @@ -555,7 +499,7 @@ impl< // [Optimization Note]: // This is one of the places that a row of s2 can be re-computed instead of unpacked from the compressed form. // let s2 = self.compute_s2_row(idx); - let s2 = s_unpack::(s2_packed, idx); + let s2 = s_unpack::(s2_packed, idx); t_i.add_ntt(&s2); t_i.conditional_add_q(); @@ -673,8 +617,8 @@ impl< fn derive_pk(&self) -> MLDSAPublicKey { // The goal here is to get t1, which we will build and compress one row at a time. - let s1_packed: [u8; S1_PACKED_LEN] = self.compute_s1_packed(); - let s2_packed: [u8; S2_PACKED_LEN] = self.compute_s2_packed(); + let s1_packed: Secret<[u8; S1_PACKED_LEN]> = self.compute_s1_packed(); + let s2_packed: Secret<[u8; S2_PACKED_LEN]> = self.compute_s2_packed(); let mut t1_packed = [0u8; T1_PACKED_LEN]; debug_assert_eq!(T1_PACKED_LEN, POLY_T1PACKED_LEN * k); @@ -702,7 +646,8 @@ impl< // 1: 𝑠𝑘 ← 𝜌||𝐾||𝑡𝑟 out[0..32].copy_from_slice(&self.rho); - out[32..64].copy_from_slice(&self.K); + // K is protected inside a Secret<[u8]>, but here we really do need to deref and copy it out. + out[32..64].copy_from_slice(&*self.K); out[64..128].copy_from_slice(&self.tr()); off += 128; @@ -710,14 +655,14 @@ impl< // 3: 𝑠𝑘 ← 𝑠𝑘 || BitPack (𝐬1[𝑖], 𝜂, 𝜂) // 4: end for let s1_packed = self.compute_s1_packed(); - out[off..off + S1_PACKED_LEN].copy_from_slice(&s1_packed); + out[off..off + S1_PACKED_LEN].copy_from_slice(&*s1_packed); off += S1_PACKED_LEN; // 5: for 𝑖 from 0 to 𝑘 − 1 do // 6: 𝑠𝑘 ← 𝑠𝑘 || BitPack (𝐬2[𝑖], 𝜂, 𝜂) // 7: end for let s2_packed = self.compute_s2_packed(); - out[off..off + S2_PACKED_LEN].copy_from_slice(&s2_packed); + out[off..off + S2_PACKED_LEN].copy_from_slice(&*s2_packed); off += S2_PACKED_LEN; // 8: for 𝑖 from 0 to 𝑘 − 1 do @@ -754,17 +699,39 @@ pub(crate) trait MLDSAPrivateKeyInternalTrait< fn rho(&self) -> &[u8; 32]; fn K(&self) -> &[u8; 32]; + /// A single entry of a privacy key vector. + /// These tend to be used very transiently, so we won't bother wrapping it as a Secret. fn compute_s1_row(&self, idx: usize) -> Polynomial; - fn compute_s1_packed(&self) -> [u8; S1_PACKED_LEN]; + /// Private key component. + /// The packed representation sticks around for the whole computation, so + /// we'll wrap in as a Secret. + fn compute_s1_packed(&self) -> Secret<[u8; S1_PACKED_LEN]>; + /// A single entry of a privacy key vector. + /// These tend to be used very transiently, so we won't bother wrapping it as a Secret. fn compute_s2_row(&self, idx: usize) -> Polynomial; - fn compute_s2_packed(&self) -> [u8; S2_PACKED_LEN]; - - fn compute_t0_row(&self, idx: usize, s1_packed: &[u8], s2_packed: &[u8]) -> Polynomial; - - fn compute_t1_row(&self, idx: usize, s1_packed: &[u8], s2_packed: &[u8]) -> Polynomial; + /// Private key component. + /// The packed representation sticks around for the whole computation, so + /// we'll wrap in as a Secret. + fn compute_s2_packed(&self) -> Secret<[u8; S2_PACKED_LEN]>; + + /// Public key component. + fn compute_t0_row( + &self, + idx: usize, + s1_packed: &Secret<[u8; S1_PACKED_LEN]>, + s2_packed: &Secret<[u8; S2_PACKED_LEN]>, + ) -> Polynomial; + + /// Public key component. + fn compute_t1_row( + &self, + idx: usize, + s1_packed: &Secret<[u8; S1_PACKED_LEN]>, + s2_packed: &Secret<[u8; S2_PACKED_LEN]>, + ) -> Polynomial; } impl< @@ -818,8 +785,8 @@ impl< rej_bounded_poly::(&self.rho_prime, &(idx as u16).to_le_bytes()) } - fn compute_s1_packed(&self) -> [u8; S1_PACKED_LEN] { - let mut s1_packed = [0u8; S1_PACKED_LEN]; + fn compute_s1_packed(&self) -> Secret<[u8; S1_PACKED_LEN]> { + let mut s1_packed: Secret<[u8; S1_PACKED_LEN]> = Secret::new(); for idx in 0..l { let s1_i = self.compute_s1_row(idx); bit_pack_eta::( @@ -835,8 +802,8 @@ impl< rej_bounded_poly::(&self.rho_prime, &((idx + l) as u16).to_le_bytes()) } - fn compute_s2_packed(&self) -> [u8; S2_PACKED_LEN] { - let mut s2_packed = [0u8; S2_PACKED_LEN]; + fn compute_s2_packed(&self) -> Secret<[u8; S2_PACKED_LEN]> { + let mut s2_packed: Secret<[u8; S2_PACKED_LEN]> = Secret::new(); for idx in 0..k { let s2_i = self.compute_s2_row(idx); bit_pack_eta::( @@ -847,7 +814,12 @@ impl< s2_packed } - fn compute_t0_row(&self, idx: usize, s1_packed: &[u8], s2_packed: &[u8]) -> Polynomial { + fn compute_t0_row( + &self, + idx: usize, + s1_packed: &Secret<[u8; S1_PACKED_LEN]>, + s2_packed: &Secret<[u8; S2_PACKED_LEN]>, + ) -> Polynomial { let mut t0 = self.compute_t_row(idx, s1_packed, s2_packed); for j in 0..N { (_, t0[j]) = power_2_round(t0[j]); @@ -856,7 +828,12 @@ impl< t0 } - fn compute_t1_row(&self, idx: usize, s1_packed: &[u8], s2_packed: &[u8]) -> Polynomial { + fn compute_t1_row( + &self, + idx: usize, + s1_packed: &Secret<[u8; S1_PACKED_LEN]>, + s2_packed: &Secret<[u8; S2_PACKED_LEN]>, + ) -> Polynomial { let mut t1 = self.compute_t_row(idx, s1_packed, s2_packed); for j in 0..N { (t1[j], _) = power_2_round(t1[j]); diff --git a/crypto/mldsa-lowmemory/src/polynomial.rs b/crypto/mldsa-lowmemory/src/polynomial.rs index dc2438c..d7abca1 100644 --- a/crypto/mldsa-lowmemory/src/polynomial.rs +++ b/crypto/mldsa-lowmemory/src/polynomial.rs @@ -2,16 +2,19 @@ use crate::aux_functions::{high_bits, low_bits, make_hint, use_hint}; use crate::mldsa::{MLDSA44_POLY_W1_PACKED_LEN, MLDSA65_POLY_W1_PACKED_LEN, N, q, q_inv}; -use bouncycastle_core::traits::Secret; -use core::fmt; -use core::fmt::{Debug, Display, Formatter}; use core::ops::{Index, IndexMut}; /// A polynomial over the ML-DSA ring. /// Dev note: this doesn't strictly need to be pub ... ie there's no good reason for a caller to use this class directly, /// but in order to test the Debug and Display traits, you need STD, so those can't be tested from inline tests in this file /// and the real unit tests are in a different crate, so here we are. -#[derive(Clone)] +/// +/// # 🚨 Security 🚨 +/// Polynomials themselves are not inherently secret since sometimes they are part of public keys +/// and sometimes private keys. +/// It is the responsibility of the caller to wrap sensitive instances in `Secret`. +/// Note: at the moment, nothing in this crate uses `Secret`, so I have left the `impl ZeroizablePrimitive` commented-out. +#[derive(Clone, Copy)] pub struct Polynomial { pub(crate) coeffs: [i32; N], } @@ -31,6 +34,11 @@ impl IndexMut for Polynomial { } } +// Turn this back on if we want to start tagging things as `Secret`. +// impl ZeroizablePrimitive for Polynomial { +// const ZEROED: Self = Self::new(); +// } + impl Polynomial { /// Create a new polynomial with all coefficients set to zero. pub const fn new() -> Self { @@ -246,26 +254,6 @@ impl Polynomial { } } -impl Secret for Polynomial {} - -impl Drop for Polynomial { - fn drop(&mut self) { - self.coeffs.fill(0i32); - } -} - -impl Debug for Polynomial { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "Polynomial (data masked)") - } -} - -impl Display for Polynomial { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "Polynomial (data masked)") - } -} - /// FIPS 204 Algorithm 49 /// As described in FIPS 204 Appendix A, montgomery reduction allows for efficient computation /// of expressions of the form c = a * b (mod q). diff --git a/crypto/mldsa-lowmemory/tests/mldsa_tests.rs b/crypto/mldsa-lowmemory/tests/mldsa_tests.rs index 27b26b3..0a06cf6 100644 --- a/crypto/mldsa-lowmemory/tests/mldsa_tests.rs +++ b/crypto/mldsa-lowmemory/tests/mldsa_tests.rs @@ -733,22 +733,6 @@ mod mldsa_tests { } } - /// Tests that no private data is displayed - #[test] - fn test_display() { - use bouncycastle_mldsa_lowmemory::Polynomial; - // Polynomials (could) contain private data, - // and therefore should be protected against accidental crash dumps: - - // fmt - let p = Polynomial::new(); - assert_eq!(format!("{}", p), "Polynomial (data masked)"); - - // debug - let p = Polynomial::new(); - assert_eq!(format!("{:?}", p), "Polynomial (data masked)"); - } - #[test] fn keypair_consistency_check() { // this is common to all parameter sets, so I'll just test MLDSA44 diff --git a/crypto/mldsa/src/aux_functions.rs b/crypto/mldsa/src/aux_functions.rs index cab95ce..92a07f1 100644 --- a/crypto/mldsa/src/aux_functions.rs +++ b/crypto/mldsa/src/aux_functions.rs @@ -8,6 +8,7 @@ use crate::mldsa::{ }; use crate::polynomial::Polynomial; use bouncycastle_core::traits::XOF; +use bouncycastle_utils::secret::Secret; /// Algorithm 14 CoeffFromThreeBytes(𝑏0, 𝑏1, 𝑏2) /// Output: An integer modulo 𝑞 or ⊥. @@ -681,12 +682,13 @@ pub(crate) fn expandA(rho: &[u8; 32]) -> Matrix< /// Samples vectors 𝐬1 ∈ 𝑅ℓ and 𝐬2 ∈ 𝑅𝑘 , each with polynomial coordinates whose coefficients are /// in the interval \[−𝜂, 𝜂]. /// Input: A seed 𝜌 ∈ 𝔹64 . -/// Output: Vectors 𝐬1, 𝐬2 of polynomials in 𝑅 +/// Output: Vectors 𝐬1, 𝐬2 of secret polynomials in 𝑅 +/// Note that this returns Secret> because s1, s2 are always part of a private key. pub(crate) fn expandS( rho: &[u8; 64], -) -> (Vector, Vector) { - let mut s1 = Vector::::new(); - let mut s2 = Vector::::new(); +) -> (Secret>, Secret>) { + let mut s1: Secret> = Secret::new(); + let mut s2: Secret> = Secret::new(); for r in 0..l { s1.vec[r] = rej_bounded_poly::(rho, &(r as u16).to_le_bytes()); diff --git a/crypto/mldsa/src/lib.rs b/crypto/mldsa/src/lib.rs index 73ea1d7..613ce3f 100644 --- a/crypto/mldsa/src/lib.rs +++ b/crypto/mldsa/src/lib.rs @@ -88,7 +88,8 @@ //! Values in parentheses are the usual sizes in our un-optimized implementation in the \[bouncycastle_mldsa] crate. //! //! -//! # Security +//! # 🚨 Security 🚨 +//! //! All functionality exposed by this crate is considered secure to use. //! In other words, this crate does not contain any "hazmat" except for the obvious points about //! handling your private keys properly: if you post your private key to github, or you generate diff --git a/crypto/mldsa/src/matrix.rs b/crypto/mldsa/src/matrix.rs index e219661..dfa82f1 100644 --- a/crypto/mldsa/src/matrix.rs +++ b/crypto/mldsa/src/matrix.rs @@ -5,6 +5,7 @@ use crate::aux_functions::multiply_ntt; use crate::mldsa::H; use crate::polynomial::Polynomial; use bouncycastle_core::traits::XOF; +use bouncycastle_utils::secret::ZeroizablePrimitive; use core::ops::{Index, IndexMut}; /// A matrix over the ML-DSA ring. @@ -62,13 +63,13 @@ impl Matrix { // Technically all matrices and some vectors are only part of the public key and might not need to be zeroized, // but I'll leave it zeroizing for now and leave this as a potential future optimization. -#[derive(Clone)] -pub(crate) struct Vector { - pub(crate) vec: [Polynomial; k], +#[derive(Clone, Copy)] +pub(crate) struct Vector { + pub(crate) vec: [Polynomial; LEN], } /// Convenience function to avoid ".0" all over the place. -impl Index for Vector { +impl Index for Vector { type Output = Polynomial; fn index(&self, index: usize) -> &Self::Output { @@ -76,15 +77,19 @@ impl Index for Vector { } } /// Convenience function to avoid ".0" all over the place. -impl IndexMut for Vector { +impl IndexMut for Vector { fn index_mut(&mut self, index: usize) -> &mut Self::Output { &mut self.vec[index] } } +impl ZeroizablePrimitive for Vector { + const ZEROED: Self = Self::new(); +} + impl Vector { - pub(crate) fn new() -> Self { - Self { vec: [(); LEN].map(|_| Polynomial::new()) } + pub(crate) const fn new() -> Self { + Self { vec: [Polynomial::new(); LEN] } } /// Algorithm 46 AddVectorNTT(𝐯, 𝐰)̂ diff --git a/crypto/mldsa/src/mldsa.rs b/crypto/mldsa/src/mldsa.rs index e5b010c..795f672 100644 --- a/crypto/mldsa/src/mldsa.rs +++ b/crypto/mldsa/src/mldsa.rs @@ -492,6 +492,7 @@ use bouncycastle_core::traits::{ }; use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_sha3::{SHAKE128, SHAKE256, SUSPENDED_SHA3_STATE_LEN}; +use bouncycastle_utils::secret::Secret; use core::marker::PhantomData; // imports needed just for docs @@ -850,7 +851,7 @@ impl< // Alg 6 line 1: (rho, rho_prime, K) <- H(𝜉||IntegerToBytes(𝑘, 1)||IntegerToBytes(ℓ, 1), 128) // ▷ expand seed let mut rho: [u8; 32] = [0u8; 32]; - let mut K: [u8; 32] = [0u8; 32]; + let mut K = Secret::<[u8; 32]>::new(); let (s1_hat, mut s2) = { // scope for h @@ -863,7 +864,7 @@ impl< let mut rho_prime: [u8; 64] = [0u8; 64]; let bytes_written = h.squeeze_out(&mut rho_prime); debug_assert_eq!(bytes_written, 64); - let bytes_written = h.squeeze_out(&mut K); + let bytes_written = h.squeeze_out(&mut *K); debug_assert_eq!(bytes_written, 32); // 4: (𝐬1, 𝐬2) ← ExpandS(𝜌′) @@ -914,10 +915,6 @@ impl< // let sk = SK::new(&rho, &K, &tr, &s1_hat, &s2, &t0, Some(seed.clone())); let sk = SK::new(rho, K, tr, s1_hat, s2, t0, Some(seed.clone())); - // Clear the secret data before returning memory to the OS - // (SK::new() copies all values) - rho.fill(0u8); - K.fill(0u8); // tr is public data, does not need to be zeroized // s1, s2, t0 are all Vectors of Polynomials, so implement a zeroizing Drop @@ -953,7 +950,7 @@ impl< // scope for h // 7: 𝜌″ ← H(𝐾||𝑟𝑛𝑑||𝜇, 64) let mut h = H::new(); - h.absorb(sk.K()).expect("absorb before squeeze is infallible"); + h.absorb(&**sk.K()).expect("absorb before squeeze is infallible"); h.absorb(&rnd).expect("absorb before squeeze is infallible"); h.absorb(mu).expect("absorb before squeeze is infallible"); let mut rho_p_p = [0u8; 64]; diff --git a/crypto/mldsa/src/mldsa_keys.rs b/crypto/mldsa/src/mldsa_keys.rs index c1643ba..13349a5 100644 --- a/crypto/mldsa/src/mldsa_keys.rs +++ b/crypto/mldsa/src/mldsa_keys.rs @@ -11,7 +11,8 @@ use crate::mldsa::{POLY_T0PACKED_LEN, POLY_T1PACKED_LEN}; use crate::{ML_DSA_44_NAME, ML_DSA_65_NAME, ML_DSA_87_NAME}; use bouncycastle_core::errors::SignatureError; use bouncycastle_core::key_material::KeyMaterial; -use bouncycastle_core::traits::{Secret, SignaturePrivateKey, SignaturePublicKey, XOF}; +use bouncycastle_core::traits::{SignaturePrivateKey, SignaturePublicKey, XOF}; +use bouncycastle_utils::secret::Secret; use core::fmt; use core::fmt::{Debug, Display, Formatter}; @@ -403,6 +404,8 @@ impl< } /// An ML-DSA private key. +/// +/// This will automatically inherit the [Secret] protections because [Polynomial] wraps the underlying data with [Secret]. #[derive(Clone)] pub struct MLDSAPrivateKey< const k: usize, @@ -412,7 +415,7 @@ pub struct MLDSAPrivateKey< const PK_LEN: usize, > { rho: [u8; 32], - K: [u8; 32], + K: Secret<[u8; 32]>, tr: [u8; 64], // Deviation from the FIPS: // s1, s2, and t0 are only ever used in their ntt form; the only time they need to be in their @@ -420,9 +423,10 @@ pub struct MLDSAPrivateKey< // So we are going to hold them as s1_hat, s2_hat, and t0_hat. // Note: these are not necessarily in their reduced form; so you'll need to reduce them before // inv_ntt()'ing them or hashing them. - s1_hat: Vector, - s2_hat: Vector, + s1_hat: Secret>, + s2_hat: Secret>, t0_hat: Vector, + // note: KeyMaterial is inherently Secret seed: Option>, } @@ -441,7 +445,7 @@ impl, tr: [u8; 64], - s1_hat: Vector, - s2_hat: Vector, + s1_hat: Secret>, + s2_hat: Secret>, t0_hat: Vector, seed: Option>, ) -> Self; /// Get a ref to K - fn K(&self) -> &[u8; 32]; + fn K(&self) -> &Secret<[u8; 32]>; /// Get a ref to s1 fn s1_hat(&self) -> &Vector; /// Get a ref to s2 @@ -605,19 +609,26 @@ impl::new(self.rho.clone(), t1) } fn sk_decode(sk: &[u8; SK_LEN]) -> Result { - let rho = sk[0..32].try_into().unwrap(); - let K = sk[32..64].try_into().unwrap(); - let tr = sk[64..128].try_into().unwrap(); - let mut s1 = Vector::::new(); - let mut s2 = Vector::::new(); - let mut t0 = Vector::::new(); + // Construct the (Secret-protected) key up front and unpack each field directly into it, + // rather than decoding into unprotected temporaries and copying them in at the end. This + // way the secret material is written straight into its protected home; and if a range + // check below fails, `key` is dropped and its `Secret` fields are zeroized on the way out. + let mut key = Self { + rho: sk[0..32].try_into().unwrap(), + K: Secret::new(), + tr: sk[64..128].try_into().unwrap(), + s1_hat: Secret::new(), + s2_hat: Secret::new(), + t0_hat: Vector::::new(), + seed: None, + }; + key.K.copy_from_slice(&sk[32..64]); let mut off = 128; - // unpack s1 - // let mut i: usize = 0; - let sk_chunks = sk[128..128 + (l * bitlen_eta(eta))].chunks(bitlen_eta(eta)); + // unpack s1 directly into key.s1_hat so that we don't make additional non-Secret copies. + let sk_chunks = sk[off..off + (l * bitlen_eta(eta))].chunks(bitlen_eta(eta)); debug_assert_eq!(sk_chunks.len(), l); - for (s1_i, sk_chunk) in s1.vec.iter_mut().zip(sk_chunks) { + for (s1_i, sk_chunk) in key.s1_hat.vec.iter_mut().zip(sk_chunks) { // 3: 𝐬1[𝑖] ← BitUnpack(𝑦𝑖, 𝜂, 𝜂) // ▷ this may lie outside [−𝜂, 𝜂] if input is malformed s1_i.coeffs.copy_from_slice(&bit_unpack_eta::(&sk_chunk).coeffs); @@ -631,13 +642,13 @@ impl(&sk_chunk).coeffs); @@ -651,10 +662,10 @@ impl(); @@ -662,14 +673,14 @@ impl, tr: [u8; 64], - s1_hat: Vector, - s2_hat: Vector, + s1_hat: Secret>, + s2_hat: Secret>, t0_hat: Vector, seed: Option>, ) -> Self { @@ -697,7 +708,7 @@ impl &[u8; 32] { + fn K(&self) -> &Secret<[u8; 32]> { &self.K } @@ -758,11 +769,6 @@ impl - Secret for MLDSAPrivateKey -{ -} - /// Debug impl mainly to prevent the secret key from being printed in logs. impl fmt::Debug for MLDSAPrivateKey @@ -805,16 +811,6 @@ impl - Drop for MLDSAPrivateKey -{ - fn drop(&mut self) { - self.K.fill(0u8); - // s1, s2, t0, seed have their own zeroizing drop - } -} - /// A fully expanded ML-DSA private key that includes the intermediate values needed for performing /// multiple sign operations with the same private key, which causes the private ey struct to take up /// more memory, but results in more efficient repeated sign() operations. @@ -863,35 +859,6 @@ impl< { } -impl< - const k: usize, - const l: usize, - const eta: usize, - PK: MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, - const SK_LEN: usize, - const PK_LEN: usize, -> Secret for MLDSAPrivateKeyExpanded -{ -} - -impl< - const k: usize, - const l: usize, - const eta: usize, - PK: MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, - const SK_LEN: usize, - const PK_LEN: usize, -> Drop for MLDSAPrivateKeyExpanded -{ - fn drop(&mut self) { - // Nothing to do since self.sk already impls zeroizing Drop - } -} - impl< const k: usize, const l: usize, diff --git a/crypto/mldsa/src/polynomial.rs b/crypto/mldsa/src/polynomial.rs index b5f0999..4344d7c 100644 --- a/crypto/mldsa/src/polynomial.rs +++ b/crypto/mldsa/src/polynomial.rs @@ -4,16 +4,18 @@ use crate::aux_functions::{ ZETAS, conditional_add_q, high_bits, low_bits, make_hint, montgomery_reduce, }; use crate::mldsa::{MLDSA44_POLY_W1_PACKED_LEN, MLDSA65_POLY_W1_PACKED_LEN, N, q}; -use bouncycastle_core::traits::Secret; -use core::fmt; -use core::fmt::{Debug, Display, Formatter}; use core::ops::{Index, IndexMut}; /// A polynomial over the ML-DSA ring. /// Dev note: this doesn't strictly need to be pub ... ie there's no good reason for a caller to use this class directly, /// but in order to test the Debug and Display traits, you need STD, so those can't be tested from inline tests in this file /// and the real unit tests are in a different crate, so here we are. -#[derive(Clone)] +/// +/// # 🚨 Security 🚨 +/// Polynomials themselves are not inherently secret since sometimes they are part of public keys +/// and sometimes private keys. +/// It is the responsibility of the caller to wrap sensitive instances in `Secret`. +#[derive(Clone, Copy)] pub struct Polynomial { pub(crate) coeffs: [i32; N], } @@ -234,23 +236,3 @@ impl Polynomial { } } } - -impl Secret for Polynomial {} - -impl Drop for Polynomial { - fn drop(&mut self) { - self.coeffs.fill(0i32); - } -} - -impl Debug for Polynomial { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "Polynomial (data masked)") - } -} - -impl Display for Polynomial { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "Polynomial (data masked)") - } -} diff --git a/crypto/mldsa/tests/mldsa_tests.rs b/crypto/mldsa/tests/mldsa_tests.rs index bfb02ff..6495d3c 100644 --- a/crypto/mldsa/tests/mldsa_tests.rs +++ b/crypto/mldsa/tests/mldsa_tests.rs @@ -17,7 +17,7 @@ mod mldsa_tests { use bouncycastle_mldsa::{ MLDSA_TR_LEN, MLDSA44, MLDSA44PrivateKey, MLDSA44PrivateKeyExpanded, MLDSA44PublicKey, MLDSA44PublicKeyExpanded, MLDSA65, MLDSA65PrivateKey, MLDSA65PublicKey, MLDSA87, - MLDSA87PrivateKey, MLDSA87PublicKey, MuBuilder, Polynomial, + MLDSA87PrivateKey, MLDSA87PublicKey, MuBuilder, }; use bouncycastle_mldsa::{ MLDSA44_PK_LEN, MLDSA44_SIG_LEN, MLDSA44_SK_LEN, MLDSA65_PK_LEN, MLDSA65_SIG_LEN, @@ -865,21 +865,6 @@ mod mldsa_tests { } } - /// Tests that no private data is displayed - #[test] - fn test_display() { - // Polynomials (could) contain private data, - // and therefore should be protected against accidental crash dumps: - - // fmt - let p = Polynomial::new(); - assert_eq!(format!("{}", p), "Polynomial (data masked)"); - - // debug - let p = Polynomial::new(); - assert_eq!(format!("{:?}", p), "Polynomial (data masked)"); - } - #[test] fn keypair_consistency_check() { // this is common to all parameter sets, so I'll just test MLDSA44 diff --git a/crypto/mlkem-lowmemory/src/lib.rs b/crypto/mlkem-lowmemory/src/lib.rs index 0aaf81a..5ee0681 100644 --- a/crypto/mlkem-lowmemory/src/lib.rs +++ b/crypto/mlkem-lowmemory/src/lib.rs @@ -202,7 +202,8 @@ //! ``` //! And that's the basic usage! //! -//! # Security +//! # 🚨 Security 🚨 +//! //! All functionality exposed by this crate is considered secure to use. //! In other words, this crate does not contain any "hazmat" except for the obvious points about //! handling your private keys properly: if you post your private key to github, or you generate diff --git a/crypto/mlkem-lowmemory/src/mlkem.rs b/crypto/mlkem-lowmemory/src/mlkem.rs index 434f165..479d7da 100644 --- a/crypto/mlkem-lowmemory/src/mlkem.rs +++ b/crypto/mlkem-lowmemory/src/mlkem.rs @@ -22,8 +22,8 @@ use bouncycastle_core::traits::{ use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_sha3::{SHA3_256, SHA3_512, SHAKE256}; use bouncycastle_utils::ct::{conditional_copy_bytes, ct_eq_bytes}; +use bouncycastle_utils::secret::Secret; use core::marker::PhantomData; - /*** Constants ***/ /// @@ -467,17 +467,19 @@ impl< // Compute the trial shared secret key // 6: (𝐾′, 𝑟′) ← G(𝑚′‖ℎ)̄ - let K_prime: [u8; MLKEM_SS_LEN]; + let K_prime: Secret<[u8; MLKEM_SS_LEN]>; let r_prime: [u8; 32]; (K_prime, r_prime) = { + let mut buf: Secret<[u8; 64]> = Secret::new(); let mut g = G::new(); g.do_update(&m_prime); g.do_update(&dk.pk().compute_hash()); - let mut buf = [0u8; 64]; - let bytes_written = g.do_final_out(&mut buf); + let bytes_written = g.do_final_out(&mut *buf); debug_assert_eq!(bytes_written, 64); - (buf[..32].try_into().unwrap(), buf[32..64].try_into().unwrap()) + let mut K_prime: Secret<[u8; MLKEM_SS_LEN]> = Secret::new(); + K_prime.copy_from_slice(&buf[..32]); + (K_prime, buf[32..64].try_into().unwrap()) }; // 7: 𝐾_bar ← J(𝑧‖𝑐) @@ -486,16 +488,16 @@ impl< // because if its computation is conditional on the Fujisaki-Okamoto check failing, then // you'll have a timing difference between success and failure. - let K_bar: [u8; MLKEM_SS_LEN]; + let K_bar: Secret<[u8; MLKEM_SS_LEN]>; K_bar = { + let mut K_bar: Secret<[u8; MLKEM_SS_LEN]> = Secret::new(); let mut j = J::new(); j.absorb(dk.z()).expect("absorb before squeeze is infallible"); j.absorb(&c).expect("absorb before squeeze is infallible"); - let mut buf = [0u8; MLKEM_SS_LEN]; - let bytes_written = j.squeeze_out(&mut buf); + let bytes_written = j.squeeze_out(&mut *K_bar); debug_assert_eq!(bytes_written, MLKEM_SS_LEN); - buf + K_bar }; // 8: 𝑐′ ← K-PKE.Encrypt(ekPKE, 𝑚′, 𝑟′) diff --git a/crypto/mlkem-lowmemory/src/mlkem_keys.rs b/crypto/mlkem-lowmemory/src/mlkem_keys.rs index 36d36bd..c27aac4 100644 --- a/crypto/mlkem-lowmemory/src/mlkem_keys.rs +++ b/crypto/mlkem-lowmemory/src/mlkem_keys.rs @@ -21,11 +21,11 @@ use bouncycastle_core::errors::KEMError; use bouncycastle_core::key_material::{ KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, }; -use bouncycastle_core::traits::{Hash, KEMPrivateKey, KEMPublicKey, Secret, SecurityStrength}; +use bouncycastle_core::traits::{Hash, KEMPrivateKey, KEMPublicKey, SecurityStrength}; use bouncycastle_sha3::SHA3_256; +use bouncycastle_utils::secret::Secret; use core::fmt; use core::fmt::{Debug, Display, Formatter}; - // imports just for docs /* Pub Types */ @@ -249,10 +249,10 @@ pub struct MLKEMSeedPrivateKey< const T_PACKED_LEN: usize, > { rho: [u8; 32], - sigma: [u8; 32], + sigma: Secret<[u8; 32]>, pk_hash: Option<[u8; 32]>, - z: [u8; 32], - seed_d: [u8; 32], + z: Secret<[u8; 32]>, + seed_d: Secret<[u8; 32]>, } impl< @@ -280,8 +280,12 @@ impl< return Err(KEMError::KeyGenError("SecurityStrength")); } - let seed_d: [u8; 32] = seed.ref_to_bytes()[..32].try_into().unwrap(); - let z: [u8; 32] = seed.ref_to_bytes()[32..].try_into().unwrap(); + // These are Secret-safe because we're using .copy_from_slice directly out of one Secret<[u8]> + // into another and the contents are never touching a non-Secret buffer. + let mut seed_d = Secret::<[u8; 32]>::new(); + seed_d.as_mut().copy_from_slice(seed.ref_to_bytes()[..32].try_into().unwrap()); + let mut z = Secret::<[u8; 32]>::new(); + z.as_mut().copy_from_slice(seed.ref_to_bytes()[32..].try_into().unwrap()); let (rho, sigma) = Self::compute_rho_and_sigma(&seed_d); @@ -294,15 +298,21 @@ impl< /// ▷ expand 32+1 bytes to two pseudorandom 32-byte seeds1 /// rho: public seed /// sigma: noise seed - fn compute_rho_and_sigma(seed_d: &[u8; 32]) -> ([u8; 32], [u8; 32]) { + fn compute_rho_and_sigma(seed_d: &[u8; 32]) -> ([u8; 32], Secret<[u8; 32]>) { + // Only the second half of the output is secret, but we'll wrap the whole thing + // so that the local copy gets zeriozed on drop. + let mut buf: Secret<[u8; 64]> = Secret::new(); + let mut g = G::new(); g.do_update(seed_d); g.do_update(&[k as u8]); - let mut buf = [0u8; 64]; - let bytes_written = g.do_final_out(&mut buf); + let bytes_written = g.do_final_out(buf.as_mut()); debug_assert_eq!(bytes_written, 64); - (buf[..32].try_into().unwrap(), buf[32..64].try_into().unwrap()) + let mut sigma = Secret::<[u8; 32]>::new(); + sigma.as_mut().copy_from_slice(buf[32..64].try_into().unwrap()); + + (buf[..32].try_into().unwrap(), sigma) } } @@ -383,10 +393,10 @@ impl< Self::new(seed) } fn seed(&self) -> Option> { - let mut tmp = [0u8; 64]; - tmp[..32].copy_from_slice(&self.seed_d); - tmp[32..].copy_from_slice(&self.z); - let mut seed = KeyMaterial::<64>::from_bytes_as_type(&tmp, KeyType::Seed).unwrap(); + let mut tmp = Secret::<[u8; 64]>::new(); + tmp[..32].as_mut().copy_from_slice(&*self.seed_d); + tmp[32..].as_mut().copy_from_slice(&*self.z); + let mut seed = KeyMaterial::<64>::from_bytes_as_type(&*tmp, KeyType::Seed).unwrap(); do_hazardous_operations(&mut seed, |seed| { seed.set_security_strength(match k { 2 => SecurityStrength::_128bit, @@ -453,7 +463,7 @@ impl< pos += 32; /* z */ - out[pos..pos + 32].copy_from_slice(&self.z); + out[pos..pos + 32].copy_from_slice(&*self.z); FULL_SK_LEN } @@ -555,8 +565,8 @@ impl< out.fill(0); - out[..32].copy_from_slice(&self.seed_d); - out[32..].copy_from_slice(&self.z); + out[..32].copy_from_slice(&*self.seed_d); + out[32..].copy_from_slice(&*self.z); SK_LEN } @@ -604,18 +614,6 @@ impl< } } -impl< - const k: usize, - const eta1: i16, - const LAMBDA: i16, - const SK_LEN: usize, - const FULL_SK_LEN: usize, - const PK_LEN: usize, - const T_PACKED_LEN: usize, -> Secret for MLKEMSeedPrivateKey -{ -} - /// Debug impl mainly to prevent the secret key from being printed in logs. impl< const k: usize, @@ -661,22 +659,3 @@ impl< write!(f, "MLKEMSeedPrivateKey {{ alg: {}, pub_key_hash: {:x?} }}", alg, &pk_hash,) } } - -/// Zeroizing drop -impl< - const k: usize, - const eta1: i16, - const LAMBDA: i16, - const SK_LEN: usize, - const FULL_SK_LEN: usize, - const PK_LEN: usize, - const T_PACKED_LEN: usize, -> Drop for MLKEMSeedPrivateKey -{ - fn drop(&mut self) { - self.rho.fill(0u8); - self.sigma.fill(0u8); - self.z.fill(0u8); - self.seed_d.fill(0u8); - } -} diff --git a/crypto/mlkem-lowmemory/src/polynomial.rs b/crypto/mlkem-lowmemory/src/polynomial.rs index bec4ee4..b30cd33 100644 --- a/crypto/mlkem-lowmemory/src/polynomial.rs +++ b/crypto/mlkem-lowmemory/src/polynomial.rs @@ -4,16 +4,19 @@ use crate::aux_functions::{ ZETAS, ZETAS_INV, barrett_reduce, montgomery_reduce, mul_mont, ntt_base_mult, }; use crate::mlkem::{N, q}; -use bouncycastle_core::traits::Secret; -use core::fmt; -use core::fmt::{Debug, Display, Formatter}; use core::ops::{Index, IndexMut}; /// A polynomial over the ML-KEM ring. /// Dev note: this doesn't strictly need to be pub ... ie there's no good reason for a caller to use this class directly, /// but in order to test the Debug and Display traits, you need STD, so those can't be tested from inline tests in this file /// and the real unit tests are in a different crate, so here we are. -#[derive(Clone)] +/// +/// # 🚨 Security 🚨 +/// Polynomials themselves are not inherently secret since sometimes they are part of public keys +/// and sometimes private keys. +/// It is the responsibility of the caller to wrap sensitive instances in `Secret`. +/// Note: at the moment, nothing in this crate uses `Secret`, so I have left the `impl ZeroizablePrimitive` commented-out. +#[derive(Clone, Copy)] pub struct Polynomial { pub(crate) coeffs: [i16; N], } @@ -33,6 +36,11 @@ impl IndexMut for Polynomial { } } +// Turn this back on if we want to start tagging things as `Secret`. +// impl ZeroizablePrimitive for Polynomial { +// const ZEROED: Self = Self::new(); +// } + impl Polynomial { /// Create a new polynomial with all coefficients set to zero. pub const fn new() -> Self { @@ -105,12 +113,20 @@ impl Polynomial { for i in 0..(N / 4) { let a1: i16 = self[4 * i]; let a2: i16 = self[4 * i + 1]; - ntt_base_mult(&mut self.coeffs, 4 * i, a1, a2, b[4 * i], b[4 * i + 1], ZETAS[64 + i]); + ntt_base_mult( + self.coeffs.as_mut(), + 4 * i, + a1, + a2, + b[4 * i], + b[4 * i + 1], + ZETAS[64 + i], + ); let a1: i16 = self[4 * i + 2]; let a2: i16 = self[4 * i + 3]; ntt_base_mult( - &mut self.coeffs, + self.coeffs.as_mut(), 4 * i + 2, a1, a2, @@ -318,26 +334,6 @@ impl Polynomial { } } -impl Secret for Polynomial {} - -impl Drop for Polynomial { - fn drop(&mut self) { - self.coeffs.fill(0i16); - } -} - -impl Debug for Polynomial { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "Polynomial (data masked)") - } -} - -impl Display for Polynomial { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "Polynomial (data masked)") - } -} - // Not currently used, but I'll leave it here because it's useful for debugging if you want to output values // that are normalized to [0,q] to compare against intermediate results from other libraries. // /// if a is in \[-q..0], then it shifts it up by q to be in \[0..q] diff --git a/crypto/mlkem-lowmemory/tests/mlkem_tests.rs b/crypto/mlkem-lowmemory/tests/mlkem_tests.rs index 348287d..e6bae11 100644 --- a/crypto/mlkem-lowmemory/tests/mlkem_tests.rs +++ b/crypto/mlkem-lowmemory/tests/mlkem_tests.rs @@ -14,7 +14,7 @@ mod mlkem_tests { MLKEM512_FULL_SK_LEN, MLKEM768_FULL_SK_LEN, MLKEM1024_FULL_SK_LEN, }; use bouncycastle_mlkem_lowmemory::{ - MLKEM_RND_LEN, MLKEM_SEED_LEN, MLKEM512, MLKEM768, MLKEM1024, Polynomial, + MLKEM_RND_LEN, MLKEM_SEED_LEN, MLKEM512, MLKEM768, MLKEM1024, }; use bouncycastle_mlkem_lowmemory::{ MLKEM_SS_LEN, MLKEM512_CT_LEN, MLKEM512_PK_LEN, MLKEM512_SK_LEN, MLKEM768_CT_LEN, @@ -514,21 +514,6 @@ mod mlkem_tests { } } - /// Tests that no private data is displayed - #[test] - fn test_display() { - // Polynomials (could) contain private data, - // and therefore should be protected against accidental crash dumps: - - // fmt - let p = Polynomial::new(); - assert_eq!(format!("{}", p), "Polynomial (data masked)"); - - // debug - let p = Polynomial::new(); - assert_eq!(format!("{:?}", p), "Polynomial (data masked)"); - } - #[test] fn keypair_consistency_check() { // this is common to all parameter sets, so I'll just test MLKEM512 diff --git a/crypto/mlkem/src/lib.rs b/crypto/mlkem/src/lib.rs index 0fc793d..35a1be1 100644 --- a/crypto/mlkem/src/lib.rs +++ b/crypto/mlkem/src/lib.rs @@ -116,7 +116,8 @@ //! All values are in bytes. The "in memory" sizes are measured by rust's `std::mem::size_of`. //! Values in parentheses are the usual sizes in our un-optimized implementation in the \[bouncycastle_mldsa] crate. //! -//! # Security +//! # 🚨 Security 🚨 +//! //! All functionality exposed by this crate is considered secure to use. //! In other words, this crate does not contain any "hazmat" except for the obvious points about //! handling your private keys properly: if you post your private key to github, or you generate diff --git a/crypto/mlkem/src/matrix.rs b/crypto/mlkem/src/matrix.rs index 100d6e0..57e16cd 100644 --- a/crypto/mlkem/src/matrix.rs +++ b/crypto/mlkem/src/matrix.rs @@ -6,6 +6,7 @@ use core::ops::{Index, IndexMut}; use crate::mlkem::{N, q}; use crate::polynomial; use crate::polynomial::Polynomial; +use bouncycastle_utils::secret::ZeroizablePrimitive; #[derive(Clone)] /// A matrix over the ML-KEM ring. @@ -81,7 +82,7 @@ impl Matrix { // Technically all matrices and some vectors are only part of the public key and might not need to be zeroized, // but I'll leave it zeroizing for now and leave this as a potential future optimization. -#[derive(Clone)] +#[derive(Clone, Copy)] pub(crate) struct Vector { pub(crate) vec: [Polynomial; k], } @@ -101,9 +102,13 @@ impl IndexMut for Vector { } } +impl ZeroizablePrimitive for Vector { + const ZEROED: Self = Self::new(); +} + impl Vector { - pub(crate) fn new() -> Self { - Self { vec: [(); k].map(|_| Polynomial::new()) } + pub(crate) const fn new() -> Self { + Self { vec: [Polynomial::new(); k] } } /// Algorithm 46 AddVectorNTT(𝐯, 𝐰)̂ diff --git a/crypto/mlkem/src/mlkem.rs b/crypto/mlkem/src/mlkem.rs index 4ba0a4a..fb1b057 100644 --- a/crypto/mlkem/src/mlkem.rs +++ b/crypto/mlkem/src/mlkem.rs @@ -150,8 +150,8 @@ use bouncycastle_core::traits::{ use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_sha3::{SHA3_256, SHA3_512, SHAKE256}; use bouncycastle_utils::ct::{conditional_copy_bytes, ct_eq_bytes}; +use bouncycastle_utils::secret::Secret; use core::marker::PhantomData; - /*** Constants ***/ /// pub const ML_KEM_512_NAME: &str = "ML-KEM-512"; @@ -368,16 +368,13 @@ impl< // 3: dk ← (dkPKE‖ek‖H(ek)‖𝑧) ▷ KEM decaps key includes PKE decryption key // 4: return (ek, dk) let pk_hash = pk.compute_hash(); - Ok(( - pk.clone(), - SK::new( - s_hat, - pk, - pk_hash, - seed.ref_to_bytes()[32..].try_into().unwrap(), - Some(seed.ref_to_bytes()[..32].try_into().unwrap()), - ), - )) + + let mut z = Secret::<[u8; 32]>::new(); + z.copy_from_slice(&seed.ref_to_bytes()[32..]); + + let mut seed_d = Secret::<[u8; 32]>::new(); + seed_d.copy_from_slice(&seed.ref_to_bytes()[..32]); + Ok((pk.clone(), SK::new(s_hat, pk, pk_hash, z, Some(seed_d)))) } /// Algorithm 13 K-PKE.KeyGen(𝑑) @@ -385,7 +382,7 @@ impl< /// Input: randomness 𝑑 ∈ 𝔹32 . /// Output: encryption key ek_PKE ∈ 𝔹384𝑘+32. /// Output: decryption key dk_PKE ∈ 𝔹384𝑘. - fn pke_keygen(d: &[u8; 32]) -> (PK, Vector) { + fn pke_keygen(d: &[u8; 32]) -> (PK, Secret>) { // 1: (𝜌, 𝜎) ← G(𝑑‖𝑘) // ▷ expand 32+1 bytes to two pseudorandom 32-byte seeds1 // rho: public seed @@ -411,8 +408,9 @@ impl< // ▷ 𝐬[𝑖] ∈ ℤ256 sampled from CBD // 10: 𝑁 ← 𝑁 + 1 // Note: here n = 0 - let s_hat = { - let mut s = sample_vector_CBD::(&sigma, 0); + let s_hat: Secret> = { + let mut s: Secret> = Secret::new(); + *s = sample_vector_CBD::(&sigma, 0); // 16: 𝐬_hat ← NTT(𝐬)̂ s.ntt(); @@ -673,7 +671,7 @@ impl< let K_bar: [u8; MLKEM_SS_LEN]; K_bar = { let mut j = J::new(); - j.absorb(dk.z()).expect("absorb before squeeze is infallible"); + j.absorb(dk.z().as_ref()).expect("absorb before squeeze is infallible"); j.absorb(&c).expect("absorb before squeeze is infallible"); let mut buf = [0u8; MLKEM_SS_LEN]; let bytes_written = j.squeeze_out(&mut buf); diff --git a/crypto/mlkem/src/mlkem_keys.rs b/crypto/mlkem/src/mlkem_keys.rs index 10fc00b..f45c5ac 100644 --- a/crypto/mlkem/src/mlkem_keys.rs +++ b/crypto/mlkem/src/mlkem_keys.rs @@ -8,11 +8,11 @@ use crate::{ML_KEM_512_NAME, ML_KEM_768_NAME, ML_KEM_1024_NAME}; use bouncycastle_core::errors::KEMError; use bouncycastle_core::key_material; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; -use bouncycastle_core::traits::{Hash, KEMPrivateKey, KEMPublicKey, Secret, SecurityStrength}; +use bouncycastle_core::traits::{Hash, KEMPrivateKey, KEMPublicKey, SecurityStrength}; use bouncycastle_sha3::SHA3_256; +use bouncycastle_utils::secret::Secret; use core::fmt; use core::fmt::{Debug, Display, Formatter}; - // imports just for docs #[allow(unused_imports)] use crate::mlkem::MLKEMTrait; @@ -363,6 +363,8 @@ impl, const PK_LEN: u } /// An ML-KEM private key. +/// +/// This will automatically inherit the [Secret] protections because [Polynomial] wraps the underlying data with [Secret]. #[derive(Clone)] pub struct MLKEMPrivateKey< const k: usize, @@ -370,11 +372,11 @@ pub struct MLKEMPrivateKey< const SK_LEN: usize, const PK_LEN: usize, > { - s_hat: Vector, + s_hat: Secret>, ek: PK, pk_hash: [u8; 32], - z: [u8; 32], - seed_d: Option<[u8; 32]>, + z: Secret<[u8; 32]>, + seed_d: Option>, } impl< @@ -412,7 +414,7 @@ impl< pos += 32; /* z */ - out[pos..pos + 32].copy_from_slice(&self.z); + out[pos..pos + 32].copy_from_slice(&*self.z); debug_assert_eq!(pos + 32, SK_LEN); SK_LEN @@ -447,12 +449,18 @@ pub(crate) trait MLKEMPrivateKeyInternalTrait< { /// Not exposing a constructor publicly because you should have to get an instance either by /// running a keygen, or by decoding an existing key. - fn new(s_hat: Vector, ek: PK, h: [u8; 32], z: [u8; 32], seed_d: Option<[u8; 32]>) -> Self; + fn new( + s_hat: Secret>, + ek: PK, + h: [u8; 32], + z: Secret<[u8; 32]>, + seed_d: Option>, + ) -> Self; /// Get a ref to s_hat fn s_hat(&self) -> &Vector; - fn z(&self) -> &[u8; 32]; + fn z(&self) -> &Secret<[u8; 32]>; } impl< @@ -466,10 +474,10 @@ impl< if self.seed_d.is_none() { None } else { - let mut tmp = [0u8; 64]; - tmp[..32].copy_from_slice(&self.seed_d.unwrap()); - tmp[32..].copy_from_slice(&self.z); - let mut seed = KeyMaterial::<64>::from_bytes_as_type(&tmp, KeyType::Seed).unwrap(); + let mut tmp = Secret::<[u8; 64]>::new(); + tmp[..32].copy_from_slice(&self.seed_d.clone().unwrap().as_ref()); + tmp[32..].copy_from_slice(&*self.z); + let mut seed = KeyMaterial::<64>::from_bytes_as_type(&*tmp, KeyType::Seed).unwrap(); key_material::do_hazardous_operations(&mut seed, |seed| { seed.set_security_strength(match k { @@ -499,7 +507,7 @@ impl< let mut pos = 0usize; /* dk_pke */ - let mut s_hat = Vector::::new(); + let mut s_hat: Secret> = Secret::new(); // for (s_i, sk_chunk) in s_hat.0.iter_mut().zip(sk_chunks) { for i in 0..k { s_hat[i] = byte_decode::<12, POLY_BYTES>( @@ -538,7 +546,8 @@ impl< } /* z */ - let z: [u8; 32] = sk[pos..pos + 32].try_into().unwrap(); + let mut z = Secret::<[u8; 32]>::new(); + z.copy_from_slice(sk[pos..pos + 32].try_into().unwrap()); Ok(Self::new(s_hat, ek, h_pk, z, None)) } @@ -553,11 +562,11 @@ impl< { /// Note to future maintainers: FIPS 203 section 7.3 requires that ek be hashed and compared to pk_hash. fn new( - s_hat: Vector, + s_hat: Secret>, ek: PK, pk_hash: [u8; 32], - z: [u8; 32], - seed_d: Option<[u8; 32]>, + z: Secret<[u8; 32]>, + seed_d: Option>, ) -> Self { Self { s_hat, ek, pk_hash, z, seed_d: seed_d.clone() } } @@ -566,7 +575,7 @@ impl< &self.s_hat } - fn z(&self) -> &[u8; 32] { + fn z(&self) -> &Secret<[u8; 32]> { &self.z } } @@ -627,15 +636,6 @@ impl< } } -impl< - const k: usize, - PK: MLKEMPublicKeyInternalTrait, - const SK_LEN: usize, - const PK_LEN: usize, -> Secret for MLKEMPrivateKey -{ -} - /// Debug impl mainly to prevent the secret key from being printed in logs. impl< const k: usize, @@ -686,24 +686,6 @@ impl< } } -/// Zeroizing drop -impl< - const k: usize, - PK: MLKEMPublicKeyInternalTrait, - const SK_LEN: usize, - const PK_LEN: usize, -> Drop for MLKEMPrivateKey -{ - fn drop(&mut self) { - // s_hat, has its own zeroizing drop - self.pk_hash.fill(0u8); - self.z.fill(0u8); - if self.seed_d.is_some() { - self.seed_d.as_mut().unwrap().fill(0u8); - } - } -} - /// A fully expanded ML-KEM private key that includes the intermediate values needed for performing /// multiple decaps operations with the same private key, which causes the private key struct to /// take up more memory, but results in more efficient repeated decaps() operations. @@ -788,31 +770,6 @@ impl< { } -impl< - const k: usize, - PK: MLKEMPublicKeyInternalTrait, - SK: MLKEMPrivateKeyTrait - + MLKEMPrivateKeyInternalTrait, - const SK_LEN: usize, - const PK_LEN: usize, -> Secret for MLKEMPrivateKeyExpanded -{ -} - -impl< - const k: usize, - PK: MLKEMPublicKeyInternalTrait, - SK: MLKEMPrivateKeyTrait - + MLKEMPrivateKeyInternalTrait, - const SK_LEN: usize, - const PK_LEN: usize, -> Drop for MLKEMPrivateKeyExpanded -{ - fn drop(&mut self) { - // Nothing to do since self.sk already impls zeroizing Drop - } -} - impl< const k: usize, PK: MLKEMPublicKeyInternalTrait, diff --git a/crypto/mlkem/src/polynomial.rs b/crypto/mlkem/src/polynomial.rs index 60ccaac..4dc27dc 100644 --- a/crypto/mlkem/src/polynomial.rs +++ b/crypto/mlkem/src/polynomial.rs @@ -1,18 +1,20 @@ //! Represents a polynomial over the ML-DSA ring. -use core::fmt; -use core::fmt::{Debug, Display, Formatter}; use core::ops::{Index, IndexMut}; use crate::aux_functions::{ ZETAS, ZETAS_INV, barrett_reduce, montgomery_reduce, mul_mont, ntt_base_mult, }; use crate::mlkem::{N, q}; -use bouncycastle_core::traits::Secret; /// A polynomial over the ML-KEM ring. /// Note: this is exposed publicly only for testing purposes and there is no good reason to use it in production code. -#[derive(Clone)] +/// +/// # 🚨 Security 🚨 +/// Polynomials themselves are not inherently secret since sometimes they are part of public keys +/// and sometimes private keys. +/// It is the responsibility of the caller to wrap sensitive instances in `Secret`. +#[derive(Clone, Copy)] pub struct Polynomial { /// Note: this is exposed publicly only for testing purposes and there is no good reason to use it in production code. pub coeffs: [i16; N], @@ -306,7 +308,7 @@ pub fn base_mult_montgomery(a: &Polynomial, b: &Polynomial) -> Polynomial { for i in 0..(N / 4) { ntt_base_mult( - &mut r.coeffs, + r.coeffs.as_mut(), 4 * i, a[4 * i], a[4 * i + 1], @@ -315,7 +317,7 @@ pub fn base_mult_montgomery(a: &Polynomial, b: &Polynomial) -> Polynomial { ZETAS[64 + i], ); ntt_base_mult( - &mut r.coeffs, + r.coeffs.as_mut(), 4 * i + 2, a[4 * i + 2], a[4 * i + 3], @@ -328,26 +330,6 @@ pub fn base_mult_montgomery(a: &Polynomial, b: &Polynomial) -> Polynomial { r } -impl Secret for Polynomial {} - -impl Drop for Polynomial { - fn drop(&mut self) { - self.coeffs.fill(0i16); - } -} - -impl Debug for Polynomial { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "Polynomial (data masked)") - } -} - -impl Display for Polynomial { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "Polynomial (data masked)") - } -} - // Not currently used, but I'll leave it here because it's useful for debugging if you want to output values // that are normalized to [0,q] to compare against intermediate results from other libraries. // /// if a is in \[-q..0], then it shifts it up by q to be in \[0..q] diff --git a/crypto/mlkem/tests/mlkem_tests.rs b/crypto/mlkem/tests/mlkem_tests.rs index 95357e7..344f03c 100644 --- a/crypto/mlkem/tests/mlkem_tests.rs +++ b/crypto/mlkem/tests/mlkem_tests.rs @@ -9,7 +9,7 @@ mod mlkem_tests { }; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_hex as hex; - use bouncycastle_mlkem::{MLKEM_RND_LEN, MLKEM512, MLKEM768, MLKEM1024, Polynomial}; + use bouncycastle_mlkem::{MLKEM_RND_LEN, MLKEM512, MLKEM768, MLKEM1024}; use bouncycastle_mlkem::{ MLKEM_SS_LEN, MLKEM512_CT_LEN, MLKEM512_PK_LEN, MLKEM512_SK_LEN, MLKEM768_CT_LEN, MLKEM768_PK_LEN, MLKEM768_SK_LEN, MLKEM1024_CT_LEN, MLKEM1024_PK_LEN, MLKEM1024_SK_LEN, @@ -548,21 +548,6 @@ mod mlkem_tests { } } - /// Tests that no private data is displayed - #[test] - fn test_display() { - // Polynomials (could) contain private data, - // and therefore should be protected against accidental crash dumps: - - // fmt - let p = Polynomial::new(); - assert_eq!(format!("{}", p), "Polynomial (data masked)"); - - // debug - let p = Polynomial::new(); - assert_eq!(format!("{:?}", p), "Polynomial (data masked)"); - } - #[test] fn keypair_consistency_check() { // this is common to all parameter sets, so I'll just test MLKEM512 diff --git a/crypto/rng/src/hash_drbg80090a.rs b/crypto/rng/src/hash_drbg80090a.rs index 2c84f73..6efc8ab 100644 --- a/crypto/rng/src/hash_drbg80090a.rs +++ b/crypto/rng/src/hash_drbg80090a.rs @@ -11,7 +11,7 @@ use bouncycastle_core::key_material::{ }; use bouncycastle_core::traits::{Hash, HashAlgParams, RNG, SecurityStrength}; use bouncycastle_sha2::{SHA256, SHA512}; -use bouncycastle_utils::min; +use bouncycastle_utils::{min, secret::Secret}; use std::fmt::{Display, Formatter}; @@ -75,11 +75,11 @@ pub struct HashDRBG80090A { } struct WorkingState { - v: [u8; SEED_LEN], - c: [u8; SEED_LEN], + v: Secret<[u8; SEED_LEN]>, + c: Secret<[u8; SEED_LEN]>, /// s 8.3: "A count of the number of requests produced since the instantiation was seeded or reseeded." - reseed_counter: u64, + reseed_counter: Secret, } struct AdministrativeInfo { @@ -88,15 +88,6 @@ struct AdministrativeInfo { instantiated: bool, } -impl Drop for WorkingState { - fn drop(&mut self) { - // zeroize - self.v.fill(0u8); - self.c.fill(0u8); - self.reseed_counter = 0; - } -} - /// Explicit implementation of Display that prevents auto-generated ones from accidentally leaking secrets. impl Display for WorkingState { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { @@ -107,7 +98,8 @@ impl Display for WorkingState { #[test] /// impl Display to not print the state data. fn test_working_state_display() { - let ws = WorkingState::<32> { v: [0u8; 32], c: [0u8; 32], reseed_counter: 0 }; + let ws = + WorkingState::<32> { v: Secret::new(), c: Secret::new(), reseed_counter: Secret::new() }; assert_eq!(format!("{}", ws), "HashDRBG80090A::WorkingState::<32>"); } @@ -126,9 +118,9 @@ impl HashDRBG80090A { Self { _phantom: core::marker::PhantomData, state: WorkingState:: { - v: [0u8; LARGEST_HASHER_OUTPUT_LEN], - c: [0u8; LARGEST_HASHER_OUTPUT_LEN], - reseed_counter: 0, + v: Secret::<[u8; LARGEST_HASHER_OUTPUT_LEN]>::new(), + c: Secret::<[u8; LARGEST_HASHER_OUTPUT_LEN]>::new(), + reseed_counter: Secret::new(), }, admin_info: AdministrativeInfo { strength: H::MAX_SECURITY_STRENGTH, @@ -240,29 +232,29 @@ impl Sp80090ADrbg for HashDRBG80090A { nonce.ref_to_bytes(), personalization_string, &[0u8; 0], - &mut self.state.v, + &mut *self.state.v, ), SupportedHash::SHA512 => hash_df::( seed.ref_to_bytes(), nonce.ref_to_bytes(), personalization_string, &[0u8; 0], - &mut self.state.v, + &mut *self.state.v, ), } // 4. C = Hash_df ((0x00 || V), seedlen). Comment: Precede V with a byte of zeros. match H::HASH { SupportedHash::SHA256 => { - hash_df::(&[0u8], &self.state.v, &[0u8; 0], &[0u8; 0], &mut self.state.c) + hash_df::(&[0u8], &*self.state.v, &[0u8; 0], &[0u8; 0], &mut *self.state.c) } SupportedHash::SHA512 => { - hash_df::(&[0u8], &self.state.v, &[0u8; 0], &[0u8; 0], &mut self.state.c) + hash_df::(&[0u8], &*self.state.v, &[0u8; 0], &[0u8; 0], &mut *self.state.c) } } // 5. reseed_counter = 1. - self.state.reseed_counter = 1; + *self.state.reseed_counter = 1; self.admin_info.strength = min(&security_strength, &H::MAX_SECURITY_STRENGTH).clone(); self.admin_info.prediction_resistance = prediction_resistance; self.admin_info.instantiated = true; @@ -317,32 +309,32 @@ impl Sp80090ADrbg for HashDRBG80090A { match H::HASH { SupportedHash::SHA256 => hash_df::( &[0x01], - &self.state.v.clone(), + &*self.state.v.clone(), seed.ref_to_bytes(), additional_input, - &mut self.state.v, + &mut *self.state.v, ), SupportedHash::SHA512 => hash_df::( &[0x01], - &self.state.v.clone(), + &*self.state.v.clone(), seed.ref_to_bytes(), additional_input, - &mut self.state.v, + &mut *self.state.v, ), } // 4. C = Hash_df ((0x00 || V), seedlen). Comment: Preceed with a byte of all zeros. match H::HASH { SupportedHash::SHA256 => { - hash_df::(&[0u8], &self.state.v, &[0u8; 0], &[0u8; 0], &mut self.state.c) + hash_df::(&[0u8], &*self.state.v, &[0u8; 0], &[0u8; 0], &mut *self.state.c) } SupportedHash::SHA512 => { - hash_df::(&[0u8], &self.state.v, &[0u8; 0], &[0u8; 0], &mut self.state.c) + hash_df::(&[0u8], &*self.state.v, &[0u8; 0], &[0u8; 0], &mut *self.state.c) } } // 5. reseed_counter = 1. - self.state.reseed_counter = 1; + *self.state.reseed_counter = 1; // 6. Return (V, C, and reseed_counter). Ok(()) @@ -381,7 +373,7 @@ impl Sp80090ADrbg for HashDRBG80090A { } // 1. If reseed_counter > reseed_interval, then return an indication that a reseed is required. - if self.state.reseed_counter > H::RESEED_INTERVAL { + if *self.state.reseed_counter > H::RESEED_INTERVAL { return Err(RNGError::ReseedRequired); } @@ -395,22 +387,22 @@ impl Sp80090ADrbg for HashDRBG80090A { SupportedHash::SHA256 => { let mut h = SHA256::new(); h.do_update(&[0x02]); - h.do_update(&self.state.v); + h.do_update(&*self.state.v); h.do_update(additional_input); let mut w = [0u8; SHA256::OUTPUT_LEN]; h.do_final_out(&mut w); - add_to_array(&mut self.state.v, &w); + add_to_array(&mut *self.state.v, &w); } SupportedHash::SHA512 => { let mut h = SHA512::new(); h.do_update(&[0x02]); - h.do_update(&self.state.v); + h.do_update(&*self.state.v); h.do_update(additional_input); let mut w = [0u8; SHA512::OUTPUT_LEN]; h.do_final_out(&mut w); - add_to_array(&mut self.state.v, &w); + add_to_array(&mut *self.state.v, &w); } } } @@ -422,10 +414,10 @@ impl Sp80090ADrbg for HashDRBG80090A { // But we do want to continue below to roll the state and increment the request counter. match H::HASH { SupportedHash::SHA256 => { - hashgen::(&self.state.v, out); + hashgen::(&*self.state.v, out); } SupportedHash::SHA512 => { - hashgen::(&self.state.v, out); + hashgen::(&*self.state.v, out); } } } @@ -437,24 +429,24 @@ impl Sp80090ADrbg for HashDRBG80090A { SupportedHash::SHA256 => { let mut sha = SHA256::default(); sha.do_update(&[0x03]); - sha.do_update(&self.state.v); + sha.do_update(&*self.state.v); sha.do_final_out(&mut h); } SupportedHash::SHA512 => { let mut sha = SHA512::default(); sha.do_update(&[0x03]); - sha.do_update(&self.state.v); + sha.do_update(&*self.state.v); sha.do_final_out(&mut h); } }; // 5. V = (V + H + C + reseed_counter) mod 2^seedlen. - add_to_array(&mut self.state.v, &h); - add_to_array(&mut self.state.v, &self.state.c); - add_to_array(&mut self.state.v, &self.state.reseed_counter.to_le_bytes()); + add_to_array(&mut *self.state.v, &h); + add_to_array(&mut *self.state.v, &*self.state.c); + add_to_array(&mut *self.state.v, &self.state.reseed_counter.to_le_bytes()); // 6. reseed_counter = reseed_counter + 1. - self.state.reseed_counter += 1; + *self.state.reseed_counter += 1; // 7. Return (SUCCESS, returned_bits, V, C, reseed_counter). Ok(out.len()) diff --git a/crypto/sha2/src/sha256.rs b/crypto/sha2/src/sha256.rs index 44520b2..68580a8 100644 --- a/crypto/sha2/src/sha256.rs +++ b/crypto/sha2/src/sha256.rs @@ -2,7 +2,7 @@ use crate::SHA2Params; use bouncycastle_core::errors::{HashError, SuspendableError}; use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, Suspendable}; -use bouncycastle_utils::min; +use bouncycastle_utils::{min, secret::Secret}; use core::slice; const SHA256_K: [u32; 64] = [ @@ -49,32 +49,27 @@ fn theta1(x: u32) -> u32 { #[derive(Clone)] pub(crate) struct Sha256State { _params: core::marker::PhantomData, - h: [u32; 8], -} - -impl Drop for Sha256State { - fn drop(&mut self) { - self.h.fill(0); - } + h: Secret<[u32; 8]>, } impl Sha256State { pub(crate) fn new() -> Self { + let mut h = Secret::<[u32; 8]>::new(); match PARAMS::OUTPUT_LEN * 8 { - 224 => Self { - _params: core::marker::PhantomData, - h: [ + 224 => { + h.copy_from_slice(&[ 0xC1059ED8, 0x367CD507, 0x3070DD17, 0xF70E5939, 0xFFC00B31, 0x68581511, 0x64F98FA7, 0xBEFA4FA4, - ], - }, - 256 => Self { - _params: std::marker::PhantomData, - h: [ + ]); + Self { _params: core::marker::PhantomData, h } + } + 256 => { + h.copy_from_slice(&[ 0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19, - ], - }, + ]); + Self { _params: std::marker::PhantomData, h } + } _ => panic!("Invalid SHA-2 bit size: {}", PARAMS::OUTPUT_LEN), } } @@ -82,7 +77,8 @@ impl Sha256State { fn compress(&mut self, blocks: &[[u8; 64]]) { let mut x = [0u32; 64]; - let s = &mut self.h; + // infallible; just unwrapping the [u32; 8] and re-casting to itself. + let s = &mut *self.h; let &mut [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut h] = s; for block in blocks { @@ -152,17 +148,11 @@ pub struct SHA256Internal { _params: core::marker::PhantomData, state: Sha256State, byte_count: u64, - x_buf: [u8; 64], + x_buf: Secret<[u8; 64]>, x_buf_off: usize, // TODO: should we add a maximum message size according to FIPS 180-4? (2^64 for SHA256 and 2^128 for SHA512) } -impl Drop for SHA256Internal { - fn drop(&mut self) { - self.x_buf.fill(0); - } -} - impl SHA256Internal { /// Creates a new SHA256 instance, ready for use. pub fn new() -> Self { @@ -170,7 +160,7 @@ impl SHA256Internal { _params: core::marker::PhantomData, state: Sha256State::::new(), byte_count: 0, - x_buf: [0; 64], + x_buf: Secret::new(), x_buf_off: 0, } } @@ -336,7 +326,7 @@ impl Suspendable for SHA256Inter out[32..40].copy_from_slice(&self.byte_count.to_le_bytes()); // x_buf: [u8; 64] - out[40..104].copy_from_slice(&self.x_buf); + out[40..104].copy_from_slice(&*self.x_buf); // x_buf_off: usize // in general, a usize should be serialized into a u64, but in this case, it can't ever be larger than 64 @@ -358,7 +348,7 @@ impl Suspendable for SHA256Inter // state.h: [u32; 8] // 4 * 8 = 32 - let mut h = [0u32; 8]; + let mut h = Secret::<[u32; 8]>::new(); for i in 0..8 { h[i] = u32::from_le_bytes(input[i * 4..(i * 4) + 4].try_into().unwrap()); } @@ -367,7 +357,8 @@ impl Suspendable for SHA256Inter let byte_count: u64 = u64::from_le_bytes(input[32..40].try_into().unwrap()); // x_buf: [u8; 64] - let x_buf: [u8; 64] = input[40..104].try_into().unwrap(); + let mut x_buf = Secret::<[u8; 64]>::new(); + x_buf.copy_from_slice(&input[40..104]); // x_buf_off: usize // in general, a usize should be serialized into a u64, but in this case, it can't ever be larger than 64 diff --git a/crypto/sha2/src/sha512.rs b/crypto/sha2/src/sha512.rs index 22ce978..e730cf0 100644 --- a/crypto/sha2/src/sha512.rs +++ b/crypto/sha2/src/sha512.rs @@ -2,7 +2,7 @@ use crate::SHA2Params; use bouncycastle_core::errors::{HashError, SuspendableError}; use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, Suspendable}; -use bouncycastle_utils::min; +use bouncycastle_utils::{min, secret::Secret}; use core::slice; const SHA512_K: [u64; 80] = [ @@ -63,32 +63,27 @@ fn theta1(x: u64) -> u64 { #[derive(Clone)] pub(crate) struct Sha512State { _params: std::marker::PhantomData, - h: [u64; 8], -} - -impl Drop for Sha512State { - fn drop(&mut self) { - self.h.fill(0); - } + h: Secret<[u64; 8]>, } impl Sha512State { pub(crate) fn new() -> Self { + let mut h = Secret::<[u64; 8]>::new(); match PARAMS::OUTPUT_LEN * 8 { - 384 => Self { - _params: std::marker::PhantomData, - h: [ + 384 => { + h.copy_from_slice(&[ 0xCBBB9D5DC1059ED8, 0x629A292A367CD507, 0x9159015A3070DD17, 0x152FECD8F70E5939, 0x67332667FFC00B31, 0x8EB44A8768581511, 0xDB0C2E0D64F98FA7, 0x47B5481DBEFA4FA4, - ], - }, - 512 => Self { - _params: std::marker::PhantomData, - h: [ + ]); + Self { _params: std::marker::PhantomData, h } + } + 512 => { + h.copy_from_slice(&[ 0x6A09E667F3BCC908, 0xBB67AE8584CAA73B, 0x3C6EF372FE94F82B, 0xA54FF53A5F1D36F1, 0x510E527FADE682D1, 0x9B05688C2B3E6C1F, 0x1F83D9ABFB41BD6B, 0x5BE0CD19137E2179, - ], - }, + ]); + Self { _params: std::marker::PhantomData, h } + } _ => panic!("Invalid SHA-2 bit size"), } } @@ -96,7 +91,7 @@ impl Sha512State { fn compress(&mut self, blocks: &[[u8; 128]]) { let mut x = [0u64; 80]; - let s = &mut self.h; + let s = &mut *self.h; let &mut [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut h] = s; for block in blocks { @@ -166,16 +161,10 @@ pub struct SHA512Internal { _params: std::marker::PhantomData, state: Sha512State, byte_count: u64, // NOTE We only support 2^67 bits, not the full 2^128 - x_buf: [u8; 128], + x_buf: Secret<[u8; 128]>, x_buf_off: usize, } -impl Drop for SHA512Internal { - fn drop(&mut self) { - self.x_buf.fill(0); - } -} - impl SHA512Internal { /// Creates a new SHA512 instance, ready for use. pub fn new() -> Self { @@ -183,7 +172,7 @@ impl SHA512Internal { _params: std::marker::PhantomData, state: Sha512State::::new(), byte_count: 0, - x_buf: [0; 128], + x_buf: Secret::new(), x_buf_off: 0_usize, } } @@ -349,7 +338,7 @@ impl Suspendable for SHA512Inter out[64..72].copy_from_slice(&self.byte_count.to_le_bytes()); // x_buf: [u8; 128] - out[72..200].copy_from_slice(&self.x_buf); + out[72..200].copy_from_slice(&*self.x_buf); // x_buf_off: usize // in general, a usize should be serialized into a u64, but in this case, it can't ever be larger than 128 @@ -369,7 +358,7 @@ impl Suspendable for SHA512Inter // state.h: [u64; 8] // 8 * 8 = 64 - let mut h = [0u64; 8]; + let mut h = Secret::<[u64; 8]>::new(); for i in 0..8 { h[i] = u64::from_le_bytes(input[i * 8..(i * 8) + 8].try_into().unwrap()); } @@ -378,7 +367,8 @@ impl Suspendable for SHA512Inter let byte_count: u64 = u64::from_le_bytes(input[64..72].try_into().unwrap()); // x_buf: [u8; 128] - let x_buf: [u8; 128] = input[72..200].try_into().unwrap(); + let mut x_buf = Secret::<[u8; 128]>::new(); + x_buf.copy_from_slice(&input[72..200]); // x_buf_off: usize // in general, a usize should be serialized into a u64, but in this case, it can't ever be larger than 128 diff --git a/crypto/sha3/src/keccak.rs b/crypto/sha3/src/keccak.rs index 3c7f63c..dc0f711 100644 --- a/crypto/sha3/src/keccak.rs +++ b/crypto/sha3/src/keccak.rs @@ -1,6 +1,7 @@ use bouncycastle_core::errors::{HashError, SuspendableError}; use bouncycastle_core::key_material::KeyType; use bouncycastle_core::traits::SecurityStrength; +use bouncycastle_utils::secret::Secret; const KECCAK_ROUND_CONSTANTS: [u64; 24] = [ 0x0000000000000001, 0x0000000000008082, 0x800000000000808A, 0x8000000080008000, @@ -13,13 +14,13 @@ const KECCAK_ROUND_CONSTANTS: [u64; 24] = [ #[derive(Clone)] pub(crate) struct KeccakState { - buf: [u64; 25], + buf: Secret<[u64; 25]>, rate: usize, } impl KeccakState { fn new(rate: usize) -> Self { - Self { buf: [0u64; 25], rate } + Self { buf: Secret::new(), rate } } fn absorb(&mut self, data: &[u8]) { @@ -60,7 +61,7 @@ impl KeccakState { mut a22, mut a23, mut a24, - ] = a.buf; + ] = *a.buf; for round_constant in KECCAK_ROUND_CONSTANTS { // theta @@ -174,26 +175,18 @@ impl KeccakState { a00 ^= round_constant; } - a.buf = [ + *a.buf = [ a00, a01, a02, a03, a04, a05, a06, a07, a08, a09, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, ]; } } -// Mutants note: this fails because you can't write unit tests for drop() -impl Drop for KeccakState { - fn drop(&mut self) { - // Zeroize the contents before returning the memory to the OS. - self.buf.fill(0u64); - } -} - // This is pub(crate) so that the SerializableState handlers can unpack it. #[derive(Clone)] pub(crate) struct KeccakInternal { state: KeccakState, - pub data_queue: [u8; 192], + pub data_queue: Secret<[u8; 192]>, rate: usize, pub bits_in_queue: usize, pub squeezing: bool, @@ -215,7 +208,7 @@ impl KeccakInternal { Self { state: KeccakState::new(rate), - data_queue: [0u8; 192], + data_queue: Secret::new(), rate, bits_in_queue: 0, squeezing: false, @@ -251,7 +244,7 @@ impl KeccakInternal { self.bits_in_queue += 8; if self.bits_in_queue == self.rate { - self.state.absorb(&self.data_queue); + self.state.absorb(&*self.data_queue); self.bits_in_queue = 0; } } @@ -328,7 +321,7 @@ impl KeccakInternal { self.bits_in_queue += 1; if self.bits_in_queue == self.rate { - self.state.absorb(&self.data_queue); + self.state.absorb(&*self.data_queue); } else { let full = self.bits_in_queue >> 6; let partial = self.bits_in_queue & 63; @@ -401,7 +394,7 @@ impl KeccakInternal { } // data_queue: [u8; 192] - out[200..392].copy_from_slice(&self.data_queue); + out[200..392].copy_from_slice(&*self.data_queue); // bits_in_queue: usize out[392..400].copy_from_slice(&(self.bits_in_queue as u64).to_le_bytes()); @@ -421,13 +414,14 @@ impl KeccakInternal { rate: usize, ) -> Result { // state.buf: [u64; 25] - let mut buf = [0u64; 25]; + let mut buf = Secret::<[u64; 25]>::new(); for i in 0..25 { buf[i] = u64::from_le_bytes(input[i * 8..(i * 8) + 8].try_into().unwrap()); } // data_queue: [u8; 192] - let data_queue: [u8; 192] = input[200..392].try_into().unwrap(); + let mut data_queue = Secret::<[u8; 192]>::new(); + data_queue.copy_from_slice(&input[200..392]); // bits_in_queue: usize. // In a legitimate state it is always a multiple of 8 AND strictly less diff --git a/crypto/utils/src/ct.rs b/crypto/utils/src/ct.rs index 6f87b8a..f41c204 100644 --- a/crypto/utils/src/ct.rs +++ b/crypto/utils/src/ct.rs @@ -267,7 +267,7 @@ pub fn ct_eq_bytes(a: &[u8], b: &[u8]) -> bool { } let mut result = 0u8; for i in 0..a.len() { - result |= std::hint::black_box(a[i] ^ b[i]); + result |= core::hint::black_box(a[i] ^ b[i]); } result == 0 } @@ -277,7 +277,7 @@ pub fn ct_eq_bytes(a: &[u8], b: &[u8]) -> bool { pub fn ct_eq_zero_bytes(a: &[u8]) -> bool { let mut result = 0u8; for i in 0..a.len() { - result |= std::hint::black_box(a[i]); + result |= core::hint::black_box(a[i]); } result == 0 } @@ -305,6 +305,6 @@ pub fn conditional_copy_bytes( debug_assert_eq!(mask, if take_a { 0xFF } else { 0x00 }); for i in 0..LEN { - out[i] = std::hint::black_box(a[i] & mask) | std::hint::black_box(b[i] & !mask); + out[i] = core::hint::black_box(a[i] & mask) | core::hint::black_box(b[i] & !mask); } } diff --git a/crypto/utils/src/lib.rs b/crypto/utils/src/lib.rs index 7928819..92205b1 100644 --- a/crypto/utils/src/lib.rs +++ b/crypto/utils/src/lib.rs @@ -6,12 +6,18 @@ //! That said, beware that this crate is not necessarily documented to the same standard as other crates. //! Since many of the contained helpers are security-critical (such as the constant time module), //! we will prioritize fixing security bugs over maintaining a stable API for this crate. +//! +//! This crate intentionally does not have `#![forbid(unsafe_code)]` because some of the constant-time +//! and zeroization techniques require unsafe code in order to force the compiler into specific +//! assembly-level behaviours. The idea is to contain the unsafe code in a central location rather +//! so that the higher-level primitives can stick to safe rust. -#![forbid(unsafe_code)] +#![no_std] #![forbid(missing_docs)] #![allow(private_bounds)] pub mod ct; +pub mod secret; /// Basic max function. If they are equal, you get back the first one. pub fn max<'a, T: PartialOrd>(x: &'a T, y: &'a T) -> &'a T { diff --git a/crypto/utils/src/secret.rs b/crypto/utils/src/secret.rs new file mode 100644 index 0000000..92109eb --- /dev/null +++ b/crypto/utils/src/secret.rs @@ -0,0 +1,353 @@ +//! A transparent wrapper type, [Secret], which is a wrapper that holds a secret value and +//! guarantees it is securely zeroized on drop, and protected from accidintal logging by implementing +//! redacting `fmt::Debug` and `fmt::Display`. +//! +//! # Why write_volatile +//! +//! Plain writes such as `slice.fill(0)` are ordinary, non-observable memory accesses. Under +//! optimization the compiler may prove that a buffer is never read again after it is scrubbed -- +//! precisely the situation just before a drop -- and elide the scrub as a dead store. To prevent +//! that, [Secret] erases through [core::ptr::write_volatile], whose accesses are defined by the +//! language as observable side effects and therefore may not be elided or coalesced. Each scrub is +//! followed by a [compiler_fence] with [SeqCst](Ordering::SeqCst) ordering so the volatile +//! writes are not reordered with respect to later memory operations. +//! +//! # Why Sized? +//! +//! The [ZeroizablePrimitive] is bounded on [`Sized`](core::marker::Sized), which explicitly forbids +//! instantiating `Secret` over something like `Vec` whose size is not known at compile time. +//! +//! The reason is that an implementation of `.zeroize()` that is guaranteed not be optimized away +//! by the compiler requires the use of `unsafe{ write_volatile() }` to directly write the `T::ZEROED` +//! byte pattern over top of the provided memory block. With a `Sized` type, this is a single line of +//! unsafe code and it is easy to prove that it is writing the number of bytes that it should be. +//! For a dynamically-sized value such as `Vec`, this is substantially trickier. +//! Taking `Vec` as an example, it is not a flat piece of memory that can be trivially over-written +//! with a static value; `Vec` is actually a stack of structs that implement a smart-pointer that +//! tracks both `length` and `capacity` of the memory referenced by the pointer. +//! Properly zeroizing this means following the pointer, filling the referenced memory with `0x00` up +//! to the `capacity`, then setting `length=0` without changing `capacity` or the pointer. +//! Zeroizing a `Vec` would require a substantial amount of unsafe code that is Vec-specific and +//! tricky to prove the correctness of. +//! Doing this for arbitrary heap-allocated objects (which may contain nested heap-allocated objects) +//! sounds like a whole research project. +//! +//! This is not to say that bc-rust will never attempt this challenge, but since bc-rust is a `[no_std]` +//! library that keeps all of its secrets in stack-allocated variables, this is not a problem that +//! needs to be solved for internal library use. + +use crate::ct; +use core::any::type_name; +use core::fmt; +use core::mem::size_of; +use core::ops::{Deref, DerefMut}; +use core::ptr; +use core::sync::atomic::{Ordering, compiler_fence}; + +/// A `Copy` type whose all-zero value is meaningful and valid, so that a [`Secret`] of it can be +/// default-constructed in a zeroed state. +/// +/// This is used instead of [`Default`] for two reasons: it lets [`Secret::new`] produce a zeroed +/// value at compile time via an associated `const`, and -- crucially -- it works for arrays of *any* +/// length, whereas `[T; N]: Default` is only implemented for `N <= 32`. +// Dev note: the `Copy` bound is load-bearing, but only in preventing impl'ng this for additional types +// that will turn out to be problematic. +// Specifically: we're using `Copy` to mean 'no Drop' which works because 'Copy' and `Drop` are +// mutually exclusive, and the byte-scrub semantics here go squirrely if you instantiate a +// Secret over a type that impls Drop. So we don't actually care about the underlying type +// impl'ing Copy; we only care that it doesn't impl Drop, and the `Copy` bound is a +// convenient way to catch that if we in the future do impl_zero_init!(T) for a T that has Drop. +pub trait ZeroizablePrimitive: Copy + Sized { + /// The zeroed value of this type. + /// This needs to be a valid static instance of Self that [Secret::zeroize] will internally + /// convert into a byte array and use to overwrite the memory location of the given instance of + /// the primitive. + const ZEROED: Self; +} + +macro_rules! impl_zero_init { + ($($t:ty),+ $(,)?) => {$( + impl ZeroizablePrimitive for $t { + const ZEROED: Self = 0 as $t; + } + )+}; +} + +impl_zero_init!(u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize); + +impl ZeroizablePrimitive for bool { + const ZEROED: Self = false; +} + +impl ZeroizablePrimitive for char { + const ZEROED: Self = '\0'; +} + +/// Blanket impl for fixed-size arrays of any length. +/// This is an alternative to `[T; N]: Default` which is capped at `N <= 32`. +impl ZeroizablePrimitive for [T; N] { + const ZEROED: Self = [T::ZEROED; N]; +} + +/// A wrapper that holds a secret value of any primitive or fixed-size array type +/// and guarantees it is securely zeroized on drop, and is +/// protected from accidental logging by implementing redacting `fmt::Debug` and `fmt::Display`. +/// +/// A `Secret` is created in a zeroed state with [`Secret::new`] / [`Default`] and populated in +/// place through [`DerefMut`]; there is intentionally no by-value constructor (see the [module +/// docs](self)). It behaves transparently as a `&T` / `&mut T` via [`Deref`]/[`DerefMut`], so a +/// `Secret<[u8; 32]>` can be indexed, sliced, and iterated exactly like the underlying array. +/// +/// `Secret` deliberately does **not** implement [`Copy`] (it owns a `Drop`), which forces move +/// semantics and prevents silent, unscrubbed duplication of secrets. It *does* implement [`Clone`] +/// for the cases where an explicit, intentional copy is required. +/// +/// Its [`Debug`](fmt::Debug) and [`Display`](fmt::Display) impls are redacting: they never print the +/// contained bytes, so a secret cannot leak into logs or panic/crash output. +/// +/// # Usage Examples +/// +/// `Secret` and other scalar types +/// +/// [Secret] can wrap any of the following scalar types: +/// u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, bool, and char. +/// +/// ``` +/// use bouncycastle_utils::secret::Secret; +/// +/// let mut nonce: Secret = Secret::default(); +/// *nonce = 0xDEAD_BEEF; +/// assert_eq!(*nonce, 0xDEAD_BEEF); +/// +/// +/// let mut counter: Secret = Secret::new(); +/// // Here you have to explicitly Deref to get at the underlying type, but it all works. +/// *counter += 1; +/// assert_eq!(*counter, 1); +/// ``` +/// +/// ## `Secret<[u8; 32]>` +/// +/// `Secret` can wrap arrays of any of the supported scalar types. +/// There is no bound on how large an array you can make. +/// +/// Here we'll construct a zeroed `Secret<[u8; 32]>` and fill them *in place* through [`DerefMut`], +/// so the plaintext is never held in a separate, unprotected variable: +/// +/// ``` +/// use bouncycastle_utils::secret::Secret; +/// +/// let mut key: Secret<[u8; 32]> = Secret::new(); +/// +/// // Here, .copy_from_slice may still produce a copy in memory, but it's the best we can do in +/// // illustrative example code. +/// // In real code an RNG or KDF or network socket read should be directly handed the mut ref to +/// // Secret so that it can write directly into it. +/// key.copy_from_slice(&[0x42u8; 32]); +/// +/// // `Secret` is (mostly) transparent: you can use it exactly as you would the underlying type T +/// // (possibly requiring a dereference). +/// // indexing, slicing, `.len()`, iteration, etc all work automatically via `Deref`. +/// // Just forget the Secret is there +/// assert_eq!(key[0], 0x42); +/// assert_eq!(key.len(), 32); +/// assert!(key.iter().all(|&b| b == 0x42)); +/// // `key` is volatile-scrubbed to zero when it drops at the end of this scope. +/// ``` +/// +/// ## On a custom type +/// +/// Since [ZeroizablePrimitive] is a public trait, you can implement it on your own types and then +/// trivially be able to wrap them in a `Secret`. The only requirement is that there is a +/// well-defined "ZEROED" value for the type. +/// +/// Toy Example: +/// +/// ``` +/// use bouncycastle_utils::secret::{Secret, ZeroizablePrimitive}; +/// +/// /// Holds a system user +/// #[derive(Clone, Copy)] +/// struct User { +/// userid: i32, +/// name: [u8; 64], +/// } +/// +/// /// Provide the const ZEROED value for the type. +/// impl ZeroizablePrimitive for User { +/// const ZEROED: Self = Self{ userid: 0i32, name: [0u8; 64] }; +/// } +/// +/// /// We will tag the admins as Secret to give them extra protections against +/// /// having their info leaked. +/// struct AllUsers { +/// users: Vec, +/// admins: Vec>, +/// } +/// ``` +/// +/// Note that `Secret` is only defined for statically-sized types -- ie types that satisfy +/// [`Sized`](core::marker::Sized). For a justification, see the module docs. +/// +/// # Redacting Debug and Display +/// +/// [`Debug`](fmt::Debug) and [`Display`](fmt::Display) are redacting, so a `Secret` can be logged +/// without leaking its contents: +/// +/// ``` +/// use bouncycastle_utils::secret::Secret; +/// +/// let mut secret: Secret = Secret::new(); +/// *secret = 0x4141_4141; // would render as "AAAA" if leaked +/// assert_eq!(format!("{secret}"), ""); +/// assert_eq!(format!("{secret:?}"), "Secret()"); +/// ``` +/// +/// This will also work for custom types; let's say the `User` struct from the previous example had +/// impl'd Display and Debug; mhen you wrap them in `Secret` then `Secret`'s Display and Debug +/// are invoked instead of `User`'s and you get the "" output. +/// +/// # Memory Usage +/// +/// As a direct wrapper of the type `T`, a `Secret` does not add any memory overhead. +/// +/// ``` +/// use bouncycastle_utils::secret::Secret; +/// +/// print!("{}\n", size_of::()); // 4 +/// print!("{}\n", size_of::>()); // also 4 +/// +/// print!("{}\n", size_of::<[u8; 32]>()); // 32 +/// print!("{}\n", size_of::>()); // also 32 +/// ``` +/// +/// # 🚨 Security 🚨 +/// +/// What this does NOT guarantee: +/// +/// [Secret] only guarantees that the *final* scrub of the wrapped value is emitted. It cannot +/// recover copies that the compiler or CPU made. To minimize copies of the underlying bytes in memory, +/// you should be careful with a few things: +/// +/// * Create a new [Secret] instance, then get a mut ref to its internal value via [Secret::deref_mut] +/// and write to that instead of having a copy in an unprotected variable and then copying it into the Secret. +/// * Avoid copying out of Secret for the same reason. +pub struct Secret(T); + +impl Secret { + /// Create a new `Secret` in a zeroed state. + /// + /// Populate it afterwards in place via [`DerefMut`] (e.g. by having an RNG or KDF write into + /// `&mut *secret`), which avoids ever materializing an unprotected copy of the secret. + #[inline] + pub const fn new() -> Self { + Self(T::ZEROED) + } +} + +impl Default for Secret { + #[inline] + fn default() -> Self { + Self::new() + } +} + +impl Secret { + /// Securely overwrite the contained value with zeros. + /// After this returns, every byte of the wrapped value has been volatile-written to `0`. + /// + /// This is called automatically on drop; call it directly only if you need to scrub the value + /// early, for example before reusing the same `Secret` object. + /// Though in many circumstances you are zeroizing because you know you're done with the object before + /// it goes out of scope, in which case you would be better served calling `drop(s)` instead + /// since this will still call `zeroize()` and also move the object, preventing you from accidentally reusing it. + #[inline] + pub fn zeroize(&mut self) { + // SAFETY: `&mut self.0` is a valid, properly aligned, mutable reference to an initialized + // `T`, which is exactly the contract `write_volatile` requires. + // `T::ZEROED` is defined above for each supported primitive and primitive-array as the + // is the all-zero value of `T`, which is a valid and correctly-sized bit pattern + // for the primitive scalar/array being zeroized. + //`write_volatile` (rather than a plain store) is what forbids the compiler from eliding + // this scrub as a dead write as per its contract: + // https://doc.rust-lang.org/std/ptr/fn.write_volatile.html + + // Just to make sure -- this should trigger on any unit tests for any instantiation of + // Secret that causes this assumption to be violated. + debug_assert_eq!(size_of::(), size_of_val(&T::ZEROED)); + + unsafe { + ptr::write_volatile(&mut self.0, T::ZEROED); + } + // Compile-time barrier: keeps the volatile scrub ordered before any later memory ops. + // (for example, if the user calls .zeroize() outside of a drop and then continues using + // the object by filling it with new data, which is valid usage.) + // Emits no machine instructions. + compiler_fence(Ordering::SeqCst); + } +} + +impl Drop for Secret { + #[inline] + fn drop(&mut self) { + self.zeroize(); + } +} + +impl Deref for Secret { + type Target = T; + #[inline] + fn deref(&self) -> &T { + &self.0 + } +} + +impl DerefMut for Secret { + #[inline] + fn deref_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +// Intentionally not `Copy`: a `Copy` type cannot have a `Drop`, and we want move semantics so a +// secret is never silently duplicated. `Clone` is provided for deliberate copies. +impl Clone for Secret { + #[inline] + fn clone(&self) -> Self { + Self(self.0) + } +} + +/// Redacting: prints the wrapped type but never its contents. +impl fmt::Debug for Secret { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Secret<{}>()", type_name::()) + } +} + +/// Redacting: never prints the contents. +impl fmt::Display for Secret { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("") + } +} + +/// Checks for equality of the secret data by casting to bytes and using a constant-time comparison. +/// +/// Both operands are the same type `T`, so both views are exactly `size_of::()` bytes and the +/// comparison never short-circuits: it always inspects every byte, avoiding a timing side channel +/// that would otherwise leak how many leading bytes of two secrets match. +impl PartialEq for Secret { + fn eq(&self, other: &Self) -> bool { + let len = size_of::(); + // SAFETY: `self.0` / `other.0` are live, initialized `T` values, so the `len` bytes starting + // at each address lie within that single object. `u8` has alignment 1, so every byte address + // is well aligned, and the slices are read-only and used only within this call. `T: Copy` + // (via `ZeroizablePrimitive`) means there is no interior mutability or drop glue to worry + // about. + let a = unsafe { core::slice::from_raw_parts((&self.0 as *const T).cast::(), len) }; + let b = unsafe { core::slice::from_raw_parts((&other.0 as *const T).cast::(), len) }; + ct::ct_eq_bytes(a, b) + } +} +impl Eq for Secret {} diff --git a/crypto/utils/tests/test_secret.rs b/crypto/utils/tests/test_secret.rs new file mode 100644 index 0000000..81cb2c2 --- /dev/null +++ b/crypto/utils/tests/test_secret.rs @@ -0,0 +1,210 @@ +#[cfg(test)] +mod test_secret { + // The crate is `#![no_std]`; the test harness links `std`, but its prelude (and `format!`) is + // not in scope automatically. Bring it in explicitly for these tests. + extern crate std; + + use std::fmt; + use std::fmt::Formatter; + use std::format; + + use bouncycastle_utils::secret::{Secret, ZeroizablePrimitive}; + + #[test] + fn new_and_default_are_zeroed() { + let s = Secret::::new(); + assert_eq!(*s, 0); + + let d: Secret = Secret::default(); + assert_eq!(*d, 0); + + let arr = Secret::<[u8; 4]>::new(); + assert_eq!(*arr, [0u8; 4]); + } + + #[test] + fn fill_in_place_via_deref_mut() { + let mut s = Secret::::new(); + assert_eq!(*s, 0u16); + *s = 0xABCD; + assert_eq!(*s, 0xABCD); + } + + #[test] + fn array_is_transparent() { + let mut key = Secret::<[u8; 4]>::new(); + *key = [1u8, 2, 3, 4]; + assert_eq!(key[0], 1); + assert_eq!(key[3], 4); + assert_eq!(key.len(), 4); + assert_eq!(key.iter().copied().sum::(), 10); + } + + #[test] + fn default_supports_arrays_larger_than_32() { + // `[u8; 64]: Default` does NOT exist (Default is capped at N <= 32), but `ZeroInit` does, + // so this must compile and produce a zeroed buffer, really just to prove that we can do + // something that Default can't. + let big = Secret::<[u8; 64]>::new(); + assert_eq!(big.len(), 64); + assert!(big.iter().all(|&b| b == 0)); + } + + #[test] + fn explicit_zeroize_scrubs_scalar() { + let mut s = Secret::::new(); + *s = 0xDEAD_BEEF_DEAD_BEEF; + s.zeroize(); + assert_eq!(*s, 0); + } + + #[test] + fn explicit_zeroize_scrubs_array() { + let mut key = Secret::<[u8; 16]>::new(); + *key = [0xFFu8; 16]; + key.zeroize(); + assert_eq!(*key, [0u8; 16]); + } + + #[test] + fn clone_duplicates_value() { + let mut a = Secret::::new(); + *a = 42; + let mut b = a.clone(); + *b += 1; + assert_eq!(*a, 42); + assert_eq!(*b, 43); + } + + /// Wrapping something in Secret will mask the type's native Debug / Display + #[test] + fn debug_and_display_are_redacted() { + // Redacts basic types + // would render as "AAAA" if leaked + let i = 0x4141_4141i32; + assert_eq!(format!("{i}"), "1094795585"); + assert_eq!(format!("{i:?}"), "1094795585"); + + let mut s = Secret::::new(); + *s = 0x4141_4141; + assert_eq!(*s, 0x4141_4141); + let dbg = format!("{s:?}"); + let disp = format!("{s}"); + assert!(dbg.contains("redacted")); + assert!(disp.contains("redacted")); + // The secret value must not appear in either rendering. + assert!(!dbg.contains("1094795585")); // 0x41414141 + assert!(!disp.contains("1094795585")); + + // Same for arrays + let a = [0x0, 0x1, 0x2, 0x3]; + // [u8] does not impl Display, but it does impl Debug + assert_eq!(format!("{a:?}"), "[0, 1, 2, 3]"); + + let mut sa = Secret::<[u8; 4]>::new(); + *sa = [0x0, 0x1, 0x2, 0x3]; + assert_eq!(*sa, [0x0, 0x1, 0x2, 0x3]); + assert!(format!("{sa:?}").contains("redacted")); + assert!(!format!("{sa:?}").contains("0, 1, 2, 3")); + + // Same for custom types + #[derive(Copy, Clone)] + struct Thing { + value: i32, + } + // Debug and Display that dump the inner value. + impl fmt::Debug for Thing { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "Thing {}", self.value) + } + } + impl fmt::Display for Thing { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "Thing {}", self.value) + } + } + + // So that it's valid for Secret + impl ZeroizablePrimitive for Thing { + const ZEROED: Self = Self { value: 0 }; + } + + // Thing can be debugged and displayed with the inner value + let non_secret_thing = Thing { value: 42 }; + assert_eq!(format!("{}", non_secret_thing), "Thing 42"); + assert_eq!(format!("{:?}", non_secret_thing), "Thing 42"); + + // But a Secret cannot + let mut secret_thing = Secret::::new(); + secret_thing.value = 42; + + assert!(format!("{}", secret_thing).contains("")); + assert!(!format!("{}", secret_thing).contains("42")); + + assert!(format!("{:?}", secret_thing).contains("")); + assert!(!format!("{:?}", secret_thing).contains("42")); + + // still behaves properly after zeroization + secret_thing.zeroize(); + assert_eq!(secret_thing.value, 0i32); + + assert!(format!("{}", secret_thing).contains("")); + assert!(!format!("{}", secret_thing).contains("0")); + + assert!(format!("{:?}", secret_thing).contains("")); + assert!(!format!("{:?}", secret_thing).contains("0")); + + secret_thing.value = 43; + assert_eq!(secret_thing.value, 43i32); + + assert!(format!("{}", secret_thing).contains("")); + assert!(!format!("{}", secret_thing).contains("43")); + + assert!(format!("{:?}", secret_thing).contains("")); + assert!(!format!("{:?}", secret_thing).contains("43")); + } + + #[test] + fn drop_does_not_panic() { + let mut s = Secret::<[u8; 32]>::new(); + *s = [7u8; 32]; + drop(s); + } + + #[test] + fn eq() { + // Scalars: equal and unequal. + let mut a = Secret::::new(); + *a = 0xDEAD_BEEF; + let mut b = Secret::::new(); + *b = 0xDEAD_BEEF; + let mut c = Secret::::new(); + *c = 0xFEED_FACE; + assert_eq!(a, b); + assert_ne!(a, c); + + // Two freshly-zeroed secrets are equal. + assert_eq!(Secret::::new(), Secret::::new()); + + // Arrays: equal, and differing in a single byte. + let mut k1 = Secret::<[u8; 16]>::new(); + *k1 = [0x42u8; 16]; + let mut k2 = Secret::<[u8; 16]>::new(); + *k2 = [0x42u8; 16]; + assert_eq!(k1, k2); + + k2[15] = 0x43; // flip only the last byte + assert_ne!(k1, k2); + + k2[15] = 0x42; + k2[0] = 0x43; // flip only the first byte + assert_ne!(k1, k2); + } + + /// The Secret wrapper does not add any memory overhead. + #[test] + fn no_size_inflation() { + assert_eq!(size_of::(), size_of::>()); + assert_eq!(size_of::<[u8; 32]>(), size_of::>()); + } +}