use chrono::Utc;
#[inline]
fn validate_aes256_key_length(k: &[u8]) -> Result<()> {
if k.len() != 32 {
return Err(CoreError::InvalidKeyLength { expected: 32, actual: k.len() });
}
Ok(())
}
#[inline]
fn current_timestamp() -> u64 {
Utc::now().timestamp().max(0).unsigned_abs()
}
use zeroize::Zeroizing;
use crate::primitives::sig::{
fndsa::FnDsaSecurityLevel, ml_dsa::MlDsaParameterSet, slh_dsa::SlhDsaSecurityLevel,
};
use crate::unified_api::crypto_types::{
DecryptKey, EncryptKey, EncryptedOutput, EncryptionScheme, HybridComponents,
};
use crate::unified_api::{
CoreConfig,
error::{CoreError, Result},
selector::CryptoPolicyEngine,
types::{AlgorithmSelection, CryptoConfig, SignedData, SignedMetadata},
};
use super::aes_gcm::{decrypt_aes_gcm_with_aad_internal, encrypt_aes_gcm_with_aad_internal};
use super::ed25519::{sign_ed25519_internal, verify_ed25519_internal};
use super::keygen::{
generate_fn_dsa_keypair_with_level, generate_keypair, generate_ml_dsa_keypair,
generate_slh_dsa_keypair,
};
use super::pq_sig::{
sign_pq_fn_dsa_unverified, sign_pq_ml_dsa_unverified, sign_pq_slh_dsa_unverified,
verify_pq_fn_dsa_unverified, verify_pq_ml_dsa_unverified, verify_pq_slh_dsa_unverified,
};
#[cfg(not(feature = "fips"))]
use crate::primitives::aead::{AeadCipher, chacha20poly1305::ChaCha20Poly1305Cipher};
use crate::unified_api::logging::op;
use crate::{
log_crypto_operation_complete, log_crypto_operation_error, log_crypto_operation_start,
};
use crate::hybrid::encrypt_hybrid::{HybridCiphertext, decrypt_hybrid, encrypt_hybrid};
use crate::hybrid::pq_only;
fn generate_hybrid_signing_keypair_for(
params: MlDsaParameterSet,
) -> Result<(crate::types::PublicKey, crate::types::PrivateKey)> {
let (pq_pk, pq_sk) = generate_ml_dsa_keypair(params)?;
let (ed_pk, ed_sk) = generate_keypair()?;
let combined_pk = [pq_pk.into_bytes(), ed_pk.into_bytes()].concat();
let combined_sk = [pq_sk.expose_secret(), ed_sk.expose_secret()].concat();
Ok((crate::types::PublicKey::new(combined_pk), crate::types::PrivateKey::new(combined_sk)))
}
fn sign_hybrid_ml_dsa_ed25519(
message: &[u8],
secret_key: &[u8],
public_key: &[u8],
params: MlDsaParameterSet,
) -> Result<(Vec<u8>, Vec<u8>)> {
use crate::primitives::ec::ed25519::ED25519_SECRET_KEY_LEN;
let pq_sk_len = params.secret_key_size();
let pq_pk_len = params.public_key_size();
let expected_sk_len = pq_sk_len
.checked_add(ED25519_SECRET_KEY_LEN)
.ok_or_else(|| CoreError::InvalidKey("Secret key length overflow".to_string()))?;
if secret_key.len() != expected_sk_len {
return Err(CoreError::InvalidKey(format!(
"Hybrid secret key length mismatch: expected {expected_sk_len}, got {}",
secret_key.len()
)));
}
use crate::primitives::ec::ed25519::ED25519_PUBLIC_KEY_LEN;
let expected_pk_len = pq_pk_len
.checked_add(ED25519_PUBLIC_KEY_LEN)
.ok_or_else(|| CoreError::InvalidKey("Public key length overflow".to_string()))?;
if public_key.len() != expected_pk_len {
return Err(CoreError::InvalidKey(format!(
"Hybrid public key length mismatch: expected {expected_pk_len}, got {}",
public_key.len()
)));
}
let pq_sk = secret_key
.get(..pq_sk_len)
.ok_or_else(|| CoreError::InvalidKey("Failed to split hybrid secret key".to_string()))?;
let ed_sk = secret_key
.get(pq_sk_len..)
.ok_or_else(|| CoreError::InvalidKey("Failed to split hybrid secret key".to_string()))?;
use crate::types::domains::{hash_with_context, sig_context};
use crate::unified_api::convenience::pq_sig::{
hybrid_scheme_label_for_param_set, sign_pq_ml_dsa_internal_with_ctx,
};
let hybrid_ctx = sig_context(hybrid_scheme_label_for_param_set(params));
let pq_sig = sign_pq_ml_dsa_internal_with_ctx(message, pq_sk, params, hybrid_ctx)?;
let ed_digest = hash_with_context(hybrid_ctx, message);
let ed_sig = sign_ed25519_internal(&ed_digest, ed_sk)?;
let combined_sig = [pq_sig, ed_sig].concat();
Ok((public_key.to_vec(), combined_sig))
}
#[cfg(feature = "fips-self-test")]
pub(super) fn fips_verify_operational() -> Result<()> {
use std::sync::Once;
static INIT: Once = Once::new();
INIT.call_once(|| {
let _passed = crate::primitives::self_test::initialize_and_test();
});
crate::primitives::self_test::verify_operational().map_err(|e| CoreError::SelfTestFailed {
component: "FIPS module".to_string(),
status: e.to_string(),
})
}
#[cfg(not(feature = "fips-self-test"))]
#[expect(
clippy::unnecessary_wraps,
reason = "Result returned for API parity with the fips-self-test-enabled path; this no-op variant must keep the same signature"
)]
pub(super) fn fips_verify_operational() -> Result<()> {
Ok(())
}
use crate::primitives::resource_limits::{
validate_decryption_size, validate_encryption_size, validate_signature_size,
};
fn extract_nonce_tag(encrypted: &EncryptedOutput) -> Result<([u8; 12], [u8; 16])> {
let opaque = || CoreError::DecryptionFailed("decryption failed".to_string());
let nonce: [u8; 12] = encrypted.nonce().try_into().map_err(|_slice_err| {
tracing::debug!(
nonce_len = encrypted.nonce().len(),
"decrypt rejected: nonce length != 12"
);
opaque()
})?;
let tag: [u8; 16] = encrypted.tag().try_into().map_err(|_slice_err| {
tracing::debug!(tag_len = encrypted.tag().len(), "decrypt rejected: tag length != 16");
opaque()
})?;
Ok((nonce, tag))
}
fn select_encryption_scheme_typed(options: &CryptoConfig) -> Result<EncryptionScheme> {
let mode = options.get_crypto_mode();
match options.get_selection() {
AlgorithmSelection::UseCase(use_case) => {
let config = CoreConfig::default();
let scheme = CryptoPolicyEngine::recommend_encryption_scheme(use_case, &config)?;
if matches!(mode, crate::types::types::CryptoMode::PqOnly)
&& let Some(pq_scheme) = scheme.to_pq_equivalent()
{
return Ok(pq_scheme);
}
Ok(scheme)
}
AlgorithmSelection::SecurityLevel(level) => {
let config = CoreConfig::default().with_security_level(*level);
Ok(CryptoPolicyEngine::select_encryption_scheme_typed_with_mode(&config, mode))
}
AlgorithmSelection::ForcedScheme(scheme) => {
let scheme_str = CryptoPolicyEngine::force_scheme(scheme);
EncryptionScheme::parse_str(&scheme_str).ok_or_else(|| {
CoreError::InvalidInput(format!(
"Forced scheme '{}' is not an encryption scheme (may be a signature scheme)",
scheme_str
))
})
}
}
}
fn select_signature_scheme(options: &CryptoConfig) -> Result<String> {
let pq_only = options.get_crypto_mode() == crate::types::types::CryptoMode::PqOnly;
match options.get_selection() {
AlgorithmSelection::UseCase(use_case) => {
let uc_config = crate::types::config::UseCaseConfig::new(*use_case);
if pq_only {
Ok(CryptoPolicyEngine::select_pq_signature_scheme(&uc_config.signature)?)
} else {
Ok(CryptoPolicyEngine::select_signature_scheme(&uc_config.signature)?)
}
}
AlgorithmSelection::SecurityLevel(level) => {
let config = CoreConfig::default().with_security_level(*level);
if pq_only {
Ok(CryptoPolicyEngine::select_pq_signature_scheme(&config)?)
} else {
Ok(CryptoPolicyEngine::select_signature_scheme(&config)?)
}
}
AlgorithmSelection::ForcedScheme(scheme) => Ok(CryptoPolicyEngine::force_scheme(scheme)),
}
}
#[cfg(not(feature = "fips"))]
fn encrypt_chacha20_internal(data: &[u8], key: &[u8], aad: &[u8]) -> Result<Vec<u8>> {
use crate::primitives::aead::AeadError;
let cipher = ChaCha20Poly1305Cipher::new(key).map_err(|e| match e {
AeadError::WeakKey => CoreError::InvalidKey(
"All-zero key rejected by ChaCha20-Poly1305 — generate a fresh key via \
`latticearc::primitives::security::generate_secure_random_bytes(32)`."
.to_string(),
),
AeadError::InvalidKeyLength => {
CoreError::InvalidKeyLength { expected: 32, actual: key.len() }
}
other => CoreError::EncryptionFailed(other.to_string()),
})?;
let nonce = ChaCha20Poly1305Cipher::generate_nonce();
let (ciphertext, tag) = cipher
.encrypt(&nonce, data, Some(aad))
.map_err(|e| CoreError::EncryptionFailed(e.to_string()))?;
let mut result =
Vec::with_capacity(12_usize.saturating_add(ciphertext.len()).saturating_add(tag.len()));
result.extend_from_slice(&nonce);
result.extend_from_slice(&ciphertext);
result.extend_from_slice(&tag);
Ok(result)
}
#[cfg(not(feature = "fips"))]
fn decrypt_chacha20_internal(
encrypted: &[u8],
key: &[u8],
aad: &[u8],
) -> Result<Zeroizing<Vec<u8>>> {
let opaque = || CoreError::DecryptionFailed("decryption failed".to_string());
if encrypted.len() < 28 {
log_crypto_operation_error!(op::CHACHA20_DECRYPT, "input too short");
return Err(opaque());
}
let nonce_slice = encrypted.get(..12).ok_or_else(|| {
log_crypto_operation_error!(op::CHACHA20_DECRYPT, "nonce slice extraction failed");
opaque()
})?;
let mut nonce = [0u8; 12];
nonce.copy_from_slice(nonce_slice);
let ciphertext_and_tag = encrypted.get(12..).ok_or_else(|| {
log_crypto_operation_error!(op::CHACHA20_DECRYPT, "ct+tag slice extraction failed");
opaque()
})?;
let tag_start = ciphertext_and_tag.len().saturating_sub(16);
let ciphertext = ciphertext_and_tag.get(..tag_start).ok_or_else(|| {
log_crypto_operation_error!(op::CHACHA20_DECRYPT, "ciphertext slice extraction failed");
opaque()
})?;
let tag_slice = ciphertext_and_tag.get(tag_start..).ok_or_else(|| {
log_crypto_operation_error!(op::CHACHA20_DECRYPT, "tag slice extraction failed");
opaque()
})?;
let mut tag = [0u8; 16];
tag.copy_from_slice(tag_slice);
use crate::primitives::aead::AeadError;
let cipher = ChaCha20Poly1305Cipher::new(key).map_err(|e| match e {
AeadError::WeakKey => CoreError::InvalidKey(
"All-zero key rejected by ChaCha20-Poly1305 — generate a fresh key via \
`latticearc::primitives::security::generate_secure_random_bytes(32)`."
.to_string(),
),
AeadError::InvalidKeyLength => {
CoreError::InvalidKeyLength { expected: 32, actual: key.len() }
}
other => CoreError::DecryptionFailed(other.to_string()),
})?;
cipher.decrypt(&nonce, ciphertext, &tag, Some(aad)).map_err(|_aead_err| {
log_crypto_operation_error!(op::CHACHA20_DECRYPT, "AEAD authentication failed");
opaque()
})
}
#[must_use = "encryption result must be used or errors will be silently dropped"]
pub fn encrypt(data: &[u8], key: EncryptKey<'_>, config: CryptoConfig) -> Result<EncryptedOutput> {
encrypt_with_aad(data, key, config, &[])
}
#[must_use = "encryption result must be used or errors will be silently dropped"]
pub fn encrypt_with_aad(
data: &[u8],
key: EncryptKey<'_>,
config: CryptoConfig,
aad: &[u8],
) -> Result<EncryptedOutput> {
fips_verify_operational()?;
config.validate()?;
let scheme = select_encryption_scheme_typed(&config)?;
config.validate_scheme_compliance(scheme.as_str())?;
CryptoPolicyEngine::validate_key_matches_scheme(&key, &scheme)
.map_err(|e| CoreError::ConfigurationError(e.to_string()))?;
if let Err(e) = validate_encryption_size(data.len()) {
tracing::debug!(error = %e, data_len = data.len(), "encrypt rejected: plaintext exceeds resource limit");
return Err(CoreError::ResourceExceeded("plaintext exceeds resource limit".to_string()));
}
log_crypto_operation_start!(op::ENCRYPT, scheme = %scheme, data_size = data.len());
let output = match (&key, &scheme) {
(EncryptKey::Symmetric(k), EncryptionScheme::Aes256Gcm) => {
validate_aes256_key_length(k)?;
let encrypted = encrypt_aes_gcm_with_aad_internal(data, k, aad)?;
symmetric_bytes_to_output(scheme, &encrypted)?
}
#[cfg(not(feature = "fips"))]
(EncryptKey::Symmetric(k), EncryptionScheme::ChaCha20Poly1305) => {
validate_aes256_key_length(k)?;
let encrypted = encrypt_chacha20_internal(data, k, aad)?;
symmetric_bytes_to_output(scheme, &encrypted)?
}
#[cfg(feature = "fips")]
(EncryptKey::Symmetric(_), EncryptionScheme::ChaCha20Poly1305) => {
return Err(CoreError::ComplianceViolation(
"ChaCha20-Poly1305 is not FIPS 140-3 approved. Use AES-256-GCM.".to_string(),
));
}
(EncryptKey::Hybrid(pk), _) if scheme.requires_hybrid_key() => {
let ctx = if aad.is_empty() {
None
} else {
Some(crate::hybrid::encrypt_hybrid::HybridEncryptionContext::with_aad(aad.to_vec()))
};
let ct = encrypt_hybrid(pk, data, ctx.as_ref()).map_err(|e| {
CoreError::EncryptionFailed(format!("Hybrid encryption failed: {e}"))
})?;
let timestamp = current_timestamp();
EncryptedOutput::new(
scheme,
ct.symmetric_ciphertext().to_vec(),
ct.nonce().to_vec(),
ct.tag().to_vec(),
Some(HybridComponents::new(
ct.kem_ciphertext().to_vec(),
ct.ecdh_ephemeral_pk().to_vec(),
)),
timestamp,
None,
)
.map_err(|e| CoreError::ConfigurationError(e.to_string()))?
}
(EncryptKey::PqOnly(pk), _) if scheme.requires_pq_key() => {
let ct = pq_only::encrypt_pq_only_with_aad(pk, data, aad).map_err(|e| {
CoreError::EncryptionFailed(format!("PQ-only encryption failed: {e}"))
})?;
let timestamp = current_timestamp();
let (kem_ct, sym_ct, nonce, tag) = ct.into_parts();
EncryptedOutput::new(
scheme,
sym_ct,
nonce.to_vec(),
tag.to_vec(),
Some(HybridComponents::new(kem_ct, vec![])),
timestamp,
None,
)
.map_err(|e| CoreError::ConfigurationError(e.to_string()))?
}
_ => {
return Err(CoreError::InvalidInput(format!(
"Key type does not match scheme '{}'",
scheme
)));
}
};
log_crypto_operation_complete!(op::ENCRYPT, result_size = output.ciphertext().len(), scheme = %output.scheme());
Ok(output)
}
fn symmetric_bytes_to_output(
scheme: EncryptionScheme,
encrypted: &[u8],
) -> Result<EncryptedOutput> {
if encrypted.len() < 28 {
return Err(CoreError::EncryptionFailed(
"Encrypted data too short (need nonce + tag)".to_string(),
));
}
let nonce = encrypted
.get(..12)
.ok_or_else(|| CoreError::EncryptionFailed("Failed to extract nonce".to_string()))?
.to_vec();
let tag_start = encrypted.len().saturating_sub(16);
let tag = encrypted
.get(tag_start..)
.ok_or_else(|| CoreError::EncryptionFailed("Failed to extract tag".to_string()))?
.to_vec();
let timestamp = current_timestamp();
EncryptedOutput::new(scheme, encrypted.to_vec(), nonce, tag, None, timestamp, None)
.map_err(|e| CoreError::ConfigurationError(e.to_string()))
}
#[must_use = "decryption result must be used or errors will be silently dropped"]
pub fn decrypt(
encrypted: &EncryptedOutput,
key: DecryptKey<'_>,
config: CryptoConfig,
) -> Result<Zeroizing<Vec<u8>>> {
decrypt_with_aad(encrypted, key, config, &[])
}
#[must_use = "decryption result must be used or errors will be silently dropped"]
pub fn decrypt_with_aad(
encrypted: &EncryptedOutput,
key: DecryptKey<'_>,
config: CryptoConfig,
aad: &[u8],
) -> Result<Zeroizing<Vec<u8>>> {
fips_verify_operational()?;
config.validate()?;
config.validate_scheme_compliance(encrypted.scheme().as_str())?;
if let Some(max_age) = config.max_age_seconds() {
let now = current_timestamp();
let stamped = encrypted.timestamp();
let age = now.saturating_sub(stamped);
if age > max_age {
return Err(CoreError::Replay { age_seconds: age, max_age_seconds: max_age });
}
}
CryptoPolicyEngine::validate_decrypt_key_matches_scheme(&key, encrypted.scheme())
.map_err(|e| CoreError::ConfigurationError(e.to_string()))?;
log_crypto_operation_start!(op::DECRYPT, scheme = %encrypted.scheme(), data_size = encrypted.ciphertext().len());
validate_decryption_size(encrypted.ciphertext().len()).map_err(|_e| {
CoreError::ResourceExceeded("ciphertext exceeds resource limit".to_string())
})?;
let result: Result<Zeroizing<Vec<u8>>> = match (&key, encrypted.scheme()) {
(DecryptKey::Symmetric(k), EncryptionScheme::Aes256Gcm) => {
validate_aes256_key_length(k)?;
decrypt_aes_gcm_with_aad_internal(encrypted.ciphertext(), k, aad)
}
#[cfg(not(feature = "fips"))]
(DecryptKey::Symmetric(k), EncryptionScheme::ChaCha20Poly1305) => {
validate_aes256_key_length(k)?;
decrypt_chacha20_internal(encrypted.ciphertext(), k, aad)
}
#[cfg(feature = "fips")]
(DecryptKey::Symmetric(_), EncryptionScheme::ChaCha20Poly1305) => {
return Err(CoreError::ComplianceViolation(
"ChaCha20-Poly1305 is not FIPS 140-3 approved. Use AES-256-GCM.".to_string(),
));
}
(DecryptKey::Hybrid(sk), scheme) if scheme.requires_hybrid_key() => {
let hybrid_data = encrypted.hybrid_data().ok_or_else(|| {
tracing::debug!(
scheme = ?scheme,
"decrypt rejected: hybrid scheme tag but EncryptedOutput.hybrid_data is None"
);
CoreError::DecryptionFailed("decryption failed".to_string())
})?;
let ct = HybridCiphertext::new(
hybrid_data.ml_kem_ciphertext().to_vec(),
hybrid_data.ecdh_ephemeral_pk().to_vec(),
encrypted.ciphertext().to_vec(),
encrypted.nonce().to_vec(),
encrypted.tag().to_vec(),
);
let ctx = if aad.is_empty() {
None
} else {
Some(crate::hybrid::encrypt_hybrid::HybridEncryptionContext::with_aad(aad.to_vec()))
};
decrypt_hybrid(sk, &ct, ctx.as_ref())
.map_err(|_e| CoreError::DecryptionFailed("decryption failed".to_string()))
}
(DecryptKey::PqOnly(sk), scheme) if scheme.requires_pq_key() => {
let hybrid_data = encrypted.hybrid_data().ok_or_else(|| {
tracing::debug!(
scheme = ?scheme,
"decrypt rejected: PQ-only scheme tag but EncryptedOutput.hybrid_data is None"
);
CoreError::DecryptionFailed("decryption failed".to_string())
})?;
let (nonce, tag) = extract_nonce_tag(encrypted)?;
pq_only::decrypt_pq_only_with_aad(
sk,
hybrid_data.ml_kem_ciphertext(),
encrypted.ciphertext(),
&nonce,
&tag,
aad,
)
.map_err(|_e| CoreError::DecryptionFailed("decryption failed".to_string()))
}
_ => {
return Err(CoreError::InvalidInput(format!(
"Key type does not match scheme '{}'",
encrypted.scheme()
)));
}
};
match result {
Ok(plaintext) => {
log_crypto_operation_complete!(op::DECRYPT, scheme = %encrypted.scheme());
Ok(plaintext)
}
Err(e) => {
log_crypto_operation_error!(op::DECRYPT, e, scheme = %encrypted.scheme());
Err(e)
}
}
}
pub struct SigningKeypair {
public_key: Vec<u8>,
secret_key: Zeroizing<Vec<u8>>,
scheme: String,
}
impl SigningKeypair {
#[must_use]
pub fn public_key(&self) -> &[u8] {
&self.public_key
}
#[must_use]
pub fn expose_secret_key(&self) -> &[u8] {
&self.secret_key
}
#[must_use]
pub fn scheme(&self) -> &str {
&self.scheme
}
#[must_use]
pub fn into_parts(self) -> (Vec<u8>, Zeroizing<Vec<u8>>, String) {
(self.public_key, self.secret_key, self.scheme)
}
}
impl core::fmt::Debug for SigningKeypair {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("SigningKeypair")
.field("public_key", &format_args!("<{} bytes>", self.public_key.len()))
.field("secret_key", &"[REDACTED]")
.field("scheme", &self.scheme)
.finish()
}
}
impl From<(Vec<u8>, Zeroizing<Vec<u8>>, String)> for SigningKeypair {
fn from(tuple: (Vec<u8>, Zeroizing<Vec<u8>>, String)) -> Self {
Self { public_key: tuple.0, secret_key: tuple.1, scheme: tuple.2 }
}
}
#[must_use = "keypair result must be used or errors will be silently dropped"]
pub fn generate_signing_keypair(config: CryptoConfig) -> Result<SigningKeypair> {
fips_verify_operational()?;
config.validate()?;
let scheme = select_signature_scheme(&config)?;
let (public_key, secret_key) = match scheme.as_str() {
"ml-dsa-44" | "pq-ml-dsa-44" => {
let (pk, sk) = generate_ml_dsa_keypair(MlDsaParameterSet::MlDsa44)?;
(pk, sk)
}
"ml-dsa-65" | "pq-ml-dsa-65" => {
let (pk, sk) = generate_ml_dsa_keypair(MlDsaParameterSet::MlDsa65)?;
(pk, sk)
}
"ml-dsa-87" | "pq-ml-dsa-87" => {
let (pk, sk) = generate_ml_dsa_keypair(MlDsaParameterSet::MlDsa87)?;
(pk, sk)
}
"slh-dsa-shake-128s" => {
let (pk, sk) = generate_slh_dsa_keypair(SlhDsaSecurityLevel::Shake128s)?;
(pk, sk)
}
"slh-dsa-shake-192s" => {
let (pk, sk) = generate_slh_dsa_keypair(SlhDsaSecurityLevel::Shake192s)?;
(pk, sk)
}
"slh-dsa-shake-256s" => {
let (pk, sk) = generate_slh_dsa_keypair(SlhDsaSecurityLevel::Shake256s)?;
(pk, sk)
}
"fn-dsa-512" | "fn-dsa" => {
let (pk, sk) = generate_fn_dsa_keypair_with_level(FnDsaSecurityLevel::Level512)?;
(pk, sk)
}
"fn-dsa-1024" => {
let (pk, sk) = generate_fn_dsa_keypair_with_level(FnDsaSecurityLevel::Level1024)?;
(pk, sk)
}
"hybrid-ml-dsa-44-ed25519" | "ml-dsa-44-hybrid-ed25519" => {
generate_hybrid_signing_keypair_for(MlDsaParameterSet::MlDsa44)?
}
"hybrid-ml-dsa-65-ed25519" | "ml-dsa-65-hybrid-ed25519" => {
generate_hybrid_signing_keypair_for(MlDsaParameterSet::MlDsa65)?
}
"hybrid-ml-dsa-87-ed25519" | "ml-dsa-87-hybrid-ed25519" => {
generate_hybrid_signing_keypair_for(MlDsaParameterSet::MlDsa87)?
}
#[cfg(not(feature = "fips"))]
"ed25519" => {
use crate::primitives::ec::ed25519::Ed25519KeyPair;
use crate::primitives::ec::traits::EcKeyPair;
let kp = Ed25519KeyPair::generate()
.map_err(|e| CoreError::InvalidInput(format!("Ed25519 keygen failed: {e}")))?;
let pk_bytes = kp.public_key_bytes();
let sk_bytes = kp.secret_key_bytes();
return Ok((pk_bytes, sk_bytes, scheme).into());
}
_ => {
return Err(CoreError::InvalidInput(format!("Unsupported signing scheme: {scheme}")));
}
};
Ok((public_key.into_bytes(), Zeroizing::new(secret_key.expose_secret().to_vec()), scheme)
.into())
}
#[must_use = "signing result must be used or errors will be silently dropped"]
pub fn sign_with_key(
message: &[u8],
secret_key: &[u8],
public_key: &[u8],
config: CryptoConfig,
) -> Result<SignedData> {
fips_verify_operational()?;
config.validate()?;
let scheme = select_signature_scheme(&config)?;
config.validate_scheme_compliance(&scheme)?;
if let Err(e) = validate_signature_size(message.len()) {
tracing::debug!(error = %e, msg_len = message.len(), "sign rejected: message exceeds resource limit");
return Err(CoreError::ResourceExceeded("message exceeds resource limit".to_string()));
}
log_crypto_operation_start!(op::SIGN_WITH_KEY, scheme = %scheme, message_size = message.len());
let (result_pk, signature) = match scheme.as_str() {
"ml-dsa-44" | "pq-ml-dsa-44" => {
let sig = sign_pq_ml_dsa_unverified(message, secret_key, MlDsaParameterSet::MlDsa44)?;
(public_key.to_vec(), sig)
}
"ml-dsa-65" | "pq-ml-dsa-65" => {
let sig = sign_pq_ml_dsa_unverified(message, secret_key, MlDsaParameterSet::MlDsa65)?;
(public_key.to_vec(), sig)
}
"ml-dsa-87" | "pq-ml-dsa-87" => {
let sig = sign_pq_ml_dsa_unverified(message, secret_key, MlDsaParameterSet::MlDsa87)?;
(public_key.to_vec(), sig)
}
"slh-dsa-shake-128s" => {
let sig =
sign_pq_slh_dsa_unverified(message, secret_key, SlhDsaSecurityLevel::Shake128s)?;
(public_key.to_vec(), sig)
}
"slh-dsa-shake-192s" => {
let sig =
sign_pq_slh_dsa_unverified(message, secret_key, SlhDsaSecurityLevel::Shake192s)?;
(public_key.to_vec(), sig)
}
"slh-dsa-shake-256s" => {
let sig =
sign_pq_slh_dsa_unverified(message, secret_key, SlhDsaSecurityLevel::Shake256s)?;
(public_key.to_vec(), sig)
}
"fn-dsa-512" | "fn-dsa" => {
let sig = sign_pq_fn_dsa_unverified(message, secret_key, FnDsaSecurityLevel::Level512)?;
(public_key.to_vec(), sig)
}
"fn-dsa-1024" => {
let sig =
sign_pq_fn_dsa_unverified(message, secret_key, FnDsaSecurityLevel::Level1024)?;
(public_key.to_vec(), sig)
}
#[cfg(not(feature = "fips"))]
"ed25519" => {
use crate::types::domains::{SigSchemeLabel, hash_with_context, sig_context};
let ed_ctx = sig_context(SigSchemeLabel::Ed25519);
let ed_digest = hash_with_context(ed_ctx, message);
let sig = sign_ed25519_internal(&ed_digest, secret_key)?;
(public_key.to_vec(), sig)
}
"hybrid-ml-dsa-44-ed25519" | "ml-dsa-44-hybrid-ed25519" => {
sign_hybrid_ml_dsa_ed25519(message, secret_key, public_key, MlDsaParameterSet::MlDsa44)?
}
"hybrid-ml-dsa-65-ed25519" | "ml-dsa-65-hybrid-ed25519" => {
sign_hybrid_ml_dsa_ed25519(message, secret_key, public_key, MlDsaParameterSet::MlDsa65)?
}
"hybrid-ml-dsa-87-ed25519" | "ml-dsa-87-hybrid-ed25519" => {
sign_hybrid_ml_dsa_ed25519(message, secret_key, public_key, MlDsaParameterSet::MlDsa87)?
}
_ => {
return Err(CoreError::InvalidInput(format!("Unsupported signing scheme: {scheme}")));
}
};
let timestamp = current_timestamp();
log_crypto_operation_complete!(op::SIGN_WITH_KEY, signature_size = signature.len(), scheme = %scheme);
Ok(SignedData::new(
message.to_vec(),
SignedMetadata::new(signature, scheme.clone(), result_pk, None),
scheme,
timestamp,
))
}
#[must_use = "verification result must be used or errors will be silently dropped"]
pub fn verify(signed: &SignedData, config: CryptoConfig) -> Result<bool> {
fips_verify_operational()?;
config.validate()?;
config.validate_scheme_compliance(&signed.scheme)?;
log_crypto_operation_start!(op::VERIFY, scheme = ?signed.scheme, message_size = signed.data.len());
if let Err(e) = validate_signature_size(signed.data.len()) {
tracing::debug!(error = %e, msg_len = signed.data.len(), "verify rejected: message exceeds resource limit");
return Ok(false);
}
let result = match signed.scheme.as_str() {
"ml-dsa-44" | "pq-ml-dsa-44" => verify_pq_ml_dsa_unverified(
&signed.data,
&signed.metadata.signature,
&signed.metadata.public_key,
MlDsaParameterSet::MlDsa44,
),
"ml-dsa-65" | "pq-ml-dsa-65" => verify_pq_ml_dsa_unverified(
&signed.data,
&signed.metadata.signature,
&signed.metadata.public_key,
MlDsaParameterSet::MlDsa65,
),
"ml-dsa-87" | "pq-ml-dsa-87" => verify_pq_ml_dsa_unverified(
&signed.data,
&signed.metadata.signature,
&signed.metadata.public_key,
MlDsaParameterSet::MlDsa87,
),
"slh-dsa-shake-128s" => verify_pq_slh_dsa_unverified(
&signed.data,
&signed.metadata.signature,
&signed.metadata.public_key,
SlhDsaSecurityLevel::Shake128s,
),
"slh-dsa-shake-192s" => verify_pq_slh_dsa_unverified(
&signed.data,
&signed.metadata.signature,
&signed.metadata.public_key,
SlhDsaSecurityLevel::Shake192s,
),
"slh-dsa-shake-256s" => verify_pq_slh_dsa_unverified(
&signed.data,
&signed.metadata.signature,
&signed.metadata.public_key,
SlhDsaSecurityLevel::Shake256s,
),
"fn-dsa-512" | "fn-dsa" => verify_pq_fn_dsa_unverified(
&signed.data,
&signed.metadata.signature,
&signed.metadata.public_key,
FnDsaSecurityLevel::Level512,
),
"fn-dsa-1024" => verify_pq_fn_dsa_unverified(
&signed.data,
&signed.metadata.signature,
&signed.metadata.public_key,
FnDsaSecurityLevel::Level1024,
),
"hybrid-ml-dsa-44-ed25519" | "ml-dsa-44-hybrid-ed25519" => verify_hybrid_ml_dsa_ed25519(
&signed.data,
&signed.metadata.signature,
&signed.metadata.public_key,
MlDsaParameterSet::MlDsa44.public_key_size(),
MlDsaParameterSet::MlDsa44.signature_size(),
MlDsaParameterSet::MlDsa44,
),
"hybrid-ml-dsa-65-ed25519" | "ml-dsa-65-hybrid-ed25519" => verify_hybrid_ml_dsa_ed25519(
&signed.data,
&signed.metadata.signature,
&signed.metadata.public_key,
MlDsaParameterSet::MlDsa65.public_key_size(),
MlDsaParameterSet::MlDsa65.signature_size(),
MlDsaParameterSet::MlDsa65,
),
"hybrid-ml-dsa-87-ed25519" | "ml-dsa-87-hybrid-ed25519" => verify_hybrid_ml_dsa_ed25519(
&signed.data,
&signed.metadata.signature,
&signed.metadata.public_key,
MlDsaParameterSet::MlDsa87.public_key_size(),
MlDsaParameterSet::MlDsa87.signature_size(),
MlDsaParameterSet::MlDsa87,
),
#[cfg(not(feature = "fips"))]
"ed25519" => {
use crate::types::domains::{SigSchemeLabel, hash_with_context, sig_context};
let ed_ctx = sig_context(SigSchemeLabel::Ed25519);
let ed_digest = hash_with_context(ed_ctx, &signed.data);
verify_ed25519_internal(
&ed_digest,
&signed.metadata.signature,
&signed.metadata.public_key,
)
}
#[cfg(feature = "fips")]
"ed25519" => Ok(false),
_ => {
return Err(CoreError::InvalidInput(format!(
"Unsupported verification scheme: {}",
signed.scheme
)));
}
};
match &result {
Ok(valid) => {
log_crypto_operation_complete!(op::VERIFY, valid = *valid, scheme = ?signed.scheme);
}
Err(e) => {
log_crypto_operation_error!(op::VERIFY, e, scheme = ?signed.scheme);
}
}
result
}
#[must_use = "verification result must be used or errors will be silently dropped"]
pub fn verify_with_anchor(
signed: &SignedData,
expected_pk: &[u8],
expected_scheme: &str,
config: CryptoConfig,
) -> Result<bool> {
use crate::types::domains::SigSchemeLabel;
use subtle::ConstantTimeEq;
let Some(envelope_label) = SigSchemeLabel::from_scheme_str(&signed.scheme) else {
tracing::debug!(
envelope_scheme = %signed.scheme,
"verify_with_anchor rejected: envelope scheme not in M5 allowlist"
);
return Ok(false);
};
let Some(expected_label) = SigSchemeLabel::from_scheme_str(expected_scheme) else {
tracing::debug!(
expected_scheme = %expected_scheme,
"verify_with_anchor rejected: expected scheme not in M5 allowlist"
);
return Ok(false);
};
if envelope_label != expected_label {
tracing::debug!(
envelope_scheme = %signed.scheme,
expected_scheme = %expected_scheme,
"verify_with_anchor rejected: envelope scheme does not match expected scheme"
);
return Ok(false);
}
if expected_pk.len() != signed.metadata.public_key.len() {
tracing::debug!(
expected_pk_len = expected_pk.len(),
envelope_pk_len = signed.metadata.public_key.len(),
"verify_with_anchor rejected: trust-anchor pk length differs from envelope pk length"
);
return Ok(false);
}
if expected_pk.ct_eq(&signed.metadata.public_key).unwrap_u8() != 1u8 {
tracing::debug!("verify_with_anchor rejected: trust-anchor pk does not match envelope pk");
return Ok(false);
}
verify(signed, config)
}
fn verify_hybrid_ml_dsa_ed25519(
data: &[u8],
full_sig: &[u8],
full_pk: &[u8],
pq_pk_len: usize,
min_total_sig_len: usize,
param_set: MlDsaParameterSet,
) -> Result<bool> {
use crate::primitives::ec::ed25519::ED25519_PUBLIC_KEY_LEN as ED25519_PK_LEN;
const ED25519_SIG_LEN: usize = 64;
let sig_len = full_sig.len();
let pk_len = full_pk.len();
let Some(expected_pk_len) = pq_pk_len.checked_add(ED25519_PK_LEN) else {
return Err(CoreError::InvalidInput("Invalid hybrid signature".to_string()));
};
let shape_ok = sig_len >= min_total_sig_len && pk_len == expected_pk_len;
let (pq_pk_bytes, ed_pk_bytes, pq_sig_bytes, ed_sig_bytes, real_inputs) = if shape_ok {
let pq_pk = full_pk.get(..pq_pk_len).unwrap_or(&[]);
let ed_pk = full_pk.get(pq_pk_len..).unwrap_or(&[]);
let pq_sig_len = sig_len.saturating_sub(ED25519_SIG_LEN);
let pq_sig = full_sig.get(..pq_sig_len).unwrap_or(&[]);
let ed_sig = full_sig.get(pq_sig_len..).unwrap_or(&[]);
(pq_pk, ed_pk, pq_sig, ed_sig, true)
} else {
let dummy = crate::hybrid::verify_equalizer::hybrid_verify_dummy_material(param_set);
(
dummy.pq_pk.as_slice(),
dummy.ed_pk.as_slice(),
dummy.pq_sig.as_slice(),
dummy.ed_sig.as_slice(),
false,
)
};
use crate::types::domains::{hash_with_context, sig_context};
use crate::unified_api::convenience::pq_sig::{
hybrid_scheme_label_for_param_set, verify_pq_ml_dsa_internal_with_ctx,
};
let hybrid_ctx = sig_context(hybrid_scheme_label_for_param_set(param_set));
let pq_valid =
verify_pq_ml_dsa_internal_with_ctx(data, pq_sig_bytes, pq_pk_bytes, param_set, hybrid_ctx)
.unwrap_or(false);
let ed_digest = hash_with_context(hybrid_ctx, data);
let ed_valid = verify_ed25519_internal(&ed_digest, ed_sig_bytes, ed_pk_bytes).unwrap_or(false);
let combined = pq_valid & ed_valid;
Ok(if real_inputs { combined } else { false })
}
#[cfg(test)]
#[expect(
clippy::panic,
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::panic_in_result_fn,
clippy::redundant_clone,
clippy::useless_vec,
clippy::clone_on_copy,
clippy::single_match,
unused_qualifications,
reason = "test/bench scaffolding: lints suppressed for this module"
)]
#[allow(clippy::unnecessary_wraps, reason = "feature-config-dependent; see above")]
mod tests {
use super::*;
use crate::types::types::CryptoScheme;
use crate::unified_api::test_helpers::non_fips_config;
use crate::{CryptoConfig, CryptoMode, SecurityLevel, UseCase};
use static_assertions::assert_not_impl_any;
assert_not_impl_any!(SigningKeypair: PartialEq, Eq);
fn sign_message(message: &[u8], config: CryptoConfig) -> Result<SignedData> {
let (pk, sk, _scheme) = generate_signing_keypair(config.clone())?.into_parts();
sign_with_key(message, &sk, &pk, config)
}
#[test]
fn test_sign_verify_with_standard_security_succeeds() -> Result<()> {
let message = b"Test message with standard security";
let config = CryptoConfig::new().security_level(SecurityLevel::Standard);
let signed = sign_message(message, config)?;
assert!(!signed.metadata.signature.is_empty());
assert!(!signed.metadata.public_key.is_empty());
let is_valid = verify(&signed, CryptoConfig::new())?;
assert!(is_valid, "Signature should be valid");
Ok(())
}
#[test]
fn test_sign_verify_with_high_security_succeeds() -> Result<()> {
let message = b"Test message with high security";
let config = CryptoConfig::new().security_level(SecurityLevel::High);
let signed = sign_message(message, config)?;
let is_valid = verify(&signed, CryptoConfig::new())?;
assert!(is_valid, "Signature should be valid");
Ok(())
}
#[test]
fn test_sign_verify_with_maximum_security_succeeds() -> Result<()> {
let message = b"Test message with maximum security";
let config = CryptoConfig::new().security_level(SecurityLevel::Maximum);
let signed = sign_message(message, config)?;
let is_valid = verify(&signed, CryptoConfig::new())?;
assert!(is_valid, "Signature should be valid");
Ok(())
}
#[test]
fn test_sign_verify_wrong_message_fails() -> Result<()> {
let message = b"Original message";
let config = CryptoConfig::new();
let signed = sign_message(message, config)?;
let mut modified_signed = signed.clone();
modified_signed.data = b"Modified message".to_vec();
match verify(&modified_signed, CryptoConfig::new()) {
Ok(valid) => assert!(!valid, "Modified message should fail verification"),
Err(_) => {} }
Ok(())
}
#[test]
fn test_sign_verify_corrupted_signature_fails() -> Result<()> {
let message = b"Test message";
let config = CryptoConfig::new();
let signed = sign_message(message, config)?;
let mut corrupted_signed = signed.clone();
if let Some(byte) = corrupted_signed.metadata.signature.first_mut() {
*byte ^= 0xFF;
}
match verify(&corrupted_signed, CryptoConfig::new()) {
Ok(valid) => assert!(!valid, "Corrupted signature should fail verification"),
Err(_) => {} }
Ok(())
}
#[test]
fn test_sign_empty_message_succeeds() -> Result<()> {
let message = b"";
let config = CryptoConfig::new();
let signed = sign_message(message, config)?;
let is_valid = verify(&signed, CryptoConfig::new())?;
assert!(is_valid, "Empty message signature should be valid");
Ok(())
}
#[test]
fn test_sign_large_message_succeeds() -> Result<()> {
let message = vec![0xABu8; 10_000]; let config = CryptoConfig::new();
let signed = sign_message(&message, config)?;
let is_valid = verify(&signed, CryptoConfig::new())?;
assert!(is_valid, "Large message signature should be valid");
Ok(())
}
#[test]
fn test_sign_with_financial_transactions_use_case_succeeds() -> Result<()> {
let message = b"Financial transaction data";
let config = non_fips_config(UseCase::FinancialTransactions);
let signed = sign_message(message, config)?;
assert!(
signed.scheme.contains("ml-dsa") || signed.scheme.contains("ed25519"),
"Financial transactions should use strong signatures"
);
let is_valid = verify(&signed, CryptoConfig::new())?;
assert!(is_valid);
Ok(())
}
#[test]
fn test_sign_with_authentication_use_case_succeeds() -> Result<()> {
let message = b"Authentication data";
let config = CryptoConfig::new().use_case(UseCase::Authentication);
let signed = sign_message(message, config)?;
let is_valid = verify(&signed, CryptoConfig::new())?;
assert!(is_valid);
Ok(())
}
#[test]
fn test_sign_with_firmware_signing_use_case_succeeds() -> Result<()> {
let message = b"Firmware binary data";
let config = CryptoConfig::new().use_case(UseCase::FirmwareSigning);
let signed = sign_message(message, config)?;
let is_valid = verify(&signed, CryptoConfig::new())?;
assert!(is_valid);
Ok(())
}
#[test]
fn test_encrypt_with_invalid_key_length_returns_error() {
let message = b"Test message";
let short_key = vec![0x42u8; 16]; let config = CryptoConfig::new().force_scheme(CryptoScheme::Symmetric);
let result = encrypt(message, EncryptKey::Symmetric(&short_key), config);
assert!(result.is_err(), "Encryption with short key should fail");
}
#[test]
fn test_decrypt_empty_ciphertext_returns_error() {
let key = vec![0x42u8; 32];
let empty_encrypted = EncryptedOutput::new(
EncryptionScheme::Aes256Gcm,
vec![],
vec![],
vec![],
None,
0,
None,
)
.expect("symmetric scheme + None hybrid_data is a valid shape");
let result = decrypt(&empty_encrypted, DecryptKey::Symmetric(&key), CryptoConfig::new());
assert!(result.is_err(), "Empty ciphertext should be rejected");
}
#[test]
fn test_sign_verify_multiple_security_levels_succeed_roundtrip() -> Result<()> {
let message = b"Test cross-level signatures";
let levels = [SecurityLevel::Standard, SecurityLevel::High, SecurityLevel::Maximum];
for level in &levels {
let config = CryptoConfig::new().security_level(level.clone());
let signed = sign_message(message, config)?;
let is_valid = verify(&signed, CryptoConfig::new())?;
assert!(is_valid, "Failed for security level: {:?}", level);
}
Ok(())
}
#[test]
fn test_sign_verify_metadata_is_populated_roundtrip() -> Result<()> {
let message = b"Test metadata";
let config = CryptoConfig::new();
let signed = sign_message(message, config)?;
assert!(!signed.metadata.signature.is_empty(), "Signature should not be empty");
assert!(!signed.metadata.public_key.is_empty(), "Public key should not be empty");
assert!(!signed.metadata.signature_algorithm.is_empty(), "Algorithm should be set");
assert!(!signed.scheme.is_empty(), "Scheme should be set");
assert!(signed.timestamp > 0, "Timestamp should be set");
Ok(())
}
#[test]
fn test_verify_with_corrupted_public_key_fails() -> Result<()> {
let message = b"Test message";
let config = CryptoConfig::new();
let signed = sign_message(message, config)?;
let mut corrupted_signed = signed.clone();
if let Some(byte) = corrupted_signed.metadata.public_key.first_mut() {
*byte ^= 0xFF;
}
match verify(&corrupted_signed, CryptoConfig::new()) {
Ok(valid) => assert!(!valid, "Corrupted public key should fail verification"),
Err(_) => {} }
Ok(())
}
#[test]
fn test_sign_verify_binary_message_succeeds() -> Result<()> {
let message = vec![0x00, 0xFF, 0x7F, 0x80, 0x01, 0xFE];
let config = CryptoConfig::new();
let signed = sign_message(&message, config)?;
let is_valid = verify(&signed, CryptoConfig::new())?;
assert!(is_valid, "Binary message signature should be valid");
Ok(())
}
#[test]
fn test_sign_verify_unicode_message_succeeds() -> Result<()> {
let message = "Test with Unicode: 你好世界 🔐".as_bytes();
let config = CryptoConfig::new();
let signed = sign_message(message, config)?;
let is_valid = verify(&signed, CryptoConfig::new())?;
assert!(is_valid, "Unicode message signature should be valid");
Ok(())
}
#[test]
fn test_sign_verify_with_blockchain_transaction_use_case_succeeds() -> Result<()> {
let message = b"Blockchain transaction data";
let config = CryptoConfig::new().use_case(UseCase::BlockchainTransaction);
let signed = sign_message(message, config)?;
let is_valid = verify(&signed, CryptoConfig::new())?;
assert!(is_valid);
Ok(())
}
#[test]
fn test_sign_verify_with_legal_documents_use_case_succeeds() -> Result<()> {
let message = b"Legal document hash";
let config = CryptoConfig::new().use_case(UseCase::LegalDocuments);
let signed = sign_message(message, config)?;
let is_valid = verify(&signed, CryptoConfig::new())?;
assert!(is_valid);
Ok(())
}
#[test]
fn test_sign_multiple_messages_succeeds() -> Result<()> {
let config = CryptoConfig::new();
let messages =
vec![b"First message".as_ref(), b"Second message".as_ref(), b"Third message".as_ref()];
for message in messages {
let signed = sign_message(message, config.clone())?;
let is_valid = verify(&signed, CryptoConfig::new())?;
assert!(is_valid, "Message: {:?}", String::from_utf8_lossy(message));
}
Ok(())
}
#[test]
fn test_sign_produces_unique_signatures_are_unique() -> Result<()> {
let message = b"Same message";
let config = CryptoConfig::new();
let signed1 = sign_message(message, config.clone())?;
let signed2 = sign_message(message, config)?;
assert_ne!(signed1.metadata.signature, signed2.metadata.signature);
assert_ne!(signed1.metadata.public_key, signed2.metadata.public_key);
let is_valid1 = verify(&signed1, CryptoConfig::new())?;
let is_valid2 = verify(&signed2, CryptoConfig::new())?;
assert!(is_valid1);
assert!(is_valid2);
Ok(())
}
#[test]
fn test_verify_rejects_empty_signature_fails() {
let signed = SignedData::new(
b"Test message".to_vec(),
SignedMetadata::new(vec![], "ml-dsa-44".to_string(), vec![0u8; 1312], None),
"ml-dsa-44".to_string(),
0,
);
let result = verify(&signed, CryptoConfig::new());
assert!(result.is_err() || (result.is_ok() && !result.unwrap()));
}
#[test]
fn test_verify_rejects_empty_public_key_fails() {
let signed = SignedData::new(
b"Test message".to_vec(),
SignedMetadata::new(vec![0u8; 2420], "ml-dsa-44".to_string(), vec![], None),
"ml-dsa-44".to_string(),
0,
);
let result = verify(&signed, CryptoConfig::new());
assert!(result.is_err() || (result.is_ok() && !result.unwrap()));
}
#[test]
fn test_decrypt_with_short_key_returns_error() {
let encrypted = EncryptedOutput::new(
EncryptionScheme::Aes256Gcm,
vec![1, 2, 3, 4],
vec![0u8; 12],
vec![0u8; 16],
None,
0,
None,
)
.expect("symmetric scheme + None hybrid_data is a valid shape");
let short_key = vec![0x42u8; 16];
let result = decrypt(&encrypted, DecryptKey::Symmetric(&short_key), CryptoConfig::new());
assert!(result.is_err(), "Decryption with short key should fail");
}
#[test]
fn test_encrypted_output_rejects_hybrid_scheme_without_hybrid_data() {
let result = EncryptedOutput::new(
EncryptionScheme::HybridMlKem768Aes256Gcm,
vec![0x12u8; 40],
vec![0u8; 12],
vec![0u8; 16],
None, 0,
None,
);
assert!(
result.is_err(),
"hybrid scheme without hybrid_data must be rejected at construction"
);
}
#[test]
fn test_slh_dsa_shake_128s_roundtrip() {
std::thread::Builder::new()
.name("slh_dsa_128s".to_string())
.stack_size(32 * 1024 * 1024)
.spawn(|| {
use crate::unified_api::convenience::keygen::generate_slh_dsa_keypair;
use crate::unified_api::convenience::pq_sig::sign_pq_slh_dsa_unverified;
let (pk, sk) = generate_slh_dsa_keypair(SlhDsaSecurityLevel::Shake128s).unwrap();
let message = b"SLH-DSA-128s test message";
let signature = sign_pq_slh_dsa_unverified(
message,
sk.expose_secret(),
SlhDsaSecurityLevel::Shake128s,
)
.unwrap();
let signed_data = SignedData::new(
message.to_vec(),
SignedMetadata::new(
signature,
"slh-dsa-shake-128s".to_string(),
pk.into_bytes(),
None,
),
"slh-dsa-shake-128s".to_string(),
0,
);
let verified = verify(&signed_data, CryptoConfig::new()).unwrap();
assert!(verified, "SLH-DSA-128s signature should verify");
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_slh_dsa_shake_192s_roundtrip() {
std::thread::Builder::new()
.name("slh_dsa_192s".to_string())
.stack_size(32 * 1024 * 1024)
.spawn(|| {
use crate::unified_api::convenience::keygen::generate_slh_dsa_keypair;
use crate::unified_api::convenience::pq_sig::sign_pq_slh_dsa_unverified;
let (pk, sk) = generate_slh_dsa_keypair(SlhDsaSecurityLevel::Shake192s).unwrap();
let message = b"SLH-DSA-192s test message";
let signature = sign_pq_slh_dsa_unverified(
message,
sk.expose_secret(),
SlhDsaSecurityLevel::Shake192s,
)
.unwrap();
let signed_data = SignedData::new(
message.to_vec(),
SignedMetadata::new(
signature,
"slh-dsa-shake-192s".to_string(),
pk.into_bytes(),
None,
),
"slh-dsa-shake-192s".to_string(),
0,
);
let verified = verify(&signed_data, CryptoConfig::new()).unwrap();
assert!(verified, "SLH-DSA-192s signature should verify");
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_slh_dsa_shake_256s_roundtrip() {
std::thread::Builder::new()
.name("slh_dsa_256s".to_string())
.stack_size(32 * 1024 * 1024)
.spawn(|| {
use crate::unified_api::convenience::keygen::generate_slh_dsa_keypair;
use crate::unified_api::convenience::pq_sig::sign_pq_slh_dsa_unverified;
let (pk, sk) = generate_slh_dsa_keypair(SlhDsaSecurityLevel::Shake256s).unwrap();
let message = b"SLH-DSA-256s test message";
let signature = sign_pq_slh_dsa_unverified(
message,
sk.expose_secret(),
SlhDsaSecurityLevel::Shake256s,
)
.unwrap();
let signed_data = SignedData::new(
message.to_vec(),
SignedMetadata::new(
signature,
"slh-dsa-shake-256s".to_string(),
pk.into_bytes(),
None,
),
"slh-dsa-shake-256s".to_string(),
0,
);
let verified = verify(&signed_data, CryptoConfig::new()).unwrap();
assert!(verified, "SLH-DSA-256s signature should verify");
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_fn_dsa_roundtrip() {
std::thread::Builder::new()
.name("fn_dsa".to_string())
.stack_size(32 * 1024 * 1024)
.spawn(|| {
use crate::unified_api::convenience::keygen::generate_fn_dsa_keypair;
use crate::unified_api::convenience::pq_sig::sign_pq_fn_dsa_unverified;
let (pk, sk) = generate_fn_dsa_keypair().unwrap();
let message = b"FN-DSA test message";
let signature = sign_pq_fn_dsa_unverified(
message,
sk.expose_secret(),
FnDsaSecurityLevel::Level512,
)
.unwrap();
let signed_data = SignedData::new(
message.to_vec(),
SignedMetadata::new(signature, "fn-dsa".to_string(), pk.into_bytes(), None),
"fn-dsa".to_string(),
0,
);
let verified = verify(&signed_data, CryptoConfig::new()).unwrap();
assert!(verified, "FN-DSA signature should verify");
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_hybrid_ml_dsa_44_ed25519_roundtrip() {
std::thread::Builder::new()
.name("hybrid_44".to_string())
.stack_size(32 * 1024 * 1024)
.spawn(|| {
use crate::primitives::sig::ml_dsa::MlDsaParameterSet;
use crate::unified_api::convenience::keygen::{
generate_keypair, generate_ml_dsa_keypair,
};
let (pq_pk, pq_sk) = generate_ml_dsa_keypair(MlDsaParameterSet::MlDsa44).unwrap();
let (ed_pk, ed_sk) = generate_keypair().unwrap();
let combined_pk = [pq_pk.into_bytes(), ed_pk.into_bytes()].concat();
let combined_sk = [pq_sk.expose_secret(), ed_sk.expose_secret()].concat();
let message = b"Hybrid ML-DSA-44 + Ed25519 test";
let config = CryptoConfig::new().security_level(SecurityLevel::Standard);
let signed_data =
sign_with_key(message, &combined_sk, &combined_pk, config).unwrap();
assert!(signed_data.metadata.signature_algorithm.contains("hybrid"));
let verified = verify(&signed_data, CryptoConfig::new()).unwrap();
assert!(verified, "Hybrid ML-DSA-44+Ed25519 should verify");
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_hybrid_ml_dsa_87_ed25519_roundtrip() {
std::thread::Builder::new()
.name("hybrid_87".to_string())
.stack_size(32 * 1024 * 1024)
.spawn(|| {
use crate::primitives::sig::ml_dsa::MlDsaParameterSet;
use crate::unified_api::convenience::keygen::{
generate_keypair, generate_ml_dsa_keypair,
};
let (pq_pk, pq_sk) = generate_ml_dsa_keypair(MlDsaParameterSet::MlDsa87).unwrap();
let (ed_pk, ed_sk) = generate_keypair().unwrap();
let combined_pk = [pq_pk.into_bytes(), ed_pk.into_bytes()].concat();
let combined_sk = [pq_sk.expose_secret(), ed_sk.expose_secret()].concat();
let message = b"Hybrid ML-DSA-87 + Ed25519 test";
let config = CryptoConfig::new().security_level(SecurityLevel::Maximum);
let signed_data =
sign_with_key(message, &combined_sk, &combined_pk, config).unwrap();
assert!(signed_data.metadata.signature_algorithm.contains("hybrid"));
let verified = verify(&signed_data, CryptoConfig::new()).unwrap();
assert!(verified, "Hybrid ML-DSA-87+Ed25519 should verify");
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_sign_with_invalid_hybrid_key_lengths_returns_error() {
std::thread::Builder::new()
.name("hybrid_bad_key".to_string())
.stack_size(32 * 1024 * 1024)
.spawn(|| {
let message = b"test";
let _short_key = vec![1u8; 10];
let short_pk = vec![2u8; 10];
let signed = SignedData::new(
message.to_vec(),
SignedMetadata::new(
vec![0u8; 10],
"hybrid-ml-dsa-65-ed25519".to_string(),
short_pk,
None,
),
"hybrid-ml-dsa-65-ed25519".to_string(),
0,
);
let result = verify(&signed, CryptoConfig::new());
assert!(result.is_err() || (result.is_ok() && !result.unwrap()));
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_verify_with_unsupported_scheme_rejected_fails() {
let signed = SignedData::new(
b"test".to_vec(),
SignedMetadata::new(
vec![0u8; 64],
"unsupported-scheme".to_string(),
vec![0u8; 32],
None,
),
"unsupported-scheme".to_string(),
0,
);
let result = verify(&signed, CryptoConfig::new());
assert!(result.is_err(), "Unsupported scheme should be rejected");
}
#[test]
fn test_encrypt_decrypt_with_maximum_security_succeeds() {
std::thread::Builder::new()
.name("max_security_enc".to_string())
.stack_size(32 * 1024 * 1024)
.spawn(|| {
let data = b"Maximum security encryption test";
let key = vec![0x42u8; 32];
let config = CryptoConfig::new()
.security_level(SecurityLevel::Maximum)
.force_scheme(CryptoScheme::Symmetric);
let encrypted = encrypt(data, EncryptKey::Symmetric(&key), config.clone()).unwrap();
let decrypted =
decrypt(&encrypted, DecryptKey::Symmetric(&key), CryptoConfig::new()).unwrap();
assert_eq!(data.as_slice(), decrypted.as_slice());
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_select_scheme_with_use_case_succeeds() {
let config = CryptoConfig::new().use_case(UseCase::SecureMessaging);
let scheme = select_signature_scheme(&config);
assert!(scheme.is_ok(), "Should select a scheme for SecureMessaging");
let config = CryptoConfig::new().use_case(UseCase::FinancialTransactions);
let scheme = select_signature_scheme(&config);
assert!(scheme.is_ok(), "Should select a scheme for FinancialTransactions");
}
#[test]
fn test_select_encryption_scheme_with_use_case_succeeds() {
let config = CryptoConfig::new().use_case(UseCase::SecureMessaging);
let scheme = select_encryption_scheme_typed(&config);
assert!(scheme.is_ok(), "Should select encryption scheme for SecureMessaging");
}
#[test]
fn test_verify_hybrid_44_signature_too_short_returns_error() {
let signed = SignedData::new(
b"test".to_vec(),
SignedMetadata::new(
vec![0u8; 100],
"hybrid-ml-dsa-44-ed25519".to_string(),
vec![0u8; 1344],
None,
),
"hybrid-ml-dsa-44-ed25519".to_string(),
0,
);
let result = verify(&signed, CryptoConfig::new());
assert!(!result.unwrap(), "shape failure must collapse to Ok(false)");
}
#[test]
fn test_verify_hybrid_44_invalid_pk_length_returns_error() {
let signed = SignedData::new(
b"test".to_vec(),
SignedMetadata::new(
vec![0u8; 3000],
"hybrid-ml-dsa-44-ed25519".to_string(),
vec![0u8; 100],
None,
),
"hybrid-ml-dsa-44-ed25519".to_string(),
0,
);
let result = verify(&signed, CryptoConfig::new());
assert!(!result.unwrap(), "shape failure must collapse to Ok(false)");
}
#[test]
fn test_verify_hybrid_65_signature_too_short_returns_error() {
let signed = SignedData::new(
b"test".to_vec(),
SignedMetadata::new(
vec![0u8; 100],
"hybrid-ml-dsa-65-ed25519".to_string(),
vec![0u8; 1984],
None,
),
"hybrid-ml-dsa-65-ed25519".to_string(),
0,
);
let result = verify(&signed, CryptoConfig::new());
assert!(!result.unwrap(), "shape failure must collapse to Ok(false)");
}
#[test]
fn test_verify_hybrid_65_invalid_pk_length_returns_error() {
let signed = SignedData::new(
b"test".to_vec(),
SignedMetadata::new(
vec![0u8; 4000],
"hybrid-ml-dsa-65-ed25519".to_string(),
vec![0u8; 100],
None,
),
"hybrid-ml-dsa-65-ed25519".to_string(),
0,
);
let result = verify(&signed, CryptoConfig::new());
assert!(!result.unwrap(), "shape failure must collapse to Ok(false)");
}
#[test]
fn test_verify_hybrid_87_signature_too_short_returns_error() {
let signed = SignedData::new(
b"test".to_vec(),
SignedMetadata::new(
vec![0u8; 100],
"hybrid-ml-dsa-87-ed25519".to_string(),
vec![0u8; 2624],
None,
),
"hybrid-ml-dsa-87-ed25519".to_string(),
0,
);
let result = verify(&signed, CryptoConfig::new());
assert!(!result.unwrap(), "shape failure must collapse to Ok(false)");
}
#[test]
fn test_verify_hybrid_87_invalid_pk_length_returns_error() {
let signed = SignedData::new(
b"test".to_vec(),
SignedMetadata::new(
vec![0u8; 5000],
"hybrid-ml-dsa-87-ed25519".to_string(),
vec![0u8; 100],
None,
),
"hybrid-ml-dsa-87-ed25519".to_string(),
0,
);
let result = verify(&signed, CryptoConfig::new());
assert!(!result.unwrap(), "shape failure must collapse to Ok(false)");
}
#[test]
fn test_hybrid_ml_dsa_44_ed25519_forged_pq_component_fails() {
std::thread::Builder::new()
.name("hybrid_44_forge_pq".to_string())
.stack_size(32 * 1024 * 1024)
.spawn(|| {
use crate::primitives::sig::ml_dsa::MlDsaParameterSet;
use crate::unified_api::convenience::keygen::{
generate_keypair, generate_ml_dsa_keypair,
};
let (pq_pk, pq_sk) = generate_ml_dsa_keypair(MlDsaParameterSet::MlDsa44).unwrap();
let (ed_pk, ed_sk) = generate_keypair().unwrap();
let combined_pk = [pq_pk.into_bytes(), ed_pk.into_bytes()].concat();
let combined_sk = [pq_sk.expose_secret(), ed_sk.expose_secret()].concat();
let message = b"Hybrid forgery test: PQ component flipped";
let config = CryptoConfig::new().security_level(SecurityLevel::Standard);
let mut signed =
sign_with_key(message, &combined_sk, &combined_pk, config).unwrap();
assert!(verify(&signed, CryptoConfig::new()).unwrap());
signed.metadata.signature[16] ^= 0x01;
let result = verify(&signed, CryptoConfig::new()).unwrap();
assert!(
!result,
"Hybrid signature with forged PQ component must NOT verify \
(regression to bitwise OR would pass this case)"
);
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_hybrid_ml_dsa_44_ed25519_forged_ed25519_component_fails() {
std::thread::Builder::new()
.name("hybrid_44_forge_ed".to_string())
.stack_size(32 * 1024 * 1024)
.spawn(|| {
use crate::primitives::sig::ml_dsa::MlDsaParameterSet;
use crate::unified_api::convenience::keygen::{
generate_keypair, generate_ml_dsa_keypair,
};
let (pq_pk, pq_sk) = generate_ml_dsa_keypair(MlDsaParameterSet::MlDsa44).unwrap();
let (ed_pk, ed_sk) = generate_keypair().unwrap();
let combined_pk = [pq_pk.into_bytes(), ed_pk.into_bytes()].concat();
let combined_sk = [pq_sk.expose_secret(), ed_sk.expose_secret()].concat();
let message = b"Hybrid forgery test: Ed25519 component flipped";
let config = CryptoConfig::new().security_level(SecurityLevel::Standard);
let mut signed =
sign_with_key(message, &combined_sk, &combined_pk, config).unwrap();
assert!(verify(&signed, CryptoConfig::new()).unwrap());
let len = signed.metadata.signature.len();
signed.metadata.signature[len - 32] ^= 0x01;
let result = verify(&signed, CryptoConfig::new()).unwrap();
assert!(
!result,
"Hybrid signature with forged Ed25519 component must NOT verify \
(regression to bitwise OR would pass this case)"
);
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_sign_with_key_hybrid_44_invalid_sk_length_returns_error() {
std::thread::Builder::new()
.name("hybrid_44_bad_sk".to_string())
.stack_size(32 * 1024 * 1024)
.spawn(|| {
use crate::primitives::sig::ml_dsa::MlDsaParameterSet;
let pq_pk_len = MlDsaParameterSet::MlDsa44.public_key_size();
let message = b"test";
let bad_sk = vec![0u8; 10]; let pk = vec![0u8; pq_pk_len + 32];
let signed = SignedData::new(
message.to_vec(),
SignedMetadata::new(
vec![],
"hybrid-ml-dsa-44-ed25519".to_string(),
pk.clone(),
None,
),
"hybrid-ml-dsa-44-ed25519".to_string(),
0,
);
let config = CryptoConfig::new().security_level(SecurityLevel::Standard);
let result = sign_with_key(message, &bad_sk, &pk, config);
let _ = result;
let result = verify(&signed, CryptoConfig::new());
assert!(result.is_err() || matches!(result, Ok(false)));
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_decrypt_unknown_scheme_short_key_returns_error() {
let encrypted = EncryptedOutput::new(
EncryptionScheme::Aes256Gcm,
vec![1, 2, 3, 4],
vec![0u8; 12],
vec![0u8; 16],
None,
0,
None,
)
.expect("symmetric scheme + None hybrid_data is a valid shape");
let short_key = vec![0x42u8; 16]; let result = decrypt(&encrypted, DecryptKey::Symmetric(&short_key), CryptoConfig::new());
assert!(result.is_err());
}
#[test]
fn test_decrypt_short_key_ml_kem_scheme_returns_error() {
let result = EncryptedOutput::new(
EncryptionScheme::HybridMlKem768Aes256Gcm,
vec![1, 2, 3, 4],
vec![0u8; 12],
vec![0u8; 16],
None,
0,
None,
);
assert!(result.is_err());
}
#[test]
fn test_encrypt_empty_data_returns_error() {
let key = vec![0x42u8; 32];
let config = CryptoConfig::new().force_scheme(CryptoScheme::Symmetric);
let encrypted = encrypt(b"", EncryptKey::Symmetric(&key), config).unwrap();
let decrypted =
decrypt(&encrypted, DecryptKey::Symmetric(&key), CryptoConfig::new()).unwrap();
assert!(decrypted.is_empty());
}
#[test]
fn test_encrypt_symmetric_key_default_config_rejects_hybrid_scheme_fails() {
let key = vec![0x42u8; 32];
let result = encrypt(b"hello", EncryptKey::Symmetric(&key), CryptoConfig::new());
assert!(result.is_err(), "Symmetric key + hybrid scheme (default config) must be rejected");
let err = format!("{}", result.unwrap_err());
assert!(
err.contains("requires a hybrid key")
|| err.contains("requires a hybrid or PQ-only key")
|| err.contains("mismatch"),
"Error should mention key type mismatch, got: {}",
err
);
}
#[test]
fn test_encrypt_hybrid_key_symmetric_scheme_rejects_fails() {
let (pk, _sk) = crate::unified_api::convenience::generate_hybrid_keypair().unwrap();
let config = CryptoConfig::new().force_scheme(CryptoScheme::Symmetric);
let result = encrypt(b"hello", EncryptKey::Hybrid(&pk), config);
assert!(result.is_err(), "Hybrid key + symmetric scheme must be rejected");
let err = format!("{}", result.unwrap_err());
assert!(
err.contains("requires a symmetric key")
|| err.contains("does not accept a hybrid key")
|| err.contains("mismatch"),
"Error should mention key type mismatch, got: {}",
err
);
}
#[test]
fn test_decrypt_symmetric_key_hybrid_encrypted_data_rejects_fails() {
let (pk, _sk) = crate::unified_api::convenience::generate_hybrid_keypair().unwrap();
let encrypted = encrypt(b"hello", EncryptKey::Hybrid(&pk), CryptoConfig::new()).unwrap();
let fake_key = vec![0x42u8; 32];
let result = decrypt(&encrypted, DecryptKey::Symmetric(&fake_key), CryptoConfig::new());
assert!(result.is_err(), "Symmetric key cannot decrypt hybrid-encrypted data");
}
#[test]
fn test_encrypt_decrypt_with_use_case_succeeds() {
let key = vec![0x42u8; 32];
let data = b"UseCase-based encryption test";
let config = CryptoConfig::new()
.use_case(UseCase::FileStorage)
.force_scheme(CryptoScheme::Symmetric);
let encrypted = encrypt(data, EncryptKey::Symmetric(&key), config).unwrap();
let decrypted =
decrypt(&encrypted, DecryptKey::Symmetric(&key), CryptoConfig::new()).unwrap();
assert_eq!(decrypted.as_slice(), data.as_slice());
}
#[test]
fn test_encrypt_with_iot_use_case_succeeds() {
let key = vec![0x42u8; 32];
let data = b"IoT device data";
let config =
CryptoConfig::new().use_case(UseCase::IoTDevice).force_scheme(CryptoScheme::Symmetric);
let encrypted = encrypt(data, EncryptKey::Symmetric(&key), config).unwrap();
assert!(!encrypted.ciphertext().is_empty());
}
#[test]
fn test_generate_signing_keypair_iot_use_case_succeeds() {
let config = CryptoConfig::new().use_case(UseCase::IoTDevice);
let (pk, sk, scheme) =
generate_signing_keypair(config).expect("IoT signing keygen must succeed").into_parts();
assert_eq!(scheme, "hybrid-ml-dsa-44-ed25519");
assert!(!pk.is_empty());
assert!(!sk.is_empty());
}
#[test]
fn test_generate_signing_keypair_file_storage_use_case_succeeds() {
let config = CryptoConfig::new().use_case(UseCase::FileStorage);
let (_, _, scheme) = generate_signing_keypair(config)
.expect("FileStorage signing keygen must succeed")
.into_parts();
assert_eq!(scheme, "hybrid-ml-dsa-87-ed25519");
}
#[test]
fn test_generate_signing_keypair_secure_messaging_use_case_succeeds() {
let config = CryptoConfig::new().use_case(UseCase::SecureMessaging);
let (_, _, scheme) = generate_signing_keypair(config)
.expect("SecureMessaging signing keygen must succeed")
.into_parts();
assert_eq!(scheme, "hybrid-ml-dsa-65-ed25519");
}
#[test]
fn test_generate_signing_keypair_blockchain_use_case_succeeds() {
let config = CryptoConfig::new().use_case(UseCase::BlockchainTransaction);
let result = generate_signing_keypair(config);
assert!(result.is_ok());
let (pk, sk, scheme) = result.unwrap().into_parts();
assert!(!pk.is_empty());
assert!(!sk.is_empty());
assert!(!scheme.is_empty());
}
#[test]
fn test_generate_signing_keypair_legal_use_case_succeeds() {
let config = CryptoConfig::new().use_case(UseCase::LegalDocuments);
let result = generate_signing_keypair(config);
assert!(result.is_ok());
}
#[test]
fn test_generate_signing_keypair_firmware_use_case_succeeds() {
let config = CryptoConfig::new().use_case(UseCase::FirmwareSigning);
let result = generate_signing_keypair(config);
assert!(result.is_ok());
}
#[test]
fn test_encrypt_decrypt_with_verified_session_succeeds() {
std::thread::Builder::new()
.name("enc_verified".to_string())
.stack_size(32 * 1024 * 1024)
.spawn(|| {
let data = b"Verified session test data";
let key = vec![0x42u8; 32];
let (auth_pk, auth_sk) =
crate::unified_api::convenience::keygen::generate_keypair().unwrap();
let session =
crate::VerifiedSession::establish(auth_pk.as_slice(), auth_sk.expose_secret())
.unwrap();
let config =
CryptoConfig::new().session(&session).force_scheme(CryptoScheme::Symmetric);
let encrypted = encrypt(data, EncryptKey::Symmetric(&key), config.clone()).unwrap();
let decrypted =
decrypt(&encrypted, DecryptKey::Symmetric(&key), CryptoConfig::new()).unwrap();
assert_eq!(data.as_slice(), decrypted.as_slice());
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_sign_verify_with_verified_session_succeeds() {
std::thread::Builder::new()
.name("sign_verified".to_string())
.stack_size(32 * 1024 * 1024)
.spawn(|| {
let message = b"Verified session sign test";
let (auth_pk, auth_sk) =
crate::unified_api::convenience::keygen::generate_keypair().unwrap();
let session =
crate::VerifiedSession::establish(auth_pk.as_slice(), auth_sk.expose_secret())
.unwrap();
let config = CryptoConfig::new().session(&session);
let (pk, sk, _scheme) =
generate_signing_keypair(config.clone()).unwrap().into_parts();
let signed = sign_with_key(message, &sk, &pk, config.clone()).unwrap();
let valid = verify(&signed, config).unwrap();
assert!(valid);
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_encrypt_with_expired_session_returns_session_expired_succeeds() {
std::thread::Builder::new()
.name("enc_expired".to_string())
.stack_size(32 * 1024 * 1024)
.spawn(|| {
let (auth_pk, auth_sk) =
crate::unified_api::convenience::keygen::generate_keypair().unwrap();
let session =
crate::VerifiedSession::establish(auth_pk.as_slice(), auth_sk.expose_secret())
.unwrap();
let expired = session.expired_clone();
let key = vec![0x42u8; 32];
let config =
CryptoConfig::new().session(&expired).force_scheme(CryptoScheme::Symmetric);
let result = encrypt(b"test", EncryptKey::Symmetric(&key), config);
assert!(result.is_err(), "Encrypt with expired session should fail");
match result.unwrap_err() {
CoreError::SessionExpired => {}
other => panic!("Expected SessionExpired, got: {other:?}"),
}
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_decrypt_with_expired_session_returns_session_expired_succeeds() {
std::thread::Builder::new()
.name("dec_expired".to_string())
.stack_size(32 * 1024 * 1024)
.spawn(|| {
let key = vec![0x42u8; 32];
let config = CryptoConfig::new().force_scheme(CryptoScheme::Symmetric);
let encrypted = encrypt(b"test data", EncryptKey::Symmetric(&key), config).unwrap();
let (auth_pk, auth_sk) =
crate::unified_api::convenience::keygen::generate_keypair().unwrap();
let session =
crate::VerifiedSession::establish(auth_pk.as_slice(), auth_sk.expose_secret())
.unwrap();
let expired = session.expired_clone();
let config = CryptoConfig::new().session(&expired);
let result = decrypt(&encrypted, DecryptKey::Symmetric(&key), config);
assert!(result.is_err(), "Decrypt with expired session should fail");
match result.unwrap_err() {
CoreError::SessionExpired => {}
other => panic!("Expected SessionExpired, got: {other:?}"),
}
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_sign_with_expired_session_returns_session_expired_succeeds() {
std::thread::Builder::new()
.name("sign_expired".to_string())
.stack_size(32 * 1024 * 1024)
.spawn(|| {
let (auth_pk, auth_sk) =
crate::unified_api::convenience::keygen::generate_keypair().unwrap();
let session =
crate::VerifiedSession::establish(auth_pk.as_slice(), auth_sk.expose_secret())
.unwrap();
let (pk, sk, _scheme) =
generate_signing_keypair(CryptoConfig::new()).unwrap().into_parts();
let expired = session.expired_clone();
let config = CryptoConfig::new().session(&expired);
let result = sign_with_key(b"test message", &sk, &pk, config);
assert!(result.is_err(), "Sign with expired session should fail");
match result.unwrap_err() {
CoreError::SessionExpired => {}
other => panic!("Expected SessionExpired, got: {other:?}"),
}
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_verify_with_expired_session_returns_session_expired_succeeds() {
std::thread::Builder::new()
.name("verify_expired".to_string())
.stack_size(32 * 1024 * 1024)
.spawn(|| {
let message = b"test message";
let config = CryptoConfig::new();
let (pk, sk, _scheme) =
generate_signing_keypair(config.clone()).unwrap().into_parts();
let signed = sign_with_key(message, &sk, &pk, config).unwrap();
let (auth_pk, auth_sk) =
crate::unified_api::convenience::keygen::generate_keypair().unwrap();
let session =
crate::VerifiedSession::establish(auth_pk.as_slice(), auth_sk.expose_secret())
.unwrap();
let expired = session.expired_clone();
let config = CryptoConfig::new().session(&expired);
let result = verify(&signed, config);
assert!(result.is_err(), "Verify with expired session should fail");
match result.unwrap_err() {
CoreError::SessionExpired => {}
other => panic!("Expected SessionExpired, got: {other:?}"),
}
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_sign_with_key_hybrid_87_wrong_sk_length_returns_error() {
std::thread::Builder::new()
.name("hybrid_87_bad_sk".to_string())
.stack_size(32 * 1024 * 1024)
.spawn(|| {
use crate::primitives::sig::ml_dsa::MlDsaParameterSet;
let pq_pk_len = MlDsaParameterSet::MlDsa87.public_key_size();
let message = b"test";
let bad_sk = vec![0u8; 10]; let pk = vec![0u8; pq_pk_len + 32];
let signed = SignedData::new(
message.to_vec(),
SignedMetadata::new(
vec![0u8; 100],
"hybrid-ml-dsa-87-ed25519".to_string(),
pk,
None,
),
"hybrid-ml-dsa-87-ed25519".to_string(),
0,
);
let result = verify(&signed, CryptoConfig::new());
assert!(!result.unwrap(), "shape failure must collapse to Ok(false)");
let _ = bad_sk; })
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_sign_with_key_hybrid_44_wrong_pk_length_returns_error() {
std::thread::Builder::new()
.name("hybrid_44_bad_pk".to_string())
.stack_size(32 * 1024 * 1024)
.spawn(|| {
use crate::primitives::sig::ml_dsa::MlDsaParameterSet;
let pq_sk_len = MlDsaParameterSet::MlDsa44.secret_key_size();
let message = b"test";
let sk = vec![0u8; pq_sk_len + 32]; let bad_pk = vec![0u8; 100];
let config = CryptoConfig::new().security_level(SecurityLevel::Standard);
let _result = sign_with_key(message, &sk, &bad_pk, config);
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_encrypt_short_key_with_use_case_returns_error() {
let short_key = vec![0x42u8; 16]; let config = CryptoConfig::new()
.use_case(UseCase::FileStorage)
.force_scheme(CryptoScheme::Symmetric);
let result = encrypt(b"test", EncryptKey::Symmetric(&short_key), config);
assert!(result.is_err(), "Short key should fail even with use case");
}
#[test]
fn test_select_encryption_scheme_with_security_level_succeeds() {
let levels = vec![SecurityLevel::Standard, SecurityLevel::High, SecurityLevel::Maximum];
for level in levels {
let config = CryptoConfig::new().security_level(level.clone());
let scheme = select_encryption_scheme_typed(&config);
assert!(scheme.is_ok(), "Should select scheme for security level: {:?}", level);
}
}
#[test]
fn test_select_signature_scheme_with_security_levels_succeeds() {
let levels = vec![SecurityLevel::Standard, SecurityLevel::High, SecurityLevel::Maximum];
for level in levels {
let config = CryptoConfig::new().security_level(level.clone());
let scheme = select_signature_scheme(&config);
assert!(scheme.is_ok(), "Should select sig scheme for security level: {:?}", level);
}
}
#[test]
fn test_encrypt_decrypt_standard_security_level_succeeds() {
let key = vec![0x42u8; 32];
let data = b"Standard level encryption";
let config = CryptoConfig::new()
.security_level(SecurityLevel::Standard)
.force_scheme(CryptoScheme::Symmetric);
let encrypted = encrypt(data, EncryptKey::Symmetric(&key), config).unwrap();
let decrypted =
decrypt(&encrypted, DecryptKey::Symmetric(&key), CryptoConfig::new()).unwrap();
assert_eq!(decrypted.as_slice(), data.as_slice());
}
#[test]
fn test_encrypt_decrypt_maximum_security_level_succeeds() {
let key = vec![0x42u8; 32];
let data = b"Maximum level encryption";
let config = CryptoConfig::new()
.security_level(SecurityLevel::Maximum)
.force_scheme(CryptoScheme::Symmetric);
let encrypted = encrypt(data, EncryptKey::Symmetric(&key), config).unwrap();
let decrypted =
decrypt(&encrypted, DecryptKey::Symmetric(&key), CryptoConfig::new()).unwrap();
assert_eq!(decrypted.as_slice(), data.as_slice());
}
#[cfg(not(feature = "fips"))]
#[test]
fn test_encrypt_decrypt_chacha20poly1305_roundtrip() {
let key = [0x55u8; 32];
let data = b"ChaCha20-Poly1305 roundtrip test";
let encrypted = encrypt_chacha20_internal(data, &key, &[]).unwrap();
let decrypted = decrypt_chacha20_internal(&encrypted, &key, &[]).unwrap();
assert_eq!(decrypted.as_slice(), data.as_slice());
}
#[cfg(not(feature = "fips"))]
#[test]
fn test_encrypt_decrypt_chacha20poly1305_via_unified_api_succeeds() {
let key = [0xAAu8; 32];
let data = b"ChaCha20 unified dispatch test";
let encrypted_bytes = encrypt_chacha20_internal(data, &key, &[]).unwrap();
let output =
symmetric_bytes_to_output(EncryptionScheme::ChaCha20Poly1305, &encrypted_bytes)
.unwrap();
let decrypted = decrypt(&output, DecryptKey::Symmetric(&key), CryptoConfig::new()).unwrap();
assert_eq!(decrypted.as_slice(), data.as_slice());
}
#[cfg(feature = "fips")]
#[test]
fn test_chacha20poly1305_rejected_in_fips_mode_fails() {
let key = [0xBBu8; 32];
let encrypted_bytes = vec![0u8; 100]; let output = EncryptedOutput::new(
EncryptionScheme::ChaCha20Poly1305,
encrypted_bytes,
vec![0u8; 12],
vec![0u8; 16],
None,
0,
None,
)
.expect("symmetric scheme + None hybrid_data is a valid shape");
let result = decrypt(&output, DecryptKey::Symmetric(&key), CryptoConfig::new());
assert!(result.is_err(), "ChaCha20 should be rejected in FIPS mode");
}
#[test]
fn test_generate_signing_keypair_authentication_use_case_succeeds() {
std::thread::Builder::new()
.name("auth_keygen".to_string())
.stack_size(32 * 1024 * 1024)
.spawn(|| {
let config = CryptoConfig::new().use_case(UseCase::Authentication);
let (pk, sk, scheme) = generate_signing_keypair(config).unwrap().into_parts();
assert!(
scheme.contains("hybrid-ml-dsa-87"),
"Auth should use hybrid-87: {}",
scheme
);
assert!(!pk.is_empty());
assert!(!sk.is_empty());
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_generate_signing_keypair_digital_certificate_use_case_succeeds() {
std::thread::Builder::new()
.name("cert_keygen".to_string())
.stack_size(32 * 1024 * 1024)
.spawn(|| {
let config = CryptoConfig::new().use_case(UseCase::DigitalCertificate);
let (pk, sk, scheme) = generate_signing_keypair(config).unwrap().into_parts();
assert!(
scheme.contains("hybrid-ml-dsa-87"),
"DigitalCertificate should use hybrid-87: {}",
scheme
);
assert!(!pk.is_empty());
assert!(!sk.is_empty());
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_sign_message_standard_level_succeeds() {
std::thread::Builder::new()
.name("sign_standard".to_string())
.stack_size(32 * 1024 * 1024)
.spawn(|| {
let config = CryptoConfig::new().security_level(SecurityLevel::Standard);
let signed = sign_message(b"Standard", config).unwrap();
assert!(
signed.scheme.contains("hybrid-ml-dsa-44"),
"Standard should use hybrid-44: {}",
signed.scheme
);
let valid = verify(&signed, CryptoConfig::new()).unwrap();
assert!(valid);
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_cnsa_verify_rejects_ed25519_fails() -> Result<()> {
let message = b"compliance test";
let config_default = CryptoConfig::new().security_level(SecurityLevel::Standard);
let signed = sign_message(message, config_default)?;
if signed.scheme == "ed25519" {
let cnsa_config = CryptoConfig::new()
.security_level(SecurityLevel::Maximum)
.crypto_mode(crate::types::types::CryptoMode::PqOnly)
.compliance(crate::types::types::ComplianceMode::Cnsa2_0);
let result = verify(&signed, cnsa_config);
assert!(result.is_err(), "CNSA 2.0 should reject ed25519 verification");
let err_msg = format!("{}", result.unwrap_err());
assert!(err_msg.contains("Compliance violation"));
}
Ok(())
}
#[cfg(feature = "fips")]
#[test]
fn test_cnsa_verify_rejects_standalone_ed25519_signature_fails() -> Result<()> {
let signed = SignedData::new(
b"test data".to_vec(),
SignedMetadata::new(vec![0u8; 64], "ed25519".to_string(), vec![0u8; 32], None),
"ed25519".to_string(),
0,
);
let cnsa_config = CryptoConfig::new()
.security_level(SecurityLevel::Maximum)
.crypto_mode(crate::types::types::CryptoMode::PqOnly)
.compliance(crate::types::types::ComplianceMode::Cnsa2_0);
let result = verify(&signed, cnsa_config);
assert!(result.is_err(), "CNSA 2.0 must reject ed25519 signatures");
let err_msg = format!("{}", result.unwrap_err());
assert!(err_msg.contains("CNSA 2.0"));
Ok(())
}
#[cfg(feature = "fips")]
#[test]
fn test_fips_decrypt_allows_aes_gcm_succeeds() -> Result<()> {
let key = vec![0x42u8; 32];
let data = b"fips compliance test";
let encrypted = encrypt(
data,
EncryptKey::Symmetric(&key),
CryptoConfig::new().force_scheme(CryptoScheme::Symmetric),
)?;
assert_eq!(encrypted.scheme(), &EncryptionScheme::Aes256Gcm);
let fips_config =
CryptoConfig::new().compliance(crate::types::types::ComplianceMode::Fips140_3);
let plaintext = decrypt(&encrypted, DecryptKey::Symmetric(&key), fips_config)?;
assert_eq!(plaintext.as_slice(), data.as_slice());
Ok(())
}
#[cfg(feature = "fips")]
#[test]
fn test_cnsa_decrypt_rejects_aes_gcm_fails() -> Result<()> {
let key = vec![0x42u8; 32];
let data = b"cnsa decrypt test";
let encrypted = encrypt(
data,
EncryptKey::Symmetric(&key),
CryptoConfig::new().force_scheme(CryptoScheme::Symmetric),
)?;
assert_eq!(encrypted.scheme(), &EncryptionScheme::Aes256Gcm);
let cnsa_config = CryptoConfig::new()
.security_level(SecurityLevel::Maximum)
.crypto_mode(crate::types::types::CryptoMode::PqOnly)
.compliance(crate::types::types::ComplianceMode::Cnsa2_0);
let result = decrypt(&encrypted, DecryptKey::Symmetric(&key), cnsa_config);
assert!(result.is_err(), "CNSA 2.0 should reject standalone AES-256-GCM");
let err_msg = format!("{}", result.unwrap_err());
assert!(err_msg.contains("CNSA 2.0"));
Ok(())
}
#[test]
fn test_default_compliance_allows_all_in_verify_succeeds() -> Result<()> {
let message = b"default compliance test";
let config = CryptoConfig::new();
let signed = sign_message(message, config)?;
let default_config = CryptoConfig::new();
let is_valid = verify(&signed, default_config)?;
assert!(is_valid);
Ok(())
}
#[test]
fn test_default_compliance_allows_all_in_decrypt_succeeds() -> Result<()> {
let key = vec![0x42u8; 32];
let data = b"default compliance decrypt test";
let encrypted = encrypt(
data,
EncryptKey::Symmetric(&key),
CryptoConfig::new().force_scheme(CryptoScheme::Symmetric),
)?;
let default_config = CryptoConfig::new();
let plaintext = decrypt(&encrypted, DecryptKey::Symmetric(&key), default_config)?;
assert_eq!(plaintext.as_slice(), data.as_slice());
Ok(())
}
#[cfg(feature = "fips")]
#[test]
fn test_fips_verify_allows_pq_signatures_succeeds() -> Result<()> {
let message = b"pq fips compliance test";
let config = CryptoConfig::new().security_level(SecurityLevel::High);
let signed = sign_message(message, config)?;
let fips_config =
CryptoConfig::new().compliance(crate::types::types::ComplianceMode::Fips140_3);
let is_valid = verify(&signed, fips_config)?;
assert!(is_valid);
Ok(())
}
#[cfg(feature = "fips")]
#[test]
fn test_cnsa_verify_allows_hybrid_signatures_succeeds() -> Result<()> {
let message = b"hybrid cnsa test";
let config = CryptoConfig::new().security_level(SecurityLevel::High);
let signed = sign_message(message, config)?;
if signed.scheme.contains("hybrid") {
let cnsa_config = CryptoConfig::new()
.security_level(SecurityLevel::High)
.crypto_mode(crate::types::types::CryptoMode::PqOnly)
.compliance(crate::types::types::ComplianceMode::Cnsa2_0);
let is_valid = verify(&signed, cnsa_config)?;
assert!(is_valid);
}
Ok(())
}
#[test]
fn test_pq_only_encrypt_decrypt_roundtrip_768_succeeds() {
use crate::hybrid::pq_only::generate_pq_keypair;
let (pk, sk) = generate_pq_keypair().unwrap();
let data = b"PQ-only unified API roundtrip test";
let config =
CryptoConfig::new().crypto_mode(CryptoMode::PqOnly).security_level(SecurityLevel::High);
let encrypted = encrypt(data, EncryptKey::PqOnly(&pk), config.clone()).unwrap();
assert_eq!(encrypted.scheme().as_str(), "pq-ml-kem-768-aes-256-gcm");
let decrypted = decrypt(&encrypted, DecryptKey::PqOnly(&sk), config).unwrap();
assert_eq!(decrypted.as_slice(), data.as_slice());
}
#[test]
fn test_pq_only_encrypt_decrypt_roundtrip_512_succeeds() {
use crate::hybrid::pq_only::generate_pq_keypair_with_level;
use crate::primitives::kem::ml_kem::MlKemSecurityLevel;
let (pk, sk) = generate_pq_keypair_with_level(MlKemSecurityLevel::MlKem512).unwrap();
let data = b"PQ-only 512 roundtrip";
let config = CryptoConfig::new()
.crypto_mode(CryptoMode::PqOnly)
.security_level(SecurityLevel::Standard);
let encrypted = encrypt(data, EncryptKey::PqOnly(&pk), config.clone()).unwrap();
assert_eq!(encrypted.scheme().as_str(), "pq-ml-kem-512-aes-256-gcm");
let decrypted = decrypt(&encrypted, DecryptKey::PqOnly(&sk), config).unwrap();
assert_eq!(decrypted.as_slice(), data.as_slice());
}
#[test]
fn test_pq_only_encrypt_decrypt_roundtrip_1024_succeeds() {
use crate::hybrid::pq_only::generate_pq_keypair_with_level;
use crate::primitives::kem::ml_kem::MlKemSecurityLevel;
let (pk, sk) = generate_pq_keypair_with_level(MlKemSecurityLevel::MlKem1024).unwrap();
let data = b"PQ-only 1024 roundtrip";
let config = CryptoConfig::new()
.crypto_mode(CryptoMode::PqOnly)
.security_level(SecurityLevel::Maximum);
let encrypted = encrypt(data, EncryptKey::PqOnly(&pk), config.clone()).unwrap();
assert_eq!(encrypted.scheme().as_str(), "pq-ml-kem-1024-aes-256-gcm");
let decrypted = decrypt(&encrypted, DecryptKey::PqOnly(&sk), config).unwrap();
assert_eq!(decrypted.as_slice(), data.as_slice());
}
#[test]
fn test_pq_only_key_rejects_hybrid_scheme_fails() {
use crate::hybrid::pq_only::generate_pq_keypair;
let (pk, _sk) = generate_pq_keypair().unwrap();
let config = CryptoConfig::new(); let result = encrypt(b"test", EncryptKey::PqOnly(&pk), config);
assert!(result.is_err(), "PQ-only key + hybrid scheme must fail");
}
#[test]
fn test_hybrid_key_rejects_pq_only_scheme_fails() {
let (pk, _sk) = crate::unified_api::convenience::generate_hybrid_keypair().unwrap();
let config =
CryptoConfig::new().crypto_mode(CryptoMode::PqOnly).security_level(SecurityLevel::High);
let result = encrypt(b"test", EncryptKey::Hybrid(&pk), config);
assert!(result.is_err(), "Hybrid key + PQ-only scheme must fail");
}
#[test]
fn test_pq_only_empty_data_roundtrip_succeeds() {
use crate::hybrid::pq_only::generate_pq_keypair;
let (pk, sk) = generate_pq_keypair().unwrap();
let config =
CryptoConfig::new().crypto_mode(CryptoMode::PqOnly).security_level(SecurityLevel::High);
let encrypted = encrypt(b"", EncryptKey::PqOnly(&pk), config.clone()).unwrap();
let decrypted = decrypt(&encrypted, DecryptKey::PqOnly(&sk), config).unwrap();
assert!(decrypted.is_empty());
}
}