//! 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::::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 = (0x00..=0x4f).collect(); let salt: Vec = (0x60..=0xaf).collect(); let info: Vec = (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()); } }