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

331 lines
10 KiB
Rust
Raw Normal View History

// 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 должны давать одинаковую подпись"
);
}