Skip to content

Commit bc86bb3

Browse files
authored
Various code hygiene issues (#858)
* Use direct sha2 updates and clarify sizes * Fix blake3 hash elements * Allow padded Digest192 hex and match zeroize paths * Reject all-zero X25519 shared secret * Handle zeroize generics and target dir * Add subtree format header * Use trailing zero checks in benches * Fix partial MMR tracking * Reject unknown subtree versions * chore: comment adjustment * Fix zeroize-audit target path * Clarify Digest192 parsing and add check-features * Use subtle for zero shared secret check * addressing review comments Remove legacy deserialization paths for PartialMmr and subtree data. Require the current marker or magic header and reject old formats. Add small comments that explain key behavior: empty-input handling in x25519 all-zero checks, mutation dedup order in subtree updates, marker purpose in PartialMmr, and invalid leaf position handling in untrack(). Fix outdated docs wording in padded_elements_to_bytes and update tests to match the hard cutoff behavior.
1 parent 7bf7ed2 commit bc86bb3

File tree

16 files changed

+467
-107
lines changed

16 files changed

+467
-107
lines changed

Makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ cargo-deny: ## Run cargo-deny to check dependencies for security vulnerabilities
5757
.PHONY: zeroize-audit
5858
zeroize-audit: ## Run Zeroize audit using rustdoc JSON
5959
cargo +nightly rustdoc -p miden-crypto --all-features -- -Zunstable-options --output-format json --document-private-items
60-
cargo run --quiet --manifest-path tools/zeroize-audit/Cargo.toml -- target/doc/miden_crypto.json
60+
@target_dir="$${CARGO_TARGET_DIR:-target}"; \
61+
if [ "$$target_dir" = "/" ]; then target_dir=target; fi; \
62+
cargo run --quiet --manifest-path tools/zeroize-audit/Cargo.toml -- "$$target_dir/doc/miden_crypto.json"
6163

6264
.PHONY: lint
6365
lint: format fix clippy toml typos-check machete cargo-deny ## Run all linting tasks at once (Clippy, fixing, formatting, machete, cargo-deny)
@@ -100,6 +102,11 @@ test: test-default test-no-std test-docs test-large-smt ## Run all tests except
100102
check: ## Check all targets and features for errors without code generation
101103
cargo check --all-targets --all-features
102104

105+
.PHONY: check-features
106+
check-features: ## Check miden-crypto feature combinations
107+
cargo check -p miden-crypto --all-targets --no-default-features
108+
cargo check -p miden-crypto --all-targets --features ${ALL_FEATURES_EXCEPT_ROCKSDB}
109+
103110
.PHONY: check-fuzz
104111
check-fuzz: ## Check miden-crypto-fuzz compilation
105112
cd miden-crypto-fuzz && cargo check

miden-crypto/benches/common/macros.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -779,17 +779,17 @@ macro_rules! benchmark_rand_draw_integers {
779779
macro_rules! benchmark_rand_check_leading_zeros {
780780
($func_name:ident, $coin_type:ty, $seed:expr) => {
781781
fn $func_name(c: &mut Criterion) {
782-
let mut group = c.benchmark_group("rand-check-leading-zeros");
782+
let mut group = c.benchmark_group("rand-check-trailing-zeros");
783783
group.measurement_time($crate::common::config::DEFAULT_MEASUREMENT_TIME);
784784
group.sample_size($crate::common::config::DEFAULT_SAMPLE_SIZE);
785785

786786
let coin = <$coin_type>::new($seed);
787787
let test_values: Vec<u64> = (0u64..100).collect();
788788

789-
group.bench_function("check_leading_zeros", |b| {
789+
group.bench_function("check_trailing_zeros", |b| {
790790
b.iter(|| {
791791
for &value in &test_values {
792-
let _zeros = coin.check_leading_zeros(black_box(value));
792+
let _zeros = coin.check_trailing_zeros(black_box(value));
793793
}
794794
});
795795
});

miden-crypto/src/aead/aead_poseidon2/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,8 @@ impl AuthTag {
147147

148148
impl PartialEq for AuthTag {
149149
fn eq(&self, other: &Self) -> bool {
150-
// Use constant-time comparison to prevent timing attacks
150+
// Use constant-time comparison on non-wasm targets to reduce timing leaks. On wasm, the
151+
// canonicalization helper is a host call and not guaranteed constant-time.
151152
let mut result = true;
152153
for (a, b) in self.0.iter().zip(other.0.iter()) {
153154
result &= bool::from(a.as_canonical_u64_ct().ct_eq(&b.as_canonical_u64_ct()));

miden-crypto/src/ecdh/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,6 @@ pub(crate) trait KeyAgreementScheme {
6161
pub(crate) enum KeyAgreementError {
6262
#[error("hkdf expansion failed")]
6363
HkdfExpansionFailed,
64+
#[error("shared secret is invalid")]
65+
InvalidSharedSecret,
6466
}

miden-crypto/src/ecdh/x25519.rs

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use alloc::vec::Vec;
1717
use hkdf::{Hkdf, hmac::SimpleHmac};
1818
use k256::sha2::Sha256;
1919
use rand::{CryptoRng, RngCore};
20+
use subtle::ConstantTimeEq;
2021

2122
use crate::{
2223
dsa::eddsa_25519_sha512::{PublicKey, SecretKey},
@@ -200,7 +201,9 @@ impl KeyAgreementScheme for X25519 {
200201
static_pk: &Self::PublicKey,
201202
) -> Result<Self::SharedSecret, super::KeyAgreementError> {
202203
let shared = ephemeral_sk.diffie_hellman(static_pk);
203-
debug_assert!(!shared.as_ref().iter().all(|byte| *byte == 0), "all-zero shared secret");
204+
if is_all_zero(shared.as_ref()) {
205+
return Err(super::KeyAgreementError::InvalidSharedSecret);
206+
}
204207
Ok(shared)
205208
}
206209

@@ -209,7 +212,9 @@ impl KeyAgreementScheme for X25519 {
209212
ephemeral_pk: &Self::EphemeralPublicKey,
210213
) -> Result<Self::SharedSecret, super::KeyAgreementError> {
211214
let shared = static_sk.get_shared_secret(ephemeral_pk.clone());
212-
debug_assert!(!shared.as_ref().iter().all(|byte| *byte == 0), "all-zero shared secret");
215+
if is_all_zero(shared.as_ref()) {
216+
return Err(super::KeyAgreementError::InvalidSharedSecret);
217+
}
213218
Ok(shared)
214219
}
215220

@@ -226,6 +231,15 @@ impl KeyAgreementScheme for X25519 {
226231
}
227232
}
228233

234+
fn is_all_zero(bytes: &[u8]) -> bool {
235+
// Empty input is treated as invalid caller input rather than "all zero".
236+
if bytes.is_empty() {
237+
return false;
238+
}
239+
let acc = bytes.iter().fold(0u8, |acc, &byte| acc | byte);
240+
acc.ct_eq(&0u8).into()
241+
}
242+
229243
// TESTS
230244
// ================================================================================================
231245

@@ -235,7 +249,8 @@ mod tests {
235249

236250
use super::*;
237251
use crate::{
238-
dsa::eddsa_25519_sha512::SecretKey, rand::test_utils::seeded_rng, utils::Deserializable,
252+
dsa::eddsa_25519_sha512::SecretKey, ecdh::KeyAgreementError, rand::test_utils::seeded_rng,
253+
utils::Deserializable,
239254
};
240255

241256
#[test]
@@ -277,6 +292,27 @@ mod tests {
277292
assert!(result.is_err());
278293
}
279294

295+
#[test]
296+
fn exchange_static_ephemeral_rejects_zero_shared_secret() {
297+
let mut rng = seeded_rng([0u8; 32]);
298+
let static_sk = SecretKey::with_rng(&mut rng);
299+
300+
let low_order_bytes = EIGHT_TORSION[0].to_montgomery().to_bytes();
301+
let low_order_pk = EphemeralPublicKey {
302+
inner: x25519_dalek::PublicKey::from(low_order_bytes),
303+
};
304+
305+
let result = X25519::exchange_static_ephemeral(&static_sk, &low_order_pk);
306+
assert!(matches!(result, Err(KeyAgreementError::InvalidSharedSecret)));
307+
}
308+
309+
#[test]
310+
fn is_all_zero_accepts_arbitrary_lengths() {
311+
assert!(!is_all_zero(&[]));
312+
assert!(is_all_zero(&[0u8; 16]));
313+
assert!(!is_all_zero(&[0u8, 1u8, 0u8, 0u8]));
314+
}
315+
280316
fn find_twist_point_bytes() -> [u8; 32] {
281317
let mut bytes = [0u8; 32];
282318
for i in 0u16..=u16::MAX {

miden-crypto/src/hash/blake/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ impl Blake3_192 {
125125

126126
/// Returns a hash of the provided field elements.
127127
#[inline(always)]
128-
pub fn hash_elements<E: BasedVectorSpace<Felt>>(elements: &[E]) -> Digest256 {
128+
pub fn hash_elements<E: BasedVectorSpace<Felt>>(elements: &[E]) -> Digest192 {
129129
Digest::new(hash_elements(elements))
130130
}
131131

miden-crypto/src/hash/blake/tests.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,18 @@ fn blake3_hash_elements() {
2121
assert_eq!(&expected, &actual);
2222
}
2323

24+
#[test]
25+
fn blake3_256_hash_elements_matches_hash() {
26+
let elements = rand_vector::<Felt>(17);
27+
let expected = Blake3_256::hash_elements(&elements);
28+
let mut bytes = Vec::new();
29+
for element in elements.iter() {
30+
bytes.extend_from_slice(&((*element).as_canonical_u64()).to_le_bytes());
31+
}
32+
let actual = Blake3_256::hash(&bytes);
33+
assert_eq!(expected, actual);
34+
}
35+
2436
proptest! {
2537
#[test]
2638
fn blake192_wont_panic_with_arbitrary_input(ref vec in any::<Vec<u8>>()) {

miden-crypto/src/hash/digest.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ pub const DIGEST512_BYTES: usize = 64;
2828
// ================================================================================================
2929

3030
/// A 192-bit (24-byte) digest. Type alias for `Digest<24>`.
31+
///
32+
/// Hex parsing also accepts zero-padded 32-byte encodings for backward compatibility.
3133
pub type Digest192 = Digest<DIGEST192_BYTES>;
3234

3335
/// A 256-bit (32-byte) digest. Type alias for `Digest<32>`.
@@ -116,6 +118,28 @@ impl<const N: usize> TryFrom<&str> for Digest<N> {
116118
type Error = HexParseError;
117119

118120
fn try_from(value: &str) -> Result<Self, Self::Error> {
121+
if N == DIGEST192_BYTES {
122+
let short_len = (DIGEST192_BYTES * 2) + 2;
123+
let long_len = (DIGEST256_BYTES * 2) + 2;
124+
125+
if value.len() == short_len {
126+
let bytes = hex_to_bytes::<N>(value)?;
127+
return Ok(Self(bytes));
128+
}
129+
130+
if value.len() == long_len {
131+
let bytes = hex_to_bytes::<DIGEST256_BYTES>(value)?;
132+
let padding = &bytes[DIGEST192_BYTES..];
133+
if padding.iter().all(|byte| *byte == 0) {
134+
let mut trimmed = [0u8; N];
135+
trimmed.copy_from_slice(&bytes[..N]);
136+
return Ok(Self(trimmed));
137+
}
138+
}
139+
140+
return Err(HexParseError::InvalidLength { expected: short_len, actual: value.len() });
141+
}
142+
119143
hex_to_bytes(value).map(Self)
120144
}
121145
}
@@ -198,6 +222,20 @@ mod tests {
198222
assert_eq!(digest.as_bytes(), &bytes);
199223
}
200224

225+
#[test]
226+
fn test_digest192_accepts_zero_padded_hex() {
227+
let mut bytes = [0u8; DIGEST192_BYTES];
228+
bytes[0] = 1;
229+
bytes[23] = 255;
230+
231+
let mut padded = [0u8; DIGEST256_BYTES];
232+
padded[..DIGEST192_BYTES].copy_from_slice(&bytes);
233+
234+
let hex = bytes_to_hex_string(padded);
235+
let parsed = Digest192::try_from(hex.as_str()).expect("digest192 should accept padding");
236+
assert_eq!(parsed.as_bytes(), &bytes);
237+
}
238+
201239
#[test]
202240
fn test_digest_hex_roundtrip_32() {
203241
let bytes = [0xab; 32];

miden-crypto/src/hash/sha2/mod.rs

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -162,27 +162,14 @@ where
162162
const { assert!(FELT_BYTES == 8, "buffer arithmetic assumes 8-byte field elements") };
163163

164164
let mut hasher = sha2::Sha256::new();
165-
// SHA-256 block size: 64 bytes
166-
let mut buf = [0_u8; 64];
167-
let mut buf_offset = 0;
168165

169166
for elem in elements.iter() {
170167
for &felt in E::as_basis_coefficients_slice(elem) {
171-
buf[buf_offset..buf_offset + FELT_BYTES]
172-
.copy_from_slice(&felt.as_canonical_u64().to_le_bytes());
173-
buf_offset += FELT_BYTES;
174-
175-
if buf_offset == 64 {
176-
hasher.update(buf);
177-
buf_offset = 0;
178-
}
168+
let felt_bytes = felt.as_canonical_u64().to_le_bytes();
169+
hasher.update(felt_bytes);
179170
}
180171
}
181172

182-
if buf_offset > 0 {
183-
hasher.update(&buf[..buf_offset]);
184-
}
185-
186173
hasher.finalize()
187174
};
188175
digest.into()
@@ -198,27 +185,14 @@ where
198185
const { assert!(FELT_BYTES == 8, "buffer arithmetic assumes 8-byte field elements") };
199186

200187
let mut hasher = sha2::Sha512::new();
201-
// SHA-512 block size: 128 bytes
202-
let mut buf = [0_u8; 128];
203-
let mut buf_offset = 0;
204188

205189
for elem in elements.iter() {
206190
for &felt in E::as_basis_coefficients_slice(elem) {
207-
buf[buf_offset..buf_offset + FELT_BYTES]
208-
.copy_from_slice(&felt.as_canonical_u64().to_le_bytes());
209-
buf_offset += FELT_BYTES;
210-
211-
if buf_offset == 128 {
212-
hasher.update(buf);
213-
buf_offset = 0;
214-
}
191+
let felt_bytes = felt.as_canonical_u64().to_le_bytes();
192+
hasher.update(felt_bytes);
215193
}
216194
}
217195

218-
if buf_offset > 0 {
219-
hasher.update(&buf[..buf_offset]);
220-
}
221-
222196
hasher.finalize()
223197
};
224198
digest.into()

0 commit comments

Comments
 (0)