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>

//! SP 800-108 — key-based key derivation functions (KBKDF), NIST SP 800-108r1.
//!
//! Derives keying material from a **key-derivation key** `KDK` (a
//! cryptographically strong key, *not* a password) using a PRF — here
//! `HMAC-H` for any [`Digest`] `H`. Three modes (SP 800-108r1 §4):
//!
//! - [`counter`] (§4.1): `K(i) = PRF(KDK, [i]₃₂ ‖ FixedInput)`.
//! - [`feedback`] (§4.2): `K(i) = PRF(KDK, K(i-1) ‖ [i]₃₂ ‖ FixedInput)`,
//!   `K(0) = IV`.
//! - [`double_pipeline`] (§4.3): `A(0) = FixedInput`, `A(i) = PRF(KDK,
//!   A(i-1))`, `K(i) = PRF(KDK, A(i) ‖ [i]₃₂ ‖ FixedInput)`.
//!
//! This module uses the standard fixed-input layout
//! `FixedInput = Label ‖ 0x00 ‖ Context ‖ [L]₃₂` with a 32-bit big-endian
//! counter and a 32-bit big-endian length field `L = 8 · out.len()` (bits),
//! the counter placed **before** the fixed input — the common instantiation
//! (matches the ACVP / `cryptography` KBKDF defaults). Output is the
//! concatenation `K(1) ‖ K(2) ‖ …` truncated to `out.len()`.
//!
//! For low-entropy passwords use [`pbkdf2`](mod@crate::pbkdf2) / scrypt /
//! Argon2 instead; SP 800-108 assumes a strong `KDK`.
//!
//! # Side-channel posture
//!
//! Inherits [`crate::hmac`](mod@crate::hmac): data-oblivious HMAC core; the
//! `KDK` is secret (HMAC key). Counter, label, context, length and loop
//! counts are public. The HMAC-key DPA caveat on SHA-2 applies to the `KDK`.

use crate::Digest;
use crate::hmac::hmac_multi;
use alloc::vec;

// Fixed-input length field and counter are 32-bit big-endian per this
// module's fixed layout.
#[inline]
fn l_bits(out_len: usize) -> [u8; 4] {
    ((out_len as u32).wrapping_mul(8)).to_be_bytes()
}

/// SP 800-108 §4.1 counter-mode KBKDF over HMAC.
///
/// # Parameters
/// - `kdk`: the key-derivation key `K_IN` (HMAC key).
/// - `label`: the `Label` field of the fixed input (the derivation purpose).
/// - `context`: the `Context` field of the fixed input.
/// - `out`: destination; its bit-length becomes the `[L]₃₂` field, and it is
///   filled with `⌈out.len()/HashLen⌉` PRF invocations.
pub fn counter<H: Digest>(kdk: &[u8], label: &[u8], context: &[u8], out: &mut [u8]) {
    let hlen = H::OUTPUT_LEN;
    let l = l_bits(out.len());
    let mut ki = vec![0u8; hlen];
    let mut offset = 0;
    let mut i: u32 = 1;
    while offset < out.len() {
        hmac_multi::<H>(kdk, &[&i.to_be_bytes(), label, &[0x00], context, &l], &mut ki);
        let take = core::cmp::min(hlen, out.len() - offset);
        out[offset..offset + take].copy_from_slice(&ki[..take]);
        offset += take;
        i += 1;
    }
}

/// SP 800-108 §4.2 feedback-mode KBKDF over HMAC.
///
/// # Parameters
/// - `kdk`: the key-derivation key `K_IN` (HMAC key).
/// - `label`: the `Label` field of the fixed input.
/// - `context`: the `Context` field of the fixed input.
/// - `iv`: the initial value `K(0)` fed back into the first PRF call; may be
///   empty.
/// - `out`: destination; its bit-length becomes the `[L]₃₂` field.
pub fn feedback<H: Digest>(kdk: &[u8], label: &[u8], context: &[u8], iv: &[u8], out: &mut [u8]) {
    let hlen = H::OUTPUT_LEN;
    let l = l_bits(out.len());
    let mut k = vec![0u8; hlen]; // K(i)
    let mut prev: &[u8] = iv; // K(i-1); starts at the IV
    let mut offset = 0;
    let mut i: u32 = 1;
    while offset < out.len() {
        let mut ki = vec![0u8; hlen];
        hmac_multi::<H>(kdk, &[prev, &i.to_be_bytes(), label, &[0x00], context, &l], &mut ki);
        k = ki;
        let take = core::cmp::min(hlen, out.len() - offset);
        out[offset..offset + take].copy_from_slice(&k[..take]);
        offset += take;
        i += 1;
        prev = &k;
    }
}

/// SP 800-108 §4.3 double-pipeline (feedback-with-counter) mode over HMAC.
///
/// # Parameters
/// - `kdk`: the key-derivation key `K_IN` (HMAC key).
/// - `label`: the `Label` field of the fixed input.
/// - `context`: the `Context` field of the fixed input.
/// - `out`: destination; its bit-length becomes the `[L]₃₂` field. Each block
///   runs the `A(i)` pipeline PRF and the `K(i)` output PRF.
pub fn double_pipeline<H: Digest>(kdk: &[u8], label: &[u8], context: &[u8], out: &mut [u8]) {
    let hlen = H::OUTPUT_LEN;
    let l = l_bits(out.len());
    // A(0) = FixedInput; A(i) = PRF(KDK, A(i-1)).
    let mut a = vec![0u8; hlen];
    let mut ki = vec![0u8; hlen];
    let mut offset = 0;
    let mut i: u32 = 1;
    while offset < out.len() {
        if i == 1 {
            // A(1) = PRF(KDK, FixedInput).
            hmac_multi::<H>(kdk, &[label, &[0x00], context, &l], &mut a);
        } else {
            // A(i) = PRF(KDK, A(i-1)).
            let a_prev = a.clone();
            hmac_multi::<H>(kdk, &[&a_prev], &mut a);
        }
        // K(i) = PRF(KDK, A(i) ‖ [i] ‖ FixedInput).
        hmac_multi::<H>(kdk, &[&a, &i.to_be_bytes(), label, &[0x00], context, &l], &mut ki);
        let take = core::cmp::min(hlen, out.len() - offset);
        out[offset..offset + take].copy_from_slice(&ki[..take]);
        offset += take;
        i += 1;
    }
}

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

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

    // Counter-mode smoke, gold-checked against Python `cryptography` KBKDFHMAC.
    // The full 3-mode cross-check lives in the integration suite `tests/kdf.rs`.
    #[test]
    fn counter_sha256_smoke() {
        let kdk = hx("00112233445566778899aabbccddeeff");
        let mut out = [0u8; 32];
        counter::<Sha256>(&kdk, b"label", b"context", &mut out);
        assert_eq!(
            out[..],
            hx("0221984163d8ce7453e80fe3d91b9f262bf7d316fd29aeb415d6ae903adae919")[..]
        );
    }
}