diff --git a/Cargo.toml b/Cargo.toml index 6e2ed3f..529f27d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,19 +9,22 @@ edition = "2024" # *** Internal Dependencies *** bouncycastle = { path = "./", version = "0.1.2" } bouncycastle-base64 = { path = "./crypto/base64", version = "0.1.2" } -bouncycastle-core = { path = "crypto/core", version = "0.1.2" } +# `default-features = false` here (rather than on each member) is required so that individual +# crates can opt out of the `alloc` feature for `no_std` builds; members re-enable it through +# their own `alloc` feature. Building the whole workspace keeps `alloc` on via feature unification. +bouncycastle-core = { path = "crypto/core", version = "0.1.2", default-features = false } bouncycastle-core-test-framework = { path = "./crypto/core-test-framework", version = "0.1.2" } bouncycastle-factory = { path = "./crypto/factory", version = "0.1.2" } bouncycastle-hex = { path = "./crypto/hex", version = "0.1.2" } bouncycastle-hkdf = { path = "./crypto/hkdf", version = "0.1.2" } -bouncycastle-hmac = { path = "./crypto/hmac", version = "0.1.2" } +bouncycastle-hmac = { path = "./crypto/hmac", version = "0.1.2", default-features = false } bouncycastle-mlkem = { path = "./crypto/mlkem", version = "0.1.2" } bouncycastle-mlkem-lowmemory = { path = "./crypto/mlkem-lowmemory", version = "0.1.2" } bouncycastle-mldsa = { path = "./crypto/mldsa", version = "0.1.2" } bouncycastle-mldsa-lowmemory = { path = "./crypto/mldsa-lowmemory", version = "0.1.2" } bouncycastle-rng = { path = "./crypto/rng", version = "0.1.2" } -bouncycastle-sha2 = { path = "./crypto/sha2", version = "0.1.2" } -bouncycastle-sha3 = { path = "./crypto/sha3", version = "0.1.2" } +bouncycastle-sha2 = { path = "./crypto/sha2", version = "0.1.2", default-features = false } +bouncycastle-sha3 = { path = "./crypto/sha3", version = "0.1.2", default-features = false } bouncycastle-utils = { path = "./crypto/utils", version = "0.1.2" } @@ -39,17 +42,20 @@ name = "bouncycastle" version = "0.1.2" edition.workspace = true +# The umbrella crate re-exports the whole library for downstream users who want everything in one +# dependency, so it enables `alloc` on the primitive crates whose workspace deps default it off. +# (base64/hex/hkdf/rng/factory already default `alloc` on.) [dependencies] bouncycastle-base64.workspace = true -bouncycastle-core.workspace = true +bouncycastle-core = { workspace = true, features = ["alloc"] } bouncycastle-factory.workspace = true bouncycastle-hex.workspace = true bouncycastle-hkdf.workspace = true -bouncycastle-hmac.workspace = true +bouncycastle-hmac = { workspace = true, features = ["alloc"] } bouncycastle-mldsa.workspace = true bouncycastle-mldsa-lowmemory.workspace = true bouncycastle-mlkem.workspace = true bouncycastle-mlkem-lowmemory.workspace = true bouncycastle-rng.workspace = true -bouncycastle-sha2.workspace = true -bouncycastle-sha3.workspace = true \ No newline at end of file +bouncycastle-sha2 = { workspace = true, features = ["alloc"] } +bouncycastle-sha3 = { workspace = true, features = ["alloc"] } \ No newline at end of file diff --git a/crypto/base64/Cargo.toml b/crypto/base64/Cargo.toml index 7059b7c..b835831 100644 --- a/crypto/base64/Cargo.toml +++ b/crypto/base64/Cargo.toml @@ -3,6 +3,12 @@ name = "bouncycastle-base64" version = "0.1.2" edition.workspace = true +[features] +# `alloc` enables the (entire) `String`/`Vec`-returning encoder/decoder API. On by default; a +# `no_std` build without it exposes only the type definitions (there are no streaming `_out` variants). +default = ["alloc"] +alloc = [] + [dependencies] bouncycastle-core.workspace = true bouncycastle-utils.workspace = true diff --git a/crypto/base64/src/lib.rs b/crypto/base64/src/lib.rs index 93f4101..8a80f96 100644 --- a/crypto/base64/src/lib.rs +++ b/crypto/base64/src/lib.rs @@ -81,17 +81,29 @@ // /// "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_=" // URLSafe, +#![cfg_attr(not(test), no_std)] #![forbid(unsafe_code)] #![forbid(missing_docs)] +// The entire base64 encoder/decoder API returns `String`/`Vec`, so it lives behind the +// default-on `alloc` feature. base64 is inherently an allocating codec (there are currently no +// streaming `_out` variants), so a `no_std` build without `alloc` exposes only the type definitions. +#[cfg(feature = "alloc")] +extern crate alloc; +#[cfg(feature = "alloc")] +use alloc::{string::String, vec, vec::Vec}; + +#[cfg(feature = "alloc")] use bouncycastle_utils::ct::Condition; /// One-shot encode from bytes to a base64-encoded string using a constant-time implementation. +#[cfg(feature = "alloc")] pub fn encode>(input: T) -> String { Base64Encoder::new().do_final(input) } /// One-shot decode from a base64-encoded string to bytes using a constant-time implementation. +#[cfg(feature = "alloc")] pub fn decode>(input: T) -> Result, Base64Error> { Base64Decoder::new(true).do_final(input) } @@ -111,11 +123,13 @@ pub enum Base64Error { } /// The stateful base64 encoder that supports streaming. +#[cfg(feature = "alloc")] pub struct Base64Encoder { buf: [u8; 3], vals_in_buf: usize, } +#[cfg(feature = "alloc")] impl Base64Encoder { /// Create a new instance. pub fn new() -> Self { @@ -188,7 +202,7 @@ impl Base64Encoder { if self.vals_in_buf == 1 { out_buf[2] = b'='; } - out.push_str(std::str::from_utf8(&out_buf).unwrap()); + out.push_str(core::str::from_utf8(&out_buf).unwrap()); } out } @@ -208,12 +222,14 @@ impl Base64Encoder { } /// The stateful base64 decoder that supports streaming. +#[cfg(feature = "alloc")] pub struct Base64Decoder { buf: [u8; 4], vals_in_buf: usize, skip_whitespace: bool, } +#[cfg(feature = "alloc")] impl Base64Decoder { /// Create a new instance. pub fn new(skip_whitespace: bool) -> Self { diff --git a/crypto/core-test-framework/Cargo.toml b/crypto/core-test-framework/Cargo.toml index b4aca48..341c900 100644 --- a/crypto/core-test-framework/Cargo.toml +++ b/crypto/core-test-framework/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.2" edition.workspace = true [dependencies] -bouncycastle-core.workspace = true +# The KAT test framework exercises the `Vec`/`Box`-returning trait APIs, so it always needs `alloc`. +bouncycastle-core = { workspace = true, features = ["alloc"] } [dev-dependencies] \ No newline at end of file diff --git a/crypto/core-test-framework/src/hash.rs b/crypto/core-test-framework/src/hash.rs index 9793232..e04924a 100644 --- a/crypto/core-test-framework/src/hash.rs +++ b/crypto/core-test-framework/src/hash.rs @@ -36,6 +36,20 @@ impl TestFrameworkHash { H::default().hash_out(input, &mut output_buf); assert_eq!(output_buf, expected_output); + /*** fn hash_array(self, data: &[u8]) -> [u8; N] (no_std alternative) ***/ + // Use N = 64, the maximum output length across all hashes; hash_array zero-pads the tail + // beyond output_len, so the digest lands in the first OUTPUT_LEN bytes. + let arr: [u8; 64] = H::default().hash_array(input); + assert_eq!(&arr[..H::OUTPUT_LEN], expected_output, "hash_array digest mismatch"); + assert!(arr[H::OUTPUT_LEN..].iter().all(|&b| b == 0), "hash_array tail not zero-padded"); + + /*** fn do_final_array(self) -> [u8; N] (no_std alternative) ***/ + let mut message_digest = H::default(); + message_digest.do_update(input); + let arr: [u8; 64] = message_digest.do_final_array(); + assert_eq!(&arr[..H::OUTPUT_LEN], expected_output, "do_final_array digest mismatch"); + assert!(arr[H::OUTPUT_LEN..].iter().all(|&b| b == 0), "do_final_array tail not zero-padded"); + /*** fn do_update(&mut self, data: &[u8]) -> Result<(), HashError> ***/ /*** fn do_final(self) -> Result, HashError> **/ diff --git a/crypto/core-test-framework/src/mac.rs b/crypto/core-test-framework/src/mac.rs index 5a171a5..09dccb4 100644 --- a/crypto/core-test-framework/src/mac.rs +++ b/crypto/core-test-framework/src/mac.rs @@ -64,6 +64,22 @@ impl TestFrameworkMAC { // Test .output_len() assert_eq!(output_len, out.len()); + // Test ::mac_array() and ::do_final_array() (no_std alternatives). + // N = 64 is >= every supported MAC output length (and >= the FIPS minimum), so the tag lands + // in the first output_len bytes with a zero-padded tail. + let arr: [u8; 64] = M::new_allow_weak_key(key).unwrap().mac_array(input).unwrap(); + assert_eq!(&arr[..expected_output.len()], expected_output, "mac_array digest mismatch"); + assert!(arr[expected_output.len()..].iter().all(|&b| b == 0), "mac_array tail not zero-padded"); + + let mut mac = M::new_allow_weak_key(key).unwrap(); + mac.do_update(input); + let arr: [u8; 64] = mac.do_final_array().unwrap(); + assert_eq!(&arr[..expected_output.len()], expected_output, "do_final_array digest mismatch"); + assert!( + arr[expected_output.len()..].iter().all(|&b| b == 0), + "do_final_array tail not zero-padded" + ); + // Test .init(), .do_update(), .do_mac_final_out() let mut mac = M::new_allow_weak_key(key).unwrap(); mac.do_update(input); diff --git a/crypto/core/Cargo.toml b/crypto/core/Cargo.toml index 784d3fe..9109510 100644 --- a/crypto/core/Cargo.toml +++ b/crypto/core/Cargo.toml @@ -4,11 +4,11 @@ version = "0.1.2" edition.workspace = true [features] -# `std` gates the ergonomic, allocating (`Vec`-returning) one-shot APIs. It is on by -# default today; a future `--no-default-features` build is what will let `core` become -# `#![no_std]` (see the TODO at the top of `src/lib.rs`). -default = ["std"] -std = [] +# `alloc` pulls in the `Vec`/`Box`-returning convenience APIs. It is on by default so that +# regular (std) builds are unaffected. `no_std` users who cannot allocate should build with +# `default-features = false` and use the `*_out(&mut [u8])` / `*_array::()` APIs instead. +default = ["alloc"] +alloc = [] [dependencies] bouncycastle-utils.workspace = true diff --git a/crypto/core/src/lib.rs b/crypto/core/src/lib.rs index a75792d..1f8482f 100644 --- a/crypto/core/src/lib.rs +++ b/crypto/core/src/lib.rs @@ -1,11 +1,15 @@ //! This crate defines the core traits and types used by the rest of the bc-rust.test library. -// todo -- this is the goal, but first need to remove all the Vec in favour of compile-time array sizing. -// #![no_std] - +#![cfg_attr(not(test), no_std)] #![forbid(unsafe_code)] #![forbid(missing_docs)] +// The `Vec`/`Box`-returning convenience APIs live behind the (default-on) `alloc` feature. +// When it is enabled we pull in the `alloc` crate; `no_std` users who disable it get the +// allocation-free `*_out(&mut [u8])` and `*_array::()` APIs only. +#[cfg(feature = "alloc")] +extern crate alloc; + pub mod errors; pub mod key_material; pub mod suspendable_state; diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index 4086dd1..3a1e52d 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -5,6 +5,14 @@ use crate::key_material::KeyMaterialTrait; use core::fmt::{Debug, Display}; use core::marker::Sized; +// `Vec`/`Box` come from the `alloc` crate under `#![no_std]` and are gated behind the (default-on) +// `alloc` feature. When the feature is disabled, use the allocation-free alternatives: the +// `*_out(&mut [u8])` fill methods, or the `*_array::()` methods that return a fixed-size array. +#[cfg(feature = "alloc")] +use alloc::boxed::Box; +#[cfg(feature = "alloc")] +use alloc::vec::Vec; + // Imports needed for docs #[allow(unused_imports)] use crate::key_material::KeyMaterial; @@ -37,11 +45,11 @@ pub trait AlgorithmOID { /// 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 { - #[cfg(feature = "std")] + #[cfg(feature = "alloc")] /// 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. + /// This function returns the ciphertext as a Vec, and therefore is only available when the `alloc` feature is enabled. /// Returns a tuple containing the initialization data and the ciphertext. - /// This is not available if building for no_std. + /// This is not available in a `no_std` build without the `alloc` feature. fn encrypt( key: &KeyMaterial, plaintext: &[u8], @@ -57,10 +65,10 @@ pub trait SymmetricCipher: Alg plaintext: &[u8], ciphertext: &mut [u8], ) -> Result<([u8; INIT_DATA_LEN], usize), SymmetricCipherError>; - #[cfg(feature = "std")] + #[cfg(feature = "alloc")] /// A one-shot API to decrypt some ciphertext with the given key. - /// This function returns the ciphertext as a Vec, and therefore is only available when compiling with std. - /// This is not available if building for no_std. + /// This function returns the ciphertext as a Vec, and therefore is only available when the `alloc` feature is enabled. + /// This is not available in a `no_std` build without the `alloc` feature. fn decrypt( key: &KeyMaterial, init_data: [u8; INIT_DATA_LEN], @@ -153,12 +161,12 @@ pub trait BlockCipher: SymmetricCipher + Sized { - #[cfg(feature = "std")] + #[cfg(feature = "alloc")] /// A one-shot API to encrypt some plaintext with the given key. /// A distinguishing feature of AEAD ciphers is the ability to provide additional authenticated data (AAD) /// that is not encrypted but is protected by the authentication tag; ie it can be sent along with the ciphertext /// and any tampering with it will result in the decryption operation failing the tag check. - /// This function returns the ciphertext as a Vec, and therefore is only available when compiling with std. + /// This function returns the ciphertext as a Vec, and therefore is only available when the `alloc` feature is enabled. /// Returns a tuple containing a generated nonce, the ciphertext and the tag. fn aead_encrypt( key: &KeyMaterial, @@ -183,9 +191,9 @@ pub trait AEADCipher Result<[u8; TAG_LEN], SymmetricCipherError>; - #[cfg(feature = "std")] + #[cfg(feature = "alloc")] /// A one-shot API to decrypt some ciphertext with the given key. - /// This function returns the ciphertext as a Vec, and therefore is only available when compiling with std. + /// This function returns the ciphertext as a Vec, and therefore is only available when the `alloc` feature is enabled. fn aead_decrypt( key: &KeyMaterial, nonce: &[u8; NONCE_LEN], @@ -280,8 +288,23 @@ pub trait Hash: Algorithm + Default { /// A static one-shot API that hashes the provided data. /// `data` can be of any length, including zero bytes. + #[cfg(feature = "alloc")] fn hash(self, data: &[u8]) -> Vec; + /// A static one-shot, `no_std`-friendly API that hashes the provided data and returns the digest + /// in a caller-sized array. This is the allocation-free counterpart to [Hash::hash]. + /// + /// `N` should equal [Hash::output_len]; the same truncation / zero-padding rules as + /// [Hash::hash_out] apply if `N` differs from the output length. + fn hash_array(self, data: &[u8]) -> [u8; N] + where + Self: Sized, + { + let mut output = [0u8; N]; + let _ = self.hash_out(data, &mut output); + output + } + /// A static one-shot API that hashes the provided data into the provided output slice. /// `data` can be of any length, including zero bytes. /// The entire output buffer is zeroized before the hash output is written. @@ -295,8 +318,24 @@ pub trait Hash: Algorithm + Default { /// Finish absorbing input and produce the hashes output. /// Consumes self, so this must be the final call to this object. + #[cfg(feature = "alloc")] fn do_final(self) -> Vec; + /// Finish absorbing input and produce the hashes output in a caller-sized array. + /// This is the allocation-free, `no_std`-friendly counterpart to [Hash::do_final]. + /// Consumes self, so this must be the final call to this object. + /// + /// `N` should equal [Hash::output_len]; the same truncation / zero-padding rules as + /// [Hash::do_final_out] apply if `N` differs from the output length. + fn do_final_array(self) -> [u8; N] + where + Self: Sized, + { + let mut output = [0u8; N]; + let _ = self.do_final_out(&mut output); + output + } + /// Finish absorbing input and produce the hashes output. /// Consumes self, so this must be the final call to this object. /// @@ -311,12 +350,31 @@ pub trait Hash: Algorithm + Default { /// The same as [Hash::do_final], but allows for supplying a partial byte as the last input. /// Assumes that the input is in the least significant bits (big endian). + #[cfg(feature = "alloc")] fn do_final_partial_bits( self, partial_byte: u8, num_partial_bits: usize, ) -> Result, HashError>; + /// The same as [Hash::do_final_partial_bits], but returns the output in a caller-sized array. + /// This is the allocation-free, `no_std`-friendly counterpart. + /// + /// `N` should equal [Hash::output_len]; the same truncation / zero-padding rules as + /// [Hash::do_final_partial_bits_out] apply if `N` differs from the output length. + fn do_final_partial_bits_array( + self, + partial_byte: u8, + num_partial_bits: usize, + ) -> Result<[u8; N], HashError> + where + Self: Sized, + { + let mut output = [0u8; N]; + self.do_final_partial_bits_out(partial_byte, num_partial_bits, &mut output)?; + Ok(output) + } + /// The same as [Hash::do_final_out], but allows for supplying a partial byte as the last input. /// Assumes that the input is in the least significant bits (big endian). /// will be placed in the first [Hash::output_len] bytes. @@ -376,6 +434,9 @@ pub trait KDF: Default { /// /// Output length: this function will create a KeyMaterial populated with the default output length /// of the underlying hash primitive. + /// + /// `no_std` alternative: use [KDF::derive_key_out] to fill a caller-provided [KeyMaterial]. + #[cfg(feature = "alloc")] fn derive_key( self, key: &impl KeyMaterialTrait, @@ -416,6 +477,9 @@ pub trait KDF: Default { /// /// Output length: this function will create a KeyMaterial populated with the default output length /// of the underlying hash primitive. + /// + /// `no_std` alternative: use [KDF::derive_key_from_multiple_out] to fill a caller-provided [KeyMaterial]. + #[cfg(feature = "alloc")] fn derive_key_from_multiple( self, keys: &[&impl KeyMaterialTrait], @@ -591,8 +655,23 @@ pub trait MAC: Sized { /// ```text /// MACError::KeyMaterialError(KeyMaterialError::SecurityStrength("HMAC::init(): provided key has a lower security strength than the instantiated HMAC") /// ``` + #[cfg(feature = "alloc")] fn mac(self, data: &[u8]) -> Vec; + /// One-shot, `no_std`-friendly API that computes a MAC for the provided data and returns it in a + /// caller-sized array. This is the allocation-free counterpart to [MAC::mac]. + /// + /// `N` should equal [MAC::output_len]; the same rules as [MAC::mac_out] apply (including the + /// possible [MACError::InvalidLength] for undersized buffers). + fn mac_array(self, data: &[u8]) -> Result<[u8; N], MACError> + where + Self: Sized, + { + let mut out = [0u8; N]; + self.mac_out(data, &mut out)?; + Ok(out) + } + /// One-shot API that computes a MAC for the provided data and writes it into the provided output slice. /// `data` can be of any length, including zero bytes. /// @@ -622,8 +701,23 @@ pub trait MAC: Sized { fn do_update(&mut self, data: &[u8]); /// Finish absorbing input and produce the MAC value. + #[cfg(feature = "alloc")] fn do_final(self) -> Vec; + /// The allocation-free, `no_std`-friendly counterpart to [MAC::do_final]: returns the MAC value + /// in a caller-sized array. Consumes self, so this must be the final call to this object. + /// + /// `N` should equal [MAC::output_len]; the same rules as [MAC::do_final_out] apply (including the + /// possible [MACError::InvalidLength] for undersized buffers). + fn do_final_array(self) -> Result<[u8; N], MACError> + where + Self: Sized, + { + let mut out = [0u8; N]; + self.do_final_out(&mut out)?; + Ok(out) + } + /// Depending on the underlying MAC implementation, NIST may require that the library enforce /// a minimum length on the mac output value. See documentation for the underlying implementation /// to see conditions under which it throws [MACError::InvalidLength]. @@ -751,8 +845,23 @@ pub trait RNG { fn next_int(&mut self) -> Result; /// Returns the number of requested bytes. + #[cfg(feature = "alloc")] fn next_bytes(&mut self, len: usize) -> Result, RNGError>; + /// The allocation-free, `no_std`-friendly counterpart to [RNG::next_bytes]: returns `N` random + /// bytes in a fixed-size array. + /// + /// Bounded `where Self: Sized` so that adding this generic method does not make [RNG] + /// dyn-incompatible: `&mut dyn RNG` is used widely (e.g. [KEMEncapsulator::encaps_rng]). + fn next_bytes_array(&mut self) -> Result<[u8; N], RNGError> + where + Self: Sized, + { + let mut out = [0u8; N]; + self.next_bytes_out(&mut out)?; + Ok(out) + } + /// Returns the number of bytes written. /// The entire output buffer is zeroized before the random bytes are written. fn next_bytes_out(&mut self, out: &mut [u8]) -> Result; @@ -1058,8 +1167,20 @@ pub trait SignatureVerifier< /// distinguishing attacks should consider adding a cryptographic salt to diversify the inputs. pub trait XOF: Default { /// A static one-shot API that digests the input data and produces `result_len` bytes of output. + #[cfg(feature = "alloc")] fn hash_xof(self, data: &[u8], result_len: usize) -> Vec; + /// The allocation-free, `no_std`-friendly counterpart to [XOF::hash_xof]: digests the input data + /// and produces exactly `N` bytes of output in a fixed-size array. + fn hash_xof_array(self, data: &[u8]) -> [u8; N] + where + Self: Sized, + { + let mut output = [0u8; N]; + let _ = self.hash_xof_out(data, &mut output); + output + } + /// A static one-shot API that digests the input data and produces `result_len` bytes of output. /// Fills the provided output slice. /// The entire output buffer is zeroized before the output is written. @@ -1076,8 +1197,20 @@ pub trait XOF: Default { ) -> Result<(), HashError>; /// Can be called multiple times. + #[cfg(feature = "alloc")] fn squeeze(&mut self, num_bytes: usize) -> Vec; + /// The allocation-free, `no_std`-friendly counterpart to [XOF::squeeze]: squeezes exactly `N` + /// bytes into a fixed-size array. Can be called multiple times. + fn squeeze_array(&mut self) -> [u8; N] + where + Self: Sized, + { + let mut output = [0u8; N]; + let _ = self.squeeze_out(&mut output); + output + } + /// Can be called multiple times. /// Fills the provided output slice. /// The entire output buffer is zeroized before the output is written. diff --git a/crypto/factory/Cargo.toml b/crypto/factory/Cargo.toml index 0f31c93..64b0427 100644 --- a/crypto/factory/Cargo.toml +++ b/crypto/factory/Cargo.toml @@ -3,13 +3,18 @@ name = "bouncycastle-factory" version = "0.1.2" edition.workspace = true +# The factory is a convenience aggregation layer (and includes RNGFactory, which wraps the std-only +# `rng` crate), so it always requires `alloc`: it enables the `alloc` feature on every internal +# dependency unconditionally. Users needing `no_std` should depend on the individual primitive crates +# directly with `default-features = false`. Note the factory types still expose the allocation-free +# `*_array::()` / `*_out(&mut [u8])` APIs via the core trait default methods. [dependencies] -bouncycastle-core.workspace = true -bouncycastle-hkdf.workspace = true -bouncycastle-hmac.workspace = true -bouncycastle-sha2.workspace = true -bouncycastle-sha3.workspace = true -bouncycastle-rng.workspace = true +bouncycastle-core = { workspace = true, features = ["alloc"] } +bouncycastle-hkdf = { workspace = true, features = ["alloc"] } +bouncycastle-hmac = { workspace = true, features = ["alloc"] } +bouncycastle-sha2 = { workspace = true, features = ["alloc"] } +bouncycastle-sha3 = { workspace = true, features = ["alloc"] } +bouncycastle-rng = { workspace = true, features = ["alloc"] } [dev-dependencies] bouncycastle-core-test-framework.workspace = true diff --git a/crypto/hex/Cargo.toml b/crypto/hex/Cargo.toml index 8d88707..0723852 100644 --- a/crypto/hex/Cargo.toml +++ b/crypto/hex/Cargo.toml @@ -3,6 +3,12 @@ name = "bouncycastle-hex" version = "0.1.2" edition.workspace = true +[features] +# `alloc` enables the `String`-returning `encode` and `Vec`-returning `decode`. The allocation-free +# `encode_out`/`decode_out` (fill a caller `&mut [u8]`) work in `no_std` without it. On by default. +default = ["alloc"] +alloc = [] + [dependencies] bouncycastle-utils.workspace = true diff --git a/crypto/hex/src/lib.rs b/crypto/hex/src/lib.rs index 4e89caa..4ef2eef 100644 --- a/crypto/hex/src/lib.rs +++ b/crypto/hex/src/lib.rs @@ -20,9 +20,17 @@ //! //! The decoder ignores whitespace and "\x". +#![cfg_attr(not(test), no_std)] #![forbid(unsafe_code)] #![forbid(missing_docs)] +// `encode` (returns `String`) and `decode` (returns `Vec`) live behind the default-on `alloc` +// feature. `no_std` users should use `encode_out` / `decode_out`, which fill a caller `&mut [u8]`. +#[cfg(feature = "alloc")] +extern crate alloc; +#[cfg(feature = "alloc")] +use alloc::{string::String, vec, vec::Vec}; + use bouncycastle_utils::ct::Condition; /// Return type for errors relating to Hex encoding and decoding. @@ -37,6 +45,9 @@ pub enum HexError { } /// One-shot encode from bytes to a hex-encoded string using a constant-time implementation. +/// +/// `no_std` alternative: [encode_out], which writes into a caller-provided `&mut [u8]`. +#[cfg(feature = "alloc")] pub fn encode>(input: T) -> String { let mut out = vec![0u8; input.as_ref().len() * 2]; encode_out(input.as_ref(), &mut out).unwrap(); @@ -81,6 +92,9 @@ pub fn encode_out>(input: T, out: &mut [u8]) -> Result>(input: T) -> Result, HexError> { let inref = input.as_ref(); let mut out: Vec = vec![0u8; inref.len() / 2]; diff --git a/crypto/hkdf/Cargo.toml b/crypto/hkdf/Cargo.toml index 1d8ca45..11ae95b 100644 --- a/crypto/hkdf/Cargo.toml +++ b/crypto/hkdf/Cargo.toml @@ -3,6 +3,17 @@ name = "bouncycastle-hkdf" version = "0.1.2" edition.workspace = true +[features] +# `alloc` enables the `Box`-returning `derive_key`/`derive_key_from_multiple`. +# Forwarded to every internal dependency to keep the `alloc` gates consistent across the graph. +# On by default; disable with `default-features = false` for `no_std` use. +default = ["alloc"] +alloc = [ + "bouncycastle-core/alloc", + "bouncycastle-hmac/alloc", + "bouncycastle-sha2/alloc", +] + [dependencies] bouncycastle-core.workspace = true bouncycastle-hmac.workspace = true diff --git a/crypto/hkdf/src/lib.rs b/crypto/hkdf/src/lib.rs index 30f8598..21bb976 100644 --- a/crypto/hkdf/src/lib.rs +++ b/crypto/hkdf/src/lib.rs @@ -190,14 +190,22 @@ //! let _prk = hkdf.do_extract_final().unwrap(); //! ``` +#![cfg_attr(not(test), no_std)] #![forbid(unsafe_code)] #![forbid(missing_docs)] +// The `Box`-returning `derive_key` / `derive_key_from_multiple` live behind the +// default-on `alloc` feature. `no_std` users should use the `derive_key_out` / `derive_key_from_multiple_out` +// APIs that fill a caller-provided `KeyMaterial` instead. +#[cfg(feature = "alloc")] +extern crate alloc; + use bouncycastle_core::errors::{KDFError, KeyMaterialError, MACError, SuspendableError}; use bouncycastle_core::key_material; -use bouncycastle_core::key_material::{ - KeyMaterial, KeyMaterial0, KeyMaterial512, KeyMaterialTrait, KeyType, -}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterial0, KeyMaterialTrait, KeyType}; +// Only used by the alloc-gated `derive_key` / `derive_key_from_multiple`. +#[cfg(feature = "alloc")] +use bouncycastle_core::key_material::KeyMaterial512; use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; use bouncycastle_core::traits::{ Hash, HashAlgParams, KDF, MAC, SecurityStrength, SuspendableKeyed, @@ -205,7 +213,10 @@ use bouncycastle_core::traits::{ use bouncycastle_hmac::{HMAC, SUSPENDED_HMAC_SHA256_STATE_LEN, SUSPENDED_HMAC_SHA512_STATE_LEN}; use bouncycastle_sha2::{SHA256, SHA512}; use bouncycastle_utils::{max, min}; -use std::marker::PhantomData; +use core::marker::PhantomData; + +#[cfg(feature = "alloc")] +use alloc::boxed::Box; // Imports needed only for docs #[allow(unused_imports)] use bouncycastle_core::traits::XOF; @@ -708,6 +719,7 @@ impl HKDF { impl KDF for HKDF { /// This invokes [HKDF::extract_and_expand_out] with a zero salt and using the provided key as ikm. /// This provides a fixed-length output, which may be truncated as needed. + #[cfg(feature = "alloc")] fn derive_key( self, key: &impl KeyMaterialTrait, @@ -746,6 +758,7 @@ impl KDF for HKDF { /// Therefore, derive_key_from_multiple(&[KeyMaterial0::new(), &key], &info) is equivalent to derive_key(&key, &info). /// /// This provides a fixed-length output, which may be truncated as needed. + #[cfg(feature = "alloc")] fn derive_key_from_multiple( self, keys: &[&impl KeyMaterialTrait], diff --git a/crypto/hmac/Cargo.toml b/crypto/hmac/Cargo.toml index f1e4377..ed4b7e2 100644 --- a/crypto/hmac/Cargo.toml +++ b/crypto/hmac/Cargo.toml @@ -3,6 +3,20 @@ name = "bouncycastle-hmac" version = "0.1.2" edition.workspace = true +[features] +# `alloc` enables the `Vec`-returning convenience APIs (`mac`, `do_final`) and turns on the same +# feature in bouncycastle-core. The allocation-free streaming / `_out` / `_array::()` APIs work +# without it. On by default; disable with `default-features = false` for `no_std` use. +default = ["alloc"] +# Forward `alloc` to every internal dependency so that the `alloc` gates stay consistent across the +# dependency graph (otherwise a dep's default features could turn on `core/alloc` while hmac's own +# `alloc` is off, leaving the gated trait method unimplemented). +alloc = [ + "bouncycastle-core/alloc", + "bouncycastle-sha2/alloc", + "bouncycastle-sha3/alloc", +] + [dependencies] bouncycastle-core.workspace = true bouncycastle-rng.workspace = true diff --git a/crypto/hmac/src/lib.rs b/crypto/hmac/src/lib.rs index 9ee4076..e377bb1 100644 --- a/crypto/hmac/src/lib.rs +++ b/crypto/hmac/src/lib.rs @@ -180,9 +180,16 @@ //! let h: Vec = hmac_resumed.do_final(); //! ``` +#![cfg_attr(not(test), no_std)] #![forbid(unsafe_code)] #![forbid(missing_docs)] +// The `Vec`-returning convenience APIs (`mac`, `do_final`) live behind the default-on `alloc` +// feature. The streaming API and the `mac_out`/`do_final_out`/`mac_array::()` APIs are all +// allocation-free and work in `no_std` without it. +#[cfg(feature = "alloc")] +extern crate alloc; + use bouncycastle_core::errors::{KeyMaterialError, MACError, RNGError, SuspendableError}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::traits::{ @@ -196,6 +203,9 @@ use bouncycastle_sha3::{SHA3_224, SHA3_256, SHA3_384, SHA3_512, SUSPENDED_SHA3_S use bouncycastle_utils::{ct, secret::Secret}; use core::fmt::{Debug, Display, Formatter}; +#[cfg(feature = "alloc")] +use alloc::{vec, vec::Vec}; + /*** String constants ***/ /// pub const HMAC_SHA224_NAME: &str = "HMAC-SHA224"; @@ -333,6 +343,10 @@ impl AlgorithmOID for HMAC_SHA3_512 { // largest block length across all supported hashes, so it is always large enough. const LARGEST_HASHER_BLOCK_LEN: usize = 144; +// The largest output length across all supported hashes (SHA-512 / SHA3-512 = 64 bytes). Used to +// size allocation-free stack scratch buffers so that the streaming / `_out` APIs work in `no_std`. +const LARGEST_HASHER_OUTPUT_LEN: usize = 64; + /// Internal struct for HKDF. /// HMAC implements RFC 2104. /// Can, in theory, be instantiated with hash functions other than the ones provided by this crate (even custom ones). @@ -346,13 +360,13 @@ pub struct HMAC Debug for HMAC { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { write!(f, "HMAC-{} instance", HASH::ALG_NAME,) } } impl Display for HMAC { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { write!(f, "HMAC-{} instance", HASH::ALG_NAME,) } } @@ -375,20 +389,24 @@ pub const MIN_FIPS_DIGEST_LEN: usize = 4; impl HMAC { fn pad_key_into_hasher(&mut self, padding: u8) { - // TODO: it would be nice to be able to statically extract the length of HASH and not need a Vec or over-sized array here. - // TODO: make this no_std-friendly - let mut padded = vec![0u8; self.hasher.block_bitlen() / 8]; + // Allocation-free scratch: the padded key block is exactly `block_len` bytes (`block_len` is + // never larger than [LARGEST_HASHER_BLOCK_LEN]), so use an oversized stack buffer sliced down + // to `block_len`. This keeps the streaming API `no_std`-friendly (no `Vec`). + let block_len = self.hasher.block_bitlen() / 8; + let mut padded_buf = [0u8; LARGEST_HASHER_BLOCK_LEN]; + let padded = &mut padded_buf[..block_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 { + // Per RFC 2104 Section 2, the key is zero-padded to the block length and then XORed with the + // pad byte over the entire block. + for entry in padded.iter_mut() { *entry ^= padding; } // Per RFC 2104 Section 2, write the padded key into the stream prior // to any other data. - self.hasher.do_update(&padded) + self.hasher.do_update(padded) } /// Per RFC 2104 Section 2, if the application key exceeds the block @@ -459,16 +477,19 @@ impl HMAC { // scratch pad here: if we're truncating the output but not // truncating the underlying hashes, we'd lose bytes and compute an // invalid outer hashes. - // TODO: rework this to be no_std friendly (ie no vec!) - let mut ihash = vec![0u8; self.hasher.output_len()]; + // Allocation-free scratch: the inner digest is exactly `output_len` bytes (never larger than + // [LARGEST_HASHER_OUTPUT_LEN]), so use an oversized stack buffer sliced down to `output_len`. + let out_len = self.hasher.output_len(); + let mut ihash_buf = [0u8; LARGEST_HASHER_OUTPUT_LEN]; + let ihash = &mut ihash_buf[..out_len]; // `HMAC` implements `Drop` (required by `Secret`), so we cannot move `self.hasher` out // directly. Swap in a fresh default and consume the taken-out hasher instead. - core::mem::take(&mut self.hasher).do_final_out(&mut ihash); + core::mem::take(&mut self.hasher).do_final_out(ihash); // ohash self.hasher = HASH::default(); self.pad_key_into_hasher(OPAD_BYTE); - self.hasher.do_update(&ihash); + self.hasher.do_update(ihash); Ok(core::mem::take(&mut self.hasher).do_final_out(out)) } } @@ -495,6 +516,7 @@ impl MAC for HMAC Vec { let mut out = vec![0_u8; self.hasher.output_len()]; let bytes_written = self.mac_out(data, &mut out).expect("HMAC::mac(): should not have failed because we gave it a sufficiently large output buffer to meet FIPS rules."); @@ -517,6 +539,7 @@ impl MAC for HMAC Vec { let mut out = vec![0_u8; self.hasher.output_len()]; self.do_final_internal_out(&mut out).expect("HMAC::do_final(): should not have failed because we gave it a sufficiently large output buffer to meet FIPS rules."); @@ -530,8 +553,10 @@ impl MAC for HMAC bool { - let mut out = vec![0_u8; HASH::default().output_len()]; - let output_len = self.do_final_internal_out(&mut out).expect("HMAC::do_final(): should not have failed because we gave it a sufficiently large output buffer to meet FIPS rules."); + // Allocation-free scratch buffer (see [LARGEST_HASHER_OUTPUT_LEN]). + let mut out_buf = [0_u8; LARGEST_HASHER_OUTPUT_LEN]; + let out = &mut out_buf[..HASH::default().output_len()]; + let output_len = self.do_final_internal_out(out).expect("HMAC::do_final(): should not have failed because we gave it a sufficiently large output buffer to meet FIPS rules."); if mac.len() != output_len { return false; } diff --git a/crypto/rng/Cargo.toml b/crypto/rng/Cargo.toml index 69d3a6e..124c0fb 100644 --- a/crypto/rng/Cargo.toml +++ b/crypto/rng/Cargo.toml @@ -3,6 +3,14 @@ name = "bouncycastle-rng" version = "0.1.2" edition.workspace = true +[features] +# `alloc` enables the `Vec`-returning `next_bytes` / `generate` APIs. Kept consistent with the +# (gated) `RNG::next_bytes` trait method. Note this crate is not `no_std` (it needs OS entropy via +# `getrandom`); the feature only toggles the `Vec`-returning convenience methods. On by default. +default = ["alloc"] +# Forward to every internal dep so the `alloc` gates stay consistent across the graph. +alloc = ["bouncycastle-core/alloc", "bouncycastle-sha2/alloc"] + [dependencies] bouncycastle-core.workspace = true bouncycastle-sha2.workspace = true diff --git a/crypto/rng/src/hash_drbg80090a.rs b/crypto/rng/src/hash_drbg80090a.rs index 6efc8ab..57bca06 100644 --- a/crypto/rng/src/hash_drbg80090a.rs +++ b/crypto/rng/src/hash_drbg80090a.rs @@ -340,6 +340,7 @@ impl Sp80090ADrbg for HashDRBG80090A { Ok(()) } + #[cfg(feature = "alloc")] fn generate(&mut self, additional_input: &[u8], len: usize) -> Result, RNGError> { let mut out = vec![0u8; len]; self.generate_out(additional_input, &mut out)?; @@ -503,6 +504,7 @@ impl RNG for HashDRBG80090A { Ok(u32::from_le_bytes(out)) } + #[cfg(feature = "alloc")] fn next_bytes(&mut self, len: usize) -> Result, RNGError> { self.generate("next_bytes".as_bytes(), len) } diff --git a/crypto/rng/src/lib.rs b/crypto/rng/src/lib.rs index 1f36c5d..8c7b7e1 100644 --- a/crypto/rng/src/lib.rs +++ b/crypto/rng/src/lib.rs @@ -121,6 +121,9 @@ pub trait Sp80090ADrbg { /// not pass FIPS certification. /// /// Throws a [RNGError::InsufficientSeedEntropy] if `len` exceeds [SecurityStrength]. + /// + /// `no_std` / no-alloc alternative: [Sp80090ADrbg::generate_out]. + #[cfg(feature = "alloc")] fn generate(&mut self, additional_input: &[u8], len: usize) -> Result, RNGError>; /// As per [Sp80090ADrbg::generate], but writes to the provided output slice. diff --git a/crypto/sha2/Cargo.toml b/crypto/sha2/Cargo.toml index 6de8662..7513024 100644 --- a/crypto/sha2/Cargo.toml +++ b/crypto/sha2/Cargo.toml @@ -3,6 +3,12 @@ name = "bouncycastle-sha2" version = "0.1.2" edition.workspace = true +[features] +# `alloc` enables the `Vec`-returning convenience APIs (and turns on the same feature in +# bouncycastle-core). On by default; disable with `default-features = false` for `no_std` use. +default = ["alloc"] +alloc = ["bouncycastle-core/alloc"] + [dependencies] bouncycastle-core.workspace = true bouncycastle-utils.workspace = true diff --git a/crypto/sha2/src/lib.rs b/crypto/sha2/src/lib.rs index 4d00a77..ad7f2a1 100644 --- a/crypto/sha2/src/lib.rs +++ b/crypto/sha2/src/lib.rs @@ -65,10 +65,17 @@ //! let h: Vec = sha2_resumed.do_final(); //! ``` +#![cfg_attr(not(test), no_std)] #![forbid(unsafe_code)] #![forbid(missing_docs)] #![allow(private_bounds)] +// The `Vec`-returning convenience APIs (`hash`, `do_final`, ...) live behind the default-on `alloc` +// feature. `no_std` users without an allocator should use the `*_out(&mut [u8])` / `*_array::()` +// APIs from the [bouncycastle_core::traits::Hash] trait instead. +#[cfg(feature = "alloc")] +extern crate alloc; + mod sha256; mod sha512; diff --git a/crypto/sha2/src/sha256.rs b/crypto/sha2/src/sha256.rs index 68580a8..b17523e 100644 --- a/crypto/sha2/src/sha256.rs +++ b/crypto/sha2/src/sha256.rs @@ -5,6 +5,9 @@ use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, Suspendable}; use bouncycastle_utils::{min, secret::Secret}; use core::slice; +#[cfg(feature = "alloc")] +use alloc::{vec, vec::Vec}; + const SHA256_K: [u32; 64] = [ 0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5, 0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174, @@ -68,7 +71,7 @@ impl Sha256State { 0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19, ]); - Self { _params: std::marker::PhantomData, h } + Self { _params: core::marker::PhantomData, h } } _ => panic!("Invalid SHA-2 bit size: {}", PARAMS::OUTPUT_LEN), } @@ -187,6 +190,7 @@ impl Hash for SHA256Internal { PARAMS::OUTPUT_LEN } + #[cfg(feature = "alloc")] fn hash(self, data: &[u8]) -> Vec { let mut output = vec![0u8; PARAMS::OUTPUT_LEN]; self.hash_out(data, &mut output); @@ -233,6 +237,7 @@ impl Hash for SHA256Internal { self.x_buf_off = remaining; } + #[cfg(feature = "alloc")] fn do_final(self) -> Vec { let mut output = vec![0u8; PARAMS::OUTPUT_LEN]; self.do_final_out(&mut output); @@ -276,6 +281,7 @@ impl Hash for SHA256Internal { /// TODO: This is defined in FIPS 180-4 s. 5.1.2 /// TODO: /// TODO: Could implement if there is demand. + #[cfg(feature = "alloc")] #[allow(unused)] fn do_final_partial_bits( self, diff --git a/crypto/sha2/src/sha512.rs b/crypto/sha2/src/sha512.rs index e730cf0..b989396 100644 --- a/crypto/sha2/src/sha512.rs +++ b/crypto/sha2/src/sha512.rs @@ -5,6 +5,9 @@ use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, Suspendable}; use bouncycastle_utils::{min, secret::Secret}; use core::slice; +#[cfg(feature = "alloc")] +use alloc::{vec, vec::Vec}; + const SHA512_K: [u64; 80] = [ 0x428A2F98D728AE22, 0x7137449123EF65CD, 0xB5C0FBCFEC4D3B2F, 0xE9B5DBA58189DBBC, 0x3956C25BF348B538, 0x59F111F1B605D019, 0x923F82A4AF194F9B, 0xAB1C5ED5DA6D8118, @@ -62,7 +65,7 @@ fn theta1(x: u64) -> u64 { // #[derive(Clone, Copy)] #[derive(Clone)] pub(crate) struct Sha512State { - _params: std::marker::PhantomData, + _params: core::marker::PhantomData, h: Secret<[u64; 8]>, } @@ -75,14 +78,14 @@ impl Sha512State { 0xCBBB9D5DC1059ED8, 0x629A292A367CD507, 0x9159015A3070DD17, 0x152FECD8F70E5939, 0x67332667FFC00B31, 0x8EB44A8768581511, 0xDB0C2E0D64F98FA7, 0x47B5481DBEFA4FA4, ]); - Self { _params: std::marker::PhantomData, h } + Self { _params: core::marker::PhantomData, h } } 512 => { h.copy_from_slice(&[ 0x6A09E667F3BCC908, 0xBB67AE8584CAA73B, 0x3C6EF372FE94F82B, 0xA54FF53A5F1D36F1, 0x510E527FADE682D1, 0x9B05688C2B3E6C1F, 0x1F83D9ABFB41BD6B, 0x5BE0CD19137E2179, ]); - Self { _params: std::marker::PhantomData, h } + Self { _params: core::marker::PhantomData, h } } _ => panic!("Invalid SHA-2 bit size"), } @@ -158,7 +161,7 @@ impl Sha512State { /// provided and NIST-approved parameters. #[derive(Clone)] pub struct SHA512Internal { - _params: std::marker::PhantomData, + _params: core::marker::PhantomData, state: Sha512State, byte_count: u64, // NOTE We only support 2^67 bits, not the full 2^128 x_buf: Secret<[u8; 128]>, @@ -169,7 +172,7 @@ impl SHA512Internal { /// Creates a new SHA512 instance, ready for use. pub fn new() -> Self { Self { - _params: std::marker::PhantomData, + _params: core::marker::PhantomData, state: Sha512State::::new(), byte_count: 0, x_buf: Secret::new(), @@ -199,6 +202,7 @@ impl Hash for SHA512Internal { PARAMS::OUTPUT_LEN } + #[cfg(feature = "alloc")] fn hash(self, data: &[u8]) -> Vec { let mut output = vec![0u8; self.output_len()]; self.hash_out(data, &mut output); @@ -244,6 +248,7 @@ impl Hash for SHA512Internal { self.x_buf_off = remaining; } + #[cfg(feature = "alloc")] fn do_final(self) -> Vec { let mut output = vec![0u8; PARAMS::OUTPUT_LEN]; self.do_final_out(&mut output); @@ -288,6 +293,7 @@ impl Hash for SHA512Internal { /// TODO: This is defined in FIPS 180-4 s. 5.1.2 /// TODO: /// TODO: Could implement if there is demand. + #[cfg(feature = "alloc")] #[allow(unused)] fn do_final_partial_bits( self, diff --git a/crypto/sha3/Cargo.toml b/crypto/sha3/Cargo.toml index 0050958..f7b1cfd 100644 --- a/crypto/sha3/Cargo.toml +++ b/crypto/sha3/Cargo.toml @@ -3,6 +3,12 @@ name = "bouncycastle-sha3" version = "0.1.2" edition.workspace = true +[features] +# `alloc` enables the `Vec`/`Box`-returning convenience APIs (and turns on the same feature in +# bouncycastle-core). On by default; disable with `default-features = false` for `no_std` use. +default = ["alloc"] +alloc = ["bouncycastle-core/alloc"] + [dependencies] bouncycastle-core.workspace = true bouncycastle-utils.workspace = true diff --git a/crypto/sha3/src/keccak.rs b/crypto/sha3/src/keccak.rs index dc0f711..f53d668 100644 --- a/crypto/sha3/src/keccak.rs +++ b/crypto/sha3/src/keccak.rs @@ -350,7 +350,7 @@ impl KeccakInternal { /*** State serialization ***/ // -// The SHA3 and SHAKE public objects have identical state: a [KeccakDigest] plus three pieces of +// The SHA3 and SHAKE public objects have identical state: a [KeccakInternal] plus three pieces of // KDF metadata. The helpers below serialize that shared state so the `SerializableState` impls in // `sha3.rs` and `shake.rs` are just thin wrappers that add/check the library version header. diff --git a/crypto/sha3/src/lib.rs b/crypto/sha3/src/lib.rs index 6034fb9..c2fce7d 100644 --- a/crypto/sha3/src/lib.rs +++ b/crypto/sha3/src/lib.rs @@ -139,10 +139,17 @@ //! let h: Vec = sha3_resumed.do_final(); //! ``` +#![cfg_attr(not(test), no_std)] #![forbid(unsafe_code)] #![forbid(missing_docs)] #![allow(private_bounds)] +// The `Vec`/`Box`-returning convenience APIs (`hash`, `do_final`, `hash_xof`, `squeeze`, `derive_key`, +// ...) live behind the default-on `alloc` feature. `no_std` users without an allocator should use the +// `*_out(&mut [u8])` / `*_array::()` APIs instead. +#[cfg(feature = "alloc")] +extern crate alloc; + use crate::keccak::KeccakSize; use bouncycastle_core::traits::{Algorithm, AlgorithmOID, HashAlgParams, SecurityStrength}; diff --git a/crypto/sha3/src/sha3.rs b/crypto/sha3/src/sha3.rs index e683201..2b98dde 100644 --- a/crypto/sha3/src/sha3.rs +++ b/crypto/sha3/src/sha3.rs @@ -5,30 +5,36 @@ use crate::keccak::{ }; use bouncycastle_core::errors::{HashError, KDFError, SuspendableError}; use bouncycastle_core::key_material; -use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::key_material::{KeyMaterialTrait, KeyType}; +// `KeyMaterial` (the concrete type) is only used by the alloc-gated `derive_key_final_internal`. +#[cfg(feature = "alloc")] +use bouncycastle_core::key_material::KeyMaterial; use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; use bouncycastle_core::traits::{Algorithm, Hash, KDF, SecurityStrength, Suspendable}; use bouncycastle_utils::{max, min}; +#[cfg(feature = "alloc")] +use alloc::{boxed::Box, vec, vec::Vec}; + /// Internal struct for SHA3. /// This uses a private bound so that you cannot instantiate it directly and have to use the /// provided and NIST-approved parameters. #[derive(Clone)] pub struct SHA3Internal { - _params: std::marker::PhantomData, + _params: core::marker::PhantomData, keccak: KeccakInternal, kdf_key_type: KeyType, kdf_security_strength: SecurityStrength, kdf_entropy: usize, } -// Note: don't need a zeroizing Drop here because all the sensitive info is in KeccakDigest, which has one. +// Note: don't need a zeroizing Drop here because all the sensitive info is in KeccakInternal, which has one. impl SHA3Internal { /// Get a new SHA3 instance, ready for use. pub fn new() -> Self { Self { - _params: std::marker::PhantomData, + _params: core::marker::PhantomData, keccak: KeccakInternal::new(PARAMS::SIZE), kdf_key_type: KeyType::Zeroized, kdf_security_strength: SecurityStrength::None, @@ -63,6 +69,7 @@ impl SHA3Internal { self.do_update(key.ref_to_bytes()) } + #[cfg(feature = "alloc")] fn derive_key_final_internal( self, additional_input: &[u8], @@ -144,6 +151,7 @@ impl Hash for SHA3Internal { PARAMS::OUTPUT_LEN } + #[cfg(feature = "alloc")] fn hash(self, data: &[u8]) -> Vec { let mut output: Vec = vec![0u8; PARAMS::OUTPUT_LEN]; _ = self.hash_internal(data, &mut output[..]); @@ -160,6 +168,7 @@ impl Hash for SHA3Internal { self.keccak.absorb(data) } + #[cfg(feature = "alloc")] fn do_final(self) -> Vec { let dbg_rslt_len = self.output_len(); let mut output: Vec = vec![0u8; self.output_len()]; @@ -186,6 +195,7 @@ impl Hash for SHA3Internal { bytes_written } + #[cfg(feature = "alloc")] fn do_final_partial_bits( self, partial_byte: u8, @@ -235,6 +245,7 @@ impl KDF for SHA3Internal { /// Returns a [KeyMaterial]. /// For the KDF to be considered "fully-seeded" and be capable of outputting full-entropy KeyMaterials, /// it requires full-entropy input that is at least the bit size (ie 256 bits for SHA3-256, etc). + #[cfg(feature = "alloc")] fn derive_key( mut self, key: &impl KeyMaterialTrait, @@ -255,6 +266,7 @@ impl KDF for SHA3Internal { self.derive_key_out_final_internal(additional_input, output_key) } + #[cfg(feature = "alloc")] fn derive_key_from_multiple( mut self, keys: &[&impl KeyMaterialTrait], @@ -318,7 +330,7 @@ impl Suspendable for SHA3Internal< deserialize_sha3_family_state(input, PARAMS::STATE_TAG, rate)?; Ok(SHA3Internal { - _params: std::marker::PhantomData, + _params: core::marker::PhantomData, keccak, kdf_key_type, kdf_security_strength, diff --git a/crypto/sha3/src/shake.rs b/crypto/sha3/src/shake.rs index 86bbc3a..4f471c0 100644 --- a/crypto/sha3/src/shake.rs +++ b/crypto/sha3/src/shake.rs @@ -1,15 +1,23 @@ use crate::SHAKEParams; use crate::keccak::{ - KeccakInternal, KeccakSize, SHA3_FAMILY_STATE_LEN, SUSPENDED_SHA3_STATE_LEN, - deserialize_sha3_family_state, serialize_sha3_family_state, + KeccakInternal, SHA3_FAMILY_STATE_LEN, SUSPENDED_SHA3_STATE_LEN, deserialize_sha3_family_state, + serialize_sha3_family_state, }; use bouncycastle_core::errors::{HashError, KDFError, SuspendableError}; use bouncycastle_core::key_material; -use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::key_material::{KeyMaterialTrait, KeyType}; +// `KeyMaterial` and `KeccakSize` are only used by the alloc-gated `derive_key_final_internal`. +#[cfg(feature = "alloc")] +use crate::keccak::KeccakSize; +#[cfg(feature = "alloc")] +use bouncycastle_core::key_material::KeyMaterial; use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; use bouncycastle_core::traits::{Algorithm, KDF, SecurityStrength, Suspendable, XOF}; use bouncycastle_utils::{max, min}; +#[cfg(feature = "alloc")] +use alloc::{boxed::Box, vec, vec::Vec}; + /// Internal struct for SHAKE. /// This uses a private bound so that you cannot instantiate it directly and have to use the /// provided and NIST-approved parameters. @@ -38,7 +46,7 @@ pub struct SHAKEInternal { kdf_entropy: usize, } -// Note: don't need a zeroizing Drop here because all the sensitive info is in KeccakDigest, which has one. +// Note: don't need a zeroizing Drop here because all the sensitive info is in KeccakInternal, which has one. impl Algorithm for SHAKEInternal { const ALG_NAME: &'static str = PARAMS::ALG_NAME; @@ -58,6 +66,7 @@ impl SHAKEInternal { } /// Swallows errors and simply returns an empty Vec if the hashes fails for whatever reason. + #[cfg(feature = "alloc")] fn hash_internal(mut self, data: &[u8], result_len: usize) -> Vec { // Infallible: this is the only absorb, and it precedes the squeeze below. self.absorb(data).expect("absorb precedes squeeze on a fresh SHAKE"); @@ -92,6 +101,7 @@ impl SHAKEInternal { self.absorb(key.ref_to_bytes()).expect("absorb precedes squeeze during key mixing"); } + #[cfg(feature = "alloc")] fn derive_key_final_internal( mut self, additional_input: &[u8], @@ -204,6 +214,7 @@ impl KDF for SHAKEInternal { /// To produce longer keys, use [KDF::derive_key_out]. /// To produce shorter keys, either use [KDF::derive_key_out] or truncate this result down with /// [KeyMaterial::set_key_len]. + #[cfg(feature = "alloc")] fn derive_key( mut self, key: &impl KeyMaterialTrait, @@ -232,6 +243,7 @@ impl KDF for SHAKEInternal { /// To produce longer keys, use [KDF::derive_key_out]. /// To produce shorter keys, either use [KDF::derive_key_out] or truncate this result down with /// [KeyMaterial::set_key_len]. + #[cfg(feature = "alloc")] fn derive_key_from_multiple( mut self, keys: &[&impl KeyMaterialTrait], @@ -267,6 +279,7 @@ impl Default for SHAKEInternal { } impl XOF for SHAKEInternal { + #[cfg(feature = "alloc")] fn hash_xof(self, data: &[u8], result_len: usize) -> Vec { self.hash_internal(data, result_len) } @@ -327,6 +340,7 @@ impl XOF for SHAKEInternal { Ok(()) } + #[cfg(feature = "alloc")] fn squeeze(&mut self, num_bytes: usize) -> Vec { let mut out: Vec = vec![0u8; num_bytes]; self.squeeze_out(&mut out); diff --git a/recommendations.md b/recommendations.md new file mode 100644 index 0000000..2b0df5a --- /dev/null +++ b/recommendations.md @@ -0,0 +1,149 @@ +# PR #48 — Round 2: Comments to Post + +Each item = file, line, the exact code it anchors to, and the comment to paste. Ordered by priority. + +--- + +## 1. Keccak deserialization can panic on corrupt state (blocking) + +**File:** `crypto/sha3/src/keccak.rs` +**Line:** 420–422 (inside `KeccakDigest::from_serialized_state`) + +```rust +let bits_in_queue = u64::from_le_bytes(input[392..400].try_into().unwrap()) as usize; +if bits_in_queue > rate { + return Err(SuspendableError::InvalidData); +} +``` + +**Comment to post:** +> This only rejects `bits_in_queue > rate`. A corrupt/tampered state with `squeezing == false` and an +> odd `bits_in_queue` (or `bits_in_queue == rate`) still deserializes here, then panics on the next +> call: `absorb` hits `panic!("attempt to absorb with odd length queue")` (line 227), and +> `pad_and_switch_to_squeezing_phase` trips `debug_assert!(bits_in_queue < rate)`. Per the trait +> contract, corrupt input must return `InvalidData`, not panic. Please tighten to: +> ```rust +> if bits_in_queue > rate || (!/* squeezing */ && (bits_in_queue & 7 != 0 || bits_in_queue == rate)) { +> return Err(SuspendableError::InvalidData); +> } +> ``` +> (i.e. move this check below the `squeezing` parse and gate the extra conditions on `!squeezing`), and +> add a negative test — the current test only corrupts the `squeezing` byte, not `bits_in_queue`. + +--- + +## 2. `HashFactory` ships a knowingly-broken `Algorithm` impl (blocking) + +**File:** `crypto/factory/src/hash_factory.rs` +**Line:** 86–90 + +```rust +// TODO -- this does't work. Perhaps Algorithm needs to be re-worked so that these are functions instead? +impl Algorithm for HashFactory { + const ALG_NAME: &'static str = "TODO"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::None; +} +``` + +**Comment to post:** +> This public `impl Algorithm for HashFactory` ships `ALG_NAME = "TODO"` and +> `MAX_SECURITY_STRENGTH = None`, with a "this does't work" TODO. It was only added to satisfy the new +> `Hash: Algorithm` supertrait bound (so HMAC's new `Display`/`Debug` can read `HASH::ALG_NAME`), but +> `Algorithm`'s associated **consts** can't express a per-variant value for a runtime factory enum — +> hence the stub. Please don't merge a knowingly-wrong public `Algorithm` impl. Suggest sourcing HMAC's +> display name from HMAC's own `Algorithm` alias (or a small `fn alg_name(&self)`) instead of widening +> the `Hash` supertrait, so `HashFactory` doesn't need this impl at all. + +--- + +## 3. Add a "Breaking changes" changelog entry (three breaking items) + +**File:** `alpha_0.1.2_release_notes.md` +**Line:** 54 (add a section under `## Minor features / bug fixes`, or a new `## Breaking changes`) + +```markdown +## Minor features / bug fixes +``` + +The three breaking items live at: +- `crypto/sha2/src/lib.rs:75` — `pub use self::sha512::SHA512Internal;` (was `Sha512Internal`) +- `crypto/core/src/traits.rs:20` — `pub trait Hash: Algorithm + Default {` (new supertrait) +- `MLDSATrait::verify_mu_internal(->bool)` → public `verify_mu(->Result)` (mldsa & mldsa-lowmemory) + +**Comment to post:** +> Please add a "Breaking changes" section. This PR has three source-breaking public changes not listed: +> (1) `Sha512Internal` → `SHA512Internal` (sha2/src/lib.rs:75); (2) `Hash` now requires `Algorithm` as +> a supertrait (core/src/traits.rs:20), so every external `Hash` impl must now also impl `Algorithm`; +> (3) the public `MLDSATrait` method `verify_mu_internal(->bool)` became `verify_mu(->Result)`. + +--- + +## 4. `LIB_VERSION` is `core`'s version (0.1.1) and is the only compat gate + +**File:** `crypto/core/Cargo.toml` +**Line:** 3 + +```toml +version = "0.1.1" +``` + +Related anchor — **File:** `crypto/core/src/suspendable_state.rs`, **Line:** 112–113: + +```rust +let patch_stream = SemVer::from([LIB_VERSION.major, LIB_VERSION.minor, 255]); +if ver > patch_stream { +``` + +**Comment to post:** +> `LIB_VERSION` comes from `bouncycastle-core`, which is still `0.1.1` here while the release is +> `0.1.2` — so every serialized state gets stamped `0.1.1`. Two asks: (a) reconcile `core`'s version +> with the release; (b) since this stamp is the *only* compat gate and the policy (suspendable_state.rs:112) +> accepts any future patch on the same major.minor, any change to a serialized layout MUST bump `core`'s +> **minor** (never just the patch) or old readers will silently misparse newer blobs. Worth a one-line +> maintainer note next to `check_lib_ver`. + +--- + +## 5. (Optional, style) Uncommented infallible `.unwrap()`s in serialization paths + +QUALITY_AND_STYLE requires each `.unwrap()` to carry a justification. These fixed-size slice→array +unwraps are infallible by const construction but uncommented: + +- `crypto/sha2/src/sha256.rs:322` — `let out: &mut [u8; 105] = add_lib_ver(&mut out_to_return).try_into().unwrap();` +- `crypto/sha2/src/sha256.rs:351` — `let input: &[u8; 105] = check_lib_ver(&serialized_state, None)?.try_into().unwrap();` +- `crypto/sha2/src/sha512.rs:337` — `add_lib_ver(&mut out_to_return).try_into().unwrap();` +- `crypto/sha2/src/sha512.rs:364` — `check_lib_ver(&serialized_state, None)?.try_into().unwrap();` +- `crypto/hkdf/src/lib.rs:852` and `:871` — `state[..].try_into().unwrap()` + +**Comment to post (put on sha256.rs:322, reference the rest):** +> Style nit per QUALITY_AND_STYLE: these `.try_into().unwrap()`s are infallible by const sizing but +> lack the required justification comment. Suggest a one-line `// infallible: slice is exactly N bytes +> by const construction` on each (same pattern in sha512.rs:337/364 and hkdf/src/lib.rs:852/871), and +> a `quality_stats.sh` before/after to confirm the fallibility count didn't rise. + +--- + +## 6. (Separate / pre-existing) wycheproof sign harness fails on the `Randomized` case + +**File:** `crypto/mldsa/tests/wycheproof.rs` +**Line:** 353 (and identically 421, 489, and the `sign_seed` equivalents) + +```rust +let sig = MLDSA44::sign_mu_deterministic(&sk, None, &mu, [0u8; 32]).unwrap(); +assert_eq!(sig, hex::decode(&self.sig).unwrap().as_slice()); +``` + +**Comment to post:** +> Not introduced by this PR (pre-existing on base), but the 6 `*_sign_*` wycheproof tests fail because +> the harness signs every case with a hardcoded all-zero `rnd` and byte-compares to `sig`, while each +> file's one `Randomized`-flagged case (tcId 73/90/78/109/69/100) was generated with a non-zero `rnd` +> (per wycheproof `doc/mldsa.md` lines 33–36). The `MLDSASign*TestCase` struct doesn't parse `rnd`. +> Fix: skip cases flagged `Randomized` / carrying an `rnd` field, or parse `rnd` and pass it through. +> Worth a tracking issue so the suite goes green. + +--- + +## Just reply "resolved" (no code-anchored comment needed) + +F1 (HKDF hmac/state consistency), F3 (HKDF version header), F5 (HMAC `Secret`/`Drop`), F6 +(`IncorrectKey` removed), F9 (version policy) — all addressed in round 2; close them with a reply.