libsoliton/soliton/src/primitives/hkdf.rs
Kamal Tufekcic 6853b30036
Some checks failed
CI / lint (push) Failing after 52s
CI / test-python (push) Successful in 1m40s
CI / test (push) Has been cancelled
CI / test-wasm (push) Has been cancelled
CI / publish-wasm (push) Has been cancelled
CI / miri (push) Has been cancelled
CI / fuzz-regression (push) Has been cancelled
CI / build (push) Has been cancelled
CI / publish (push) Has been cancelled
CI / publish-python (push) Has been cancelled
CI / test-zig (push) Has been cancelled
version bumps, MAX_RECV_SEEN patch, FIPS interop, doc updates
2026-09-09 00:49:53 +03:00

170 lines
5.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! HKDF-SHA3-256 (RFC 5869 construction with SHA3-256).
use crate::error::{Error, Result};
use hkdf::Hkdf;
use sha3::Sha3_256;
/// Perform HKDF-SHA3-256 extract-and-expand.
///
/// Derives `out.len()` bytes of keying material.
/// Maximum output length: 255 * 32 = 8160 bytes.
///
/// # Errors
///
/// Returns `InvalidLength` if `out` is empty or exceeds 8160 bytes.
///
/// # Security
///
/// The caller is responsible for zeroizing the `out` buffer when the derived
/// key material is no longer needed.
///
/// What the dependencies zeroize, verified against the pinned sources
/// (`hkdf 0.13.0-rc.5`, `hmac 0.13.0-rc.5`, `sha3 0.11.0-rc.7`,
/// `digest 0.11.0-rc.11`, `block-buffer 0.11.0`, all with their `zeroize`
/// features resolved on): dropping the `Hkdf` zeroizes the PRK-keyed HMAC
/// state — both SHA3 cores through `sha3`'s `Drop` and the wrapper's block
/// buffer through `block-buffer`'s. `hkdf` itself has no `zeroize` feature
/// and leaves three plain stack copies behind: the raw 32-byte PRK that
/// `Hkdf::new` extracts and discards, the `key ^ OPAD` block that
/// `HmacCore::new_from_slice` builds in a local, and the last output block
/// that `expand` keeps in a local. All three are bounded to this function's
/// frame, and `panic = "abort"` prevents unwinding past it.
pub fn hkdf_sha3_256(salt: &[u8], ikm: &[u8], info: &[u8], out: &mut [u8]) -> Result<()> {
if out.is_empty() {
return Err(Error::InvalidLength {
expected: 1,
got: 0,
});
}
// HKDF-Expand maximum: 255 × HashLen (SHA3-256 = 32 bytes) = 8160 bytes.
if out.len() > 255 * 32 {
return Err(Error::InvalidLength {
expected: 255 * 32,
got: out.len(),
});
}
let hk = Hkdf::<Sha3_256>::new(Some(salt), ikm);
// Length already validated above — expand cannot fail.
hk.expand(info, out)
.expect("HKDF output length already validated");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use hex_literal::hex;
// Test vectors: RFC 5869 input patterns computed with SHA3-256 via Python 3
// hashlib HKDF implementation (provenance: manual HMAC-SHA3-256-based
// extract+expand, verified against this Rust `hkdf`+`sha3` crate output).
#[test]
fn case1() {
let ikm = [0x0b; 22];
let salt = hex!("000102030405060708090a0b0c");
let info = hex!("f0f1f2f3f4f5f6f7f8f9");
let expected = hex!(
"0c5160501d65021deaf2c14f5abce04c"
"5bd2635abceeba61c2edb6e8ed726749"
"00557728f2c9f2c4c179"
);
let mut out = [0u8; 42];
hkdf_sha3_256(&salt, &ikm, &info, &mut out).unwrap();
assert_eq!(out, expected);
}
#[test]
fn case2() {
let ikm: Vec<u8> = (0x00..=0x4f).collect();
let salt: Vec<u8> = (0x60..=0xaf).collect();
let info: Vec<u8> = (0xb0..=0xff).collect();
let expected = hex!(
"3dc251e66c75da6560405ec5ac10e17d"
"851eedfbfdc13feafbec16964c25d021"
"bd971465a3e9c615f27769019e3f0407"
"d84986fb0ba24e729c99834624baa21c"
"b623dc0098f430d52e18bbdf694df4ed"
"d8b2"
);
let mut out = [0u8; 82];
hkdf_sha3_256(&salt, &ikm, &info, &mut out).unwrap();
assert_eq!(out, expected);
}
#[test]
fn case3() {
let ikm = [0x0b; 22];
let salt = b"";
let info = b"";
let expected = hex!(
"bc1342cdd75c05e8b0c3ae609ce44106"
"84d197232875073499b30cdfe2de2853"
"c1c1bed63d725e885e78"
);
let mut out = [0u8; 42];
hkdf_sha3_256(salt, &ikm, info, &mut out).unwrap();
assert_eq!(out, expected);
}
#[test]
fn output_32_bytes() {
let mut out = [0u8; 32];
hkdf_sha3_256(b"salt", b"ikm", b"info", &mut out).unwrap();
assert!(out.iter().any(|&b| b != 0));
}
#[test]
fn output_64_bytes() {
let mut out = [0u8; 64];
hkdf_sha3_256(b"salt", b"ikm", b"info", &mut out).unwrap();
assert!(out.iter().any(|&b| b != 0));
}
#[test]
fn output_1_byte() {
let mut out = [0u8; 1];
hkdf_sha3_256(b"salt", b"ikm", b"info", &mut out).unwrap();
assert_ne!(out[0], 0u8);
}
#[test]
fn max_output() {
let mut out = vec![0u8; 255 * 32];
hkdf_sha3_256(b"salt", b"ikm", b"info", &mut out).unwrap();
assert!(out.iter().any(|&b| b != 0));
}
#[test]
fn max_output_kat() {
// KAT for T(255) boundary — verifies the final HMAC iteration of
// HKDF-Expand produces correct output. Reference: Python 3.12+
// hmac + hashlib (sha3_256).
let salt = [0u8; 32];
let ikm = b"test input keying material";
let info = b"test info";
let mut out = vec![0u8; 8160];
hkdf_sha3_256(&salt, ikm, info, &mut out).unwrap();
assert_eq!(
&out[..32],
&hex_literal::hex!("fe631a871e206c76b2328b44340b951ec80635132cef2b633d9fffbed3b03af6")
);
assert_eq!(
&out[8128..],
&hex_literal::hex!("8e358e5b1b40fcdeb1a51bda1f65c3d76908111a55738dd5d5e6991541aa8b4c")
);
}
#[test]
fn empty_output_returns_error() {
let mut out: [u8; 0] = [];
assert!(hkdf_sha3_256(b"salt", b"ikm", b"info", &mut out).is_err());
}
#[test]
fn oversized_output_returns_error() {
let mut out = vec![0u8; 255 * 32 + 1];
assert!(hkdf_sha3_256(b"salt", b"ikm", b"info", &mut out).is_err());
}
}