krypteia-quantica 0.2.0

Pure-Rust post-quantum cryptography: FIPS 203 ML-KEM, FIPS 204 ML-DSA, and FIPS 205 SLH-DSA. First-order arithmetic masking, shuffled NTT, FORS recompute-and-compare redundancy, constant-time rejection sampling. Targets embedded (no_std), STM32 M0/M4/M33, ESP32-C3 RISC-V. Zero runtime dependencies.
Documentation
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Cédric Mesnil <cslashm@pm.me>

//! SHA-2 instantiation of the SLH-DSA tweakable hash functions
//! (FIPS 205 §11.2), built on the shared `krypteia-tessera` SHA-256 /
//! SHA-512 / MGF1 / HMAC primitives.
//!
//! Security-category split (FIPS 205 §11.2.1 vs §11.2.2):
//! - `F` and `PRF` are **always SHA-256** (the hot inner-loop hashes),
//!   with `PK.seed` zero-padded to one SHA-256 block (`64 − n` zeros).
//! - `H_msg`, `PRF_msg`, `T_l`, `H` use SHA-256 at category 1 (`n = 16`)
//!   and SHA-512 at categories 3/5 (`n ∈ {24, 32}`); the SHA-512 path
//!   pads `PK.seed` to one SHA-512 block (`128 − n` zeros).
//!
//! The padded form is `Trunc_n(SHA-x(PK.seed ‖ toByte(0, B−n) ‖ ADRS^c ‖ …))`
//! where `B = block size` and `ADRS^c` is the 22-byte compressed address
//! (FIPS 205 §11.2). `H_msg` is `MGF1-SHA-x(R ‖ PK.seed ‖ SHA-x(R ‖
//! PK.seed ‖ PK.root ‖ M), m)` and `PRF_msg` is `Trunc_n(HMAC-SHA-x(
//! SK.prf, opt_rand ‖ M))`.
//!
//! # Side-channel posture
//!
//! `PRF` (over `SK.seed`), `PRF_msg` (over `SK.prf`) and `F`/`H`/`T_l`
//! (over secret WOTS+/FORS values) take secret input. SHA-256/SHA-512
//! are data-oblivious (no secret-dependent branch, index, or table), and
//! the `P::N == 16` family split, the `B − n` padding, and the `Trunc_n`
//! copy are all on public/compile-time lengths — classic constant-time.
//!
//! **DPA caveat:** `PRF_msg = HMAC-SHA-2(SK.prf, …)` is carry-DPA-leaky
//! on `SK.prf` (Belenky et al., TCHES 2023/3); the leak is in the SHA-2
//! modular additions, not a branch. A masked SHA-2 is out of scope here.
//! `[CESTI: SLH-DSA-SHA2 PRF_msg exposes SK.prf to CDPA — document the
//! residual risk and the masked-SHA-2 roadmap item, mirroring arcana
//! T2-D.]`
//!
//! # Stability (hazmat tier)
//!
//! This module is part of the `hazmat` expert building-block layer: it is
//! public only with the `hazmat` Cargo feature, for KAT/test-vector tooling
//! and research. Read each function's invariants before use. **No stability,
//! misuse-resistance, or side-channel promise** applies to this tier — its
//! API may change in any release. The typed facade is the guaranteed API.

use super::address::Adrs;
use super::hash::TweakableHash;
use super::params::Params;
use tessera::{Digest, Sha256, Sha512, hmac_multi, mgf1};

/// SHA-2 instantiation of the tweakable hash family (FIPS 205 §11.2).
pub struct Sha2Family;

/// `Trunc_n(SHA-x(PK.seed ‖ toByte(0, B−n) ‖ ADRS^c ‖ parts…))`, the
/// padded-block form shared by `F`, `PRF`, `H`, and `T_l`. The pad length
/// `B − n` (with `B = D::BLOCK_LEN`) yields `64 − n` for SHA-256 and
/// `128 − n` for SHA-512.
#[inline]
fn padded_sha2<D: Digest>(pk_seed: &[u8], adrs_c: &[u8; 22], parts: &[&[u8]], n: usize, out: &mut [u8]) {
    let mut h = D::new();
    h.update(pk_seed); // PK.seed (n bytes)
    let zeros = [0u8; 128];
    h.update(&zeros[..D::BLOCK_LEN - n]); // toByte(0, B − n)
    h.update(adrs_c); // 22-byte compressed address
    for p in parts {
        h.update(p);
    }
    let mut digest = [0u8; 64];
    h.finalize(&mut digest[..D::OUTPUT_LEN]);
    out[..n].copy_from_slice(&digest[..n]); // Trunc_n
}

