#![deny(unsafe_code)]
#![deny(missing_docs)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::panic)]
use crate::prelude::error::{LatticeArcError, Result};
use subtle::ConstantTimeEq;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SelfTestResult {
Pass,
Fail(String),
}
impl SelfTestResult {
#[must_use]
pub fn is_pass(&self) -> bool {
matches!(self, SelfTestResult::Pass)
}
#[must_use]
pub fn is_fail(&self) -> bool {
matches!(self, SelfTestResult::Fail(_))
}
pub fn to_result(&self) -> Result<()> {
match self {
SelfTestResult::Pass => Ok(()),
SelfTestResult::Fail(msg) => Err(LatticeArcError::ValidationError {
message: format!("FIPS 140-3 self-test failed: {}", msg),
}),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndividualTestResult {
pub algorithm: String,
pub result: SelfTestResult,
pub duration_us: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct SelfTestReport {
pub overall_result: SelfTestResult,
pub tests: Vec<IndividualTestResult>,
pub total_duration_us: u64,
}
#[must_use]
pub fn run_power_up_tests() -> SelfTestResult {
if let Err(e) = integrity_test() {
return SelfTestResult::Fail(format!("Module Integrity Test failed: {}", e));
}
if let Err(e) = kat_sha256() {
return SelfTestResult::Fail(format!("SHA-256 KAT failed: {}", e));
}
if let Err(e) = kat_hkdf_sha256() {
return SelfTestResult::Fail(format!("HKDF-SHA256 KAT failed: {}", e));
}
if let Err(e) = kat_aes_256_gcm() {
return SelfTestResult::Fail(format!("AES-256-GCM KAT failed: {}", e));
}
if let Err(e) = kat_sha3_256() {
return SelfTestResult::Fail(format!("SHA3-256 KAT failed: {}", e));
}
if let Err(e) = kat_hmac_sha256() {
return SelfTestResult::Fail(format!("HMAC-SHA256 KAT failed: {}", e));
}
if let Err(e) = roundtrip_ml_kem_768() {
return SelfTestResult::Fail(format!("ML-KEM-768 roundtrip failed: {}", e));
}
if let Err(e) = kat_ml_dsa() {
return SelfTestResult::Fail(format!("ML-DSA-44 KAT failed: {}", e));
}
if let Err(e) = kat_slh_dsa() {
return SelfTestResult::Fail(format!("SLH-DSA-SHAKE-192s KAT failed: {}", e));
}
if let Err(e) = kat_fn_dsa() {
return SelfTestResult::Fail(format!("FN-DSA roundtrip failed: {}", e));
}
SELF_TEST_PASSED.store(true, Ordering::SeqCst);
SelfTestResult::Pass
}
#[must_use]
pub fn run_power_up_tests_with_report() -> SelfTestReport {
use std::time::Instant;
fn duration_to_us(duration: std::time::Duration) -> u64 {
u64::try_from(duration.as_micros()).unwrap_or(u64::MAX)
}
let start = Instant::now();
let mut tests = Vec::new();
let mut overall_pass = true;
let integrity_start = Instant::now();
let integrity_outcome = integrity_test();
let integrity_result = match &integrity_outcome {
Ok(()) => SelfTestResult::Pass,
Err(e) => {
overall_pass = false;
SelfTestResult::Fail(e.to_string())
}
};
tests.push(IndividualTestResult {
algorithm: "Module-Integrity".to_string(),
result: integrity_result,
duration_us: Some(duration_to_us(integrity_start.elapsed())),
});
if integrity_outcome.is_err() {
set_module_error(ModuleErrorCode::IntegrityFailure);
const SKIPPED_TESTS: &[&str] = &[
"SHA-256",
"HKDF-SHA256",
"AES-256-GCM",
"SHA3-256",
"HMAC-SHA256",
"ML-KEM-768",
"ML-DSA-44",
"SLH-DSA-SHAKE-192s",
"FN-DSA-512",
];
for name in SKIPPED_TESTS {
tests.push(IndividualTestResult {
algorithm: (*name).to_string(),
result: SelfTestResult::Fail(
"Not run: inhibited by integrity-test failure (FIPS 140-3 §9.2.2)".to_string(),
),
duration_us: None,
});
}
return SelfTestReport {
overall_result: SelfTestResult::Fail(
"Module-Integrity Test failed; downstream KATs inhibited per FIPS 140-3 §9.2.2"
.to_string(),
),
tests,
total_duration_us: duration_to_us(start.elapsed()),
};
}
let sha_start = Instant::now();
let sha_result = match kat_sha256() {
Ok(()) => SelfTestResult::Pass,
Err(e) => {
overall_pass = false;
SelfTestResult::Fail(e.to_string())
}
};
tests.push(IndividualTestResult {
algorithm: "SHA-256".to_string(),
result: sha_result,
duration_us: Some(duration_to_us(sha_start.elapsed())),
});
let hkdf_start = Instant::now();
let hkdf_result = match kat_hkdf_sha256() {
Ok(()) => SelfTestResult::Pass,
Err(e) => {
overall_pass = false;
SelfTestResult::Fail(e.to_string())
}
};
tests.push(IndividualTestResult {
algorithm: "HKDF-SHA256".to_string(),
result: hkdf_result,
duration_us: Some(duration_to_us(hkdf_start.elapsed())),
});
let aes_start = Instant::now();
let aes_result = match kat_aes_256_gcm() {
Ok(()) => SelfTestResult::Pass,
Err(e) => {
overall_pass = false;
SelfTestResult::Fail(e.to_string())
}
};
tests.push(IndividualTestResult {
algorithm: "AES-256-GCM".to_string(),
result: aes_result,
duration_us: Some(duration_to_us(aes_start.elapsed())),
});
let sha3_start = Instant::now();
let sha3_result = match kat_sha3_256() {
Ok(()) => SelfTestResult::Pass,
Err(e) => {
overall_pass = false;
SelfTestResult::Fail(e.to_string())
}
};
tests.push(IndividualTestResult {
algorithm: "SHA3-256".to_string(),
result: sha3_result,
duration_us: Some(duration_to_us(sha3_start.elapsed())),
});
let hmac_start = Instant::now();
let hmac_result = match kat_hmac_sha256() {
Ok(()) => SelfTestResult::Pass,
Err(e) => {
overall_pass = false;
SelfTestResult::Fail(e.to_string())
}
};
tests.push(IndividualTestResult {
algorithm: "HMAC-SHA256".to_string(),
result: hmac_result,
duration_us: Some(duration_to_us(hmac_start.elapsed())),
});
let kem_start = Instant::now();
let kem_result = match roundtrip_ml_kem_768() {
Ok(()) => SelfTestResult::Pass,
Err(e) => {
overall_pass = false;
SelfTestResult::Fail(e.to_string())
}
};
tests.push(IndividualTestResult {
algorithm: "ML-KEM-768".to_string(),
result: kem_result,
duration_us: Some(duration_to_us(kem_start.elapsed())),
});
let mldsa_start = Instant::now();
let mldsa_result = match kat_ml_dsa() {
Ok(()) => SelfTestResult::Pass,
Err(e) => {
overall_pass = false;
SelfTestResult::Fail(e.to_string())
}
};
tests.push(IndividualTestResult {
algorithm: "ML-DSA-44".to_string(),
result: mldsa_result,
duration_us: Some(duration_to_us(mldsa_start.elapsed())),
});
let slhdsa_start = Instant::now();
let slhdsa_result = match kat_slh_dsa() {
Ok(()) => SelfTestResult::Pass,
Err(e) => {
overall_pass = false;
SelfTestResult::Fail(e.to_string())
}
};
tests.push(IndividualTestResult {
algorithm: "SLH-DSA-SHAKE-192s".to_string(),
result: slhdsa_result,
duration_us: Some(duration_to_us(slhdsa_start.elapsed())),
});
let fndsa_start = Instant::now();
let fndsa_result = match kat_fn_dsa() {
Ok(()) => SelfTestResult::Pass,
Err(e) => {
overall_pass = false;
SelfTestResult::Fail(e.to_string())
}
};
tests.push(IndividualTestResult {
algorithm: "FN-DSA-512".to_string(),
result: fndsa_result,
duration_us: Some(duration_to_us(fndsa_start.elapsed())),
});
let overall_result = if overall_pass {
SELF_TEST_PASSED.store(true, Ordering::SeqCst);
SelfTestResult::Pass
} else {
set_module_error(ModuleErrorCode::SelfTestFailure);
let failed: Vec<_> =
tests.iter().filter(|t| t.result.is_fail()).map(|t| t.algorithm.clone()).collect();
SelfTestResult::Fail(format!("Failed tests: {}", failed.join(", ")))
};
SelfTestReport { overall_result, tests, total_duration_us: duration_to_us(start.elapsed()) }
}
pub fn kat_sha256() -> Result<()> {
use crate::primitives::hash::sha256;
const INPUT: &[u8] = b"abc";
const EXPECTED: [u8; 32] = [
0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22,
0x23, 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c, 0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00,
0x15, 0xad,
];
let result = sha256(INPUT).map_err(|e| LatticeArcError::ValidationError {
message: format!("SHA-256 KAT: hash computation failed: {}", e),
})?;
if bool::from(result.ct_eq(&EXPECTED)) {
Ok(())
} else {
Err(LatticeArcError::ValidationError {
message: "SHA-256 KAT: computed hash does not match expected value".to_string(),
})
}
}
pub fn kat_hkdf_sha256() -> Result<()> {
use crate::primitives::kdf::hkdf;
const IKM: [u8; 22] = [
0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
];
const SALT: [u8; 13] =
[0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c];
const INFO: [u8; 10] = [0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9];
const EXPECTED_OKM: [u8; 42] = [
0x3c, 0xb2, 0x5f, 0x25, 0xfa, 0xac, 0xd5, 0x7a, 0x90, 0x43, 0x4f, 0x64, 0xd0, 0x36, 0x2f,
0x2a, 0x2d, 0x2d, 0x0a, 0x90, 0xcf, 0x1a, 0x5a, 0x4c, 0x5d, 0xb0, 0x2d, 0x56, 0xec, 0xc4,
0xc5, 0xbf, 0x34, 0x00, 0x72, 0x08, 0xd5, 0xb8, 0x87, 0x18, 0x58, 0x65,
];
let result = hkdf(&IKM, Some(&SALT), Some(&INFO), 42)?;
if bool::from(result.expose_secret().ct_eq(&EXPECTED_OKM)) {
Ok(())
} else {
Err(LatticeArcError::ValidationError {
message: "HKDF-SHA256 KAT: derived key does not match expected value".to_string(),
})
}
}
pub fn kat_sha3_256() -> Result<()> {
use crate::primitives::hash::sha3::sha3_256;
const INPUT: &[u8] = b"abc";
const EXPECTED: [u8; 32] = [
0x3a, 0x98, 0x5d, 0xa7, 0x4f, 0xe2, 0x25, 0xb2, 0x04, 0x5c, 0x17, 0x2d, 0x6b, 0xd3, 0x90,
0xbd, 0x85, 0x5f, 0x08, 0x6e, 0x3e, 0x9d, 0x52, 0x5b, 0x46, 0xbf, 0xe2, 0x45, 0x11, 0x43,
0x15, 0x32,
];
let result = sha3_256(INPUT);
if bool::from(result.ct_eq(&EXPECTED)) {
Ok(())
} else {
Err(LatticeArcError::ValidationError {
message: "SHA3-256 KAT: computed hash does not match expected value".to_string(),
})
}
}
pub fn kat_hmac_sha256() -> Result<()> {
use crate::primitives::mac::hmac::hmac_sha256;
const KEY: &[u8] = b"Jefe";
const DATA: &[u8] = b"what do ya want for nothing?";
const EXPECTED: [u8; 32] = [
0x5b, 0xdc, 0xc1, 0x46, 0xbf, 0x60, 0x75, 0x4e, 0x6a, 0x04, 0x24, 0x26, 0x08, 0x95, 0x75,
0xc7, 0x5a, 0x00, 0x3f, 0x08, 0x9d, 0x27, 0x39, 0x83, 0x9d, 0xec, 0x58, 0xb9, 0x64, 0xec,
0x38, 0x43,
];
let result = hmac_sha256(KEY, DATA).map_err(|e| LatticeArcError::ValidationError {
message: format!("HMAC-SHA256 KAT: computation failed: {}", e),
})?;
if bool::from(result.ct_eq(&EXPECTED)) {
Ok(())
} else {
Err(LatticeArcError::ValidationError {
message: "HMAC-SHA256 KAT: computed HMAC does not match expected value".to_string(),
})
}
}
pub fn kat_aes_256_gcm() -> Result<()> {
use crate::primitives::aead::{AeadCipher, aes_gcm::AesGcm256};
const KEY: [u8; 32] = [
0x31, 0xbd, 0xad, 0xd9, 0x66, 0x98, 0xc2, 0x04, 0xaa, 0x9c, 0xe1, 0x44, 0x8e, 0xa9, 0x4a,
0xe1, 0xfb, 0x4a, 0x9a, 0x0b, 0x3c, 0x9d, 0x77, 0x3b, 0x51, 0xbb, 0x18, 0x22, 0x66, 0x6b,
0x8f, 0x22,
];
const NONCE: [u8; 12] =
[0x0d, 0x18, 0xe0, 0x6c, 0x7c, 0x72, 0x5a, 0xc9, 0xe3, 0x62, 0xe1, 0xce];
const PLAINTEXT: [u8; 16] = [
0x2d, 0xb5, 0x16, 0x8e, 0x93, 0x25, 0x56, 0xf8, 0x08, 0x9a, 0x06, 0x22, 0x98, 0x1d, 0x01,
0x7d,
];
const EXPECTED_CT: [u8; 16] = [
0xfa, 0x43, 0x62, 0x18, 0x96, 0x61, 0xd1, 0x63, 0xfc, 0xd6, 0xa5, 0x6d, 0x8b, 0xf0, 0x40,
0x5a,
];
const EXPECTED_TAG: [u8; 16] = [
0xd6, 0x36, 0xac, 0x1b, 0xbe, 0xdd, 0x5c, 0xc3, 0xee, 0x72, 0x7d, 0xc2, 0xab, 0x4a, 0x94,
0x89,
];
let cipher = AesGcm256::new(&KEY).map_err(|e| LatticeArcError::ValidationError {
message: format!("AES-256-GCM KAT: cipher initialization failed: {}", e),
})?;
let (ciphertext, tag) =
cipher.encrypt(&NONCE, &PLAINTEXT, None).map_err(|e| LatticeArcError::ValidationError {
message: format!("AES-256-GCM KAT: encryption failed: {}", e),
})?;
if !bool::from(ciphertext.ct_eq(&EXPECTED_CT)) {
return Err(LatticeArcError::ValidationError {
message: "AES-256-GCM KAT: ciphertext does not match expected value".to_string(),
});
}
if !bool::from(tag.ct_eq(&EXPECTED_TAG)) {
return Err(LatticeArcError::ValidationError {
message: "AES-256-GCM KAT: tag does not match expected value".to_string(),
});
}
let decrypted = cipher.decrypt(&NONCE, &EXPECTED_CT, &EXPECTED_TAG, None).map_err(|e| {
LatticeArcError::ValidationError {
message: format!("AES-256-GCM KAT: decryption failed: {}", e),
}
})?;
if bool::from(decrypted.ct_eq(&PLAINTEXT)) {
Ok(())
} else {
Err(LatticeArcError::ValidationError {
message: "AES-256-GCM KAT: decrypted plaintext does not match original".to_string(),
})
}
}
#[cfg(feature = "fips-self-test")]
struct FixedBytesRng {
data: Vec<Vec<u8>>,
}
#[cfg(feature = "fips-self-test")]
impl FixedBytesRng {
fn new() -> Self {
Self { data: Vec::new() }
}
fn push(&mut self, bytes: &[u8]) {
self.data.push(bytes.to_vec());
}
}
#[cfg(feature = "fips-self-test")]
impl rand_core_0_6::RngCore for FixedBytesRng {
fn next_u32(&mut self) -> u32 {
0
}
fn next_u64(&mut self) -> u64 {
0
}
fn fill_bytes(&mut self, out: &mut [u8]) {
out.fill(0);
match self.data.pop() {
Some(bytes) if bytes.len() == out.len() => out.copy_from_slice(&bytes),
Some(bytes) => {
tracing::debug!(
requested = out.len(),
available = bytes.len(),
"FixedBytesRng size mismatch — KAT vector format may have changed upstream"
);
}
None => {
tracing::debug!(
requested = out.len(),
"FixedBytesRng stack underflow — KAT vector exhausted, upstream call sequence likely changed"
);
}
}
}
fn try_fill_bytes(&mut self, out: &mut [u8]) -> core::result::Result<(), rand_core_0_6::Error> {
self.fill_bytes(out);
Ok(())
}
}
#[cfg(feature = "fips-self-test")]
impl rand_core_0_6::CryptoRng for FixedBytesRng {}
pub fn roundtrip_ml_kem_768() -> Result<()> {
use crate::primitives::kem::ml_kem::{MlKem, MlKemSecurityLevel};
let dk = MlKem::generate_decapsulation_keypair(MlKemSecurityLevel::MlKem768).map_err(|e| {
LatticeArcError::ValidationError {
message: format!("ML-KEM-768 roundtrip: key generation failed: {}", e),
}
})?;
let pk = dk.public_key();
if pk.as_bytes().len() != MlKemSecurityLevel::MlKem768.public_key_size() {
return Err(LatticeArcError::ValidationError {
message: format!(
"ML-KEM-768 roundtrip: public key size mismatch: expected {}, got {}",
MlKemSecurityLevel::MlKem768.public_key_size(),
pk.as_bytes().len()
),
});
}
let (ss_encap, ciphertext) =
MlKem::encapsulate(pk).map_err(|e| LatticeArcError::ValidationError {
message: format!("ML-KEM-768 roundtrip: encapsulation failed: {}", e),
})?;
if ciphertext.as_bytes().len() != MlKemSecurityLevel::MlKem768.ciphertext_size() {
return Err(LatticeArcError::ValidationError {
message: format!(
"ML-KEM-768 roundtrip: ciphertext size mismatch: expected {}, got {}",
MlKemSecurityLevel::MlKem768.ciphertext_size(),
ciphertext.as_bytes().len()
),
});
}
let ss_decap = dk.decapsulate(&ciphertext).map_err(|e| LatticeArcError::ValidationError {
message: format!("ML-KEM-768 roundtrip: decapsulation failed: {}", e),
})?;
if !bool::from(ss_encap.expose_secret().ct_eq(ss_decap.expose_secret())) {
return Err(LatticeArcError::ValidationError {
message:
"ML-KEM-768 roundtrip: encapsulated and decapsulated shared secrets do not match"
.to_string(),
});
}
let all_zeros = ss_encap.expose_secret().iter().all(|&b| b == 0);
if all_zeros {
return Err(LatticeArcError::ValidationError {
message: "ML-KEM-768 roundtrip: shared secret is all zeros".to_string(),
});
}
Ok(())
}
pub fn kat_ml_dsa() -> Result<()> {
use crate::primitives::sig::ml_dsa::{MlDsaParameterSet, generate_keypair};
use fips204::ml_dsa_44;
use fips204::traits::{KeyGen, SerDes};
const SEED: [u8; 32] = [
0x93, 0xef, 0x2e, 0x6e, 0xf1, 0xfb, 0x08, 0x99, 0x9d, 0x14, 0x2a, 0xbe, 0x02, 0x95, 0x48,
0x23, 0x70, 0xd3, 0xf4, 0x3b, 0xdb, 0x25, 0x4a, 0x78, 0xe2, 0xb0, 0xd5, 0x16, 0x8e, 0xca,
0x06, 0x5f,
];
let mut rng = FixedBytesRng::new();
rng.push(&SEED);
let (pk_act, sk_act) = ml_dsa_44::KG::try_keygen_with_rng(&mut rng).map_err(|e| {
LatticeArcError::ValidationError {
message: format!("ML-DSA-44 KAT: deterministic keygen failed: {e}"),
}
})?;
let pk_bytes = pk_act.into_bytes();
let sk_bytes = sk_act.into_bytes();
const PK_HEAD: [u8; 32] = [
0xbc, 0x5f, 0xf8, 0x10, 0xeb, 0x08, 0x90, 0x48, 0xb8, 0xab, 0x30, 0x20, 0xa7, 0xbd, 0x3b,
0x16, 0xc0, 0xe0, 0xca, 0x3d, 0x6b, 0x97, 0xe4, 0x64, 0x6c, 0x2c, 0xca, 0xe0, 0xbb, 0xf1,
0x9e, 0xf7,
];
const PK_TAIL: [u8; 32] = [
0x50, 0x33, 0x2f, 0xaf, 0x1a, 0xc2, 0x19, 0x1e, 0x71, 0x71, 0x25, 0xf6, 0x3e, 0x25, 0x86,
0xc4, 0xd8, 0x6d, 0xca, 0x6b, 0xcd, 0x3d, 0x03, 0x8f, 0x9d, 0x3a, 0x7b, 0x66, 0xcb, 0xc7,
0xdf, 0x34,
];
const SK_HEAD: [u8; 32] = [
0xbc, 0x5f, 0xf8, 0x10, 0xeb, 0x08, 0x90, 0x48, 0xb8, 0xab, 0x30, 0x20, 0xa7, 0xbd, 0x3b,
0x16, 0xc0, 0xe0, 0xca, 0x3d, 0x6b, 0x97, 0xe4, 0x64, 0x6c, 0x2c, 0xca, 0xe0, 0xbb, 0xf1,
0x9e, 0xf7,
];
const SK_TAIL: [u8; 32] = [
0x10, 0xd5, 0x19, 0xd3, 0x31, 0xf9, 0xc4, 0x00, 0xaa, 0xe1, 0xe5, 0x0d, 0x48, 0x0c, 0xaa,
0xe5, 0xa1, 0xc0, 0xfa, 0x99, 0xd7, 0x79, 0x24, 0xcf, 0x8d, 0xfe, 0x56, 0xcd, 0x70, 0x92,
0xe7, 0xb9,
];
if pk_bytes.len() != 1312 {
return Err(LatticeArcError::ValidationError {
message: format!("ML-DSA-44 KAT: pk size {} ≠ 1312", pk_bytes.len()),
});
}
if sk_bytes.len() != 2560 {
return Err(LatticeArcError::ValidationError {
message: format!("ML-DSA-44 KAT: sk size {} ≠ 2560", sk_bytes.len()),
});
}
if !bool::from(pk_bytes[..32].ct_eq(&PK_HEAD)) || !bool::from(pk_bytes[1280..].ct_eq(&PK_TAIL))
{
return Err(LatticeArcError::ValidationError {
message: "ML-DSA-44 KAT: pk does not match ACVP vector tcId=1".to_string(),
});
}
if !bool::from(sk_bytes[..32].ct_eq(&SK_HEAD)) || !bool::from(sk_bytes[2528..].ct_eq(&SK_TAIL))
{
return Err(LatticeArcError::ValidationError {
message: "ML-DSA-44 KAT: sk does not match ACVP vector tcId=1".to_string(),
});
}
const TEST_MESSAGE: &[u8] = b"FIPS 140-3 ML-DSA Known Answer Test";
const CONTEXT: &[u8] = b"";
let (public_key, secret_key) = generate_keypair(MlDsaParameterSet::MlDsa44).map_err(|e| {
LatticeArcError::ValidationError {
message: format!("ML-DSA roundtrip: key generation failed: {e}"),
}
})?;
let signature =
secret_key.sign(TEST_MESSAGE, CONTEXT).map_err(|e| LatticeArcError::ValidationError {
message: format!("ML-DSA roundtrip: signing failed: {e}"),
})?;
let is_valid = public_key.verify(TEST_MESSAGE, &signature, CONTEXT).map_err(|e| {
LatticeArcError::ValidationError {
message: format!("ML-DSA roundtrip: verification failed: {e}"),
}
})?;
if !is_valid {
return Err(LatticeArcError::ValidationError {
message: "ML-DSA roundtrip: valid signature was rejected".to_string(),
});
}
const WRONG_MESSAGE: &[u8] = b"FIPS 140-3 ML-DSA Wrong Message";
let is_valid_wrong = public_key.verify(WRONG_MESSAGE, &signature, CONTEXT).map_err(|e| {
LatticeArcError::ValidationError {
message: format!("ML-DSA roundtrip: verification check failed: {e}"),
}
})?;
if is_valid_wrong {
return Err(LatticeArcError::ValidationError {
message: "ML-DSA roundtrip: invalid signature was accepted".to_string(),
});
}
Ok(())
}
pub fn kat_slh_dsa() -> Result<()> {
use crate::primitives::sig::slh_dsa::{SigningKey, SlhDsaSecurityLevel};
use fips205::slh_dsa_shake_192s;
use fips205::traits::{KeyGen, SerDes};
const SK_SEED: [u8; 24] = [
0xbc, 0x95, 0x43, 0xf9, 0x1d, 0x3e, 0x83, 0xdf, 0x79, 0x3a, 0xcc, 0x0b, 0xbc, 0xdf, 0x54,
0x81, 0x06, 0x91, 0xc7, 0x70, 0xf2, 0xdc, 0x5d, 0xad,
];
const SK_PRF: [u8; 24] = [
0x3a, 0xcf, 0xa7, 0x97, 0x32, 0xcd, 0xb7, 0x1d, 0x4e, 0xf1, 0xe9, 0xb2, 0xec, 0xa1, 0xa4,
0x90, 0xf9, 0x77, 0xcb, 0x0c, 0xce, 0x3a, 0xaa, 0xc5,
];
const PK_SEED: [u8; 24] = [
0xf6, 0xc6, 0x4e, 0x2b, 0x66, 0x2b, 0xe5, 0xdd, 0xb6, 0xf9, 0xc2, 0x8c, 0xc6, 0x2c, 0x20,
0xc7, 0x69, 0x7e, 0xfe, 0xda, 0xab, 0x1c, 0x90, 0x28,
];
const PK_EXPECTED: [u8; 48] = [
0xf6, 0xc6, 0x4e, 0x2b, 0x66, 0x2b, 0xe5, 0xdd, 0xb6, 0xf9, 0xc2, 0x8c, 0xc6, 0x2c, 0x20,
0xc7, 0x69, 0x7e, 0xfe, 0xda, 0xab, 0x1c, 0x90, 0x28, 0xac, 0x30, 0xc2, 0x49, 0xf7, 0x5b,
0x8b, 0x7f, 0x44, 0x73, 0x0e, 0x53, 0x41, 0x69, 0x88, 0x53, 0xb3, 0xf4, 0x8b, 0x5d, 0x15,
0x0c, 0x80, 0x2e,
];
const SK_EXPECTED: [u8; 96] = [
0xbc, 0x95, 0x43, 0xf9, 0x1d, 0x3e, 0x83, 0xdf, 0x79, 0x3a, 0xcc, 0x0b, 0xbc, 0xdf, 0x54,
0x81, 0x06, 0x91, 0xc7, 0x70, 0xf2, 0xdc, 0x5d, 0xad, 0x3a, 0xcf, 0xa7, 0x97, 0x32, 0xcd,
0xb7, 0x1d, 0x4e, 0xf1, 0xe9, 0xb2, 0xec, 0xa1, 0xa4, 0x90, 0xf9, 0x77, 0xcb, 0x0c, 0xce,
0x3a, 0xaa, 0xc5, 0xf6, 0xc6, 0x4e, 0x2b, 0x66, 0x2b, 0xe5, 0xdd, 0xb6, 0xf9, 0xc2, 0x8c,
0xc6, 0x2c, 0x20, 0xc7, 0x69, 0x7e, 0xfe, 0xda, 0xab, 0x1c, 0x90, 0x28, 0xac, 0x30, 0xc2,
0x49, 0xf7, 0x5b, 0x8b, 0x7f, 0x44, 0x73, 0x0e, 0x53, 0x41, 0x69, 0x88, 0x53, 0xb3, 0xf4,
0x8b, 0x5d, 0x15, 0x0c, 0x80, 0x2e,
];
let mut rng = FixedBytesRng::new();
rng.push(&PK_SEED);
rng.push(&SK_PRF);
rng.push(&SK_SEED);
let (pk_act, sk_act) = slh_dsa_shake_192s::KG::try_keygen_with_rng(&mut rng).map_err(|e| {
LatticeArcError::ValidationError {
message: format!("SLH-DSA-SHAKE-192s KAT: deterministic keygen failed: {e}"),
}
})?;
let pk_bytes = pk_act.into_bytes();
let sk_bytes = sk_act.into_bytes();
if !bool::from(pk_bytes.ct_eq(&PK_EXPECTED)) {
return Err(LatticeArcError::ValidationError {
message: "SLH-DSA-SHAKE-192s KAT: pk does not match ACVP vector tcId=21".to_string(),
});
}
if !bool::from(sk_bytes.ct_eq(&SK_EXPECTED)) {
return Err(LatticeArcError::ValidationError {
message: "SLH-DSA-SHAKE-192s KAT: sk does not match ACVP vector tcId=21".to_string(),
});
}
const TEST_MESSAGE: &[u8] = b"FIPS 140-3 SLH-DSA Known Answer Test";
let (signing_key, verifying_key) = SigningKey::generate(SlhDsaSecurityLevel::Shake128s)
.map_err(|e| LatticeArcError::ValidationError {
message: format!("SLH-DSA roundtrip: key generation failed: {e}"),
})?;
let signature =
signing_key.sign(TEST_MESSAGE, &[]).map_err(|e| LatticeArcError::ValidationError {
message: format!("SLH-DSA roundtrip: signing failed: {e}"),
})?;
let is_valid = verifying_key.verify(TEST_MESSAGE, &signature, &[]).map_err(|e| {
LatticeArcError::ValidationError {
message: format!("SLH-DSA roundtrip: verification failed: {e}"),
}
})?;
if !is_valid {
return Err(LatticeArcError::ValidationError {
message: "SLH-DSA roundtrip: valid signature was rejected".to_string(),
});
}
const WRONG_MESSAGE: &[u8] = b"FIPS 140-3 SLH-DSA Wrong Message";
let is_valid_wrong = verifying_key.verify(WRONG_MESSAGE, &signature, &[]).map_err(|e| {
LatticeArcError::ValidationError {
message: format!("SLH-DSA roundtrip: verification check failed: {e}"),
}
})?;
if is_valid_wrong {
return Err(LatticeArcError::ValidationError {
message: "SLH-DSA roundtrip: invalid signature was accepted".to_string(),
});
}
Ok(())
}
pub fn kat_fn_dsa() -> Result<()> {
use crate::primitives::sig::fndsa::{FnDsaSecurityLevel, KeyPair};
use rand_core_0_6::OsRng;
const TEST_MESSAGE: &[u8] = b"FIPS 140-3 FN-DSA Known Answer Test";
std::thread::Builder::new()
.stack_size(32 * 1024 * 1024) .spawn(|| -> Result<()> {
let mut rng = OsRng;
let mut keypair = KeyPair::generate_with_rng(&mut rng, FnDsaSecurityLevel::Level512)
.map_err(|e| LatticeArcError::ValidationError {
message: format!("FN-DSA KAT: key generation failed: {}", e),
})?;
let expected_pk_size = FnDsaSecurityLevel::Level512.verifying_key_size();
if keypair.verifying_key().to_bytes().len() != expected_pk_size {
return Err(LatticeArcError::ValidationError {
message: format!(
"FN-DSA KAT: verifying key size mismatch: expected {}, got {}",
expected_pk_size,
keypair.verifying_key().to_bytes().len()
),
});
}
let expected_sk_size = FnDsaSecurityLevel::Level512.signing_key_size();
if keypair.signing_key().to_bytes().len() != expected_sk_size {
return Err(LatticeArcError::ValidationError {
message: format!(
"FN-DSA KAT: signing key size mismatch: expected {}, got {}",
expected_sk_size,
keypair.signing_key().to_bytes().len()
),
});
}
let mut rng = OsRng;
let signature = keypair.sign_with_rng(&mut rng, TEST_MESSAGE).map_err(|e| {
LatticeArcError::ValidationError {
message: format!("FN-DSA KAT: signing failed: {}", e),
}
})?;
let expected_sig_size = FnDsaSecurityLevel::Level512.signature_size();
if signature.len() != expected_sig_size {
return Err(LatticeArcError::ValidationError {
message: format!(
"FN-DSA KAT: signature size mismatch: expected {}, got {}",
expected_sig_size,
signature.len()
),
});
}
let is_valid = keypair.verify(TEST_MESSAGE, &signature).map_err(|e| {
LatticeArcError::ValidationError {
message: format!("FN-DSA KAT: verification failed: {}", e),
}
})?;
if !is_valid {
return Err(LatticeArcError::ValidationError {
message: "FN-DSA KAT: valid signature was rejected".to_string(),
});
}
const WRONG_MESSAGE: &[u8] = b"FIPS 140-3 FN-DSA Wrong Message";
let is_valid_wrong = keypair.verify(WRONG_MESSAGE, &signature).map_err(|e| {
LatticeArcError::ValidationError {
message: format!("FN-DSA KAT: verification check failed: {}", e),
}
})?;
if is_valid_wrong {
return Err(LatticeArcError::ValidationError {
message: "FN-DSA KAT: invalid signature was accepted".to_string(),
});
}
Ok(())
})
.map_err(|e| LatticeArcError::ValidationError {
message: format!("FN-DSA KAT: failed to spawn thread: {}", e),
})?
.join()
.map_err(|_e| LatticeArcError::ValidationError {
message: "FN-DSA KAT: thread panicked".to_string(),
})?
}
fn path_looks_like_latticearc_module(path: &std::path::Path) -> bool {
let Some(file_name) = path.file_name().and_then(|n| n.to_str()) else {
return false;
};
let lower = file_name.to_ascii_lowercase();
let exact_names = [
"liblatticearc.so",
"liblatticearc.dylib",
"latticearc.dll",
"latticearc",
"latticearc.exe",
"latticearc-cli",
"latticearc-cli.exe",
];
if exact_names.iter().any(|n| lower == *n) {
return true;
}
let parent_ok = path.parent().and_then(|p| {
if p.file_name().and_then(|n| n.to_str()) != Some("deps") {
return None;
}
p.ancestors()
.skip(1)
.take(3)
.any(|a| a.file_name().and_then(|n| n.to_str()) == Some("target"))
.then_some(())
});
if parent_ok.is_some() {
let stem = lower.strip_suffix(".exe").unwrap_or(&lower);
if let Some((_crate_name, suffix)) = stem.rsplit_once('-')
&& suffix.len() == 16
&& suffix.chars().all(|c| c.is_ascii_hexdigit())
{
return true;
}
}
false
}
pub fn integrity_test() -> Result<()> {
const INTEGRITY_KEY: &[u8] = crate::types::domains::MODULE_INTEGRITY_HMAC_KEY;
let module_path = std::env::current_exe().map_err(|e| LatticeArcError::ValidationError {
message: format!("Integrity test: cannot locate module binary: {}", e),
})?;
if !path_looks_like_latticearc_module(&module_path) {
return Err(LatticeArcError::ValidationError {
message: format!(
"Integrity test: current_exe() = {:?} does not appear to be a \
LatticeArc library or a binary that statically links it. \
This build cannot verify dynamic-library integrity without \
platform dynamic-loader APIs (forbidden by the workspace \
`unsafe_code` lint). Run the integrity test from a binary \
that statically links latticearc, or supply an external \
library path via the FIPS deployment manifest.",
module_path,
),
});
}
const MAX_MODULE_SIZE: u64 = 512 * 1024 * 1024;
use std::io::Read;
let f = std::fs::File::open(&module_path).map_err(|e| LatticeArcError::ValidationError {
message: format!("Integrity test: cannot open module binary: {e}"),
})?;
let mut module_bytes = Vec::new();
f.take(MAX_MODULE_SIZE + 1).read_to_end(&mut module_bytes).map_err(|e| {
LatticeArcError::ValidationError {
message: format!("Integrity test: cannot read module binary: {e}"),
}
})?;
if module_bytes.len() as u64 > MAX_MODULE_SIZE {
return Err(LatticeArcError::ValidationError {
message: format!("Integrity test: module binary exceeds {MAX_MODULE_SIZE}-byte cap"),
});
}
let computed_hmac = crate::primitives::mac::hmac::hmac_sha256(INTEGRITY_KEY, &module_bytes)
.map_err(|e| LatticeArcError::ValidationError {
message: format!("Integrity test: HMAC computation failed: {}", e),
})?;
mod generated {
include!(concat!(env!("OUT_DIR"), "/integrity_hmac.rs"));
}
let expected_hmac = generated::EXPECTED_HMAC;
let Some(expected_hmac) = expected_hmac else {
tracing::warn!(
hmac_computed = ?computed_hmac.as_slice(),
"FIPS integrity test SKIPPED — no PRODUCTION_HMAC.txt configured. \
Module-integrity verification did NOT run. Provision \
PRODUCTION_HMAC.txt for FIPS 140-3 §9 compliance; under the \
fips-strict-integrity feature, verify_operational will refuse \
to enter operational state until this is configured."
);
#[expect(
clippy::print_stderr,
reason = "Operator-facing diagnostic — must surface even when tracing is unconfigured"
)]
{
eprintln!("WARNING: FIPS Integrity Test SKIPPED (no PRODUCTION_HMAC.txt)");
eprintln!(" Module-integrity verification did NOT run.");
eprintln!(" Computed HMAC: {:02x?}", computed_hmac.as_slice());
eprintln!(
" Build with --features fips (or --features fips-strict-integrity) \
and provision PRODUCTION_HMAC.txt for FIPS 140-3 §9 compliance."
);
}
return Ok(());
};
INTEGRITY_TEST_CONFIGURED.store(true, Ordering::SeqCst);
use subtle::ConstantTimeEq;
let hmac_match = computed_hmac.ct_eq(expected_hmac);
if hmac_match.into() {
Ok(())
} else {
Err(LatticeArcError::ValidationError {
message: "FIPS Integrity Test FAILED: Module binary has been modified or corrupted. \
This is a critical security violation."
.to_string(),
})
}
}
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static SELF_TEST_PASSED: AtomicBool = AtomicBool::new(false);
static INTEGRITY_TEST_CONFIGURED: AtomicBool = AtomicBool::new(false);
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum ModuleErrorCode {
NoError = 0,
SelfTestFailure = 1,
EntropyFailure = 2,
IntegrityFailure = 3,
CriticalCryptoError = 4,
KeyZeroizationFailure = 5,
AuthenticationFailure = 6,
HsmError = 7,
UnknownCriticalError = 255,
}
impl ModuleErrorCode {
#[must_use]
pub fn from_u32(value: u32) -> Self {
match value {
0 => Self::NoError,
1 => Self::SelfTestFailure,
2 => Self::EntropyFailure,
3 => Self::IntegrityFailure,
4 => Self::CriticalCryptoError,
5 => Self::KeyZeroizationFailure,
6 => Self::AuthenticationFailure,
7 => Self::HsmError,
_ => Self::UnknownCriticalError,
}
}
#[must_use]
pub fn is_error(&self) -> bool {
*self != Self::NoError
}
#[must_use]
pub fn description(&self) -> &'static str {
match self {
Self::NoError => "No error",
Self::SelfTestFailure => "FIPS 140-3 self-test failure",
Self::EntropyFailure => "Entropy source failure",
Self::IntegrityFailure => "Software/firmware integrity check failure",
Self::CriticalCryptoError => "Critical cryptographic operation error",
Self::KeyZeroizationFailure => "Sensitive key material zeroization failure",
Self::AuthenticationFailure => "Repeated authentication failures",
Self::HsmError => "Hardware security module error",
Self::UnknownCriticalError => "Unknown critical error",
}
}
}
#[derive(Debug, Clone)]
pub struct ModuleErrorState {
pub error_code: ModuleErrorCode,
pub timestamp: u64,
}
impl ModuleErrorState {
#[must_use]
pub fn is_error(&self) -> bool {
self.error_code.is_error()
}
}
static MODULE_ERROR_CODE: AtomicU32 = AtomicU32::new(0);
static MODULE_ERROR_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
fn current_timestamp() -> u64 {
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
}
pub fn set_module_error(code: ModuleErrorCode) {
let timestamp = current_timestamp();
MODULE_ERROR_CODE.store(code as u32, Ordering::SeqCst);
MODULE_ERROR_TIMESTAMP.store(timestamp, Ordering::SeqCst);
if code.is_error() {
SELF_TEST_PASSED.store(false, Ordering::SeqCst);
}
}
#[must_use]
pub fn get_module_error_state() -> ModuleErrorState {
ModuleErrorState {
error_code: ModuleErrorCode::from_u32(MODULE_ERROR_CODE.load(Ordering::SeqCst)),
timestamp: MODULE_ERROR_TIMESTAMP.load(Ordering::SeqCst),
}
}
#[must_use]
pub fn is_module_operational() -> bool {
let error_code = ModuleErrorCode::from_u32(MODULE_ERROR_CODE.load(Ordering::SeqCst));
!error_code.is_error() && SELF_TEST_PASSED.load(Ordering::SeqCst)
}
#[cfg(any(test, feature = "test-utils"))]
#[doc(hidden)]
pub fn clear_error_state() {
MODULE_ERROR_CODE.store(ModuleErrorCode::NoError as u32, Ordering::SeqCst);
MODULE_ERROR_TIMESTAMP.store(0, Ordering::SeqCst);
SELF_TEST_PASSED.store(false, Ordering::SeqCst);
}
#[cfg(any(test, feature = "test-utils"))]
#[doc(hidden)]
pub fn restore_operational_state() {
MODULE_ERROR_CODE.store(ModuleErrorCode::NoError as u32, Ordering::SeqCst);
MODULE_ERROR_TIMESTAMP.store(0, Ordering::SeqCst);
SELF_TEST_PASSED.store(true, Ordering::SeqCst);
}
#[must_use]
pub fn self_tests_passed() -> bool {
SELF_TEST_PASSED.load(Ordering::SeqCst)
}
#[must_use]
pub fn initialize_and_test() -> SelfTestResult {
let result = run_power_up_tests();
if result.is_pass() {
SELF_TEST_PASSED.store(true, Ordering::SeqCst);
} else {
set_module_error(ModuleErrorCode::SelfTestFailure);
std::process::abort();
}
result
}
pub fn verify_operational() -> Result<()> {
let error_state = get_module_error_state();
if error_state.is_error() {
return Err(LatticeArcError::ValidationError {
message: format!(
"FIPS module not operational: {} (error set at timestamp {})",
error_state.error_code.description(),
error_state.timestamp
),
});
}
if !self_tests_passed() {
return Err(LatticeArcError::ValidationError {
message: "FIPS module not operational: self-tests have not passed".to_string(),
});
}
#[cfg(all(feature = "fips-strict-integrity", not(any(test, feature = "test-utils"))))]
{
if !INTEGRITY_TEST_CONFIGURED.load(Ordering::SeqCst) {
return Err(LatticeArcError::ValidationError {
message: "FIPS module not operational: fips-strict-integrity is enabled but \
no PRODUCTION_HMAC.txt was provisioned, so the pre-operational \
integrity test (FIPS 140-3 §9.2) could not establish module \
authenticity."
.to_string(),
});
}
}
Ok(())
}
#[cfg(test)]
#[expect(
clippy::indexing_slicing,
reason = "indexing into a slice whose length is known at this site"
)]
mod tests {
use super::*;
struct FipsStateGuard {
_not_send_or_sync: core::marker::PhantomData<*mut ()>,
}
impl FipsStateGuard {
const fn new() -> Self {
Self { _not_send_or_sync: core::marker::PhantomData }
}
}
impl Drop for FipsStateGuard {
fn drop(&mut self) {
restore_operational_state();
}
}
#[test]
fn test_sha256_kat_passes() {
assert!(kat_sha256().is_ok());
}
#[test]
fn test_hkdf_sha256_kat_passes() {
assert!(kat_hkdf_sha256().is_ok());
}
#[test]
fn test_aes_256_gcm_kat_passes() {
assert!(kat_aes_256_gcm().is_ok());
}
#[test]
fn test_ml_kem_768_roundtrip_passes() {
assert!(roundtrip_ml_kem_768().is_ok());
}
#[test]
fn test_power_up_tests_pass_succeeds() {
let result = run_power_up_tests();
assert!(result.is_pass(), "Power-up tests should pass: {:?}", result);
}
#[test]
fn test_power_up_tests_with_report_succeeds() {
let report = run_power_up_tests_with_report();
assert!(report.overall_result.is_pass(), "Overall result should pass");
assert!(!report.tests.is_empty(), "Should have individual test results");
for test in &report.tests {
assert!(test.result.is_pass(), "Test {} should pass", test.algorithm);
assert!(test.duration_us.is_some(), "Duration should be measured");
}
}
#[test]
fn test_path_looks_like_latticearc_module_accepts_known_shapes() {
use std::path::PathBuf;
for name in [
"liblatticearc.so",
"liblatticearc.dylib",
"latticearc.dll",
"latticearc",
"latticearc.exe",
"latticearc-cli",
"latticearc-cli.exe",
] {
assert!(
path_looks_like_latticearc_module(&PathBuf::from(name)),
"exact-name binary should be accepted: {name}"
);
}
for path in [
"/work/repo/target/release/deps/latticearc-0123456789abcdef",
"/work/repo/target/debug/deps/audit_regression_signatures-fedcba9876543210",
"/work/repo/target/valgrind/deps/latticearc-0123456789abcdef",
"/work/repo/target/llvm-cov-target/release/deps/latticearc-0123456789abcdef",
] {
assert!(
path_looks_like_latticearc_module(&PathBuf::from(path)),
"cargo-test deps shape should be accepted: {path}"
);
}
}
#[test]
fn test_path_looks_like_latticearc_module_rejects_adversarial_shapes() {
use std::path::PathBuf;
for path in [
"/work/repo/target/release/deps/latticearc-0123456789abcde",
"/work/repo/target/release/deps/latticearc-evil-not-hex-here",
"/work/repo/target/release/latticearc-0123456789abcdef",
"/usr/bin/python3.12",
"/opt/node/bin/node",
"/work/repo/target/some-tool/release/instrumented/deps/foo-0123456789abcdef",
] {
assert!(
!path_looks_like_latticearc_module(&PathBuf::from(path)),
"non-LatticeArc path should be rejected: {path}"
);
}
}
#[test]
fn test_self_test_result_methods_return_correct_values_succeeds() {
let pass = SelfTestResult::Pass;
let fail = SelfTestResult::Fail("test failure".to_string());
assert!(pass.is_pass());
assert!(!pass.is_fail());
assert!(pass.to_result().is_ok());
assert!(!fail.is_pass());
assert!(fail.is_fail());
assert!(fail.to_result().is_err());
}
#[test]
fn test_initialize_and_verify_sets_passed_flag_succeeds() {
let _guard = FipsStateGuard::new();
SELF_TEST_PASSED.store(false, Ordering::SeqCst);
assert!(verify_operational().is_err());
let result = initialize_and_test();
assert!(result.is_pass());
assert!(verify_operational().is_ok());
assert!(self_tests_passed());
}
#[test]
fn test_ml_dsa_kat_passes() {
let result = kat_ml_dsa();
assert!(result.is_ok(), "ML-DSA-44 KAT should pass: {:?}", result);
}
#[test]
fn test_slh_dsa_kat_passes() {
let result = kat_slh_dsa();
assert!(result.is_ok(), "SLH-DSA-SHAKE-192s KAT should pass: {:?}", result);
}
#[test]
fn test_fn_dsa_kat_passes() {
let result = kat_fn_dsa();
assert!(result.is_ok(), "FN-DSA KAT should pass: {:?}", result);
}
#[test]
fn test_integrity_test_returns_ok_when_no_hmac_regardless_of_feature() {
assert!(
integrity_test().is_ok(),
"missing PRODUCTION_HMAC.txt is a deployment-config condition, not tamper — \
integrity_test must Ok so initialize_and_test does not §9.1-abort on it"
);
}
#[test]
fn test_module_error_code_from_u32_fails() {
assert_eq!(ModuleErrorCode::from_u32(0), ModuleErrorCode::NoError);
assert_eq!(ModuleErrorCode::from_u32(1), ModuleErrorCode::SelfTestFailure);
assert_eq!(ModuleErrorCode::from_u32(2), ModuleErrorCode::EntropyFailure);
assert_eq!(ModuleErrorCode::from_u32(3), ModuleErrorCode::IntegrityFailure);
assert_eq!(ModuleErrorCode::from_u32(4), ModuleErrorCode::CriticalCryptoError);
assert_eq!(ModuleErrorCode::from_u32(5), ModuleErrorCode::KeyZeroizationFailure);
assert_eq!(ModuleErrorCode::from_u32(6), ModuleErrorCode::AuthenticationFailure);
assert_eq!(ModuleErrorCode::from_u32(7), ModuleErrorCode::HsmError);
assert_eq!(ModuleErrorCode::from_u32(100), ModuleErrorCode::UnknownCriticalError);
assert_eq!(ModuleErrorCode::from_u32(255), ModuleErrorCode::UnknownCriticalError);
}
#[test]
fn test_module_error_code_is_error_fails() {
assert!(!ModuleErrorCode::NoError.is_error());
assert!(ModuleErrorCode::SelfTestFailure.is_error());
assert!(ModuleErrorCode::EntropyFailure.is_error());
assert!(ModuleErrorCode::IntegrityFailure.is_error());
assert!(ModuleErrorCode::CriticalCryptoError.is_error());
assert!(ModuleErrorCode::KeyZeroizationFailure.is_error());
assert!(ModuleErrorCode::AuthenticationFailure.is_error());
assert!(ModuleErrorCode::HsmError.is_error());
assert!(ModuleErrorCode::UnknownCriticalError.is_error());
}
#[test]
fn test_module_error_code_description_returns_correct_strings_fails() {
assert_eq!(ModuleErrorCode::NoError.description(), "No error");
assert_eq!(ModuleErrorCode::SelfTestFailure.description(), "FIPS 140-3 self-test failure");
assert_eq!(ModuleErrorCode::EntropyFailure.description(), "Entropy source failure");
}
#[test]
fn test_set_and_get_module_error_succeeds() {
let _guard = FipsStateGuard::new();
clear_error_state();
let state = get_module_error_state();
assert!(!state.is_error());
assert_eq!(state.error_code, ModuleErrorCode::NoError);
set_module_error(ModuleErrorCode::SelfTestFailure);
let state = get_module_error_state();
assert!(state.is_error());
assert_eq!(state.error_code, ModuleErrorCode::SelfTestFailure);
assert!(state.timestamp > 0);
clear_error_state();
let state = get_module_error_state();
assert!(!state.is_error());
assert_eq!(state.error_code, ModuleErrorCode::NoError);
assert_eq!(state.timestamp, 0);
}
#[test]
fn test_is_module_operational_succeeds() {
let _guard = FipsStateGuard::new();
clear_error_state();
SELF_TEST_PASSED.store(false, Ordering::SeqCst);
assert!(!is_module_operational());
SELF_TEST_PASSED.store(true, Ordering::SeqCst);
assert!(is_module_operational());
set_module_error(ModuleErrorCode::EntropyFailure);
assert!(!is_module_operational());
clear_error_state();
SELF_TEST_PASSED.store(true, Ordering::SeqCst);
assert!(is_module_operational());
}
#[test]
fn test_verify_operational_with_error_state_fails() {
let _guard = FipsStateGuard::new();
clear_error_state();
let result = initialize_and_test();
assert!(result.is_pass());
assert!(verify_operational().is_ok());
set_module_error(ModuleErrorCode::CriticalCryptoError);
let result = verify_operational();
assert!(result.is_err());
if let Err(LatticeArcError::ValidationError { message }) = result {
assert!(message.contains("Critical cryptographic operation error"));
}
clear_error_state();
let result = initialize_and_test();
assert!(result.is_pass());
assert!(verify_operational().is_ok());
}
#[test]
fn test_set_error_clears_self_test_passed_fails() {
let _guard = FipsStateGuard::new();
clear_error_state();
let result = initialize_and_test();
assert!(result.is_pass());
assert!(self_tests_passed());
set_module_error(ModuleErrorCode::IntegrityFailure);
assert!(!self_tests_passed());
clear_error_state();
}
#[test]
fn test_module_error_state_struct_is_correct() {
let state = ModuleErrorState { error_code: ModuleErrorCode::NoError, timestamp: 0 };
assert!(!state.is_error());
let state =
ModuleErrorState { error_code: ModuleErrorCode::HsmError, timestamp: 1234567890 };
assert!(state.is_error());
}
#[test]
fn test_module_error_code_all_descriptions_return_correct_strings_fails() {
assert_eq!(
ModuleErrorCode::IntegrityFailure.description(),
"Software/firmware integrity check failure"
);
assert_eq!(
ModuleErrorCode::CriticalCryptoError.description(),
"Critical cryptographic operation error"
);
assert_eq!(
ModuleErrorCode::KeyZeroizationFailure.description(),
"Sensitive key material zeroization failure"
);
assert_eq!(
ModuleErrorCode::AuthenticationFailure.description(),
"Repeated authentication failures"
);
assert_eq!(ModuleErrorCode::HsmError.description(), "Hardware security module error");
assert_eq!(ModuleErrorCode::UnknownCriticalError.description(), "Unknown critical error");
}
#[test]
fn test_set_module_error_no_error_does_not_clear_self_test_fails() {
let _guard = FipsStateGuard::new();
clear_error_state();
let result = initialize_and_test();
assert!(result.is_pass());
assert!(self_tests_passed());
set_module_error(ModuleErrorCode::NoError);
assert!(self_tests_passed());
clear_error_state();
}
#[test]
fn test_self_test_result_debug_clone_work_correctly_succeeds() {
let pass = SelfTestResult::Pass;
let cloned = pass.clone();
assert_eq!(pass, cloned);
let debug = format!("{:?}", pass);
assert!(debug.contains("Pass"));
let fail = SelfTestResult::Fail("oops".to_string());
let fail_clone = fail.clone();
assert_eq!(fail, fail_clone);
let debug = format!("{:?}", fail);
assert!(debug.contains("oops"));
}
#[test]
fn test_individual_test_result_fields_succeeds() {
let result = IndividualTestResult {
algorithm: "SHA-256".to_string(),
result: SelfTestResult::Pass,
duration_us: Some(42),
};
assert_eq!(result.algorithm, "SHA-256");
assert!(result.result.is_pass());
assert_eq!(result.duration_us, Some(42));
let cloned = result.clone();
assert_eq!(cloned.algorithm, "SHA-256");
assert_eq!(cloned, result);
let debug = format!("{:?}", result);
assert!(debug.contains("SHA-256"));
}
#[test]
fn test_self_test_report_fields_succeeds() {
let report = run_power_up_tests_with_report();
assert_eq!(report.tests.len(), 10);
assert_eq!(report.tests[0].algorithm, "Module-Integrity");
assert!(report.total_duration_us > 0);
let cloned = report.clone();
assert_eq!(cloned.tests.len(), 10);
let debug = format!("{:?}", report);
assert!(debug.contains("SelfTestReport"));
}
#[test]
fn test_module_error_code_debug_produces_expected_output_fails() {
let code = ModuleErrorCode::SelfTestFailure;
let debug = format!("{:?}", code);
assert!(debug.contains("SelfTestFailure"));
let cloned = code;
assert_eq!(cloned, ModuleErrorCode::SelfTestFailure);
}
#[test]
fn test_module_error_state_debug_clone_work_correctly_fails() {
let state =
ModuleErrorState { error_code: ModuleErrorCode::EntropyFailure, timestamp: 1000 };
let cloned = state.clone();
assert_eq!(cloned.error_code, ModuleErrorCode::EntropyFailure);
assert_eq!(cloned.timestamp, 1000);
let debug = format!("{:?}", state);
assert!(debug.contains("EntropyFailure"));
}
#[test]
fn test_verify_operational_without_self_tests_fails() {
let _guard = FipsStateGuard::new();
clear_error_state();
SELF_TEST_PASSED.store(false, Ordering::SeqCst);
let result = verify_operational();
assert!(result.is_err());
if let Err(LatticeArcError::ValidationError { message }) = result {
assert!(message.contains("self-tests have not passed"));
}
let _ = initialize_and_test();
}
#[test]
fn test_multiple_error_states_in_sequence_fails() {
let _guard = FipsStateGuard::new();
clear_error_state();
set_module_error(ModuleErrorCode::EntropyFailure);
let state = get_module_error_state();
assert_eq!(state.error_code, ModuleErrorCode::EntropyFailure);
set_module_error(ModuleErrorCode::HsmError);
let state = get_module_error_state();
assert_eq!(state.error_code, ModuleErrorCode::HsmError);
set_module_error(ModuleErrorCode::KeyZeroizationFailure);
let state = get_module_error_state();
assert_eq!(state.error_code, ModuleErrorCode::KeyZeroizationFailure);
clear_error_state();
let _ = initialize_and_test();
}
#[test]
fn test_self_test_result_fail_to_result_contains_message_fails() {
let fail = SelfTestResult::Fail("module corrupted".to_string());
let result = fail.to_result();
assert!(result.is_err());
if let Err(LatticeArcError::ValidationError { message }) = result {
assert!(message.contains("module corrupted"));
assert!(message.contains("FIPS 140-3"));
}
}
#[test]
fn test_individual_test_result_with_no_duration_succeeds() {
let result = IndividualTestResult {
algorithm: "TEST".to_string(),
result: SelfTestResult::Fail("error".to_string()),
duration_us: None,
};
assert!(result.result.is_fail());
assert!(result.duration_us.is_none());
let debug = format!("{:?}", result);
assert!(debug.contains("None"));
}
#[test]
fn test_self_test_report_with_failures_has_correct_fields_fails() {
let report = SelfTestReport {
overall_result: SelfTestResult::Fail("SHA-256 failed".to_string()),
tests: vec![
IndividualTestResult {
algorithm: "SHA-256".to_string(),
result: SelfTestResult::Fail("KAT mismatch".to_string()),
duration_us: Some(100),
},
IndividualTestResult {
algorithm: "AES-GCM".to_string(),
result: SelfTestResult::Pass,
duration_us: Some(200),
},
],
total_duration_us: 300,
};
assert!(report.overall_result.is_fail());
assert_eq!(report.tests.len(), 2);
assert!(report.tests[0].result.is_fail());
assert!(report.tests[1].result.is_pass());
let debug = format!("{:?}", report);
assert!(debug.contains("SelfTestReport"));
}
#[test]
fn test_module_error_code_repr_values_fails() {
assert_eq!(ModuleErrorCode::NoError as u32, 0);
assert_eq!(ModuleErrorCode::SelfTestFailure as u32, 1);
assert_eq!(ModuleErrorCode::EntropyFailure as u32, 2);
assert_eq!(ModuleErrorCode::IntegrityFailure as u32, 3);
assert_eq!(ModuleErrorCode::CriticalCryptoError as u32, 4);
assert_eq!(ModuleErrorCode::KeyZeroizationFailure as u32, 5);
assert_eq!(ModuleErrorCode::AuthenticationFailure as u32, 6);
assert_eq!(ModuleErrorCode::HsmError as u32, 7);
assert_eq!(ModuleErrorCode::UnknownCriticalError as u32, 255);
}
#[test]
fn test_module_error_code_from_u32_boundary_fails() {
assert_eq!(ModuleErrorCode::from_u32(8), ModuleErrorCode::UnknownCriticalError);
assert_eq!(ModuleErrorCode::from_u32(128), ModuleErrorCode::UnknownCriticalError);
assert_eq!(ModuleErrorCode::from_u32(254), ModuleErrorCode::UnknownCriticalError);
assert_eq!(ModuleErrorCode::from_u32(u32::MAX), ModuleErrorCode::UnknownCriticalError);
}
#[test]
fn test_module_error_state_no_error_timestamp_zero_fails() {
let _guard = FipsStateGuard::new();
clear_error_state();
let state = get_module_error_state();
assert!(!state.is_error());
assert_eq!(state.timestamp, 0);
}
#[test]
fn test_module_error_state_error_has_nonzero_timestamp_fails() {
let _guard = FipsStateGuard::new();
clear_error_state();
set_module_error(ModuleErrorCode::SelfTestFailure);
let state = get_module_error_state();
assert!(state.is_error());
assert!(state.timestamp > 0);
clear_error_state();
let _ = initialize_and_test();
}
#[test]
fn test_verify_operational_error_message_contains_description_fails() {
let _guard = FipsStateGuard::new();
clear_error_state();
set_module_error(ModuleErrorCode::EntropyFailure);
let result = verify_operational();
assert!(result.is_err());
if let Err(LatticeArcError::ValidationError { message }) = result {
assert!(message.contains("Entropy source failure"));
assert!(message.contains("error set at timestamp"));
}
clear_error_state();
let _ = initialize_and_test();
}
#[test]
fn test_all_error_codes_block_operations_fails() {
let _guard = FipsStateGuard::new();
let error_codes = [
ModuleErrorCode::SelfTestFailure,
ModuleErrorCode::EntropyFailure,
ModuleErrorCode::IntegrityFailure,
ModuleErrorCode::CriticalCryptoError,
ModuleErrorCode::KeyZeroizationFailure,
ModuleErrorCode::AuthenticationFailure,
ModuleErrorCode::HsmError,
ModuleErrorCode::UnknownCriticalError,
];
for code in &error_codes {
clear_error_state();
SELF_TEST_PASSED.store(true, Ordering::SeqCst);
set_module_error(*code);
assert!(!is_module_operational(), "{:?} should block operations", code);
assert!(verify_operational().is_err(), "{:?} should fail verify", code);
}
clear_error_state();
SELF_TEST_PASSED.store(true, Ordering::SeqCst);
}
#[test]
fn test_initialize_and_test_sets_flag_succeeds() {
let _guard = FipsStateGuard::new();
SELF_TEST_PASSED.store(false, Ordering::SeqCst);
clear_error_state();
assert!(!self_tests_passed());
let result = initialize_and_test();
assert!(result.is_pass());
assert!(self_tests_passed());
}
#[test]
fn test_current_timestamp_reasonable_succeeds() {
let ts = current_timestamp();
assert!(ts > 1_577_836_800, "Timestamp should be after 2020");
}
#[test]
fn test_kat_sha256_is_deterministic() {
for _ in 0..5 {
assert!(kat_sha256().is_ok());
}
}
#[test]
fn test_kat_hkdf_sha256_is_deterministic() {
for _ in 0..5 {
assert!(kat_hkdf_sha256().is_ok());
}
}
#[test]
fn test_kat_aes_256_gcm_is_deterministic() {
for _ in 0..5 {
assert!(kat_aes_256_gcm().is_ok());
}
}
#[test]
fn test_roundtrip_ml_kem_768_always_succeeds() {
for _ in 0..3 {
assert!(roundtrip_ml_kem_768().is_ok());
}
}
#[test]
fn test_run_power_up_tests_is_deterministic() {
for _ in 0..3 {
let result = run_power_up_tests();
assert!(result.is_pass());
}
}
#[test]
fn test_run_power_up_tests_with_report_all_pass_succeeds() {
let report = run_power_up_tests_with_report();
assert!(report.overall_result.is_pass());
for test in &report.tests {
assert!(
test.result.is_pass(),
"Test {} should pass but got: {:?}",
test.algorithm,
test.result
);
assert!(test.duration_us.is_some());
}
assert!(report.total_duration_us > 0);
}
#[test]
fn test_kat_sha3_256_passes() {
assert!(kat_sha3_256().is_ok());
}
#[test]
fn test_kat_hmac_sha256_passes() {
assert!(kat_hmac_sha256().is_ok());
}
#[test]
fn test_self_test_report_all_fields_populated_succeeds() {
let report = run_power_up_tests_with_report();
assert!(report.overall_result.is_pass());
assert!(report.tests.len() >= 9, "Should have at least 9 KAT results");
assert!(report.total_duration_us > 0);
for test in &report.tests {
assert!(!test.algorithm.is_empty(), "Algorithm name should not be empty");
assert!(
test.duration_us.is_some(),
"Duration should be measured for {}",
test.algorithm
);
}
}
#[test]
fn test_error_state_timestamp_ordering_fails() {
let _guard = FipsStateGuard::new();
clear_error_state();
set_module_error(ModuleErrorCode::EntropyFailure);
let state1 = get_module_error_state();
let ts1 = state1.timestamp;
set_module_error(ModuleErrorCode::IntegrityFailure);
let state2 = get_module_error_state();
let ts2 = state2.timestamp;
assert!(ts2 >= ts1, "Second timestamp should be >= first");
assert_eq!(state2.error_code, ModuleErrorCode::IntegrityFailure);
clear_error_state();
}
#[test]
fn test_verify_operational_after_reset_succeeds() {
let _guard = FipsStateGuard::new();
set_module_error(ModuleErrorCode::HsmError);
assert!(verify_operational().is_err());
clear_error_state();
let result = initialize_and_test();
assert!(result.is_pass());
assert!(verify_operational().is_ok());
assert!(is_module_operational());
}
}