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
170 lines
5.5 KiB
Rust
170 lines
5.5 KiB
Rust
//! 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());
|
||
}
|
||
}
|