- Add badges, competitive comparison, and 30-sec demo to README - Add Production Status section showing OQS backend is production-ready - Mark PQC KEM/signing operations complete in roadmap - Fix GitHub URL - Create CHANGELOG.md documenting all recent changes Positions SecretumVault as first Rust vault with production PQC.
417 lines
12 KiB
Rust
417 lines
12 KiB
Rust
//! End-to-end integration tests for post-quantum cryptography
|
|
//!
|
|
//! Tests cover:
|
|
//! - ML-KEM-768 key encapsulation with Transit engine
|
|
//! - ML-DSA-65 certificate generation with PKI engine
|
|
//! - Hybrid mode (classical + PQC) cryptography
|
|
//! - Backward compatibility with classical-only mode
|
|
|
|
#![cfg(all(test, feature = "pqc"))]
|
|
|
|
use secretumvault::config::OqsCryptoConfig;
|
|
use secretumvault::crypto::hybrid::{HybridKem, HybridSignature};
|
|
use secretumvault::crypto::oqs_backend::OqsBackend;
|
|
use secretumvault::crypto::{CryptoBackend, HybridKeyPair, KeyAlgorithm};
|
|
|
|
#[tokio::test]
|
|
async fn test_oqs_backend_ml_kem_768_full_cycle() {
|
|
// Initialize OQS backend
|
|
let config = OqsCryptoConfig { enable_pqc: true };
|
|
let backend = OqsBackend::new(&config).expect("OQS backend creation failed");
|
|
|
|
// Generate ML-KEM-768 keypair
|
|
let keypair = backend
|
|
.generate_keypair(KeyAlgorithm::MlKem768)
|
|
.await
|
|
.expect("ML-KEM-768 key generation failed");
|
|
|
|
// Verify NIST-compliant key sizes
|
|
assert_eq!(
|
|
keypair.public_key.key_data.len(),
|
|
1184,
|
|
"ML-KEM-768 public key must be 1184 bytes"
|
|
);
|
|
assert_eq!(
|
|
keypair.private_key.key_data.len(),
|
|
2400,
|
|
"ML-KEM-768 private key must be 2400 bytes"
|
|
);
|
|
|
|
// KEM encapsulation
|
|
let (ciphertext, shared_secret_1) = backend
|
|
.kem_encapsulate(&keypair.public_key)
|
|
.await
|
|
.expect("ML-KEM-768 encapsulation failed");
|
|
|
|
assert_eq!(
|
|
ciphertext.len(),
|
|
1088,
|
|
"ML-KEM-768 ciphertext must be 1088 bytes"
|
|
);
|
|
assert_eq!(shared_secret_1.len(), 32, "Shared secret must be 32 bytes");
|
|
|
|
// KEM decapsulation
|
|
let shared_secret_2 = backend
|
|
.kem_decapsulate(&keypair.private_key, &ciphertext)
|
|
.await
|
|
.expect("ML-KEM-768 decapsulation failed");
|
|
|
|
// Shared secrets must match
|
|
assert_eq!(
|
|
shared_secret_1, shared_secret_2,
|
|
"KEM shared secrets must match"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_oqs_backend_ml_dsa_65_full_cycle() {
|
|
// Initialize OQS backend
|
|
let config = OqsCryptoConfig { enable_pqc: true };
|
|
let backend = OqsBackend::new(&config).expect("OQS backend creation failed");
|
|
|
|
// Generate ML-DSA-65 keypair
|
|
let keypair = backend
|
|
.generate_keypair(KeyAlgorithm::MlDsa65)
|
|
.await
|
|
.expect("ML-DSA-65 key generation failed");
|
|
|
|
// Verify NIST-compliant key sizes
|
|
assert_eq!(
|
|
keypair.public_key.key_data.len(),
|
|
1952,
|
|
"ML-DSA-65 public key must be 1952 bytes"
|
|
);
|
|
assert_eq!(
|
|
keypair.private_key.key_data.len(),
|
|
4032,
|
|
"ML-DSA-65 private key must be 4032 bytes"
|
|
);
|
|
|
|
let message = b"Test message for ML-DSA-65 signature verification";
|
|
|
|
// Sign
|
|
let signature = backend
|
|
.sign(&keypair.private_key, message)
|
|
.await
|
|
.expect("ML-DSA-65 signing failed");
|
|
|
|
assert!(!signature.is_empty(), "Signature must not be empty");
|
|
|
|
// Verify valid signature
|
|
let is_valid = backend
|
|
.verify(&keypair.public_key, message, &signature)
|
|
.await
|
|
.expect("ML-DSA-65 verification failed");
|
|
|
|
assert!(is_valid, "Valid signature must verify");
|
|
|
|
// Tamper signature and verify it fails
|
|
let mut tampered_sig = signature.clone();
|
|
tampered_sig[0] ^= 0xFF;
|
|
let is_valid = backend
|
|
.verify(&keypair.public_key, message, &tampered_sig)
|
|
.await
|
|
.unwrap_or(false);
|
|
|
|
assert!(!is_valid, "Tampered signature must not verify");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_hybrid_signature_end_to_end() {
|
|
let config = OqsCryptoConfig { enable_pqc: true };
|
|
let backend = OqsBackend::new(&config).expect("OQS backend creation failed");
|
|
|
|
// Generate keypairs (using ML-DSA for both classical and PQC for simplicity)
|
|
let classical_keypair = backend
|
|
.generate_keypair(KeyAlgorithm::MlDsa65)
|
|
.await
|
|
.expect("Classical key generation failed");
|
|
|
|
let pqc_keypair = backend
|
|
.generate_keypair(KeyAlgorithm::MlDsa65)
|
|
.await
|
|
.expect("PQC key generation failed");
|
|
|
|
let message = b"Hybrid signature test message";
|
|
|
|
// Sign with hybrid mode
|
|
let hybrid_sig = HybridSignature::sign(
|
|
&backend,
|
|
&classical_keypair.private_key,
|
|
&pqc_keypair.private_key,
|
|
message,
|
|
)
|
|
.await
|
|
.expect("Hybrid signing failed");
|
|
|
|
// Verify wire format
|
|
assert!(hybrid_sig.len() > 5, "Hybrid signature must include header");
|
|
assert_eq!(hybrid_sig[0], 1, "Version byte must be 1");
|
|
|
|
// Verify valid hybrid signature
|
|
let is_valid = HybridSignature::verify(
|
|
&backend,
|
|
&classical_keypair.public_key,
|
|
&pqc_keypair.public_key,
|
|
message,
|
|
&hybrid_sig,
|
|
)
|
|
.await
|
|
.expect("Hybrid verification failed");
|
|
|
|
assert!(is_valid, "Valid hybrid signature must verify");
|
|
|
|
// Tamper signature and verify it fails
|
|
let mut tampered = hybrid_sig.clone();
|
|
tampered[20] ^= 0xFF;
|
|
let is_valid = HybridSignature::verify(
|
|
&backend,
|
|
&classical_keypair.public_key,
|
|
&pqc_keypair.public_key,
|
|
message,
|
|
&tampered,
|
|
)
|
|
.await
|
|
.unwrap_or(false);
|
|
|
|
assert!(!is_valid, "Tampered hybrid signature must not verify");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_hybrid_kem_end_to_end() {
|
|
let config = OqsCryptoConfig { enable_pqc: true };
|
|
let backend = OqsBackend::new(&config).expect("OQS backend creation failed");
|
|
|
|
// Generate keypairs
|
|
let pqc_keypair = backend
|
|
.generate_keypair(KeyAlgorithm::MlKem768)
|
|
.await
|
|
.expect("PQC key generation failed");
|
|
|
|
// Use ML-DSA as placeholder for classical (in real scenario: RSA/ECDSA)
|
|
let classical_keypair = backend
|
|
.generate_keypair(KeyAlgorithm::MlDsa65)
|
|
.await
|
|
.expect("Classical key generation failed");
|
|
|
|
// Hybrid KEM encapsulation
|
|
let (ciphertext, shared_secret_1) = HybridKem::encapsulate(
|
|
&backend,
|
|
&classical_keypair.public_key,
|
|
&pqc_keypair.public_key,
|
|
)
|
|
.await
|
|
.expect("Hybrid KEM encapsulation failed");
|
|
|
|
// Verify wire format
|
|
assert!(
|
|
ciphertext.len() > 5,
|
|
"Hybrid KEM ciphertext must include header"
|
|
);
|
|
assert_eq!(ciphertext[0], 1, "Version byte must be 1");
|
|
assert_eq!(shared_secret_1.len(), 32, "Shared secret must be 32 bytes");
|
|
|
|
// Hybrid KEM decapsulation
|
|
let shared_secret_2 = HybridKem::decapsulate(
|
|
&backend,
|
|
&classical_keypair.private_key,
|
|
&pqc_keypair.private_key,
|
|
&ciphertext,
|
|
)
|
|
.await
|
|
.expect("Hybrid KEM decapsulation failed");
|
|
|
|
// Shared secrets must match
|
|
assert_eq!(
|
|
shared_secret_1, shared_secret_2,
|
|
"Hybrid KEM shared secrets must match"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_pqc_no_fake_crypto() {
|
|
// This test verifies that we're NOT using rand::fill_bytes() for cryptographic
|
|
// operations
|
|
let config = OqsCryptoConfig { enable_pqc: true };
|
|
let backend = OqsBackend::new(&config).expect("OQS backend creation failed");
|
|
|
|
// Generate two ML-KEM-768 keypairs
|
|
let keypair1 = backend
|
|
.generate_keypair(KeyAlgorithm::MlKem768)
|
|
.await
|
|
.expect("First key generation failed");
|
|
|
|
let keypair2 = backend
|
|
.generate_keypair(KeyAlgorithm::MlKem768)
|
|
.await
|
|
.expect("Second key generation failed");
|
|
|
|
// Keys must be different (not filled with random bytes deterministically)
|
|
assert_ne!(
|
|
keypair1.public_key.key_data, keypair2.public_key.key_data,
|
|
"Public keys must be different"
|
|
);
|
|
assert_ne!(
|
|
keypair1.private_key.key_data, keypair2.private_key.key_data,
|
|
"Private keys must be different"
|
|
);
|
|
|
|
// Encapsulate with keypair1, try to decapsulate with keypair2 - must fail
|
|
let (ciphertext, _shared_secret_1) = backend
|
|
.kem_encapsulate(&keypair1.public_key)
|
|
.await
|
|
.expect("Encapsulation failed");
|
|
|
|
let result = backend
|
|
.kem_decapsulate(&keypair2.private_key, &ciphertext)
|
|
.await;
|
|
|
|
// Should succeed but produce different shared secret (or fail if OQS validates
|
|
// keys) The important part is that it's real crypto, not fake random bytes
|
|
match result {
|
|
Ok(shared_secret_2) => {
|
|
// If decapsulation succeeds with wrong key, it's still real crypto
|
|
// (some KEM implementations always succeed but produce wrong secret)
|
|
assert_eq!(shared_secret_2.len(), 32, "Shared secret must be 32 bytes");
|
|
}
|
|
Err(_) => {
|
|
// If decapsulation fails, that's also acceptable (stricter
|
|
// validation)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_hybrid_keypair_structure() {
|
|
let config = OqsCryptoConfig { enable_pqc: true };
|
|
let backend = OqsBackend::new(&config).expect("OQS backend creation failed");
|
|
|
|
let classical = backend
|
|
.generate_keypair(KeyAlgorithm::MlDsa65)
|
|
.await
|
|
.expect("Classical key generation failed");
|
|
|
|
let pqc = backend
|
|
.generate_keypair(KeyAlgorithm::MlKem768)
|
|
.await
|
|
.expect("PQC key generation failed");
|
|
|
|
let hybrid = HybridKeyPair {
|
|
classical: classical.clone(),
|
|
pqc: pqc.clone(),
|
|
};
|
|
|
|
// Verify structure
|
|
assert_eq!(hybrid.classical.algorithm, classical.algorithm);
|
|
assert_eq!(hybrid.pqc.algorithm, pqc.algorithm);
|
|
assert_eq!(
|
|
hybrid.classical.public_key.key_data,
|
|
classical.public_key.key_data
|
|
);
|
|
assert_eq!(hybrid.pqc.public_key.key_data, pqc.public_key.key_data);
|
|
|
|
// Verify serialization/deserialization
|
|
let serialized = serde_json::to_string(&hybrid).expect("Serialization failed");
|
|
let deserialized: HybridKeyPair =
|
|
serde_json::from_str(&serialized).expect("Deserialization failed");
|
|
|
|
assert_eq!(
|
|
hybrid.classical.public_key.key_data,
|
|
deserialized.classical.public_key.key_data
|
|
);
|
|
assert_eq!(
|
|
hybrid.pqc.public_key.key_data,
|
|
deserialized.pqc.public_key.key_data
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_config_validation() {
|
|
use secretumvault::config::{AwsLcCryptoConfig, OqsCryptoConfig};
|
|
|
|
// AWS-LC config: hybrid_mode requires enable_pqc
|
|
let invalid_config = AwsLcCryptoConfig {
|
|
enable_pqc: false,
|
|
hybrid_mode: true,
|
|
};
|
|
assert!(
|
|
invalid_config.validate().is_err(),
|
|
"hybrid_mode without enable_pqc must fail validation"
|
|
);
|
|
|
|
let valid_config = AwsLcCryptoConfig {
|
|
enable_pqc: true,
|
|
hybrid_mode: true,
|
|
};
|
|
// This will fail if pqc feature not enabled, but that's expected
|
|
let _ = valid_config.validate();
|
|
|
|
// OQS config validation
|
|
let oqs_config = OqsCryptoConfig { enable_pqc: true };
|
|
// This will fail if pqc feature not enabled, but that's expected
|
|
let _ = oqs_config.validate();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_symmetric_encryption_compatibility() {
|
|
// Verify that symmetric encryption (AES-256-GCM, ChaCha20-Poly1305) still works
|
|
let config = OqsCryptoConfig { enable_pqc: true };
|
|
let backend = OqsBackend::new(&config).expect("OQS backend creation failed");
|
|
|
|
let key = backend
|
|
.random_bytes(32)
|
|
.await
|
|
.expect("Random bytes generation failed");
|
|
let plaintext = b"Symmetric encryption test";
|
|
|
|
// Test AES-256-GCM
|
|
let ciphertext = backend
|
|
.encrypt_symmetric(
|
|
&key,
|
|
plaintext,
|
|
secretumvault::crypto::SymmetricAlgorithm::Aes256Gcm,
|
|
)
|
|
.await
|
|
.expect("AES-256-GCM encryption failed");
|
|
|
|
let decrypted = backend
|
|
.decrypt_symmetric(
|
|
&key,
|
|
&ciphertext,
|
|
secretumvault::crypto::SymmetricAlgorithm::Aes256Gcm,
|
|
)
|
|
.await
|
|
.expect("AES-256-GCM decryption failed");
|
|
|
|
assert_eq!(plaintext.as_slice(), decrypted.as_slice());
|
|
|
|
// Test ChaCha20-Poly1305
|
|
let ciphertext = backend
|
|
.encrypt_symmetric(
|
|
&key,
|
|
plaintext,
|
|
secretumvault::crypto::SymmetricAlgorithm::ChaCha20Poly1305,
|
|
)
|
|
.await
|
|
.expect("ChaCha20-Poly1305 encryption failed");
|
|
|
|
let decrypted = backend
|
|
.decrypt_symmetric(
|
|
&key,
|
|
&ciphertext,
|
|
secretumvault::crypto::SymmetricAlgorithm::ChaCha20Poly1305,
|
|
)
|
|
.await
|
|
.expect("ChaCha20-Poly1305 decryption failed");
|
|
|
|
assert_eq!(plaintext.as_slice(), decrypted.as_slice());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_health_check() {
|
|
let config = OqsCryptoConfig { enable_pqc: true };
|
|
let backend = OqsBackend::new(&config).expect("OQS backend creation failed");
|
|
|
|
backend.health_check().await.expect("Health check failed");
|
|
}
|