montana/Монтана-Протокол/Код/crates/mt-crypto-native/tests/nist_acvp_kat.rs

331 lines
10 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// NIST ACVP-Server KAT cross-check для ML-DSA-65 + ML-KEM-768.
//
// Источник fixtures: https://github.com/usnistgov/ACVP-Server
// (Apache-2.0 licensed, public domain test vectors из NIST CAVP).
//
// Закрывает [C-6] Req #11 (preventive NIST KAT в Phase 1) + Req #13
// (differential testing mandatory) для consensus-critical KeyGen path.
//
// Критерий conformance: байт-в-байт совпадение output OpenSSL 3.5.5 LTS
// (наш backend) с NIST published expected values на seed inputs из
// FIPS 204 Algorithm 1 (ML-DSA KeyGen_internal) и FIPS 203 Algorithm
// 16 (ML-KEM KeyGen_internal).
//
// Закрывает F-3 audit finding M1-F (KAT-baselines self-derived без
// independent oracle).
use mt_crypto_native::{
mt_keypair_from_seed_mldsa, mt_keypair_from_seed_mlkem, mt_sign_mldsa, mt_sign_mldsa_ctx,
MLDSA65_PUBKEY_SIZE, MLDSA65_SECRETKEY_SIZE, MLDSA65_SEED_SIZE, MLDSA65_SIGNATURE_SIZE,
MLKEM768_PUBKEY_SIZE, MLKEM768_SECRETKEY_SIZE, MLKEM768_SEED_SIZE, MT_OK,
};
use serde::Deserialize;
use std::fs;
use std::path::PathBuf;
fn fixture_path(name: &str) -> PathBuf {
let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
p.push("tests");
p.push("fixtures");
p.push("nist_acvp");
p.push(name);
p
}
fn hex_decode(s: &str) -> Vec<u8> {
let s = s.trim();
assert!(
s.len() % 2 == 0,
"hex string must have even length: {}",
s.len()
);
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("invalid hex"))
.collect()
}
#[derive(Deserialize)]
struct MlDsaKeyGenFile {
source: String,
algorithm: String,
mode: String,
tests: Vec<MlDsaKeyGenTest>,
}
#[derive(Deserialize)]
struct MlDsaKeyGenTest {
#[serde(rename = "tcId")]
tc_id: u32,
seed: String,
pk: String,
sk: String,
}
#[derive(Deserialize)]
struct MlKemKeyGenFile {
source: String,
algorithm: String,
mode: String,
tests: Vec<MlKemKeyGenTest>,
}
#[derive(Deserialize)]
struct MlKemKeyGenTest {
#[serde(rename = "tcId")]
tc_id: u32,
d: String,
z: String,
ek: String,
dk: String,
}
#[derive(Deserialize)]
struct MlDsaSigGenFile {
source: String,
algorithm: String,
mode: String,
tests: Vec<MlDsaSigGenTest>,
}
#[derive(Deserialize)]
struct MlDsaSigGenTest {
#[serde(rename = "tcId")]
tc_id: u32,
sk: String,
message: String,
context: String,
signature: String,
}
#[test]
fn nist_acvp_ml_dsa_65_keygen_byte_exact() {
let path = fixture_path("ml_dsa_65_keygen.json");
let raw = fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("read fixture {}: {}", path.display(), e));
let kat: MlDsaKeyGenFile = serde_json::from_str(&raw).expect("parse ml_dsa_65_keygen.json");
assert_eq!(kat.algorithm, "ML-DSA-65");
assert_eq!(kat.mode, "KeyGen");
assert!(!kat.tests.is_empty(), "no tests in fixture");
println!(
"NIST ACVP ML-DSA-65 KeyGen — {} tests from: {}",
kat.tests.len(),
kat.source
);
let mut passed = 0u32;
for t in &kat.tests {
let seed_bytes = hex_decode(&t.seed);
assert_eq!(
seed_bytes.len(),
MLDSA65_SEED_SIZE,
"tcId={} seed wrong length",
t.tc_id
);
let expected_pk = hex_decode(&t.pk);
let expected_sk = hex_decode(&t.sk);
assert_eq!(expected_pk.len(), MLDSA65_PUBKEY_SIZE);
assert_eq!(expected_sk.len(), MLDSA65_SECRETKEY_SIZE);
let mut pk = vec![0u8; MLDSA65_PUBKEY_SIZE];
let mut sk = vec![0u8; MLDSA65_SECRETKEY_SIZE];
let rc = unsafe {
mt_keypair_from_seed_mldsa(seed_bytes.as_ptr(), pk.as_mut_ptr(), sk.as_mut_ptr())
};
assert_eq!(
rc, MT_OK,
"tcId={} mt_keypair_from_seed_mldsa failed: {}",
t.tc_id, rc
);
assert_eq!(
pk, expected_pk,
"tcId={} ML-DSA-65 pubkey diverges from NIST FIPS 204 expected",
t.tc_id
);
assert_eq!(
sk, expected_sk,
"tcId={} ML-DSA-65 secretkey diverges from NIST FIPS 204 expected",
t.tc_id
);
passed += 1;
}
println!(
"PASS: {}/{} ML-DSA-65 KeyGen NIST KAT byte-exact",
passed,
kat.tests.len()
);
}
#[test]
fn nist_acvp_ml_kem_768_keygen_byte_exact() {
let path = fixture_path("ml_kem_768_keygen.json");
let raw = fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("read fixture {}: {}", path.display(), e));
let kat: MlKemKeyGenFile = serde_json::from_str(&raw).expect("parse ml_kem_768_keygen.json");
assert_eq!(kat.algorithm, "ML-KEM-768");
assert_eq!(kat.mode, "KeyGen");
assert!(!kat.tests.is_empty(), "no tests in fixture");
println!(
"NIST ACVP ML-KEM-768 KeyGen — {} tests from: {}",
kat.tests.len(),
kat.source
);
let mut passed = 0u32;
for t in &kat.tests {
let d_bytes = hex_decode(&t.d);
let z_bytes = hex_decode(&t.z);
assert_eq!(d_bytes.len(), 32, "tcId={} d wrong length", t.tc_id);
assert_eq!(z_bytes.len(), 32, "tcId={} z wrong length", t.tc_id);
// FIPS 203 §6.1: ML-KEM.KeyGen_internal принимает (d, z) → seed = d || z.
// OpenSSL OSSL_PKEY_PARAM_ML_KEM_SEED ожидает 64-байтовый seed
// в этом порядке (d первым, z вторым).
let mut seed = [0u8; MLKEM768_SEED_SIZE];
seed[..32].copy_from_slice(&d_bytes);
seed[32..].copy_from_slice(&z_bytes);
let expected_ek = hex_decode(&t.ek);
let expected_dk = hex_decode(&t.dk);
assert_eq!(expected_ek.len(), MLKEM768_PUBKEY_SIZE);
assert_eq!(expected_dk.len(), MLKEM768_SECRETKEY_SIZE);
let mut pk = vec![0u8; MLKEM768_PUBKEY_SIZE];
let mut sk = vec![0u8; MLKEM768_SECRETKEY_SIZE];
let rc =
unsafe { mt_keypair_from_seed_mlkem(seed.as_ptr(), pk.as_mut_ptr(), sk.as_mut_ptr()) };
assert_eq!(
rc, MT_OK,
"tcId={} mt_keypair_from_seed_mlkem failed: {}",
t.tc_id, rc
);
assert_eq!(
pk, expected_ek,
"tcId={} ML-KEM-768 ek (pubkey) diverges from NIST FIPS 203 expected",
t.tc_id
);
assert_eq!(
sk, expected_dk,
"tcId={} ML-KEM-768 dk (secretkey) diverges from NIST FIPS 203 expected",
t.tc_id
);
passed += 1;
}
println!(
"PASS: {}/{} ML-KEM-768 KeyGen NIST KAT byte-exact",
passed,
kat.tests.len()
);
}
#[test]
fn nist_acvp_ml_dsa_65_siggen_deterministic_external_pure_all15() {
// Deterministic ML-DSA-65 Sign per FIPS 204 Algorithm 2 (deterministic
// variant). External interface, no preHash, **все 15 cases** (1 empty
// context + 14 non-empty context, 0..255 байт) tgId=3 в NIST CAVP
// ML-DSA-sigGen-FIPS204.
//
// Использует mt_sign_mldsa_ctx (новая API с context parameter) для
// полного покрытия SigGen NIST KAT. Empty context case также проходит
// через mt_sign_mldsa_ctx с ctx_len=0 — equivalent к старому
// mt_sign_mldsa (cross-verified в `siggen_empty_ctx_equivalence` тесте
// ниже).
let path = fixture_path("ml_dsa_65_siggen_det_external_pure_all15.json");
let raw = fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("read fixture {}: {}", path.display(), e));
let kat: MlDsaSigGenFile = serde_json::from_str(&raw).expect("parse all15");
assert!(kat.algorithm.starts_with("ML-DSA-65"));
assert!(kat.mode.contains("SigGen"));
assert!(!kat.tests.is_empty(), "no tests in fixture");
println!(
"NIST ACVP ML-DSA-65 SigGen (deterministic, external, pure) — {} tests from: {}",
kat.tests.len(),
kat.source
);
for t in &kat.tests {
let sk_bytes = hex_decode(&t.sk);
assert_eq!(sk_bytes.len(), MLDSA65_SECRETKEY_SIZE);
let msg_bytes = hex_decode(&t.message);
let ctx_bytes = hex_decode(&t.context);
let expected_sig = hex_decode(&t.signature);
assert_eq!(expected_sig.len(), MLDSA65_SIGNATURE_SIZE);
let mut sig = vec![0u8; MLDSA65_SIGNATURE_SIZE];
let rc = unsafe {
mt_sign_mldsa_ctx(
sk_bytes.as_ptr(),
msg_bytes.as_ptr(),
msg_bytes.len(),
ctx_bytes.as_ptr(),
ctx_bytes.len(),
sig.as_mut_ptr(),
)
};
assert_eq!(
rc, MT_OK,
"tcId={} mt_sign_mldsa_ctx failed: {}",
t.tc_id, rc
);
assert_eq!(
sig, expected_sig,
"tcId={} ctx_len={} ML-DSA-65 deterministic signature diverges from NIST FIPS 204 expected",
t.tc_id,
ctx_bytes.len()
);
}
println!(
"PASS: {}/{} ML-DSA-65 SigGen NIST KAT byte-exact (1 empty ctx + 14 non-empty)",
kat.tests.len(),
kat.tests.len()
);
}
#[test]
fn siggen_empty_ctx_equivalence() {
// Verify что mt_sign_mldsa(sk, msg) ≡ mt_sign_mldsa_ctx(sk, msg, &[], 0).
// Empty context default — semantic equivalent для Montana usage pattern.
let seed = [0x42u8; MLDSA65_SEED_SIZE];
let mut pk = vec![0u8; MLDSA65_PUBKEY_SIZE];
let mut sk = vec![0u8; MLDSA65_SECRETKEY_SIZE];
unsafe {
assert_eq!(
mt_keypair_from_seed_mldsa(seed.as_ptr(), pk.as_mut_ptr(), sk.as_mut_ptr()),
MT_OK
);
}
let msg = b"Montana sign equivalence test message";
let mut sig_no_ctx = vec![0u8; MLDSA65_SIGNATURE_SIZE];
let mut sig_empty_ctx = vec![0u8; MLDSA65_SIGNATURE_SIZE];
unsafe {
assert_eq!(
mt_sign_mldsa(
sk.as_ptr(),
msg.as_ptr(),
msg.len(),
sig_no_ctx.as_mut_ptr()
),
MT_OK
);
let empty_ctx: [u8; 0] = [];
assert_eq!(
mt_sign_mldsa_ctx(
sk.as_ptr(),
msg.as_ptr(),
msg.len(),
empty_ctx.as_ptr(),
0,
sig_empty_ctx.as_mut_ptr(),
),
MT_OK
);
}
assert_eq!(
sig_no_ctx, sig_empty_ctx,
"mt_sign_mldsa и mt_sign_mldsa_ctx с empty context должны давать одинаковую подпись"
);
}