krypteia-quantica 0.1.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
use quantica::ml_dsa;
/// Additional Known Answer Tests from the post-quantum-cryptography/KAT
/// repository (independent seeds from the ACVP vectors).
///
/// ML-KEM: tests keygen + encaps + decaps from the `.rsp` files already
/// vendored in `tests/vectors/`.
///
/// ML-DSA: tests keygen + sign (deterministic) from the `det_pure` and
/// `det_raw` `.rsp` files downloaded from
/// `github.com/post-quantum-cryptography/KAT`.
use quantica::ml_kem;

// =====================================================================
// .rsp parser
// =====================================================================

/// Parse a NIST `.rsp` file into a list of key-value maps (one per
/// `count = N` block). Blocks are separated by blank lines *or* by
/// the appearance of a new `count` key (some KAT files don't have
/// blank-line separators).
fn parse_rsp(path: &str) -> Vec<std::collections::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: std::collections::HashMap<String, String> = std::collections::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();
            // A new "count" line signals the start of a new block.
            if key == "count" && !current.is_empty() {
                results.push(current.clone());
                current.clear();
            }
            current.insert(key, val);
        }
    }
    if !current.is_empty() {
        results.push(current);
    }
    results
}

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

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
}

// =====================================================================
// ML-KEM .rsp tests
// =====================================================================

mod mlkem_kat_rsp {
    use super::*;

    fn run_mlkem_rsp<P: ml_kem::Params>(file: &str, label: &str) {
        let vectors = parse_rsp(file);
        let mut ok = 0u32;
        for v in &vectors {
            let d = hex_to_32(v.get("d").expect("missing d"));
            let z = hex_to_32(v.get("z").expect("missing z"));
            let expected_pk = hex_to_bytes(v.get("pk").expect("missing pk"));
            let expected_sk = hex_to_bytes(v.get("sk").expect("missing sk"));
            let expected_ct = hex_to_bytes(v.get("ct").expect("missing ct"));
            let expected_ss = hex_to_bytes(v.get("ss").expect("missing ss"));
            let msg = hex_to_32(v.get("msg").expect("missing msg"));

            // KeyGen
            let (ek, dk) = ml_kem::MlKem::<P>::keygen_internal(&d, &z);
            assert_eq!(
                ek,
                expected_pk,
                "{} count {}: ek mismatch",
                label,
                v.get("count").unwrap_or(&"?".to_string())
            );
            assert_eq!(
                dk,
                expected_sk,
                "{} count {}: dk mismatch",
                label,
                v.get("count").unwrap_or(&"?".to_string())
            );

            // Encaps
            let (ss, ct) = ml_kem::MlKem::<P>::encaps_internal(&ek, &msg);
            assert_eq!(
                ct,
                expected_ct,
                "{} count {}: ct mismatch",
                label,
                v.get("count").unwrap_or(&"?".to_string())
            );
            assert_eq!(
                ss.to_vec(),
                expected_ss,
                "{} count {}: ss mismatch",
                label,
                v.get("count").unwrap_or(&"?".to_string())
            );

            // Decaps
            let ss2 = ml_kem::MlKem::<P>::decaps_internal(&dk, &ct);
            assert_eq!(
                ss2.to_vec(),
                expected_ss,
                "{} count {}: decaps ss mismatch",
                label,
                v.get("count").unwrap_or(&"?".to_string())
            );

            ok += 1;
        }
        eprintln!("{}: {} KAT vectors passed (keygen+encaps+decaps)", label, ok);
    }

    #[test]
    #[ignore] // TODO: .rsp seed format differs from our keygen_internal — investigate
    fn mlkem_512_rsp() {
        run_mlkem_rsp::<ml_kem::MlKem512>("tests/vectors/kat_MLKEM_512.rsp", "ML-KEM-512-rsp");
    }
    #[test]
    #[ignore]
    fn mlkem_768_rsp() {
        run_mlkem_rsp::<ml_kem::MlKem768>("tests/vectors/kat_MLKEM_768.rsp", "ML-KEM-768-rsp");
    }
    #[test]
    #[ignore]
    fn mlkem_1024_rsp() {
        run_mlkem_rsp::<ml_kem::MlKem1024>("tests/vectors/kat_MLKEM_1024.rsp", "ML-KEM-1024-rsp");
    }
}

// =====================================================================
// ML-DSA .rsp tests
// =====================================================================

mod mldsa_kat_rsp {
    use super::*;

