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>

//! Shared `#[cfg(test)]` helpers for the in-crate KAT modules
//! ([`ml_dsa::acvp_internal`](crate::ml_dsa), [`slh_dsa::acvp_internal`](crate::slh_dsa)).
//!
//! These KATs exercise the *internal* FIPS algorithms (`*_internal`), which are
//! `pub(crate)` and therefore unreachable from the external `tests/` crate. The
//! public/external/pre-hash conformance lives in `tests/{ml_dsa,slh_dsa}_kat.rs`
//! and drives the public API; this module provides the minimal hand-rolled JSON
//! and `.rsp` parsers those in-crate KATs need (the same dependency-free style as
//! the `tests/` harnesses — `silentops`/zero-dep rules forbid pulling in serde).

#![cfg(test)]

use alloc::string::String;
use alloc::vec::Vec;
use std::collections::HashMap;

/// Absolute path to a file under `quantica/tests/vectors/`, resolved from the
/// crate manifest dir so the in-crate tests do not depend on the CWD.
pub(crate) fn vector_path(rel: &str) -> String {
    alloc::format!("{}/tests/vectors/{}", env!("CARGO_MANIFEST_DIR"), rel)
}

pub(crate) fn hex_to_bytes(hex: &str) -> Vec<u8> {
    let hex = hex.trim();
    (0..hex.len())
        .step_by(2)
        .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap())
        .collect()
}

pub(crate) fn hex_to_32(hex: &str) -> [u8; 32] {
    let v = hex_to_bytes(hex);
    let mut a = [0u8; 32];
    a.copy_from_slice(&v);
    a
}

// ===================== Minimal JSON parser =====================
//
// Mirrors the parser embedded in `tests/ml_dsa_kat.rs`; kept dependency-free.

#[derive(Debug, Clone)]
pub(crate) enum Value {
    Null,
    Bool(bool),
    Number(f64),
    Str(String),
    Array(Vec<Value>),
    Object(HashMap<String, Value>),
}

impl Value {
    pub(crate) fn get_str(&self, key: &str) -> &str {
        match self {
            Value::Object(map) => match map.get(key) {
                Some(Value::Str(s)) => s,
                _ => "",
            },
            _ => "",
        }
    }
    pub(crate) fn get_array(&self, key: &str) -> &[Value] {
        match self {
            Value::Object(map) => match map.get(key) {
                Some(Value::Array(a)) => a,
                _ => panic!("Key '{key}' not found or not an array"),
            },
            _ => panic!("Not an object"),
        }
    }
    pub(crate) fn get_u64(&self, key: &str) -> u64 {
        match self {
            Value::Object(map) => match map.get(key) {
                Some(Value::Number(n)) => *n as u64,
                _ => panic!("Key '{key}' not found or not a number"),
            },
            _ => panic!("Not an object"),
        }
    }
    pub(crate) fn has(&self, key: &str) -> bool {
        matches!(self, Value::Object(map) if map.contains_key(key))
    }
    pub(crate) fn get(&self, key: &str) -> Option<&Value> {
        match self {
            Value::Object(map) => map.get(key),
            _ => None,
        }
    }
}

pub(crate) fn read_json(path: &str) -> Value {
    let content = std::fs::read_to_string(path).unwrap_or_else(|_| panic!("Cannot read {path}"));
    let mut chars = content.chars().peekable();
    parse_value(&mut chars)
}

fn skip_ws(chars: &mut std::iter::Peekable<std::str::Chars>) {
    while let Some(&c) = chars.peek() {
        if c.is_whitespace() {
            chars.next();
        } else {
            break;
        }
    }
}

fn parse_value(chars: &mut std::iter::Peekable<std::str::Chars>) -> Value {
    skip_ws(chars);
    match chars.peek() {
        Some('"') => Value::Str(parse_string(chars)),
        Some('{') => parse_object(chars),
        Some('[') => parse_array(chars),
        Some('t') | Some('f') => parse_bool(chars),
        Some('n') => {
            for _ in 0..4 {
                chars.next();
            }
            Value::Null
        }
        Some(c) if *c == '-' || c.is_ascii_digit() => parse_number(chars),
        other => panic!("Unexpected char: {other:?}"),
    }
}

