anubis-age 1.4.0

Post-quantum secure encryption library with hybrid X25519+ML-KEM-1024 mode (internal dependency for anubis-rage)
Documentation
//! FIPS 140-3 Self-Tests
//!
//! Implements power-up self-tests (POST) and conditional self-tests
//! required for FIPS 140-3 compliance.

use std::fmt;
use std::io;

/// FIPS self-test error
#[derive(Debug)]
pub enum FipsError {
    /// Known Answer Test failed
    KatFailed(&'static str),
    /// Pairwise consistency test failed
    PairwiseTestFailed(&'static str),
    /// RNG repetition detected
    RngRepetitionDetected,
    /// Module integrity check failed
    IntegrityCheckFailed,
}

impl fmt::Display for FipsError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            FipsError::KatFailed(alg) => write!(f, "KAT failed for {}", alg),
            FipsError::PairwiseTestFailed(alg) => write!(f, "Pairwise test failed for {}", alg),
            FipsError::RngRepetitionDetected => write!(f, "RNG repetition detected"),
            FipsError::IntegrityCheckFailed => write!(f, "Module integrity check failed"),
        }
    }
}

impl std::error::Error for FipsError {}

impl From<FipsError> for io::Error {
    fn from(e: FipsError) -> Self {
        io::Error::new(io::ErrorKind::Other, e)
    }
}

/// Run all power-up self-tests (POST)
///
/// This must be called when the cryptographic module is loaded.
/// Failure indicates the module is compromised and must not be used.
///
/// Per FIPS 140-3 requirements, this includes:
/// - Module integrity verification
/// - Known Answer Tests (KATs) for all approved algorithms
/// - Pairwise consistency tests for asymmetric algorithms
pub fn run_power_up_tests() -> Result<(), FipsError> {
    // First, verify module integrity (required by FIPS 140-3)
    super::verify_module_integrity()?;

    // Test SHA-512
    test_sha512_kat()?;

    // Test HMAC-SHA512
    test_hmac_sha512_kat()?;

    // Test HKDF-SHA512
    test_hkdf_sha512_kat()?;

    // Test AES-256-GCM-SIV
    test_aes_gcm_siv_kat()?;

    // Test ML-KEM-1024 (basic functionality)
    test_ml_kem_1024_kat()?;

    // Test ML-DSA-87 (basic functionality)
    test_ml_dsa_87_kat()?;

    Ok(())
}

/// SHA-512 Known Answer Test
fn test_sha512_kat() -> Result<(), FipsError> {
    use sha2::{Digest, Sha512};

    // NIST test vector
    let input = b"abc";
    let expected = hex::decode(
        "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a\
         2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f",
    )
    .unwrap();

    let mut hasher = Sha512::new();
    hasher.update(input);
    let result = hasher.finalize();

    if result.as_slice() != expected.as_slice() {
        return Err(FipsError::KatFailed("SHA-512"));
    }

    Ok(())
}

/// HMAC-SHA512 Known Answer Test
fn test_hmac_sha512_kat() -> Result<(), FipsError> {
    use hmac::{Hmac, Mac};
    use sha2::Sha512;

    // NIST test vector
    let key = b"key";
    let message = b"The quick brown fox jumps over the lazy dog";
    let expected = hex::decode(
        "b42af09057bac1e2d41708e48a902e09b5ff7f12ab428a4fe86653c73dd248fb\
         82f948a549f7b791a5b41915ee4d1ec3935357e4e2317250d0372afa2ebeeb3a",
    )
    .unwrap();

    let mut mac = Hmac::<Sha512>::new_from_slice(key).unwrap();
    mac.update(message);
    let result = mac.finalize();

    if result.into_bytes().as_slice() != expected.as_slice() {
        return Err(FipsError::KatFailed("HMAC-SHA512"));
    }

    Ok(())
}

