Merge rust-bitcoin/rust-bitcoin#6856: hashes: Fix Hkdf::expand panic
What changed, and why it matters
This commit fixes a panic (sudden crash) in the HKDF key-derivation function when a caller asks for the maximum allowed output length. The previous code used an 8-bit counter that overflowed on the final block, causing the program to abort. The fix replaces the counter with a loop that cannot overflow. This is a denial-of-service-style bug: a valid, RFC-compliant request could crash the library.
Upgrade to a version of rust-bitcoin that includes this commit. If HKDF is exposed to untrusted input lengths, apply the patch immediately to prevent a panic on maximum-length requests. Review any code that catches panics around HKDF usage, as the panic is now removed.
Security signals we found
Integer overflow in block counter leading to runtime panic
Denial-of-service vector via a valid, maximum-length HKDF expand request
Violation of RFC-5869 maximum output guarantee
Fix verified by regression test against RFC-5869 test vector
Evidence from the diff
In hashes/src/hkdf/mod.rs, Hkdf::expand accepted output buffers up to 255 * T::Hash::LEN bytes per RFC 5869. The iteration counter was stored as u8 and incremented after each block. When the requested output length equals the maximum, the final block uses counter value 255; the subsequent counter += 1 overflows, triggering a Rust panic. The patch changes the loop to for counter in 1..=total_blocks using a usize iterator, then casts to u8 only for the byte appended to the HMAC input. A regression test max_okm verifies that 255 * 32 bytes can now be produced without panicking and that the first 42 bytes match the RFC 5869 test vector.
Changed components
hashes/src/hkdf/mod.rsHkdf::expandrust-bitcoin hashes crateInspect captured patch +28 / −12
### hashes/src/hkdf/mod.rs
@@ -71,36 +71,33 @@ where
return Err(MaxLengthError { max: MAX_OUTPUT_BLOCKS * T::Hash::LEN });
}
- // Counter starts at "1" based on RFC5869 spec and is committed to in the hash.
- let mut counter = 1u8;
// Ceiling calculation for the total number of blocks (iterations) required for the expand.
let total_blocks = okm.len().div_ceil(T::Hash::LEN);
- while counter <= total_blocks as u8 {
+ // Counter starts at "1" based on RFC5869 spec and is committed to in the hash.
+ for counter in 1..=total_blocks {
let mut engine: HmacEngine<T> = HmacEngine::new(self.prk.as_ref());
// First block does not have a previous block,
// all other blocks include last block in the HMAC input.
- if counter != 1u8 {
- let previous_start_index = (counter as usize - 2) * T::Hash::LEN;
- let previous_end_index = (counter as usize - 1) * T::Hash::LEN;
+ if counter != 1 {
+ let previous_start_index = (counter - 2) * T::Hash::LEN;
+ let previous_end_index = (counter - 1) * T::Hash::LEN;
engine.input(&okm[previous_start_index..previous_end_index]);
}
engine.input(info);
- engine.input(&[counter]);
+ engine.input(&[counter as u8]);
let t = engine.finalize();
- let start_index = (counter as usize - 1) * T::Hash::LEN;
+ let start_index = (counter - 1) * T::Hash::LEN;
// Last block might not take full hash length.
- let end_index = if counter == (total_blocks as u8) {
+ let end_index = if counter == total_blocks {
okm.len()
} else {
- counter as usize * T::Hash::LEN
+ counter * T::Hash::LEN
};
okm[start_index..end_index].copy_from_slice(&t.as_ref()[0..(end_index - start_index)]);
-
- counter += 1;
}
Ok(())
@@ -235,6 +232,25 @@ mod tests {
assert!(e.is_err());
}
+ #[test]
+ fn max_okm() {
+ let salt = hex::decode_to_vec("000102030405060708090a0b0c").unwrap();
+ let ikm = hex::decode_to_vec("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b").unwrap();
+ let info = hex::decode_to_vec("f0f1f2f3f4f5f6f7f8f9").unwrap();
+
+ let hkdf = Hkdf::<sha256::HashEngine>::new(&salt, &ikm);
+ // The RFC-5869 maximum output length of `255 * hash length` must not panic.
+ let mut okm = [0u8; 255 * 32];
+ hkdf.expand(&info, &mut okm).unwrap();
+
+ // HKDF output is a stable prefix stream, so the first 42 bytes match the
+ // RFC-5869 test case 1 vector.
+ assert_eq!(
+ okm[..42].to_lower_hex_string(),
+ "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865"
+ );
+ }
+
#[test]
fn short_okm() {
let salt = hex::decode_to_vec("000102030405060708090a0b0c").unwrap();Why this scored 62/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.