Skip to content

Commit 1f42d12

Browse files
Hash와 MAC 트레이트의 Vec<u8> 반환 제거
- const generic 보조 트레이트 HashFixedOutput과 MACFixedOutput 추가 - hmac 내부 vec 할당을 스택 버퍼로 교체하고 사용 후 zeroize - mldsa의 do_final try_into unwrap 제거
1 parent b3a22ae commit 1f42d12

30 files changed

Lines changed: 346 additions & 402 deletions

alpha_0.1.2_release_notes.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
* Ensure that all crates have `#![forbid(missing_docs)]`
1818
* Apply Secret trait consistently across the library --> study the `Zeroize` trait in RustCrypto
1919
* Change all "[u8;0]" to "[]" throughout the code and docs ... or better yet, change the APIs to take an Option<>
20-
* Change all `-> Vec<u8>` to `-> [u8; CONST_LEN]`, and the `output: &mut [u8]` to `output: &mut [u8; CONST_LEN]` where
21-
appropriate.
20+
* Change the `output: &mut [u8]` to `output: &mut [u8; CONST_LEN]` where appropriate. (The `-> Vec<u8>` half of this
21+
item is done, see changelog. The `*_out` slice parameters were deliberately left as `&mut [u8]` because the
22+
documented truncation / oversized-buffer semantics depend on them; revisit per-API.)
2223
* Probably it makes sense to leave Hex and Base64 as requiring std; ... or maybe add a no_std version that uses
2324
fixed-sized blocks?
2425
* Create a cargo feature #[cfg(feature='rng')] and put it around things like keygen that takes an rng so that the build
@@ -52,6 +53,15 @@
5253

5354
* ML-DSA
5455
* Low-Memory ML-DSA -- runs in about 1/10th of the usual memory (~ 30 kb of stack) with only minor performance impact.
56+
* (Breaking, progress on #14) Removed all heap-allocating `-> Vec<u8>` functions from the `Hash` and `MAC` traits
57+
(`hash`, `do_final`, `do_final_partial_bits`, `mac`). They are replaced by the new `HashFixedOutput<const OUTPUT_LEN>`
58+
and `MACFixedOutput<const OUTPUT_LEN>` traits which return `[u8; OUTPUT_LEN]` stack arrays, following the same
59+
const-generic pattern as the `KEM` and `Signature` traits. All concrete algorithms (SHA2, SHA3, HMAC) implement the
60+
new traits; the factory enums keep only the `*_out` functions because their output length is a runtime property.
61+
The XOF functions (`hash_xof`, `squeeze`) keep returning `Vec<u8>` because their output length is inherently a
62+
runtime parameter.
63+
* HMAC no longer heap-allocates its intermediate inner digest (was an internal `vec!`, now a fixed stack buffer),
64+
and the inner digest buffer is zeroized after use.
5565
* All public `*_out(.., out: &mut [u8])` functions now begin by zeroizing the entire output buffer with `.fill(0)`,
5666
preventing exposure of stale data in oversized output buffers or on early error returns.
5767
* Github issues resolved:

cli/src/mac_cmd.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,17 @@ fn do_mac(mut mac: impl MAC, verify_val: &Option<String>, output_hex: bool) {
6363

6464
if verify_val.is_none() {
6565
// compute a MAC value
66-
let out = mac.do_final();
66+
let mut out = [0u8; 64];
67+
let bytes_written =
68+
mac.do_final_out(&mut out).expect("Failed to compute the MAC value");
69+
let out = &out[..bytes_written];
6770

6871
if output_hex {
6972
for b in out.iter() {
7073
print!("{b:02x}");
7174
}
7275
} else {
73-
io::stdout().write(&out).unwrap();
76+
io::stdout().write(out).unwrap();
7477
}
7578
println!();
7679
} else {

cli/src/sha2_cmd.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@ fn do_sha2(mut sha2: impl Hash, output_hex: bool) {
2424
bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin");
2525
}
2626

27-
let out = sha2.do_final();
27+
let mut out = [0u8; 64];
28+
let bytes_written = sha2.do_final_out(&mut out);
29+
let out = &out[..bytes_written];
2830

2931
if output_hex {
3032
for b in out.iter() {
3133
print!("{b:02x}");
3234
}
33-
} else { io::stdout().write(&out).unwrap(); }
35+
} else { io::stdout().write(out).unwrap(); }
3436
println!();
3537
}

cli/src/sha3_cmd.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@ fn do_sha3(mut sha3: impl Hash, output_hex: bool) {
2525
bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin");
2626
}
2727

28-
let out = sha3.do_final();
28+
let mut out = [0u8; 64];
29+
let bytes_written = sha3.do_final_out(&mut out);
30+
let out = &out[..bytes_written];
2931

3032
if output_hex {
3133
for b in out.iter() {
3234
print!("{b:02x}");
3335
}
34-
} else { io::stdout().write(&out).unwrap(); }
36+
} else { io::stdout().write(out).unwrap(); }
3537
println!();
3638
}
3739