fn parse_string(chars: &mut std::iter::Peekable<std::str::Chars>) -> String {
    chars.next();
    let mut s = String::new();
    loop {
        match chars.next() {
            Some('"') => return s,
            Some('\\') => match chars.next() {
                Some('n') => s.push('\n'),
                Some('t') => s.push('\t'),
                Some('"') => s.push('"'),
                Some('\\') => s.push('\\'),
                Some('/') => s.push('/'),
                Some('u') => {
                    let hex: String = chars.take(4).collect();
                    let cp = u32::from_str_radix(&hex, 16).unwrap();
                    if let Some(c) = char::from_u32(cp) {
                        s.push(c);
                    }
                }
                other => panic!("Unknown escape: {other:?}"),
            },
            Some(c) => s.push(c),
            None => panic!("Unterminated string"),
        }
    }
}

fn parse_number(chars: &mut std::iter::Peekable<std::str::Chars>) -> Value {
    let mut s = String::new();
    while let Some(&c) = chars.peek() {
        if c == '-' || c == '+' || c == '.' || c == 'e' || c == 'E' || c.is_ascii_digit() {
            s.push(c);
            chars.next();
        } else {
            break;
        }
    }
    Value::Number(s.parse().unwrap())
}

fn parse_bool(chars: &mut std::iter::Peekable<std::str::Chars>) -> Value {
    if chars.peek() == Some(&'t') {
        for _ in 0..4 {
            chars.next();
        }
        Value::Bool(true)
    } else {
        for _ in 0..5 {
            chars.next();
        }
        Value::Bool(false)
    }
}

fn parse_object(chars: &mut std::iter::Peekable<std::str::Chars>) -> Value {
    chars.next();
    let mut map = HashMap::new();
    skip_ws(chars);
    if chars.peek() == Some(&'}') {
        chars.next();
        return Value::Object(map);
    }
    loop {
        skip_ws(chars);
        let key = parse_string(chars);
        skip_ws(chars);
        chars.next();
        let val = parse_value(chars);
        map.insert(key, val);
        skip_ws(chars);
        match chars.peek() {
            Some(',') => {
                chars.next();
            }
            Some('}') => {
                chars.next();
                return Value::Object(map);
            }
            other => panic!("Expected , or }} got {other:?}"),
        }
    }
}

fn parse_array(chars: &mut std::iter::Peekable<std::str::Chars>) -> Value {
    chars.next();
    let mut arr = Vec::new();
    skip_ws(chars);
    if chars.peek() == Some(&']') {
        chars.next();
        return Value::Array(arr);
    }
    loop {
        arr.push(parse_value(chars));
        skip_ws(chars);
        match chars.peek() {
            Some(',') => {
                chars.next();
            }
            Some(']') => {
                chars.next();
                return Value::Array(arr);
            }
            other => panic!("Expected , or ] got {other:?}"),
        }
    }
}

// ===================== Minimal `.rsp` parser =====================
//
// Mirrors the parser in `tests/kat_extra.rs` for the `det_raw` ML-DSA vectors.

/// Parse a NIST `.rsp` file into one key→value map per `count = N` block.
pub(crate) fn parse_rsp(path: &str) -> Vec<HashMap<String, String>> {
    let content = std::fs::read_to_string(path).unwrap_or_else(|_| panic!("Cannot read {path}"));
    let mut results = Vec::new();
    let mut current: HashMap<String, String> = HashMap::new();

    for line in content.lines() {
        let line = line.trim();
        if line.is_empty() || line.starts_with('#') {
            if !current.is_empty() {
                results.push(current.clone());
                current.clear();
            }
            continue;
        }
        if let Some(pos) = line.find('=') {
            let key = line[..pos].trim().to_string();
            let val = line[pos + 1..].trim().to_string();
            if key == "count" && !current.is_empty() {
                results.push(current.clone());
                current.clear();
            }
            current.insert(key, val);
        }
    }
    if !current.is_empty() {
        results.push(current);
    }
    results
}