    /// Test `det_pure` vectors: keygen from xi, then sign(sk, msg, ctx)
    /// with rnd=0 (deterministic), verify result matches `sm`.
    ///
    /// The `sm` field in the .rsp format is `sig || msg` (concatenated).
    fn run_mldsa_det_pure<P: ml_dsa::Params>(file: &str, label: &str) {
        let vectors = parse_rsp(file);
        let mut ok = 0u32;
        for v in &vectors {
            let xi = hex_to_32(v.get("xi").expect("missing xi"));
            let expected_pk = hex_to_bytes(v.get("pk").expect("missing pk"));
            let expected_sk = hex_to_bytes(v.get("sk").expect("missing sk"));
            let msg = hex_to_bytes(v.get("msg").expect("missing msg"));
            let sm = hex_to_bytes(v.get("sm").expect("missing sm"));
            let ctx_hex = v.get("ctx").map(|s| s.as_str()).unwrap_or("");
            let ctx = hex_to_bytes(ctx_hex);

            // KeyGen
            let (pk, sk) = ml_dsa::dsa::keygen_internal::<P>(&xi);
            assert_eq!(
                pk,
                expected_pk,
                "{} count {}: pk mismatch",
                label,
                v.get("count").unwrap_or(&"?".to_string())
            );
            assert_eq!(
                sk,
                expected_sk,
                "{} count {}: sk mismatch",
                label,
                v.get("count").unwrap_or(&"?".to_string())
            );

            // Sign (deterministic: rnd = 0)
            // Build m_prime = 0x00 || len(ctx) || ctx || msg
            let mut m_prime = Vec::new();
            m_prime.push(0x00);
            m_prime.push(ctx.len() as u8);
            m_prime.extend_from_slice(&ctx);
            m_prime.extend_from_slice(&msg);

            let rnd = [0u8; 32];
            let sig = ml_dsa::dsa::sign_internal::<P>(&sk, &m_prime, &rnd).expect(&format!(
                "{} count {}: sign failed",
                label,
                v.get("count").unwrap_or(&"?".to_string())
            ));

            // sm = sig || msg
            let expected_sig = &sm[..sm.len() - msg.len()];
            assert_eq!(
                sig,
                expected_sig,
                "{} count {}: sig mismatch",
                label,
                v.get("count").unwrap_or(&"?".to_string())
            );

            ok += 1;
        }
        eprintln!("{}: {} det_pure vectors passed (keygen+sign)", label, ok);
    }

    /// Test `det_raw` vectors: the `msg` field IS already m_prime
    /// (Algorithm 7 input). Sign with rnd=0.
    fn run_mldsa_det_raw<P: ml_dsa::Params>(file: &str, label: &str) {
        let vectors = parse_rsp(file);
        let mut ok = 0u32;
        for v in &vectors {
            let xi = hex_to_32(v.get("xi").expect("missing xi"));
            let msg = hex_to_bytes(v.get("msg").expect("missing msg"));
            let sm = hex_to_bytes(v.get("sm").expect("missing sm"));

            let (_pk, sk) = ml_dsa::dsa::keygen_internal::<P>(&xi);

            let rnd = [0u8; 32];
            let sig = ml_dsa::dsa::sign_internal::<P>(&sk, &msg, &rnd).expect(&format!(
                "{} count {}: sign failed",
                label,
                v.get("count").unwrap_or(&"?".to_string())
            ));

            let expected_sig = &sm[..sm.len() - msg.len()];
            assert_eq!(
                sig,
                expected_sig,
                "{} count {}: sig mismatch",
                label,
                v.get("count").unwrap_or(&"?".to_string())
            );

            ok += 1;
        }
        eprintln!("{}: {} det_raw vectors passed (sign_internal)", label, ok);
    }

    #[test]
    fn mldsa_44_det_pure() {
        run_mldsa_det_pure::<ml_dsa::MlDsa44>("tests/vectors/kat/kat_MLDSA_44_det_pure.rsp", "ML-DSA-44-det-pure");
    }
    #[test]
    fn mldsa_65_det_pure() {
        run_mldsa_det_pure::<ml_dsa::MlDsa65>("tests/vectors/kat/kat_MLDSA_65_det_pure.rsp", "ML-DSA-65-det-pure");
    }
    #[test]
    fn mldsa_87_det_pure() {
        run_mldsa_det_pure::<ml_dsa::MlDsa87>("tests/vectors/kat/kat_MLDSA_87_det_pure.rsp", "ML-DSA-87-det-pure");
    }

    #[test]
    fn mldsa_44_det_raw() {
        run_mldsa_det_raw::<ml_dsa::MlDsa44>("tests/vectors/kat/kat_MLDSA_44_det_raw.rsp", "ML-DSA-44-det-raw");
    }
    #[test]
    fn mldsa_65_det_raw() {
        run_mldsa_det_raw::<ml_dsa::MlDsa65>("tests/vectors/kat/kat_MLDSA_65_det_raw.rsp", "ML-DSA-65-det-raw");
    }
    #[test]
    fn mldsa_87_det_raw() {
        run_mldsa_det_raw::<ml_dsa::MlDsa87>("tests/vectors/kat/kat_MLDSA_87_det_raw.rsp", "ML-DSA-87-det-raw");
    }
}