crypto/core-test-framework/src/hash.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use bouncycastle_core::traits::{Hash, HashAlgParams};
1+
use bouncycastle_core::traits::{HashAlgParams, HashFixedOutput};
22

33
pub struct TestFrameworkHash {
44
pub enable_partial_final_input_tests: bool,
@@ -13,30 +13,31 @@ impl TestFrameworkHash {
1313
/// This gives good baseline test coverage, but is not exhaustive; for example it does not test
1414
/// do_final_partial_bits() or do_final_partial_bits_out()
1515
/// because those require different input-output pairs.
16-
pub fn test_hash<H: Hash + HashAlgParams + Default>(
16+
pub fn test_hash<H: HashFixedOutput<N> + HashAlgParams + Default, const N: usize>(
1717
&self,
1818
input: &[u8],
1919
expected_output: &[u8],
2020
) {
2121
/*** fn result_len() -> usize ***/
2222
assert_eq!(H::default().output_len(), H::OUTPUT_LEN);
23+
assert_eq!(N, H::OUTPUT_LEN);
2324

24-
/*** fn hash(self, data: &[u8]) -> Vec<u8> **/
25-
let output_vec = H::default().hash(input);
26-
assert_eq!(output_vec, expected_output);
25+
/*** fn hash(self, data: &[u8]) -> [u8; N] **/
26+
let output_arr = H::default().hash(input);
27+
assert_eq!(&output_arr[..], expected_output);
2728

2829
/*** fn hash_out(self, data: &[u8], output: &mut [u8]) -> Result<usize, HashError> ***/
2930
let mut output_buf = vec![0_u8; H::OUTPUT_LEN];
3031
H::default().hash_out(input, &mut output_buf);
3132
assert_eq!(output_buf, expected_output);
3233

3334
/*** fn do_update(&mut self, data: &[u8]) -> Result<(), HashError> ***/
34-
/*** fn do_final(self) -> Result<Vec<u8>, HashError> **/
35+
/*** fn do_final(self) -> [u8; N] **/
3536

3637
let mut message_digest = H::default();
3738
message_digest.do_update(input);
3839
let output_buf = message_digest.do_final();
39-
assert_eq!(expected_output, output_buf, "Incorrect output for input (update_bytes)");
40+
assert_eq!(expected_output, &output_buf[..], "Incorrect output for input (update_bytes)");
4041

4142
for length in 1..output_buf.len() {
4243
let mut truncated = vec![0_u8; length];
@@ -58,7 +59,7 @@ impl TestFrameworkHash {
5859
message_digest.do_update(chunk);
5960
}
6061
let output_buf = message_digest.do_final();
61-
assert_eq!(expected_output, output_buf, "Incorrect output for input (update_bytes)");
62+
assert_eq!(expected_output, &output_buf[..], "Incorrect output for input (update_bytes)");
6263

6364
/*** fn do_update(&mut self, data: &[u8]) -> Result<(), HashError> ***/
6465
/*** fn do_final_out(self, output: &mut [u8]) -> Result<usize, HashError> ***/

crypto/core-test-framework/src/mac.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::DUMMY_SEED_512;
22
use bouncycastle_core::errors::{KeyMaterialError, MACError};
33
use bouncycastle_core::key_material::{KeyMaterial512, KeyType, KeyMaterialTrait};
4-
use bouncycastle_core::traits::MAC;
4+
use bouncycastle_core::traits::MACFixedOutput;
55
use bouncycastle_core::traits::{SecurityStrength};
66

77
pub struct TestFrameworkMAC {
@@ -15,15 +15,15 @@ impl TestFrameworkMAC {
1515

1616
/// Test all the members of trait Hash against the given input-output pair.
1717
/// This gives good baseline test coverage, but is not exhaustive.
18-
pub fn test_mac<M: MAC>(
18+
pub fn test_mac<M: MACFixedOutput<N>, const N: usize>(
1919
&self,
2020
key: &impl KeyMaterialTrait,
2121
input: &[u8],
2222
expected_output: &[u8],
2323
) {
2424
// Test ::mac()
2525
let out = M::new_allow_weak_key(key).unwrap().mac(input);
26-
assert_eq!(out, expected_output);
26+
assert_eq!(&out[..], expected_output);
2727

2828
// Test ::mac_out
2929
let mut out = vec![0u8; expected_output.len()];
@@ -53,7 +53,7 @@ impl TestFrameworkMAC {
5353
let output_len = mac.output_len();
5454
mac.do_update(input);
5555
let out = mac.do_final();
56-
assert_eq!(out, expected_output);
56+
assert_eq!(&out[..], expected_output);
5757

5858
// Test .output_len()
5959
assert_eq!(output_len, out.len());

crypto/core-test-framework/src/signature.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,14 +254,16 @@ impl TestFrameworkSignature {
254254

255255
// sign_ph
256256
let (pk, sk) = SigAlg::keygen().unwrap();
257-
let ph: [u8; PH_LEN] = HASH::default().hash(msg)[..PH_LEN].try_into().unwrap();
257+
let mut ph = [0u8; PH_LEN];
258+
HASH::default().hash_out(msg, &mut ph);
258259
let sig_val = SigAlg::sign_ph(&sk, &ph, None).unwrap();
259260
SigAlg::verify(&pk, msg, None, &sig_val).unwrap();
260261
SigAlg::verify_ph(&pk, &ph, None, &sig_val).unwrap();
261262

262263
// sign_ph_out
263264
let (pk, sk) = SigAlg::keygen().unwrap();
264-
let ph: [u8; PH_LEN] = HASH::default().hash(msg)[..PH_LEN].try_into().unwrap();
265+
let mut ph = [0u8; PH_LEN];
266+
HASH::default().hash_out(msg, &mut ph);
265267
let mut sig_val = [0u8; SIG_LEN];
266268
let bytes_written = SigAlg::sign_ph_out(&sk, &ph, None, &mut sig_val).unwrap();
267269
assert_eq!(bytes_written, SIG_LEN);

crypto/core/src/traits.rs

Lines changed: 83 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,6 @@ pub trait Hash : Default {
2424
/// The size of the output in bytes.
2525
fn output_len(&self) -> usize;
2626

27-
/// A static one-shot API that hashes the provided data.
28-
/// `data` can be of any length, including zero bytes.
29-
fn hash(self, data: &[u8]) -> Vec<u8>;
30-
3127
/// A static one-shot API that hashes the provided data into the provided output slice.
3228
/// `data` can be of any length, including zero bytes.
3329
/// The entire output buffer is zeroized before the hash output is written.
@@ -40,11 +36,6 @@ pub trait Hash : Default {
4036
// fn do_update(&mut self, data: &[u8]) -> Result<(), HashError>;
4137
fn do_update(&mut self, data: &[u8]);
4238

43-
/// Finish absorbing input and produce the hashes output.
44-
/// Consumes self, so this must be the final call to this object.
45-
// fn do_final(self) -> Result<Vec<u8>, HashError>;
46-
fn do_final(self) -> Vec<u8>;
47-
4839
/// Finish absorbing input and produce the hashes output.
4940
/// Consumes self, so this must be the final call to this object.
5041
///
@@ -57,14 +48,6 @@ pub trait Hash : Default {
5748
/// The return value is the number of bytes written.
5849
fn do_final_out(self, output: &mut [u8]) -> usize;
5950

60-
/// The same as [Hash::do_final], but allows for supplying a partial byte as the last input.
61-
/// Assumes that the input is in the least significant bits (big endian).
62-
fn do_final_partial_bits(
63-
self,
64-
partial_byte: u8,
65-
num_partial_bits: usize,
66-
) -> Result<Vec<u8>, HashError>;
67-
6851
/// The same as [Hash::do_final_out], but allows for supplying a partial byte as the last input.
6952
/// Assumes that the input is in the least significant bits (big endian).
7053
/// will be placed in the first [Hash::output_len] bytes.
@@ -81,6 +64,49 @@ pub trait Hash : Default {
8164
fn max_security_strength(&self) -> SecurityStrength;
8265
}
8366

67+
/// An extension to [Hash] for algorithms whose output length is known at compile time.
68+
///
69+
/// `OUTPUT_LEN` is the output length of the hash in bytes and must equal [Hash::output_len];
70+
/// implementors are expected to declare it via the matching [HashAlgParams::OUTPUT_LEN] constant,
71+
/// for example `impl HashFixedOutput<{ SHA256Params::OUTPUT_LEN }> for SHA256 {}`.
72+
///
73+
/// The provided methods return fixed-size arrays instead of heap allocations, so they are
74+
/// usable in `no_std` environments and keep all intermediate state on the stack.
75+
/// Runtime-dispatching wrappers (such as factory enums) whose output length varies by variant
76+
/// cannot implement this trait and offer only the `*_out` functions of [Hash].
77+
pub trait HashFixedOutput<const OUTPUT_LEN: usize>: Hash {
78+
/// A static one-shot API that hashes the provided data.
79+
/// `data` can be of any length, including zero bytes.
80+
fn hash(self, data: &[u8]) -> [u8; OUTPUT_LEN] {
81+
let mut output = [0u8; OUTPUT_LEN];
82+
let written = self.hash_out(data, &mut output);
83+
debug_assert_eq!(written, OUTPUT_LEN);
84+
output
85+
}
86+
87+
/// Finish absorbing input and produce the hashes output.
88+
/// Consumes self, so this must be the final call to this object.
89+
fn do_final(self) -> [u8; OUTPUT_LEN] {
90+
let mut output = [0u8; OUTPUT_LEN];
91+
let written = self.do_final_out(&mut output);
92+
debug_assert_eq!(written, OUTPUT_LEN);
93+
output
94+
}
95+
96+
/// The same as [HashFixedOutput::do_final], but allows for supplying a partial byte as the last input.
97+
/// Assumes that the input is in the least significant bits (big endian).
98+
fn do_final_partial_bits(
99+
self,
100+
partial_byte: u8,
101+
num_partial_bits: usize,
102+
) -> Result<[u8; OUTPUT_LEN], HashError> {
103+
let mut output = [0u8; OUTPUT_LEN];
104+
let written = self.do_final_partial_bits_out(partial_byte, num_partial_bits, &mut output)?;
105+
debug_assert_eq!(written, OUTPUT_LEN);
106+
Ok(output)
107+
}
108+
}
109+
84110
pub trait HashAlgParams: Algorithm {
85111
const OUTPUT_LEN: usize;
86112
const BLOCK_LEN: usize;
@@ -234,13 +260,14 @@ pub trait KEMPrivateKey<const SK_LEN: usize> : PartialEq + Eq + Clone + Secret +
234260
/// A MAC algorithm takes in a key and some data, and produces a MAC (message authentication code) that
235261
/// can be used to verify the integrity of data.
236262
///
237-
/// This trait provides one-shot functions [MAC::mac], [MAC::mac_out], and [MAC::verify].
238-
/// It also provides streaming functions [MAC::do_update], [MAC::do_final], [MAC::do_final_out],
263+
/// This trait provides one-shot functions [MAC::mac_out] and [MAC::verify].
264+
/// It also provides streaming functions [MAC::do_update], [MAC::do_final_out],
239265
/// and [MAC::do_verify_final].
266+
/// Fixed-size variants that return the MAC value as an array are provided by [MACFixedOutput].
240267
/// The workflow is that a MAC object is initialized with a key with [MAC::new] -- or [MAC::new_allow_weak_key] if you
241268
/// need to disable the library's safety mechanism to prevent the use of weak keys -- then data is
242269
/// processed into one or more calls to [MAC::do_update],
243-
/// after that the object can either create a MAC with [MAC::do_final] or [MAC::do_final_out] (which are final functions, and so consume the object),
270+
/// after that the object can either create a MAC with [MACFixedOutput::do_final] or [MAC::do_final_out] (which are final functions, and so consume the object),
244271
/// or the object can be used to verify a MAC.
245272
///
246273
/// For varifying an existing MAC, it is functionally equivalent to use the provided [MAC::verify] and [MAC::do_verify_final]
@@ -282,17 +309,6 @@ pub trait MAC: Sized {
282309
/// The size of the output in bytes.
283310
fn output_len(&self) -> usize;
284311

285-
/// One-shot API that computes a MAC for the provided data.
286-
/// `data` can be of any length, including zero bytes.
287-
///
288-
/// Note about the security strength of the provided key:
289-
/// If the provided key is tagged at a lower [SecurityStrength] than the instantiated MAC algorithm,
290-
/// this will fail with an error:
291-
/// ```text
292-
/// MACError::KeyMaterialError(KeyMaterialError::SecurityStrength("HMAC::init(): provided key has a lower security strength than the instantiated HMAC")
293-
/// ```
294-
fn mac(self, data: &[u8]) -> Vec<u8>;
295-
296312
/// One-shot API that computes a MAC for the provided data and writes it into the provided output slice.
297313
/// `data` can be of any length, including zero bytes.
298314
///
@@ -321,8 +337,6 @@ pub trait MAC: Sized {
321337
/// do_update() is intended to be used as part of a streaming interface, and so may by called multiple times.
322338
fn do_update(&mut self, data: &[u8]);
323339

324-
fn do_final(self) -> Vec<u8>;
325-
326340
/// Depending on the underlying MAC implementation, NIST may require that the library enforce
327341
/// a minimum length on the mac output value. See documentation for the underlying implementation
328342
/// to see conditions under which it throws [MACError::InvalidLength].
@@ -344,6 +358,42 @@ pub trait MAC: Sized {
344358
fn max_security_strength(&self) -> SecurityStrength;
345359
}
346360

361+
/// An extension to [MAC] for algorithms whose output length is known at compile time.
362+
///
363+
/// `OUTPUT_LEN` is the full (untruncated) MAC output length in bytes and must equal [MAC::output_len].
364+
///
365+
/// The provided methods return fixed-size arrays instead of heap allocations, so they are
366+
/// usable in `no_std` environments and keep all intermediate state on the stack.
367+
/// Runtime-dispatching wrappers (such as factory enums) whose output length varies by variant
368+
/// cannot implement this trait and offer only the `*_out` functions of [MAC].
369+
pub trait MACFixedOutput<const OUTPUT_LEN: usize>: MAC {
370+
/// One-shot API that computes a MAC for the provided data.
371+
/// `data` can be of any length, including zero bytes.
372+
fn mac(self, data: &[u8]) -> [u8; OUTPUT_LEN] {
373+
let mut output = [0u8; OUTPUT_LEN];
374+
// Infallible: OUTPUT_LEN is the full MAC output length, so the minimum-length
375+
// check on truncated MAC values in mac_out() cannot fail.
376+
let written = self
377+
.mac_out(data, &mut output)
378+
.expect("MACFixedOutput::mac(): full-length output buffer was rejected");
379+
debug_assert_eq!(written, OUTPUT_LEN);
380+
output
381+
}
382+
383+
/// Finish absorbing input and produce the MAC value.
384+
/// Consumes self, so this must be the final call to this object.
385+
fn do_final(self) -> [u8; OUTPUT_LEN] {
386+
let mut output = [0u8; OUTPUT_LEN];
387+
// Infallible: OUTPUT_LEN is the full MAC output length, so the minimum-length
388+
// check on truncated MAC values in do_final_out() cannot fail.
389+
let written = self
390+
.do_final_out(&mut output)
391+
.expect("MACFixedOutput::do_final(): full-length output buffer was rejected");
392+
debug_assert_eq!(written, OUTPUT_LEN);
393+
output
394+
}
395+
}
396+
347397
#[derive(Eq, PartialEq, PartialOrd, Clone, Debug)]
348398
pub enum SecurityStrength {
349399
None,

0 commit comments

Comments
 (0)