/// HKDF-SHA512 Known Answer Test
fn test_hkdf_sha512_kat() -> Result<(), FipsError> {
    use hkdf::Hkdf;
    use sha2::Sha512;

    // Test vector
    let ikm = b"input key material";
    let salt = b"salt";
    let info = b"info";

    let hkdf = Hkdf::<Sha512>::new(Some(salt), ikm);
    let mut okm = [0u8; 42];
    hkdf.expand(info, &mut okm).unwrap();

    // Basic sanity check - output should not be all zeros
    if okm.iter().all(|&b| b == 0) {
        return Err(FipsError::KatFailed("HKDF-SHA512"));
    }

    Ok(())
}

/// AES-256-GCM-SIV Known Answer Test
fn test_aes_gcm_siv_kat() -> Result<(), FipsError> {
    use aes_gcm_siv::{
        aead::{Aead, KeyInit, Payload},
        Aes256GcmSiv, Nonce,
    };

    // RFC 8452 Appendix C.2 test case 1 (AES-256-GCM-SIV, empty plaintext)
    let key =
        hex::decode("0100000000000000000000000000000000000000000000000000000000000000").unwrap();
    let nonce_bytes = hex::decode("030000000000000000000000").unwrap();
    let nonce = Nonce::from_slice(&nonce_bytes);
    let plaintext = b"";

    let cipher = Aes256GcmSiv::new_from_slice(&key).unwrap();
    let ciphertext = cipher
        .encrypt(
            nonce,
            Payload {
                msg: plaintext,
                aad: b"",
            },
        )
        .map_err(|_| FipsError::KatFailed("AES-256-GCM-SIV encrypt"))?;

    // Expected: authentication tag for empty plaintext
    // From RFC 8452 Appendix C.2 test case 1
    let expected = hex::decode("07f5f4169bbf55a8400cd47ea6fd400f").unwrap();

    if ciphertext != expected {
        return Err(FipsError::KatFailed("AES-256-GCM-SIV"));
    }

    // Test decryption
    let decrypted = cipher
        .decrypt(
            nonce,
            Payload {
                msg: &ciphertext,
                aad: b"",
            },
        )
        .map_err(|_| FipsError::KatFailed("AES-256-GCM-SIV decrypt"))?;

    if decrypted != plaintext {
        return Err(FipsError::KatFailed("AES-256-GCM-SIV round-trip"));
    }

    Ok(())
}

/// ML-KEM-1024 Known Answer Test (basic functionality)
fn test_ml_kem_1024_kat() -> Result<(), FipsError> {
    use oqs::kem::Kem;

    let kem = Kem::new(oqs::kem::Algorithm::MlKem1024)
        .map_err(|_| FipsError::KatFailed("ML-KEM-1024 init"))?;

    // Generate keypair
    let (pk, sk) = kem
        .keypair()
        .map_err(|_| FipsError::KatFailed("ML-KEM-1024 keygen"))?;

    // Encapsulate
    let (ct, ss1) = kem
        .encapsulate(&pk)
        .map_err(|_| FipsError::KatFailed("ML-KEM-1024 encapsulate"))?;

    // Decapsulate
    let ss2 = kem
        .decapsulate(&sk, &ct)
        .map_err(|_| FipsError::KatFailed("ML-KEM-1024 decapsulate"))?;

    // Shared secrets must match (pairwise consistency test)
    if ss1.as_ref() != ss2.as_ref() {
        return Err(FipsError::PairwiseTestFailed("ML-KEM-1024"));
    }

    Ok(())
}

/// ML-DSA-87 Known Answer Test (basic functionality)
fn test_ml_dsa_87_kat() -> Result<(), FipsError> {
    use oqs::sig::Sig;

    let sig_alg = Sig::new(oqs::sig::Algorithm::MlDsa87)
        .map_err(|_| FipsError::KatFailed("ML-DSA-87 init"))?;

    // Generate keypair
    let (pk, sk) = sig_alg
        .keypair()
        .map_err(|_| FipsError::KatFailed("ML-DSA-87 keygen"))?;

    let message = b"FIPS 140-3 self-test message";

    // Sign
    let signature = sig_alg
        .sign(message, &sk)
        .map_err(|_| FipsError::KatFailed("ML-DSA-87 sign"))?;

    // Verify
    sig_alg
        .verify(message, &signature, &pk)
        .map_err(|_| FipsError::PairwiseTestFailed("ML-DSA-87"))?;

    Ok(())
}

