libsoliton 0.1.3

Core cryptographic library for the LO protocol — hybrid post-quantum key exchange, signatures, ratchet, and storage encryption
Documentation
//! 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. The `hkdf` crate's internal PRK is
/// **not** zeroized on drop (`hkdf 0.13.0-rc.5` has no `zeroize` feature).
/// Mitigation: `Hkdf` is a short-lived local — PRK lifetime is bounded to
/// this function scope, and `panic = "abort"` prevents unwinding.
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());
    }
}