krypteia-tessera 0.2.0

Shared pure-Rust hash, XOF, MAC and KDF primitives for the krypteia workspace: SHA-1/2/3, Keccak, SHAKE/cSHAKE, RIPEMD-160, BLAKE2/BLAKE3, HMAC, SP 800-185 (KMAC/TupleHash/ParallelHash), the KDF family (HKDF, PBKDF2, SP 800-108, scrypt, Argon2), MGF1 and CRC-16/32. no_std, zero runtime dependencies.
Documentation
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Cédric Mesnil <cslashm@pm.me>

//! HMAC — keyed-hash message authentication code (FIPS 198-1 / RFC 2104).
//!
//! `HMAC(K, m) = H((K0 ⊕ opad) ‖ H((K0 ⊕ ipad) ‖ m))` where `K0` is the
//! key zero-padded (or, when longer than the block, hashed then padded)
//! to the hash block size. Generic over any [`Digest`]; the block size
//! comes from [`Digest::BLOCK_LEN`]. Used by SLH-DSA-SHA2's `PRF_msg`
//! (FIPS 205 §11.2) and available to RSA / ECDSA-RFC6979 / MAC consumers.
//!
//! # Side-channel posture
//!
//! Classic constant-time: there is no secret-*content*-dependent branch,
//! index, or early-exit. The single `key.len() > BLOCK_LEN` branch is on
//! the (public) key length — for the SLH-DSA use the key is `SK.prf` of
//! fixed length `n`, so the branch is constant per parameter set. The
//! `ipad`/`opad` XOR and the compression are fixed-length.
//!
//! **DPA caveat (not addressed here):** HMAC over SHA-2 is vulnerable to
//! carry-based DPA on the key (Belenky et al., TCHES 2023/3). The leak is
//! in the SHA-2 modular additions, not in any branch, and a masked SHA-2
//! is out of scope of this primitive. Callers feeding a long-lived secret
//! key (e.g. SLH-DSA-SHA2 `SK.prf`) must treat the key as DPA-extractable
//! until a masked SHA-2 backend ships. `[CESTI: residual CDPA exposure of
//! HMAC keys — document and point to the masked-SHA-2 roadmap item.]`

use crate::Digest;

// Largest block among the crate's digests is SHA-512's 128 bytes; the
// largest digest output is SHA-512's 64 bytes. Fixed stack scratch keeps
// HMAC `no_std` and allocation-free on the M0 baseline.
const MAX_BLOCK: usize = 128;
const MAX_OUTPUT: usize = 64;

/// Compute `HMAC-H(key, data)` into `out` (first `H::OUTPUT_LEN` octets).
///
/// `out` must be at least `H::OUTPUT_LEN` bytes.
pub fn hmac<H: Digest>(key: &[u8], data: &[u8], out: &mut [u8]) {
    hmac_multi::<H>(key, &[data], out);
}

/// Compute `HMAC-H(key, parts[0] ‖ parts[1] ‖ …)` into `out`.
///
/// The multi-part form avoids concatenating a large message before
/// authenticating it (e.g. SLH-DSA `PRF_msg(SK.prf, opt_rand ‖ M)`).
/// `out` must be at least `H::OUTPUT_LEN` bytes.
pub fn hmac_multi<H: Digest>(key: &[u8], parts: &[&[u8]], out: &mut [u8]) {
    let b = H::BLOCK_LEN;
    let olen = H::OUTPUT_LEN;
    debug_assert!(b <= MAX_BLOCK, "HMAC scratch is sized for block sizes up to 128 octets");
    debug_assert!(olen <= MAX_OUTPUT, "HMAC scratch is sized for digests up to 64 octets");
    debug_assert!(out.len() >= olen, "HMAC output buffer too small");

    // K0: key padded to one block, or H(key) padded if the key is longer.
    let mut k0 = [0u8; MAX_BLOCK];
    if key.len() > b {
        H::digest(key, &mut k0[..olen]);
    } else {
        k0[..key.len()].copy_from_slice(key);
    }

    // Inner hash: H((K0 ⊕ ipad) ‖ parts…).
    let mut ipad = [0u8; MAX_BLOCK];
    for i in 0..b {
        ipad[i] = k0[i] ^ 0x36;
    }
    let mut inner = [0u8; MAX_OUTPUT];
    let mut hi = H::new();
    hi.update(&ipad[..b]);
    for part in parts {
        hi.update(part);
    }
    hi.finalize(&mut inner[..olen]);

    // Outer hash: H((K0 ⊕ opad) ‖ inner).
    let mut opad = [0u8; MAX_BLOCK];
    for i in 0..b {
        opad[i] = k0[i] ^ 0x5c;
    }
    let mut ho = H::new();
    ho.update(&opad[..b]);
    ho.update(&inner[..olen]);
    ho.finalize(&mut out[..olen]);
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::sha2::Sha256;

    fn hx(h: &str) -> alloc::vec::Vec<u8> {
        (0..h.len())
            .step_by(2)
            .map(|i| u8::from_str_radix(&h[i..i + 2], 16).unwrap())
            .collect()
    }

    // Smoke KAT: RFC 4231 §4.2 test case 1 (HMAC-SHA-256). The full
    // RFC 4231 set and the NIST CAVP HMAC corpus (SHA-1/224/256/384/512,
    // truncated MACs) live in the integration suite `tests/hmac.rs`.
    #[test]
    fn hmac_sha256_smoke() {
        let key = [0x0bu8; 20];
        let mut out = [0u8; 32];
        hmac::<Sha256>(&key, b"Hi There", &mut out);
        assert_eq!(
            out[..],
            hx("b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7")[..]
        );
    }

    // The multi-part form must equal the single-buffer form (contract used
    // by the FIPS 204/205 message helpers); no external corpus covers it.
    #[test]
    fn multi_part_equals_concat() {
        let key = b"the-key";
        let mut a = [0u8; 32];
        let mut b = [0u8; 32];
        hmac::<Sha256>(key, b"opt_rand-then-message", &mut a);
        hmac_multi::<Sha256>(key, &[b"opt_rand-", b"then-message"], &mut b);
        assert_eq!(a, b);
    }
}