/// Conditional self-test: Pairwise consistency test for ML-KEM keypair
pub fn test_mlkem_pairwise(pk: &[u8], sk: &[u8]) -> Result<(), FipsError> {
    use oqs::kem::Kem;

    let kem = Kem::new(oqs::kem::Algorithm::MlKem1024)
        .map_err(|_| FipsError::PairwiseTestFailed("ML-KEM-1024 init"))?;

    // Convert byte slices to key objects
    let pk_obj = kem
        .public_key_from_bytes(pk)
        .ok_or(FipsError::PairwiseTestFailed(
            "ML-KEM-1024 invalid public key",
        ))?;
    let sk_obj = kem
        .secret_key_from_bytes(sk)
        .ok_or(FipsError::PairwiseTestFailed(
            "ML-KEM-1024 invalid secret key",
        ))?;

    // Encapsulate with the generated public key
    let (ct, ss1) = kem
        .encapsulate(pk_obj)
        .map_err(|_| FipsError::PairwiseTestFailed("ML-KEM-1024 encapsulate"))?;

    // Decapsulate with the generated secret key
    let ss2 = kem
        .decapsulate(sk_obj, &ct)
        .map_err(|_| FipsError::PairwiseTestFailed("ML-KEM-1024 decapsulate"))?;

    // Shared secrets must match
    if ss1.as_ref() != ss2.as_ref() {
        return Err(FipsError::PairwiseTestFailed("ML-KEM-1024"));
    }

    Ok(())
}

/// Conditional self-test: Pairwise consistency test for ML-DSA keypair
pub fn test_mldsa_pairwise(pk: &[u8], sk: &[u8]) -> Result<(), FipsError> {
    use oqs::sig::Sig;

    let sig_alg = Sig::new(oqs::sig::Algorithm::MlDsa87)
        .map_err(|_| FipsError::PairwiseTestFailed("ML-DSA-87 init"))?;

    // Convert byte slices to key objects
    let pk_obj = sig_alg
        .public_key_from_bytes(pk)
        .ok_or(FipsError::PairwiseTestFailed(
            "ML-DSA-87 invalid public key",
        ))?;
    let sk_obj = sig_alg
        .secret_key_from_bytes(sk)
        .ok_or(FipsError::PairwiseTestFailed(
            "ML-DSA-87 invalid secret key",
        ))?;

    let test_message = b"Pairwise consistency test";

    // Sign with the generated secret key
    let signature = sig_alg
        .sign(test_message, sk_obj)
        .map_err(|_| FipsError::PairwiseTestFailed("ML-DSA-87 sign"))?;

    // Verify with the generated public key
    sig_alg
        .verify(test_message, &signature, pk_obj)
        .map_err(|_| FipsError::PairwiseTestFailed("ML-DSA-87 verify"))?;

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_power_up_tests() {
        // All self-tests should pass
        assert!(run_power_up_tests().is_ok());
    }

    #[test]
    fn test_sha512() {
        assert!(test_sha512_kat().is_ok());
    }

    #[test]
    fn test_hmac_sha512() {
        assert!(test_hmac_sha512_kat().is_ok());
    }

    #[test]
    fn test_hkdf_sha512() {
        assert!(test_hkdf_sha512_kat().is_ok());
    }

    #[test]
    fn test_aes_gcm_siv() {
        assert!(test_aes_gcm_siv_kat().is_ok());
    }

    #[test]
    fn test_ml_kem_1024() {
        assert!(test_ml_kem_1024_kat().is_ok());
    }

    #[test]
    fn test_ml_dsa_87() {
        assert!(test_ml_dsa_87_kat().is_ok());
    }

    #[test]
    fn test_module_integrity() {
        use crate::fips::{module_integrity_info, verify_module_integrity};

        // Test that module integrity verification works
        assert!(verify_module_integrity().is_ok());

        // Test that module integrity info is available
        let info = module_integrity_info();
        assert_eq!(info.hash.len(), 64); // SHA-256 hex = 64 chars
        assert!(info.hash.chars().all(|c| c.is_ascii_hexdigit()));
        assert!(!info.build_timestamp.is_empty());

        // Print info for audit trail
        println!("{}", info);
    }
}