/// `H_msg = MGF1-SHA-x(R ‖ PK.seed ‖ SHA-x(R ‖ PK.seed ‖ PK.root ‖ M), |out|)`.
#[inline]
fn h_msg_sha2<D: Digest>(r: &[u8], pk_seed: &[u8], pk_root: &[u8], m: &[u8], out: &mut [u8]) {
    // inner = SHA-x(R ‖ PK.seed ‖ PK.root ‖ M)
    let mut h = D::new();
    h.update(r);
    h.update(pk_seed);
    h.update(pk_root);
    h.update(m);
    let mut inner = [0u8; 64];
    h.finalize(&mut inner[..D::OUTPUT_LEN]);
    // mgfSeed = R ‖ PK.seed ‖ inner   (the R ‖ PK.seed prefix is repeated outside the inner hash)
    let mut seed = [0u8; 128];
    let mut pos = 0;
    seed[pos..pos + r.len()].copy_from_slice(r);
    pos += r.len();
    seed[pos..pos + pk_seed.len()].copy_from_slice(pk_seed);
    pos += pk_seed.len();
    seed[pos..pos + D::OUTPUT_LEN].copy_from_slice(&inner[..D::OUTPUT_LEN]);
    pos += D::OUTPUT_LEN;
    mgf1::<D>(&seed[..pos], out);
}

/// `PRF_msg = Trunc_n(HMAC-SHA-x(SK.prf, opt_rand ‖ M))`.
#[inline]
fn prf_msg_sha2<D: Digest>(sk_prf: &[u8], opt_rand: &[u8], m: &[u8], n: usize, out: &mut [u8]) {
    let mut tag = [0u8; 64];
    hmac_multi::<D>(sk_prf, &[opt_rand, m], &mut tag[..D::OUTPUT_LEN]);
    out[..n].copy_from_slice(&tag[..n]); // Trunc_n
}

impl TweakableHash for Sha2Family {
    fn h_msg_into<P: Params>(r: &[u8], pk_seed: &[u8], pk_root: &[u8], m: &[u8], out: &mut [u8]) {
        if P::N == 16 {
            h_msg_sha2::<Sha256>(r, pk_seed, pk_root, m, out);
        } else {
            h_msg_sha2::<Sha512>(r, pk_seed, pk_root, m, out);
        }
    }

    fn prf_into<P: Params>(pk_seed: &[u8], sk_seed: &[u8], adrs: &Adrs, out: &mut [u8]) {
        // PRF is always SHA-256.
        padded_sha2::<Sha256>(pk_seed, &adrs.as_bytes_compressed(), &[sk_seed], P::N, out);
    }

    fn prf_msg_into<P: Params>(sk_prf: &[u8], opt_rand: &[u8], m: &[u8], out: &mut [u8]) {
        if P::N == 16 {
            prf_msg_sha2::<Sha256>(sk_prf, opt_rand, m, P::N, out);
        } else {
            prf_msg_sha2::<Sha512>(sk_prf, opt_rand, m, P::N, out);
        }
    }

    fn t_l_into<P: Params>(pk_seed: &[u8], adrs: &Adrs, m: &[u8], out: &mut [u8]) {
        let c = adrs.as_bytes_compressed();
        if P::N == 16 {
            padded_sha2::<Sha256>(pk_seed, &c, &[m], P::N, out);
        } else {
            padded_sha2::<Sha512>(pk_seed, &c, &[m], P::N, out);
        }
    }

    fn hash_h_into<P: Params>(pk_seed: &[u8], adrs: &Adrs, m1: &[u8], m2: &[u8], out: &mut [u8]) {
        let c = adrs.as_bytes_compressed();
        if P::N == 16 {
            padded_sha2::<Sha256>(pk_seed, &c, &[m1, m2], P::N, out);
        } else {
            padded_sha2::<Sha512>(pk_seed, &c, &[m1, m2], P::N, out);
        }
    }

    fn f_hash_into<P: Params>(pk_seed: &[u8], adrs: &Adrs, m1: &[u8], out: &mut [u8]) {
        // F is always SHA-256.
        padded_sha2::<Sha256>(pk_seed, &adrs.as_bytes_compressed(), &[m1], P::N, out);
    }
}