#![deny(unsafe_code)]
#![deny(missing_docs)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::panic)]
use thiserror::Error;
pub const PCT_TEST_MESSAGE: &[u8] = b"FIPS PCT test";
pub const PCT_EMPTY_CONTEXT: &[u8] = &[];
#[derive(Debug, Error, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum PctError {
#[error("PCT signing failed: {0}")]
SigningFailed(String),
#[error("PCT verification failed: {0}")]
VerificationFailed(String),
#[error("PCT failed: signature verification returned false - key pair is inconsistent")]
KeyPairInconsistent,
#[error("PCT failed: parameter mismatch between public and secret keys")]
ParameterMismatch,
}
pub type PctResult<T> = Result<T, PctError>;
#[cfg(feature = "fips-self-test")]
fn enter_pct_error_state() {
crate::primitives::self_test::set_module_error(
crate::primitives::self_test::ModuleErrorCode::SelfTestFailure,
);
}
#[cfg(not(feature = "fips-self-test"))]
fn enter_pct_error_state() {}
fn pct_finalize(is_valid: bool) -> PctResult<()> {
if is_valid {
Ok(())
} else {
enter_pct_error_state();
Err(PctError::KeyPairInconsistent)
}
}
pub fn pct_ml_dsa(
public_key: &crate::primitives::sig::ml_dsa::MlDsaPublicKey,
secret_key: &crate::primitives::sig::ml_dsa::MlDsaSecretKey,
) -> PctResult<()> {
if public_key.parameter_set() != secret_key.parameter_set() {
return Err(PctError::ParameterMismatch);
}
let signature = secret_key
.sign(PCT_TEST_MESSAGE, PCT_EMPTY_CONTEXT)
.map_err(|e| PctError::SigningFailed(e.to_string()))?;
let is_valid = public_key
.verify(PCT_TEST_MESSAGE, &signature, PCT_EMPTY_CONTEXT)
.map_err(|e| PctError::VerificationFailed(e.to_string()))?;
pct_finalize(is_valid)
}
pub fn pct_ml_kem(
security_level: crate::primitives::kem::ml_kem::MlKemSecurityLevel,
) -> PctResult<()> {
use crate::primitives::kem::ml_kem::MlKem;
use subtle::ConstantTimeEq;
let dk = MlKem::generate_decapsulation_keypair(security_level)
.map_err(|e| PctError::SigningFailed(format!("ML-KEM key generation failed: {}", e)))?;
let (ss_encap, ct) = MlKem::encapsulate(dk.public_key())
.map_err(|e| PctError::SigningFailed(format!("ML-KEM encapsulation failed: {}", e)))?;
let ss_decap = dk
.decapsulate(&ct)
.map_err(|e| PctError::VerificationFailed(format!("ML-KEM decapsulation failed: {}", e)))?;
let is_valid = bool::from(ss_encap.as_bytes().ct_eq(ss_decap.as_bytes()));
pct_finalize(is_valid)
}
pub fn pct_slh_dsa(
verifying_key: &crate::primitives::sig::slh_dsa::VerifyingKey,
signing_key: &crate::primitives::sig::slh_dsa::SigningKey,
) -> PctResult<()> {
if verifying_key.security_level() != signing_key.security_level() {
return Err(PctError::ParameterMismatch);
}
let signature = signing_key
.sign(PCT_TEST_MESSAGE, &[])
.map_err(|e| PctError::SigningFailed(e.to_string()))?;
let is_valid = verifying_key
.verify(PCT_TEST_MESSAGE, &signature, &[])
.map_err(|e| PctError::VerificationFailed(e.to_string()))?;
pct_finalize(is_valid)
}
pub fn pct_fn_dsa(
verifying_key: &crate::primitives::sig::fndsa::VerifyingKey,
signing_key: &mut crate::primitives::sig::fndsa::SigningKey,
) -> PctResult<()> {
use rand::rngs::OsRng;
if verifying_key.security_level() != signing_key.security_level() {
return Err(PctError::ParameterMismatch);
}
let mut rng = OsRng;
let signature = signing_key
.sign_with_rng(&mut rng, PCT_TEST_MESSAGE)
.map_err(|e| PctError::SigningFailed(e.to_string()))?;
let is_valid = verifying_key
.verify(PCT_TEST_MESSAGE, &signature)
.map_err(|e| PctError::VerificationFailed(e.to_string()))?;
pct_finalize(is_valid)
}
pub fn pct_fn_dsa_keypair(keypair: &mut crate::primitives::sig::fndsa::KeyPair) -> PctResult<()> {
use rand::rngs::OsRng;
let mut rng = OsRng;
let signature = keypair
.sign_with_rng(&mut rng, PCT_TEST_MESSAGE)
.map_err(|e| PctError::SigningFailed(e.to_string()))?;
let is_valid = keypair
.verify(PCT_TEST_MESSAGE, &signature)
.map_err(|e| PctError::VerificationFailed(e.to_string()))?;
pct_finalize(is_valid)
}
pub fn pct_ed25519(keypair: &crate::primitives::ec::ed25519::Ed25519KeyPair) -> PctResult<()> {
use crate::primitives::ec::traits::{EcKeyPair, EcSignature};
let signature = keypair.sign(PCT_TEST_MESSAGE);
crate::primitives::ec::ed25519::Ed25519Signature::verify(
&keypair.public_key_bytes(),
PCT_TEST_MESSAGE,
&signature,
)
.map_err(|e| PctError::VerificationFailed(e.to_string()))?;
Ok(())
}
#[cfg(not(feature = "fips"))]
pub fn pct_secp256k1(
keypair: &crate::primitives::ec::secp256k1::Secp256k1KeyPair,
) -> PctResult<()> {
use crate::primitives::ec::traits::{EcKeyPair, EcSignature};
let signature =
keypair.sign(PCT_TEST_MESSAGE).map_err(|e| PctError::SigningFailed(e.to_string()))?;
crate::primitives::ec::secp256k1::Secp256k1Signature::verify(
&keypair.public_key_bytes(),
PCT_TEST_MESSAGE,
&signature,
)
.map_err(|e| PctError::VerificationFailed(e.to_string()))?;
Ok(())
}
#[cfg(test)]
#[allow(clippy::expect_used)] mod tests {
use super::*;
#[test]
fn test_pct_ml_kem_768_passes() {
use crate::primitives::kem::ml_kem::MlKemSecurityLevel;
let result = pct_ml_kem(MlKemSecurityLevel::MlKem768);
assert!(result.is_ok(), "PCT should pass for ML-KEM-768");
}
#[test]
fn test_pct_ml_kem_1024_passes() {
use crate::primitives::kem::ml_kem::MlKemSecurityLevel;
let result = pct_ml_kem(MlKemSecurityLevel::MlKem1024);
assert!(result.is_ok(), "PCT should pass for ML-KEM-1024");
}
#[test]
fn test_pct_ml_dsa_44_passes() {
use crate::primitives::sig::ml_dsa::{MlDsaParameterSet, generate_keypair};
let (pk, sk) = generate_keypair(MlDsaParameterSet::MlDsa44).expect("Key generation failed");
let result = pct_ml_dsa(&pk, &sk);
assert!(result.is_ok(), "PCT should pass for valid ML-DSA-44 keypair");
}
#[test]
fn test_pct_ml_dsa_65_passes() {
use crate::primitives::sig::ml_dsa::{MlDsaParameterSet, generate_keypair};
let (pk, sk) = generate_keypair(MlDsaParameterSet::MlDsa65).expect("Key generation failed");
let result = pct_ml_dsa(&pk, &sk);
assert!(result.is_ok(), "PCT should pass for valid ML-DSA-65 keypair");
}
#[test]
fn test_pct_ml_dsa_87_passes() {
use crate::primitives::sig::ml_dsa::{MlDsaParameterSet, generate_keypair};
let (pk, sk) = generate_keypair(MlDsaParameterSet::MlDsa87).expect("Key generation failed");
let result = pct_ml_dsa(&pk, &sk);
assert!(result.is_ok(), "PCT should pass for valid ML-DSA-87 keypair");
}
#[test]
fn test_pct_ml_dsa_mismatched_keys_fails() {
use crate::primitives::sig::ml_dsa::{MlDsaParameterSet, generate_keypair};
let (pk1, _sk1) =
generate_keypair(MlDsaParameterSet::MlDsa44).expect("Key generation failed");
let (_pk2, sk2) =
generate_keypair(MlDsaParameterSet::MlDsa44).expect("Key generation failed");
let result = pct_ml_dsa(&pk1, &sk2);
#[cfg(feature = "fips-self-test")]
crate::primitives::self_test::restore_operational_state();
assert!(
matches!(result, Err(PctError::KeyPairInconsistent)),
"PCT should fail for mismatched keys"
);
}
#[test]
fn test_pct_ml_dsa_parameter_mismatch_fails() {
use crate::primitives::sig::ml_dsa::{MlDsaParameterSet, generate_keypair};
let (pk44, _) =
generate_keypair(MlDsaParameterSet::MlDsa44).expect("Key generation failed");
let (_, sk65) =
generate_keypair(MlDsaParameterSet::MlDsa65).expect("Key generation failed");
let result = pct_ml_dsa(&pk44, &sk65);
assert!(
matches!(result, Err(PctError::ParameterMismatch)),
"PCT should fail for parameter mismatch"
);
}
#[test]
fn test_pct_slh_dsa_shake128s_passes() {
use crate::primitives::sig::slh_dsa::{SigningKey, SlhDsaSecurityLevel};
let (sk, vk) =
SigningKey::generate(SlhDsaSecurityLevel::Shake128s).expect("Key generation failed");
let result = pct_slh_dsa(&vk, &sk);
assert!(result.is_ok(), "PCT should pass for valid SLH-DSA-SHAKE-128s keypair");
}
#[test]
fn test_pct_slh_dsa_shake192s_passes() {
use crate::primitives::sig::slh_dsa::{SigningKey, SlhDsaSecurityLevel};
let (sk, vk) =
SigningKey::generate(SlhDsaSecurityLevel::Shake192s).expect("Key generation failed");
let result = pct_slh_dsa(&vk, &sk);
assert!(result.is_ok(), "PCT should pass for valid SLH-DSA-SHAKE-192s keypair");
}
#[test]
fn test_pct_slh_dsa_shake256s_passes() {
use crate::primitives::sig::slh_dsa::{SigningKey, SlhDsaSecurityLevel};
let (sk, vk) =
SigningKey::generate(SlhDsaSecurityLevel::Shake256s).expect("Key generation failed");
let result = pct_slh_dsa(&vk, &sk);
assert!(result.is_ok(), "PCT should pass for valid SLH-DSA-SHAKE-256s keypair");
}
#[test]
fn test_pct_slh_dsa_mismatched_keys_fails() {
use crate::primitives::sig::slh_dsa::{SigningKey, SlhDsaSecurityLevel};
let (sk1, _vk1) =
SigningKey::generate(SlhDsaSecurityLevel::Shake128s).expect("Key generation failed");
let (_sk2, vk2) =
SigningKey::generate(SlhDsaSecurityLevel::Shake128s).expect("Key generation failed");
let result = pct_slh_dsa(&vk2, &sk1);
#[cfg(feature = "fips-self-test")]
crate::primitives::self_test::restore_operational_state();
assert!(
matches!(result, Err(PctError::KeyPairInconsistent)),
"PCT should fail for mismatched keys"
);
}
#[test]
fn test_pct_slh_dsa_parameter_mismatch_fails() {
use crate::primitives::sig::slh_dsa::{SigningKey, SlhDsaSecurityLevel};
let (sk128, _) =
SigningKey::generate(SlhDsaSecurityLevel::Shake128s).expect("Key generation failed");
let (_, vk256) =
SigningKey::generate(SlhDsaSecurityLevel::Shake256s).expect("Key generation failed");
let result = pct_slh_dsa(&vk256, &sk128);
assert!(
matches!(result, Err(PctError::ParameterMismatch)),
"PCT should fail for parameter mismatch"
);
}
#[test]
fn test_pct_fn_dsa_512_passes() {
std::thread::Builder::new()
.stack_size(32 * 1024 * 1024)
.spawn(|| {
use crate::primitives::sig::fndsa::{FnDsaSecurityLevel, KeyPair};
use rand::rngs::OsRng;
let mut rng = OsRng;
let mut keypair =
KeyPair::generate_with_rng(&mut rng, FnDsaSecurityLevel::Level512)
.expect("Key generation failed");
let result = pct_fn_dsa_keypair(&mut keypair);
assert!(result.is_ok(), "PCT should pass for valid FN-DSA-512 keypair");
})
.expect("Thread spawn failed")
.join()
.expect("Thread join failed");
}
#[test]
fn test_pct_fn_dsa_1024_passes() {
std::thread::Builder::new()
.stack_size(32 * 1024 * 1024)
.spawn(|| {
use crate::primitives::sig::fndsa::{FnDsaSecurityLevel, KeyPair};
use rand::rngs::OsRng;
let mut rng = OsRng;
let mut keypair =
KeyPair::generate_with_rng(&mut rng, FnDsaSecurityLevel::Level1024)
.expect("Key generation failed");
let result = pct_fn_dsa_keypair(&mut keypair);
assert!(result.is_ok(), "PCT should pass for valid FN-DSA-1024 keypair");
})
.expect("Thread spawn failed")
.join()
.expect("Thread join failed");
}
#[test]
fn test_pct_fn_dsa_with_separate_keys_passes() {
std::thread::Builder::new()
.stack_size(32 * 1024 * 1024)
.spawn(|| {
use crate::primitives::sig::fndsa::{FnDsaSecurityLevel, KeyPair};
use rand::rngs::OsRng;
let mut rng = OsRng;
let keypair = KeyPair::generate_with_rng(&mut rng, FnDsaSecurityLevel::Level512)
.expect("Key generation failed");
let vk = keypair.verifying_key().clone();
let sk_bytes = keypair.signing_key().to_bytes();
let mut sk = crate::primitives::sig::fndsa::SigningKey::from_bytes(
&sk_bytes,
FnDsaSecurityLevel::Level512,
)
.expect("SigningKey reconstruction failed");
let result = pct_fn_dsa(&vk, &mut sk);
assert!(result.is_ok(), "PCT should pass for valid FN-DSA keypair components");
})
.expect("Thread spawn failed")
.join()
.expect("Thread join failed");
}
#[test]
fn test_pct_fn_dsa_parameter_mismatch_fails() {
std::thread::Builder::new()
.stack_size(32 * 1024 * 1024)
.spawn(|| {
use crate::primitives::sig::fndsa::{FnDsaSecurityLevel, KeyPair};
use rand::rngs::OsRng;
let mut rng = OsRng;
let keypair512 = KeyPair::generate_with_rng(&mut rng, FnDsaSecurityLevel::Level512)
.expect("Key generation failed");
let keypair1024 =
KeyPair::generate_with_rng(&mut rng, FnDsaSecurityLevel::Level1024)
.expect("Key generation failed");
let vk1024 = keypair1024.verifying_key().clone();
let sk_bytes = keypair512.signing_key().to_bytes();
let mut sk512 = crate::primitives::sig::fndsa::SigningKey::from_bytes(
&sk_bytes,
FnDsaSecurityLevel::Level512,
)
.expect("SigningKey reconstruction failed");
let result = pct_fn_dsa(&vk1024, &mut sk512);
assert!(
matches!(result, Err(PctError::ParameterMismatch)),
"PCT should fail for parameter mismatch"
);
})
.expect("Thread spawn failed")
.join()
.expect("Thread join failed");
}
#[test]
fn test_pct_error_display_fails() {
let errors = vec![
PctError::SigningFailed("test error".to_string()),
PctError::VerificationFailed("test error".to_string()),
PctError::KeyPairInconsistent,
PctError::ParameterMismatch,
];
for error in errors {
let display = format!("{}", error);
assert!(!display.is_empty(), "Error display should not be empty");
}
}
#[test]
fn test_pct_constants_succeeds() {
assert_eq!(PCT_TEST_MESSAGE, b"FIPS PCT test");
assert!(PCT_EMPTY_CONTEXT.is_empty());
}
}