#![deny(unsafe_code)]
#![deny(missing_docs)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::panic)]
use std::collections::BTreeMap;
use base64::{Engine, engine::general_purpose::STANDARD as BASE64_ENGINE};
const ML_KEM_PK_METADATA_KEY: &str = "ml_kem_pk";
const ENCRYPTED_ENVELOPE_VERSION: u32 = 1;
const PBKDF2_KDF_ID: &str = "PBKDF2-HMAC-SHA256";
const AES_GCM_AEAD_ID: &str = "AES-256-GCM";
const PBKDF2_DEFAULT_ITERATIONS: u32 = 600_000;
const PBKDF2_MIN_ITERATIONS: u32 = 100_000;
const PBKDF2_SALT_LEN: usize = 16;
const PBKDF2_MIN_SALT_LEN: usize = 16;
const AES_GCM_NONCE_LEN: usize = 12;
const AES_GCM_TAG_LEN: usize = 16;
const AES_256_KEY_LEN: usize = 32;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use zeroize::Zeroize;
use crate::unified_api::error::{CoreError, Result};
pub type ZeroizingKeyPair = (zeroize::Zeroizing<Vec<u8>>, zeroize::Zeroizing<Vec<u8>>);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum KeyAlgorithm {
#[serde(rename = "ml-kem-512")]
MlKem512,
#[serde(rename = "ml-kem-768")]
MlKem768,
#[serde(rename = "ml-kem-1024")]
MlKem1024,
#[serde(rename = "ml-dsa-44")]
MlDsa44,
#[serde(rename = "ml-dsa-65")]
MlDsa65,
#[serde(rename = "ml-dsa-87")]
MlDsa87,
#[serde(rename = "slh-dsa-shake-128s")]
SlhDsaShake128s,
#[serde(rename = "slh-dsa-shake-256f")]
SlhDsaShake256f,
#[serde(rename = "fn-dsa-512")]
FnDsa512,
#[serde(rename = "fn-dsa-1024")]
FnDsa1024,
#[serde(rename = "ed25519")]
Ed25519,
#[serde(rename = "x25519")]
X25519,
#[serde(rename = "aes-256")]
Aes256,
#[serde(rename = "chacha20")]
ChaCha20,
#[serde(rename = "hybrid-ml-kem-768-x25519")]
HybridMlKem768X25519,
#[serde(rename = "hybrid-ml-kem-512-x25519")]
HybridMlKem512X25519,
#[serde(rename = "hybrid-ml-kem-1024-x25519")]
HybridMlKem1024X25519,
#[serde(rename = "hybrid-ml-dsa-65-ed25519")]
HybridMlDsa65Ed25519,
#[serde(rename = "hybrid-ml-dsa-44-ed25519")]
HybridMlDsa44Ed25519,
#[serde(rename = "hybrid-ml-dsa-87-ed25519")]
HybridMlDsa87Ed25519,
}
impl KeyAlgorithm {
#[must_use]
pub fn is_hybrid(&self) -> bool {
matches!(
self,
Self::HybridMlKem512X25519
| Self::HybridMlKem768X25519
| Self::HybridMlKem1024X25519
| Self::HybridMlDsa44Ed25519
| Self::HybridMlDsa65Ed25519
| Self::HybridMlDsa87Ed25519
)
}
#[must_use]
pub fn is_symmetric(&self) -> bool {
matches!(self, Self::Aes256 | Self::ChaCha20)
}
#[must_use]
pub fn is_kem(&self) -> bool {
matches!(
self,
Self::X25519
| Self::MlKem512
| Self::MlKem768
| Self::MlKem1024
| Self::HybridMlKem512X25519
| Self::HybridMlKem768X25519
| Self::HybridMlKem1024X25519
)
}
#[must_use]
pub fn is_signature(&self) -> bool {
matches!(
self,
Self::Ed25519
| Self::MlDsa44
| Self::MlDsa65
| Self::MlDsa87
| Self::SlhDsaShake128s
| Self::SlhDsaShake256f
| Self::FnDsa512
| Self::FnDsa1024
| Self::HybridMlDsa44Ed25519
| Self::HybridMlDsa65Ed25519
| Self::HybridMlDsa87Ed25519
)
}
#[must_use]
pub fn canonical_name(self) -> &'static str {
match self {
Self::MlKem512 => "ml-kem-512",
Self::MlKem768 => "ml-kem-768",
Self::MlKem1024 => "ml-kem-1024",
Self::MlDsa44 => "ml-dsa-44",
Self::MlDsa65 => "ml-dsa-65",
Self::MlDsa87 => "ml-dsa-87",
Self::SlhDsaShake128s => "slh-dsa-shake-128s",
Self::SlhDsaShake256f => "slh-dsa-shake-256f",
Self::FnDsa512 => "fn-dsa-512",
Self::FnDsa1024 => "fn-dsa-1024",
Self::Ed25519 => "ed25519",
Self::X25519 => "x25519",
Self::Aes256 => "aes-256",
Self::ChaCha20 => "chacha20",
Self::HybridMlKem768X25519 => "hybrid-ml-kem-768-x25519",
Self::HybridMlKem512X25519 => "hybrid-ml-kem-512-x25519",
Self::HybridMlKem1024X25519 => "hybrid-ml-kem-1024-x25519",
Self::HybridMlDsa65Ed25519 => "hybrid-ml-dsa-65-ed25519",
Self::HybridMlDsa44Ed25519 => "hybrid-ml-dsa-44-ed25519",
Self::HybridMlDsa87Ed25519 => "hybrid-ml-dsa-87-ed25519",
}
}
}
impl KeyType {
#[must_use]
pub fn canonical_name(self) -> &'static str {
match self {
Self::Public => "public",
Self::Secret => "secret",
Self::Symmetric => "symmetric",
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum KeyType {
Public,
Secret,
Symmetric,
}
#[non_exhaustive]
#[derive(Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum KeyData {
Encrypted {
enc: u32,
kdf: String,
kdf_iterations: u32,
kdf_salt: String,
aead: String,
nonce: String,
ciphertext: String,
},
Single {
raw: String,
},
Composite {
pq: String,
classical: String,
},
}
impl std::fmt::Debug for KeyData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Single { .. } => f.debug_struct("Single").field("raw", &"[...]").finish(),
Self::Composite { .. } => f
.debug_struct("Composite")
.field("pq", &"[...]")
.field("classical", &"[...]")
.finish(),
Self::Encrypted { enc, kdf, kdf_iterations, aead, .. } => f
.debug_struct("Encrypted")
.field("enc", enc)
.field("kdf", kdf)
.field("kdf_iterations", kdf_iterations)
.field("aead", aead)
.field("kdf_salt", &"[...]")
.field("nonce", &"[...]")
.field("ciphertext", &"[REDACTED]")
.finish(),
}
}
}
impl Drop for KeyData {
fn drop(&mut self) {
match self {
Self::Single { raw } => {
raw.zeroize();
}
Self::Composite { pq, classical } => {
pq.zeroize();
classical.zeroize();
}
Self::Encrypted { ciphertext, nonce, kdf_salt, .. } => {
ciphertext.zeroize();
nonce.zeroize();
kdf_salt.zeroize();
}
}
}
}
impl KeyData {
pub fn decode_raw(&self) -> Result<Vec<u8>> {
match self {
Self::Single { raw } => BASE64_ENGINE
.decode(raw)
.map_err(|e| CoreError::SerializationError(format!("Invalid key base64: {e}"))),
Self::Composite { .. } => Err(CoreError::InvalidKey(
"Expected single key data but found composite".to_string(),
)),
Self::Encrypted { .. } => Err(CoreError::InvalidKey(
"Key is passphrase-encrypted; call PortableKey::decrypt_with_passphrase first"
.to_string(),
)),
}
}
pub fn decode_raw_zeroized(&self) -> Result<zeroize::Zeroizing<Vec<u8>>> {
self.decode_raw().map(zeroize::Zeroizing::new)
}
pub fn decode_composite(&self) -> Result<(Vec<u8>, Vec<u8>)> {
match self {
Self::Composite { pq, classical } => {
let pq_bytes = BASE64_ENGINE.decode(pq).map_err(|e| {
CoreError::SerializationError(format!("Invalid PQ key base64: {e}"))
})?;
let classical_bytes = BASE64_ENGINE.decode(classical).map_err(|e| {
CoreError::SerializationError(format!("Invalid classical key base64: {e}"))
})?;
Ok((pq_bytes, classical_bytes))
}
Self::Single { .. } => Err(CoreError::InvalidKey(
"Expected composite key data but found single".to_string(),
)),
Self::Encrypted { .. } => Err(CoreError::InvalidKey(
"Key is passphrase-encrypted; call PortableKey::decrypt_with_passphrase first"
.to_string(),
)),
}
}
pub fn decode_composite_zeroized(&self) -> Result<ZeroizingKeyPair> {
let (pq, classical) = self.decode_composite()?;
Ok((zeroize::Zeroizing::new(pq), zeroize::Zeroizing::new(classical)))
}
#[must_use]
pub fn from_raw(bytes: &[u8]) -> Self {
Self::Single { raw: BASE64_ENGINE.encode(bytes) }
}
#[must_use]
pub fn from_composite(pq_bytes: &[u8], classical_bytes: &[u8]) -> Self {
Self::Composite {
pq: BASE64_ENGINE.encode(pq_bytes),
classical: BASE64_ENGINE.encode(classical_bytes),
}
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct PortableKey {
version: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
use_case: Option<crate::types::types::UseCase>,
#[serde(default, skip_serializing_if = "Option::is_none")]
security_level: Option<crate::types::types::SecurityLevel>,
algorithm: KeyAlgorithm,
key_type: KeyType,
key_data: KeyData,
created: DateTime<Utc>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
metadata: BTreeMap<String, serde_json::Value>,
}
impl std::fmt::Debug for PortableKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let key_data_display = match self.key_type {
KeyType::Secret | KeyType::Symmetric => "[REDACTED]",
KeyType::Public => "[key data]",
};
f.debug_struct("PortableKey")
.field("version", &self.version)
.field("use_case", &self.use_case)
.field("security_level", &self.security_level)
.field("algorithm", &self.algorithm)
.field("key_type", &self.key_type)
.field("key_data", &key_data_display)
.field("created", &self.created)
.field("metadata", &self.metadata)
.finish()
}
}
impl subtle::ConstantTimeEq for PortableKey {
fn ct_eq(&self, other: &Self) -> subtle::Choice {
match (&self.key_data, &other.key_data) {
(KeyData::Single { raw: a }, KeyData::Single { raw: b }) => {
a.as_bytes().ct_eq(b.as_bytes())
}
(
KeyData::Composite { pq: a_pq, classical: a_cl },
KeyData::Composite { pq: b_pq, classical: b_cl },
) => a_pq.as_bytes().ct_eq(b_pq.as_bytes()) & a_cl.as_bytes().ct_eq(b_cl.as_bytes()),
_ => subtle::Choice::from(0),
}
}
}
#[must_use]
fn resolve_use_case_algorithm(use_case: crate::types::types::UseCase) -> KeyAlgorithm {
use crate::types::types::UseCase;
match use_case {
UseCase::IoTDevice => KeyAlgorithm::HybridMlKem512X25519,
UseCase::SecureMessaging
| UseCase::VpnTunnel
| UseCase::ApiSecurity
| UseCase::DatabaseEncryption
| UseCase::ConfigSecrets
| UseCase::SessionToken
| UseCase::AuditLog
| UseCase::Authentication
| UseCase::FinancialTransactions
| UseCase::BlockchainTransaction
| UseCase::FirmwareSigning
| UseCase::DigitalCertificate
| UseCase::LegalDocuments => KeyAlgorithm::HybridMlKem768X25519,
UseCase::EmailEncryption
| UseCase::FileStorage
| UseCase::CloudStorage
| UseCase::BackupArchive
| UseCase::KeyExchange
| UseCase::HealthcareRecords
| UseCase::GovernmentClassified
| UseCase::PaymentCard => KeyAlgorithm::HybridMlKem1024X25519,
}
}
#[must_use]
#[allow(deprecated)] fn resolve_security_level_algorithm(level: crate::types::types::SecurityLevel) -> KeyAlgorithm {
use crate::types::types::SecurityLevel;
match level {
SecurityLevel::Standard => KeyAlgorithm::HybridMlKem512X25519,
SecurityLevel::High => KeyAlgorithm::HybridMlKem768X25519,
SecurityLevel::Maximum => KeyAlgorithm::HybridMlKem1024X25519,
SecurityLevel::Quantum => KeyAlgorithm::MlKem1024,
}
}
impl PortableKey {
pub const CURRENT_VERSION: u32 = 1;
#[must_use]
pub fn for_use_case(
use_case: crate::types::types::UseCase,
key_type: KeyType,
key_data: KeyData,
) -> Self {
let algorithm = resolve_use_case_algorithm(use_case);
Self {
version: Self::CURRENT_VERSION,
use_case: Some(use_case),
security_level: None,
algorithm,
key_type,
key_data,
created: Utc::now(),
metadata: BTreeMap::new(),
}
}
#[must_use]
pub fn for_security_level(
level: crate::types::types::SecurityLevel,
key_type: KeyType,
key_data: KeyData,
) -> Self {
let algorithm = resolve_security_level_algorithm(level);
Self {
version: Self::CURRENT_VERSION,
use_case: None,
security_level: Some(level),
algorithm,
key_type,
key_data,
created: Utc::now(),
metadata: BTreeMap::new(),
}
}
#[must_use]
pub fn for_use_case_with_level(
use_case: crate::types::types::UseCase,
level: crate::types::types::SecurityLevel,
key_type: KeyType,
key_data: KeyData,
) -> Self {
let algorithm = resolve_security_level_algorithm(level);
Self {
version: Self::CURRENT_VERSION,
use_case: Some(use_case),
security_level: Some(level),
algorithm,
key_type,
key_data,
created: Utc::now(),
metadata: BTreeMap::new(),
}
}
#[must_use]
pub fn new(algorithm: KeyAlgorithm, key_type: KeyType, key_data: KeyData) -> Self {
Self {
version: Self::CURRENT_VERSION,
use_case: None,
security_level: None,
algorithm,
key_type,
key_data,
created: Utc::now(),
metadata: BTreeMap::new(),
}
}
#[must_use]
pub fn with_created(
algorithm: KeyAlgorithm,
key_type: KeyType,
key_data: KeyData,
created: DateTime<Utc>,
) -> Self {
Self {
version: Self::CURRENT_VERSION,
use_case: None,
security_level: None,
algorithm,
key_type,
key_data,
created,
metadata: BTreeMap::new(),
}
}
#[must_use]
pub fn version(&self) -> u32 {
self.version
}
#[must_use]
pub fn use_case(&self) -> Option<crate::types::types::UseCase> {
self.use_case
}
#[must_use]
pub fn security_level(&self) -> Option<crate::types::types::SecurityLevel> {
self.security_level
}
#[must_use]
pub fn algorithm(&self) -> KeyAlgorithm {
self.algorithm
}
#[must_use]
pub fn key_type(&self) -> KeyType {
self.key_type
}
#[must_use]
pub fn key_data(&self) -> &KeyData {
&self.key_data
}
#[must_use]
pub fn created(&self) -> &DateTime<Utc> {
&self.created
}
#[must_use]
pub fn metadata(&self) -> &BTreeMap<String, serde_json::Value> {
&self.metadata
}
pub fn set_metadata(&mut self, key: String, value: serde_json::Value) {
self.metadata.insert(key, value);
}
pub fn set_label(&mut self, label: impl Into<String>) {
self.metadata.insert("label".to_string(), serde_json::Value::String(label.into()));
}
#[must_use]
pub fn label(&self) -> Option<&str> {
self.metadata.get("label").and_then(|v| v.as_str())
}
pub fn from_hybrid_kem_keypair(
use_case: crate::types::types::UseCase,
pk: &crate::hybrid::kem_hybrid::HybridKemPublicKey,
sk: &crate::hybrid::kem_hybrid::HybridKemSecretKey,
) -> Result<(Self, Self)> {
let algorithm = match pk.security_level() {
crate::primitives::kem::MlKemSecurityLevel::MlKem512 => {
KeyAlgorithm::HybridMlKem512X25519
}
crate::primitives::kem::MlKemSecurityLevel::MlKem768 => {
KeyAlgorithm::HybridMlKem768X25519
}
crate::primitives::kem::MlKemSecurityLevel::MlKem1024 => {
KeyAlgorithm::HybridMlKem1024X25519
}
};
let pub_key = Self {
version: Self::CURRENT_VERSION,
use_case: Some(use_case),
security_level: None,
algorithm,
key_type: KeyType::Public,
key_data: KeyData::from_composite(pk.ml_kem_pk(), pk.ecdh_pk()),
created: Utc::now(),
metadata: BTreeMap::new(),
};
let ml_kem_sk = sk
.ml_kem_sk_bytes()
.map_err(|e| CoreError::InvalidKey(format!("ML-KEM SK export: {e}")))?;
let ecdh_seed = sk
.ecdh_seed_bytes()
.map_err(|e| CoreError::InvalidKey(format!("ECDH seed export: {e}")))?;
let mut sk_metadata = BTreeMap::new();
sk_metadata.insert(
ML_KEM_PK_METADATA_KEY.to_string(),
serde_json::Value::String(BASE64_ENGINE.encode(pk.ml_kem_pk())),
);
let sec_key = Self {
version: Self::CURRENT_VERSION,
use_case: Some(use_case),
security_level: None,
algorithm,
key_type: KeyType::Secret,
key_data: KeyData::from_composite(&ml_kem_sk, &*ecdh_seed),
created: Utc::now(),
metadata: sk_metadata,
};
Ok((pub_key, sec_key))
}
pub fn to_hybrid_public_key(&self) -> Result<crate::hybrid::kem_hybrid::HybridKemPublicKey> {
let level = match self.algorithm {
KeyAlgorithm::HybridMlKem512X25519 => {
crate::primitives::kem::MlKemSecurityLevel::MlKem512
}
KeyAlgorithm::HybridMlKem768X25519 => {
crate::primitives::kem::MlKemSecurityLevel::MlKem768
}
KeyAlgorithm::HybridMlKem1024X25519 => {
crate::primitives::kem::MlKemSecurityLevel::MlKem1024
}
other => {
return Err(CoreError::InvalidKey(format!(
"Not a hybrid KEM algorithm: {other:?}"
)));
}
};
let (pq_bytes, classical_bytes) = self.key_data.decode_composite()?;
Ok(crate::hybrid::kem_hybrid::HybridKemPublicKey::new(pq_bytes, classical_bytes, level))
}
pub fn to_hybrid_secret_key(&self) -> Result<crate::hybrid::kem_hybrid::HybridKemSecretKey> {
let level = match self.algorithm {
KeyAlgorithm::HybridMlKem512X25519 => {
crate::primitives::kem::MlKemSecurityLevel::MlKem512
}
KeyAlgorithm::HybridMlKem768X25519 => {
crate::primitives::kem::MlKemSecurityLevel::MlKem768
}
KeyAlgorithm::HybridMlKem1024X25519 => {
crate::primitives::kem::MlKemSecurityLevel::MlKem1024
}
other => {
return Err(CoreError::InvalidKey(format!(
"Not a hybrid KEM algorithm: {other:?}"
)));
}
};
if self.key_type != KeyType::Secret {
return Err(CoreError::InvalidKey(
"Cannot reconstruct secret key from a public key".to_string(),
));
}
let (ml_kem_sk, ecdh_seed_vec) = self.key_data.decode_composite()?;
let ml_kem_pk = self
.metadata
.get(ML_KEM_PK_METADATA_KEY)
.and_then(|v| v.as_str())
.ok_or_else(|| {
CoreError::InvalidKey(
"Secret key file missing 'ml_kem_pk' metadata. \
Re-generate the keypair with the latest CLI."
.to_string(),
)
})
.and_then(|b64| {
BASE64_ENGINE
.decode(b64)
.map_err(|e| CoreError::InvalidKey(format!("Invalid ml_kem_pk base64: {e}")))
})?;
if ecdh_seed_vec.len() != 32 {
return Err(CoreError::InvalidKey(format!(
"X25519 seed must be 32 bytes, got {}",
ecdh_seed_vec.len()
)));
}
let mut ecdh_seed = zeroize::Zeroizing::new([0u8; 32]);
ecdh_seed.copy_from_slice(&ecdh_seed_vec);
crate::hybrid::kem_hybrid::HybridKemSecretKey::from_serialized(
level, &ml_kem_sk, &ml_kem_pk, &ecdh_seed,
)
.map_err(|e| CoreError::InvalidKey(format!("Secret key reconstruction: {e}")))
}
pub fn from_hybrid_sig_keypair(
use_case: crate::types::types::UseCase,
pk: &crate::hybrid::sig_hybrid::HybridSigPublicKey,
sk: &crate::hybrid::sig_hybrid::HybridSigSecretKey,
) -> Result<(Self, Self)> {
let algorithm = match pk.ml_dsa_pk().len() {
1312 => KeyAlgorithm::HybridMlDsa44Ed25519,
1952 => KeyAlgorithm::HybridMlDsa65Ed25519,
2592 => KeyAlgorithm::HybridMlDsa87Ed25519,
n => {
return Err(CoreError::InvalidKey(format!(
"Cannot detect ML-DSA parameter set from public key length {n}: \
expected 1312 (ML-DSA-44), 1952 (ML-DSA-65), or 2592 (ML-DSA-87)"
)));
}
};
let pub_key = Self {
version: Self::CURRENT_VERSION,
use_case: Some(use_case),
security_level: None,
algorithm,
key_type: KeyType::Public,
key_data: KeyData::from_composite(pk.ml_dsa_pk(), pk.ed25519_pk()),
created: Utc::now(),
metadata: BTreeMap::new(),
};
let sec_key = Self {
version: Self::CURRENT_VERSION,
use_case: Some(use_case),
security_level: None,
algorithm,
key_type: KeyType::Secret,
key_data: KeyData::from_composite(sk.ml_dsa_sk(), sk.ed25519_sk()),
created: Utc::now(),
metadata: BTreeMap::new(),
};
Ok((pub_key, sec_key))
}
pub fn to_hybrid_sig_public_key(
&self,
) -> Result<crate::hybrid::sig_hybrid::HybridSigPublicKey> {
if !matches!(
self.algorithm,
KeyAlgorithm::HybridMlDsa44Ed25519
| KeyAlgorithm::HybridMlDsa65Ed25519
| KeyAlgorithm::HybridMlDsa87Ed25519
) {
return Err(CoreError::InvalidKey(format!(
"Not a hybrid signature algorithm: {:?}",
self.algorithm
)));
}
let (pq_bytes, classical_bytes) = self.key_data.decode_composite()?;
Ok(crate::hybrid::sig_hybrid::HybridSigPublicKey::new(pq_bytes, classical_bytes))
}
pub fn to_hybrid_sig_secret_key(
&self,
) -> Result<crate::hybrid::sig_hybrid::HybridSigSecretKey> {
if !matches!(
self.algorithm,
KeyAlgorithm::HybridMlDsa44Ed25519
| KeyAlgorithm::HybridMlDsa65Ed25519
| KeyAlgorithm::HybridMlDsa87Ed25519
) {
return Err(CoreError::InvalidKey(format!(
"Not a hybrid signature algorithm: {:?}",
self.algorithm
)));
}
if self.key_type != KeyType::Secret {
return Err(CoreError::InvalidKey(
"Cannot reconstruct secret key from a public key".to_string(),
));
}
let (pq_bytes, classical_bytes) = self.key_data.decode_composite()?;
Ok(crate::hybrid::sig_hybrid::HybridSigSecretKey::new(
zeroize::Zeroizing::new(pq_bytes),
zeroize::Zeroizing::new(classical_bytes),
))
}
#[must_use]
pub fn from_keypair(
use_case: crate::types::types::UseCase,
algorithm: KeyAlgorithm,
public_key: &[u8],
private_key: &[u8],
) -> (Self, Self) {
let pub_key = Self {
version: Self::CURRENT_VERSION,
use_case: Some(use_case),
security_level: None,
algorithm,
key_type: KeyType::Public,
key_data: KeyData::from_raw(public_key),
created: Utc::now(),
metadata: BTreeMap::new(),
};
let sec_key = Self {
version: Self::CURRENT_VERSION,
use_case: Some(use_case),
security_level: None,
algorithm,
key_type: KeyType::Secret,
key_data: KeyData::from_raw(private_key),
created: Utc::now(),
metadata: BTreeMap::new(),
};
(pub_key, sec_key)
}
#[must_use]
pub fn from_ed25519_keypair(
use_case: crate::types::types::UseCase,
verifying_key: &[u8],
signing_key: &[u8],
) -> (Self, Self) {
Self::from_keypair(use_case, KeyAlgorithm::Ed25519, verifying_key, signing_key)
}
pub fn to_ed25519_verifying_key_bytes(&self) -> Result<Vec<u8>> {
if self.algorithm != KeyAlgorithm::Ed25519 {
return Err(CoreError::InvalidKey(format!("Not an Ed25519 key: {:?}", self.algorithm)));
}
if self.key_type != KeyType::Public {
return Err(CoreError::InvalidKey(
"Ed25519 verifying key requires Public key type".to_string(),
));
}
self.key_data.decode_raw()
}
pub fn to_ed25519_signing_key_bytes(&self) -> Result<zeroize::Zeroizing<Vec<u8>>> {
if self.algorithm != KeyAlgorithm::Ed25519 {
return Err(CoreError::InvalidKey(format!("Not an Ed25519 key: {:?}", self.algorithm)));
}
if self.key_type != KeyType::Secret {
return Err(CoreError::InvalidKey(
"Ed25519 signing key requires Secret key type".to_string(),
));
}
self.key_data.decode_raw_zeroized()
}
#[must_use]
pub fn from_x25519_keypair(
use_case: crate::types::types::UseCase,
public_key: &[u8; 32],
seed: &[u8; 32],
) -> (Self, Self) {
Self::from_keypair(use_case, KeyAlgorithm::X25519, public_key, seed)
}
pub fn to_x25519_public_key_bytes(&self) -> Result<Vec<u8>> {
if self.algorithm != KeyAlgorithm::X25519 {
return Err(CoreError::InvalidKey(format!("Not an X25519 key: {:?}", self.algorithm)));
}
if self.key_type != KeyType::Public {
return Err(CoreError::InvalidKey(
"X25519 public key requires Public key type".to_string(),
));
}
self.key_data.decode_raw()
}
pub fn to_x25519_secret_key_bytes(&self) -> Result<zeroize::Zeroizing<Vec<u8>>> {
if self.algorithm != KeyAlgorithm::X25519 {
return Err(CoreError::InvalidKey(format!("Not an X25519 key: {:?}", self.algorithm)));
}
if self.key_type != KeyType::Secret {
return Err(CoreError::InvalidKey(
"X25519 secret key requires Secret key type".to_string(),
));
}
self.key_data.decode_raw_zeroized()
}
#[must_use]
pub fn from_ml_kem_keypair(
use_case: crate::types::types::UseCase,
pk: &crate::primitives::kem::ml_kem::MlKemPublicKey,
sk: &crate::primitives::kem::ml_kem::MlKemSecretKey,
) -> (Self, Self) {
let algorithm = match pk.security_level() {
crate::primitives::kem::MlKemSecurityLevel::MlKem512 => KeyAlgorithm::MlKem512,
crate::primitives::kem::MlKemSecurityLevel::MlKem768 => KeyAlgorithm::MlKem768,
crate::primitives::kem::MlKemSecurityLevel::MlKem1024 => KeyAlgorithm::MlKem1024,
};
Self::from_keypair(use_case, algorithm, pk.as_bytes(), sk.as_bytes())
}
pub fn to_ml_kem_public_key(&self) -> Result<crate::primitives::kem::ml_kem::MlKemPublicKey> {
let level = match self.algorithm {
KeyAlgorithm::MlKem512 => crate::primitives::kem::MlKemSecurityLevel::MlKem512,
KeyAlgorithm::MlKem768 => crate::primitives::kem::MlKemSecurityLevel::MlKem768,
KeyAlgorithm::MlKem1024 => crate::primitives::kem::MlKemSecurityLevel::MlKem1024,
other => {
return Err(CoreError::InvalidKey(format!(
"Not a standalone ML-KEM algorithm: {other:?}"
)));
}
};
if self.key_type != KeyType::Public {
return Err(CoreError::InvalidKey(
"ML-KEM public key requires Public key type".to_string(),
));
}
let bytes = self.key_data.decode_raw()?;
crate::primitives::kem::ml_kem::MlKemPublicKey::new(level, bytes)
.map_err(|e| CoreError::InvalidKey(format!("ML-KEM public key: {e}")))
}
pub fn to_ml_kem_secret_key(&self) -> Result<crate::primitives::kem::ml_kem::MlKemSecretKey> {
let level = match self.algorithm {
KeyAlgorithm::MlKem512 => crate::primitives::kem::MlKemSecurityLevel::MlKem512,
KeyAlgorithm::MlKem768 => crate::primitives::kem::MlKemSecurityLevel::MlKem768,
KeyAlgorithm::MlKem1024 => crate::primitives::kem::MlKemSecurityLevel::MlKem1024,
other => {
return Err(CoreError::InvalidKey(format!(
"Not a standalone ML-KEM algorithm: {other:?}"
)));
}
};
if self.key_type != KeyType::Secret {
return Err(CoreError::InvalidKey(
"ML-KEM secret key requires Secret key type".to_string(),
));
}
let bytes = self.key_data.decode_raw()?;
crate::primitives::kem::ml_kem::MlKemSecretKey::new(level, bytes)
.map_err(|e| CoreError::InvalidKey(format!("ML-KEM secret key: {e}")))
}
pub fn to_ml_dsa_verifying_key_bytes(&self) -> Result<Vec<u8>> {
if !matches!(
self.algorithm,
KeyAlgorithm::MlDsa44 | KeyAlgorithm::MlDsa65 | KeyAlgorithm::MlDsa87
) {
return Err(CoreError::InvalidKey(format!(
"Not a standalone ML-DSA algorithm: {:?}",
self.algorithm
)));
}
if self.key_type != KeyType::Public {
return Err(CoreError::InvalidKey(
"ML-DSA verifying key requires Public key type".to_string(),
));
}
self.key_data.decode_raw()
}
pub fn to_ml_dsa_signing_key_bytes(&self) -> Result<zeroize::Zeroizing<Vec<u8>>> {
if !matches!(
self.algorithm,
KeyAlgorithm::MlDsa44 | KeyAlgorithm::MlDsa65 | KeyAlgorithm::MlDsa87
) {
return Err(CoreError::InvalidKey(format!(
"Not a standalone ML-DSA algorithm: {:?}",
self.algorithm
)));
}
if self.key_type != KeyType::Secret {
return Err(CoreError::InvalidKey(
"ML-DSA signing key requires Secret key type".to_string(),
));
}
self.key_data.decode_raw_zeroized()
}
pub fn from_symmetric_key(algorithm: KeyAlgorithm, key: &[u8]) -> Result<Self> {
if !algorithm.is_symmetric() {
return Err(CoreError::InvalidKey(format!(
"{algorithm:?} is not a symmetric algorithm"
)));
}
Ok(Self {
version: Self::CURRENT_VERSION,
use_case: None,
security_level: None,
algorithm,
key_type: KeyType::Symmetric,
key_data: KeyData::from_raw(key),
created: Utc::now(),
metadata: BTreeMap::new(),
})
}
pub fn validate(&self) -> Result<()> {
if self.version != Self::CURRENT_VERSION {
return Err(CoreError::InvalidKey(format!(
"Unsupported key format version {}, expected {}",
self.version,
Self::CURRENT_VERSION
)));
}
if self.algorithm.is_symmetric() && self.key_type != KeyType::Symmetric {
return Err(CoreError::InvalidKey(format!(
"Algorithm {:?} requires KeyType::Symmetric, got {:?}",
self.algorithm, self.key_type
)));
}
if !self.algorithm.is_symmetric() && self.key_type == KeyType::Symmetric {
return Err(CoreError::InvalidKey(format!(
"KeyType::Symmetric is not valid for algorithm {:?}",
self.algorithm
)));
}
match (&self.key_data, self.algorithm.is_hybrid()) {
(KeyData::Composite { .. }, false) => {
return Err(CoreError::InvalidKey(format!(
"Non-hybrid algorithm {:?} must use single key data",
self.algorithm
)));
}
(KeyData::Single { .. }, true) => {
return Err(CoreError::InvalidKey(format!(
"Hybrid algorithm {:?} must use composite key data",
self.algorithm
)));
}
_ => {}
}
match &self.key_data {
KeyData::Single { raw } => {
let _ = BASE64_ENGINE
.decode(raw)
.map_err(|e| CoreError::SerializationError(format!("Invalid base64: {e}")))?;
}
KeyData::Composite { pq, classical } => {
let _ = BASE64_ENGINE.decode(pq).map_err(|e| {
CoreError::SerializationError(format!("Invalid PQ base64: {e}"))
})?;
let _ = BASE64_ENGINE.decode(classical).map_err(|e| {
CoreError::SerializationError(format!("Invalid classical base64: {e}"))
})?;
}
KeyData::Encrypted { enc, kdf, kdf_iterations, kdf_salt, aead, nonce, ciphertext } => {
Self::validate_encrypted_envelope_fields(
*enc,
kdf,
*kdf_iterations,
kdf_salt,
aead,
nonce,
ciphertext,
)?;
}
}
Ok(())
}
fn validate_encrypted_envelope_fields(
enc: u32,
kdf: &str,
kdf_iterations: u32,
kdf_salt: &str,
aead: &str,
nonce: &str,
ciphertext: &str,
) -> Result<()> {
if enc != ENCRYPTED_ENVELOPE_VERSION {
return Err(CoreError::InvalidKey(format!(
"Unsupported encrypted key envelope version {enc}, expected {ENCRYPTED_ENVELOPE_VERSION}",
)));
}
if kdf != PBKDF2_KDF_ID {
return Err(CoreError::InvalidKey(format!(
"Unsupported KDF {kdf:?}, expected {PBKDF2_KDF_ID:?}",
)));
}
if aead != AES_GCM_AEAD_ID {
return Err(CoreError::InvalidKey(format!(
"Unsupported AEAD {aead:?}, expected {AES_GCM_AEAD_ID:?}",
)));
}
if kdf_iterations < PBKDF2_MIN_ITERATIONS {
return Err(CoreError::InvalidKey(format!(
"PBKDF2 iteration count {kdf_iterations} below minimum {PBKDF2_MIN_ITERATIONS}",
)));
}
let salt = BASE64_ENGINE
.decode(kdf_salt)
.map_err(|e| CoreError::SerializationError(format!("Invalid KDF salt base64: {e}")))?;
if salt.len() < PBKDF2_MIN_SALT_LEN {
return Err(CoreError::InvalidKey(format!(
"PBKDF2 salt length {} below minimum {PBKDF2_MIN_SALT_LEN}",
salt.len(),
)));
}
let nonce_bytes = BASE64_ENGINE
.decode(nonce)
.map_err(|e| CoreError::SerializationError(format!("Invalid nonce base64: {e}")))?;
if nonce_bytes.len() != AES_GCM_NONCE_LEN {
return Err(CoreError::InvalidKey(format!(
"AES-GCM nonce length {} != {AES_GCM_NONCE_LEN}",
nonce_bytes.len(),
)));
}
let ct_bytes = BASE64_ENGINE.decode(ciphertext).map_err(|e| {
CoreError::SerializationError(format!("Invalid ciphertext base64: {e}"))
})?;
if ct_bytes.len() < AES_GCM_TAG_LEN {
return Err(CoreError::InvalidKey(
"Encrypted key ciphertext shorter than AES-GCM tag".to_string(),
));
}
Ok(())
}
#[must_use]
pub fn is_encrypted(&self) -> bool {
matches!(self.key_data, KeyData::Encrypted { .. })
}
pub fn encrypt_with_passphrase(&mut self, passphrase: &[u8]) -> Result<()> {
if self.is_encrypted() {
return Err(CoreError::InvalidKey("Key is already passphrase-encrypted".to_string()));
}
if passphrase.is_empty() {
return Err(CoreError::InvalidKey("Passphrase must not be empty".to_string()));
}
let plaintext_json = serde_json::to_vec(&self.key_data).map_err(|e| {
CoreError::SerializationError(format!(
"Failed to serialize key data for encryption: {e}"
))
})?;
let plaintext = zeroize::Zeroizing::new(plaintext_json);
let salt = crate::primitives::rand::csprng::random_bytes(PBKDF2_SALT_LEN);
let kdf_params = crate::primitives::kdf::pbkdf2::Pbkdf2Params::with_salt(&salt)
.iterations(PBKDF2_DEFAULT_ITERATIONS)
.key_length(AES_256_KEY_LEN);
let derived = crate::primitives::kdf::pbkdf2::pbkdf2(passphrase, &kdf_params)
.map_err(|e| CoreError::InvalidKey(format!("PBKDF2 derivation failed: {e}")))?;
use crate::primitives::aead::AeadCipher;
let cipher = crate::primitives::aead::aes_gcm::AesGcm256::new(derived.key())
.map_err(|e| CoreError::InvalidKey(format!("Failed to initialize AES-256-GCM: {e}")))?;
let aad = Self::encryption_aad(
ENCRYPTED_ENVELOPE_VERSION,
self.algorithm,
self.key_type,
PBKDF2_KDF_ID,
PBKDF2_DEFAULT_ITERATIONS,
&salt,
AES_GCM_AEAD_ID,
);
let (nonce, mut ct, tag) = cipher
.seal(&plaintext, Some(&aad))
.map_err(|e| CoreError::InvalidKey(format!("AES-256-GCM sealing failed: {e}")))?;
ct.extend_from_slice(&tag);
self.key_data = KeyData::Encrypted {
enc: ENCRYPTED_ENVELOPE_VERSION,
kdf: PBKDF2_KDF_ID.to_string(),
kdf_iterations: PBKDF2_DEFAULT_ITERATIONS,
kdf_salt: BASE64_ENGINE.encode(&salt),
aead: AES_GCM_AEAD_ID.to_string(),
nonce: BASE64_ENGINE.encode(nonce),
ciphertext: BASE64_ENGINE.encode(&ct),
};
Ok(())
}
pub fn decrypt_with_passphrase(&mut self, passphrase: &[u8]) -> Result<()> {
if passphrase.is_empty() {
return Err(CoreError::InvalidKey("Passphrase must not be empty".to_string()));
}
struct Decoded {
enc: u32,
kdf_iterations: u32,
salt: Vec<u8>,
nonce: [u8; AES_GCM_NONCE_LEN],
ct_and_tag: Vec<u8>,
}
let decoded = match &self.key_data {
KeyData::Encrypted { enc, kdf, kdf_iterations, kdf_salt, aead, nonce, ciphertext } => {
Self::validate_encrypted_envelope_fields(
*enc,
kdf,
*kdf_iterations,
kdf_salt,
aead,
nonce,
ciphertext,
)?;
let salt = BASE64_ENGINE.decode(kdf_salt).map_err(|e| {
CoreError::SerializationError(format!("Invalid KDF salt base64: {e}"))
})?;
let nonce_bytes = BASE64_ENGINE.decode(nonce).map_err(|e| {
CoreError::SerializationError(format!("Invalid nonce base64: {e}"))
})?;
let mut nonce_array = [0u8; AES_GCM_NONCE_LEN];
nonce_array.copy_from_slice(&nonce_bytes);
let ct_and_tag = BASE64_ENGINE.decode(ciphertext).map_err(|e| {
CoreError::SerializationError(format!("Invalid ciphertext base64: {e}"))
})?;
Decoded {
enc: *enc,
kdf_iterations: *kdf_iterations,
salt,
nonce: nonce_array,
ct_and_tag,
}
}
_ => {
return Err(CoreError::InvalidKey("Key is not passphrase-encrypted".to_string()));
}
};
let Decoded { enc, kdf_iterations, salt, nonce: nonce_array, ct_and_tag } = decoded;
let tag_offset = ct_and_tag
.len()
.checked_sub(AES_GCM_TAG_LEN)
.ok_or_else(|| CoreError::InvalidKey("Ciphertext shorter than tag".to_string()))?;
let (ct_bytes, tag_bytes) = ct_and_tag.split_at(tag_offset);
let mut tag_array = [0u8; AES_GCM_TAG_LEN];
tag_array.copy_from_slice(tag_bytes);
let kdf_params = crate::primitives::kdf::pbkdf2::Pbkdf2Params::with_salt(&salt)
.iterations(kdf_iterations)
.key_length(AES_256_KEY_LEN);
let derived = crate::primitives::kdf::pbkdf2::pbkdf2(passphrase, &kdf_params)
.map_err(|e| CoreError::InvalidKey(format!("PBKDF2 derivation failed: {e}")))?;
use crate::primitives::aead::AeadCipher;
let cipher = crate::primitives::aead::aes_gcm::AesGcm256::new(derived.key())
.map_err(|e| CoreError::InvalidKey(format!("Failed to initialize AES-256-GCM: {e}")))?;
let aad = Self::encryption_aad(
enc,
self.algorithm,
self.key_type,
PBKDF2_KDF_ID,
kdf_iterations,
&salt,
AES_GCM_AEAD_ID,
);
let plaintext = cipher
.decrypt(&nonce_array, ct_bytes, &tag_array, Some(&aad))
.map_err(|_e| {
CoreError::InvalidKey(
"Passphrase-protected key unwrap failed (wrong passphrase or corrupted envelope)"
.to_string(),
)
})?;
let new_key_data: KeyData = serde_json::from_slice(&plaintext).map_err(|e| {
CoreError::SerializationError(format!("Failed to deserialize decrypted key data: {e}"))
})?;
if matches!(new_key_data, KeyData::Encrypted { .. }) {
return Err(CoreError::InvalidKey(
"Decrypted payload was itself an encrypted envelope".to_string(),
));
}
self.key_data = new_key_data;
Ok(())
}
fn encryption_aad(
enc: u32,
algorithm: KeyAlgorithm,
key_type: KeyType,
kdf: &str,
kdf_iterations: u32,
kdf_salt: &[u8],
aead: &str,
) -> Vec<u8> {
let algorithm_name = algorithm.canonical_name();
let key_type_name = key_type.canonical_name();
let mut aad = Vec::with_capacity(
b"latticearc-lpk-v1-enc"
.len()
.saturating_add(1) .saturating_add(4) .saturating_add(algorithm_name.len())
.saturating_add(1)
.saturating_add(key_type_name.len())
.saturating_add(1)
.saturating_add(kdf.len())
.saturating_add(1)
.saturating_add(4) .saturating_add(4) .saturating_add(kdf_salt.len())
.saturating_add(aead.len()),
);
aad.extend_from_slice(b"latticearc-lpk-v1-enc");
aad.push(0);
aad.extend_from_slice(&enc.to_be_bytes());
aad.extend_from_slice(algorithm_name.as_bytes());
aad.push(0);
aad.extend_from_slice(key_type_name.as_bytes());
aad.push(0);
aad.extend_from_slice(kdf.as_bytes());
aad.push(0);
aad.extend_from_slice(&kdf_iterations.to_be_bytes());
let salt_len_u32 = u32::try_from(kdf_salt.len()).unwrap_or(u32::MAX);
aad.extend_from_slice(&salt_len_u32.to_be_bytes());
aad.extend_from_slice(kdf_salt);
aad.extend_from_slice(aead.as_bytes());
aad
}
pub fn to_json(&self) -> Result<String> {
serde_json::to_string(self)
.map_err(|e| CoreError::SerializationError(format!("JSON serialization failed: {e}")))
}
pub fn to_json_pretty(&self) -> Result<String> {
serde_json::to_string_pretty(self)
.map_err(|e| CoreError::SerializationError(format!("JSON serialization failed: {e}")))
}
pub const MAX_KEY_JSON_SIZE: usize = 1024 * 1024;
pub const MAX_KEY_CBOR_SIZE: usize = 1024 * 1024;
pub fn from_json(json: &str) -> Result<Self> {
if json.len() > Self::MAX_KEY_JSON_SIZE {
return Err(CoreError::ResourceExceeded(format!(
"Key JSON size {} exceeds limit {}",
json.len(),
Self::MAX_KEY_JSON_SIZE
)));
}
let key: Self = serde_json::from_str(json)
.map_err(|e| CoreError::SerializationError(format!("JSON parse failed: {e}")))?;
key.validate()?;
Ok(key)
}
pub fn to_cbor(&self) -> Result<Vec<u8>> {
let mut buf = Vec::new();
ciborium::into_writer(self, &mut buf).map_err(|e| {
CoreError::SerializationError(format!("CBOR serialization failed: {e}"))
})?;
Ok(buf)
}
pub fn from_cbor(data: &[u8]) -> Result<Self> {
if data.len() > Self::MAX_KEY_CBOR_SIZE {
return Err(CoreError::ResourceExceeded(format!(
"Key CBOR size {} exceeds limit {}",
data.len(),
Self::MAX_KEY_CBOR_SIZE
)));
}
let key: Self = ciborium::from_reader(data)
.map_err(|e| CoreError::SerializationError(format!("CBOR parse failed: {e}")))?;
key.validate()?;
Ok(key)
}
pub fn write_to_file(&self, path: &std::path::Path) -> Result<()> {
let json = self.to_json_pretty()?;
#[cfg(unix)]
if self.key_type == KeyType::Secret || self.key_type == KeyType::Symmetric {
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
let mut file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(path)?;
file.write_all(json.as_bytes())?;
return Ok(());
}
std::fs::write(path, json)?;
Ok(())
}
pub fn write_cbor_to_file(&self, path: &std::path::Path) -> Result<()> {
let cbor = self.to_cbor()?;
#[cfg(unix)]
if self.key_type == KeyType::Secret || self.key_type == KeyType::Symmetric {
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
let mut file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(path)?;
file.write_all(&cbor)?;
return Ok(());
}
std::fs::write(path, cbor)?;
Ok(())
}
pub fn read_from_file(path: &std::path::Path) -> Result<Self> {
let contents = std::fs::read_to_string(path)?;
Self::from_json(&contents)
}
pub fn read_cbor_from_file(path: &std::path::Path) -> Result<Self> {
let contents = std::fs::read(path)?;
Self::from_cbor(&contents)
}
pub fn from_legacy_json(json: &str) -> Result<Self> {
#[derive(Deserialize)]
struct LegacyKeyFile {
algorithm: String,
key_type: String,
key: String,
#[serde(default)]
label: Option<String>,
}
let legacy: LegacyKeyFile = serde_json::from_str(json)
.map_err(|e| CoreError::SerializationError(format!("Legacy JSON parse failed: {e}")))?;
let algorithm = parse_legacy_algorithm(&legacy.algorithm)?;
let key_type = match legacy.key_type.to_lowercase().as_str() {
"public" | "pub" => KeyType::Public,
"secret" | "private" | "sk" => KeyType::Secret,
"symmetric" | "sym" => KeyType::Symmetric,
other => {
return Err(CoreError::InvalidKey(format!(
"Unrecognized legacy key_type: '{other}'"
)));
}
};
let key_data = KeyData::Single { raw: legacy.key };
let mut key = Self::new(algorithm, key_type, key_data);
if let Some(label) = legacy.label {
key.set_label(label);
}
key.validate()?;
Ok(key)
}
}
fn parse_legacy_algorithm(s: &str) -> Result<KeyAlgorithm> {
match s.to_lowercase().replace('_', "-").as_str() {
"ml-kem-512" => Ok(KeyAlgorithm::MlKem512),
"ml-kem-768" => Ok(KeyAlgorithm::MlKem768),
"ml-kem-1024" => Ok(KeyAlgorithm::MlKem1024),
"ml-dsa-44" => Ok(KeyAlgorithm::MlDsa44),
"ml-dsa-65" => Ok(KeyAlgorithm::MlDsa65),
"ml-dsa-87" => Ok(KeyAlgorithm::MlDsa87),
"ed25519" => Ok(KeyAlgorithm::Ed25519),
"x25519" => Ok(KeyAlgorithm::X25519),
"aes-256" | "aes256" => Ok(KeyAlgorithm::Aes256),
"fn-dsa-512" => Ok(KeyAlgorithm::FnDsa512),
"fn-dsa-1024" => Ok(KeyAlgorithm::FnDsa1024),
"hybrid-ml-kem-768-x25519" => Ok(KeyAlgorithm::HybridMlKem768X25519),
"hybrid-ml-dsa-65-ed25519" => Ok(KeyAlgorithm::HybridMlDsa65Ed25519),
other => Err(CoreError::InvalidKey(format!("Unrecognized algorithm: '{other}'"))),
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::arithmetic_side_effects,
clippy::print_stdout,
clippy::cast_precision_loss,
clippy::useless_vec,
clippy::panic,
deprecated
)]
mod tests {
use super::*;
fn sample_single_key() -> PortableKey {
let raw = [0x11u8; 32];
PortableKey::new(KeyAlgorithm::Aes256, KeyType::Symmetric, KeyData::from_raw(&raw))
}
fn sample_composite_key() -> PortableKey {
let pq = vec![0x22u8; 1184];
let classical = vec![0x33u8; 32];
PortableKey::new(
KeyAlgorithm::HybridMlKem768X25519,
KeyType::Public,
KeyData::from_composite(&pq, &classical),
)
}
#[test]
fn test_encrypt_with_passphrase_single_roundtrip() {
let mut key = sample_single_key();
let plain_raw = key.key_data().decode_raw().unwrap();
let passphrase = b"correct horse battery staple";
key.encrypt_with_passphrase(passphrase).unwrap();
assert!(key.is_encrypted());
assert!(key.key_data().decode_raw().is_err());
key.validate().expect("encrypted envelope must validate");
key.decrypt_with_passphrase(passphrase).unwrap();
assert!(!key.is_encrypted());
assert_eq!(key.key_data().decode_raw().unwrap(), plain_raw);
}
#[test]
fn test_encrypt_with_passphrase_composite_roundtrip() {
let mut key = sample_composite_key();
let (pq_plain, cl_plain) = key.key_data().decode_composite().unwrap();
let passphrase = b"hunter2-but-stronger";
key.encrypt_with_passphrase(passphrase).unwrap();
assert!(key.is_encrypted());
assert!(key.key_data().decode_composite().is_err());
key.decrypt_with_passphrase(passphrase).unwrap();
let (pq_out, cl_out) = key.key_data().decode_composite().unwrap();
assert_eq!(pq_out, pq_plain);
assert_eq!(cl_out, cl_plain);
}
#[test]
fn test_encrypt_with_passphrase_wrong_passphrase_fails() {
let mut key = sample_single_key();
key.encrypt_with_passphrase(b"correct passphrase").unwrap();
let err = key
.decrypt_with_passphrase(b"wrong passphrase")
.expect_err("wrong passphrase must fail");
assert_eq!(
err.to_string(),
"Invalid key: Passphrase-protected key unwrap failed \
(wrong passphrase or corrupted envelope)"
);
}
#[test]
fn test_encrypt_with_passphrase_corrupted_ciphertext_matches_wrong_passphrase_error() {
let mut key = sample_single_key();
key.encrypt_with_passphrase(b"correct passphrase").unwrap();
let KeyData::Encrypted { ciphertext, .. } = &mut key.key_data else {
panic!("expected encrypted variant");
};
let mut raw = BASE64_ENGINE.decode(ciphertext.as_str()).unwrap();
let mid = raw.len() / 2;
raw[mid] ^= 0x01;
*ciphertext = BASE64_ENGINE.encode(&raw);
let err = key
.decrypt_with_passphrase(b"correct passphrase")
.expect_err("corrupted ciphertext must fail");
assert_eq!(
err.to_string(),
"Invalid key: Passphrase-protected key unwrap failed \
(wrong passphrase or corrupted envelope)"
);
}
#[test]
fn test_encrypt_with_passphrase_empty_rejected() {
let mut key = sample_single_key();
assert!(key.encrypt_with_passphrase(b"").is_err());
}
#[test]
fn test_encrypt_with_passphrase_double_encrypt_rejected() {
let mut key = sample_single_key();
key.encrypt_with_passphrase(b"once").unwrap();
assert!(key.encrypt_with_passphrase(b"twice").is_err());
}
#[test]
fn test_decrypt_with_passphrase_on_plaintext_fails() {
let mut key = sample_single_key();
assert!(key.decrypt_with_passphrase(b"anything").is_err());
}
#[test]
fn test_encrypted_key_json_roundtrip() {
let mut key = sample_single_key();
let plain_raw = key.key_data().decode_raw().unwrap();
key.encrypt_with_passphrase(b"json roundtrip").unwrap();
let json = key.to_json().unwrap();
assert!(json.contains("\"kdf\""));
assert!(json.contains("PBKDF2-HMAC-SHA256"));
let mut reloaded = PortableKey::from_json(&json).unwrap();
assert!(reloaded.is_encrypted());
reloaded.decrypt_with_passphrase(b"json roundtrip").unwrap();
assert_eq!(reloaded.key_data().decode_raw().unwrap(), plain_raw);
}
#[test]
fn test_encrypted_key_aad_binds_algorithm() {
let mut key = sample_single_key();
key.encrypt_with_passphrase(b"aad binding").unwrap();
let tampered_data = key.key_data.clone();
let mut tampered =
PortableKey::new(KeyAlgorithm::ChaCha20, KeyType::Symmetric, tampered_data);
assert!(tampered.decrypt_with_passphrase(b"aad binding").is_err());
}
#[test]
fn test_encrypted_key_aad_binds_key_type() {
let mut key = sample_single_key();
key.encrypt_with_passphrase(b"aad binding").unwrap();
let tampered_data = key.key_data.clone();
let mut tampered = PortableKey::new(KeyAlgorithm::Aes256, KeyType::Secret, tampered_data);
assert!(tampered.decrypt_with_passphrase(b"aad binding").is_err());
}
#[derive(Clone)]
struct EnvelopeSnapshot {
enc: u32,
kdf: String,
kdf_iterations: u32,
kdf_salt: String,
aead: String,
nonce: String,
ciphertext: String,
}
impl EnvelopeSnapshot {
fn capture(key: &PortableKey) -> Option<Self> {
match &key.key_data {
KeyData::Encrypted {
enc,
kdf,
kdf_iterations,
kdf_salt,
aead,
nonce,
ciphertext,
} => Some(Self {
enc: *enc,
kdf: kdf.clone(),
kdf_iterations: *kdf_iterations,
kdf_salt: kdf_salt.clone(),
aead: aead.clone(),
nonce: nonce.clone(),
ciphertext: ciphertext.clone(),
}),
_ => None,
}
}
fn into_key_data(self) -> KeyData {
KeyData::Encrypted {
enc: self.enc,
kdf: self.kdf,
kdf_iterations: self.kdf_iterations,
kdf_salt: self.kdf_salt,
aead: self.aead,
nonce: self.nonce,
ciphertext: self.ciphertext,
}
}
}
fn make_valid_encrypted_key() -> (PortableKey, EnvelopeSnapshot) {
let mut key = sample_single_key();
key.encrypt_with_passphrase(b"envelope validation").unwrap();
let snapshot = EnvelopeSnapshot::capture(&key).expect("sample key is freshly encrypted");
(key, snapshot)
}
#[test]
fn test_validate_rejects_wrong_envelope_version() {
let (mut key, mut snapshot) = make_valid_encrypted_key();
snapshot.enc = 99;
key.key_data = snapshot.into_key_data();
let err = key.validate().expect_err("wrong envelope version must be rejected");
assert!(err.to_string().contains("Unsupported encrypted key envelope version 99"));
}
#[test]
fn test_validate_rejects_unknown_kdf() {
let (mut key, mut snapshot) = make_valid_encrypted_key();
snapshot.kdf = "scrypt".to_string();
key.key_data = snapshot.into_key_data();
let err = key.validate().expect_err("unknown KDF must be rejected");
assert!(err.to_string().contains("Unsupported KDF"));
}
#[test]
fn test_validate_rejects_unknown_aead() {
let (mut key, mut snapshot) = make_valid_encrypted_key();
snapshot.aead = "ChaCha20-Poly1305".to_string();
key.key_data = snapshot.into_key_data();
let err = key.validate().expect_err("unknown AEAD must be rejected");
assert!(err.to_string().contains("Unsupported AEAD"));
}
#[test]
fn test_validate_rejects_too_few_pbkdf2_iterations() {
let (mut key, mut snapshot) = make_valid_encrypted_key();
snapshot.kdf_iterations = 50_000; key.key_data = snapshot.into_key_data();
let err = key.validate().expect_err("low iteration count must be rejected");
assert!(err.to_string().contains("PBKDF2 iteration count 50000 below minimum"));
}
#[test]
fn test_validate_rejects_short_salt() {
let (mut key, mut snapshot) = make_valid_encrypted_key();
snapshot.kdf_salt = BASE64_ENGINE.encode([0u8; 8]); key.key_data = snapshot.into_key_data();
let err = key.validate().expect_err("short salt must be rejected");
assert!(err.to_string().contains("PBKDF2 salt length 8 below minimum"));
}
#[test]
fn test_validate_rejects_wrong_nonce_length() {
let (mut key, mut snapshot) = make_valid_encrypted_key();
snapshot.nonce = BASE64_ENGINE.encode([0u8; 8]); key.key_data = snapshot.into_key_data();
let err = key.validate().expect_err("wrong nonce length must be rejected");
assert!(err.to_string().contains("AES-GCM nonce length 8"));
}
#[test]
fn test_validate_rejects_ciphertext_shorter_than_tag() {
let (mut key, mut snapshot) = make_valid_encrypted_key();
snapshot.ciphertext = BASE64_ENGINE.encode([0u8; 4]); key.key_data = snapshot.into_key_data();
let err = key.validate().expect_err("short ciphertext must be rejected");
assert!(err.to_string().contains("Encrypted key ciphertext shorter than AES-GCM tag"));
}
#[test]
fn test_encryption_aad_byte_layout_is_stable() {
let salt = [0xAA_u8; 16];
let aad = PortableKey::encryption_aad(
1,
KeyAlgorithm::Aes256,
KeyType::Symmetric,
"PBKDF2-HMAC-SHA256",
600_000,
&salt,
"AES-256-GCM",
);
let mut expected: Vec<u8> = Vec::new();
expected.extend_from_slice(b"latticearc-lpk-v1-enc");
expected.push(0);
expected.extend_from_slice(&1u32.to_be_bytes());
expected.extend_from_slice(b"aes-256");
expected.push(0);
expected.extend_from_slice(b"symmetric");
expected.push(0);
expected.extend_from_slice(b"PBKDF2-HMAC-SHA256");
expected.push(0);
expected.extend_from_slice(&600_000u32.to_be_bytes());
expected.extend_from_slice(&16u32.to_be_bytes());
expected.extend_from_slice(&salt);
expected.extend_from_slice(b"AES-256-GCM");
assert_eq!(aad, expected);
}
#[test]
fn test_canonical_names_match_serde_rename() {
fn serde_name<T: Serialize>(t: &T) -> String {
let s = serde_json::to_string(t).unwrap();
s.trim_matches('"').to_string()
}
let algorithms = [
KeyAlgorithm::MlKem512,
KeyAlgorithm::MlKem768,
KeyAlgorithm::MlKem1024,
KeyAlgorithm::MlDsa44,
KeyAlgorithm::MlDsa65,
KeyAlgorithm::MlDsa87,
KeyAlgorithm::SlhDsaShake128s,
KeyAlgorithm::SlhDsaShake256f,
KeyAlgorithm::FnDsa512,
KeyAlgorithm::FnDsa1024,
KeyAlgorithm::Ed25519,
KeyAlgorithm::X25519,
KeyAlgorithm::Aes256,
KeyAlgorithm::ChaCha20,
KeyAlgorithm::HybridMlKem512X25519,
KeyAlgorithm::HybridMlKem768X25519,
KeyAlgorithm::HybridMlKem1024X25519,
KeyAlgorithm::HybridMlDsa44Ed25519,
KeyAlgorithm::HybridMlDsa65Ed25519,
KeyAlgorithm::HybridMlDsa87Ed25519,
];
for alg in algorithms {
assert_eq!(alg.canonical_name(), serde_name(&alg), "canonical_name drift for {alg:?}");
}
for kt in [KeyType::Public, KeyType::Secret, KeyType::Symmetric] {
assert_eq!(kt.canonical_name(), serde_name(&kt), "canonical_name drift for {kt:?}");
}
}
#[test]
fn test_key_algorithm_serde_all_variants_roundtrip() {
let variants = [
(KeyAlgorithm::MlKem512, "\"ml-kem-512\""),
(KeyAlgorithm::MlKem768, "\"ml-kem-768\""),
(KeyAlgorithm::MlKem1024, "\"ml-kem-1024\""),
(KeyAlgorithm::MlDsa44, "\"ml-dsa-44\""),
(KeyAlgorithm::MlDsa65, "\"ml-dsa-65\""),
(KeyAlgorithm::MlDsa87, "\"ml-dsa-87\""),
(KeyAlgorithm::SlhDsaShake128s, "\"slh-dsa-shake-128s\""),
(KeyAlgorithm::SlhDsaShake256f, "\"slh-dsa-shake-256f\""),
(KeyAlgorithm::FnDsa512, "\"fn-dsa-512\""),
(KeyAlgorithm::FnDsa1024, "\"fn-dsa-1024\""),
(KeyAlgorithm::Ed25519, "\"ed25519\""),
(KeyAlgorithm::X25519, "\"x25519\""),
(KeyAlgorithm::Aes256, "\"aes-256\""),
(KeyAlgorithm::ChaCha20, "\"chacha20\""),
(KeyAlgorithm::HybridMlKem768X25519, "\"hybrid-ml-kem-768-x25519\""),
(KeyAlgorithm::HybridMlKem512X25519, "\"hybrid-ml-kem-512-x25519\""),
(KeyAlgorithm::HybridMlKem1024X25519, "\"hybrid-ml-kem-1024-x25519\""),
(KeyAlgorithm::HybridMlDsa65Ed25519, "\"hybrid-ml-dsa-65-ed25519\""),
(KeyAlgorithm::HybridMlDsa44Ed25519, "\"hybrid-ml-dsa-44-ed25519\""),
(KeyAlgorithm::HybridMlDsa87Ed25519, "\"hybrid-ml-dsa-87-ed25519\""),
];
for (variant, expected_json) in &variants {
let json = serde_json::to_string(variant).unwrap();
assert_eq!(&json, expected_json, "serialize {:?}", variant);
let deserialized: KeyAlgorithm = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, *variant, "roundtrip {:?}", variant);
}
}
#[test]
fn test_key_algorithm_is_hybrid_returns_correct_bool_succeeds() {
assert!(KeyAlgorithm::HybridMlKem768X25519.is_hybrid());
assert!(KeyAlgorithm::HybridMlDsa65Ed25519.is_hybrid());
assert!(!KeyAlgorithm::MlKem768.is_hybrid());
assert!(!KeyAlgorithm::Aes256.is_hybrid());
}
#[test]
fn test_key_algorithm_is_symmetric_returns_correct_bool_succeeds() {
assert!(KeyAlgorithm::Aes256.is_symmetric());
assert!(KeyAlgorithm::ChaCha20.is_symmetric());
assert!(!KeyAlgorithm::MlKem768.is_symmetric());
}
#[test]
fn test_key_type_serde_roundtrip() {
for (variant, expected) in [
(KeyType::Public, "\"public\""),
(KeyType::Secret, "\"secret\""),
(KeyType::Symmetric, "\"symmetric\""),
] {
let json = serde_json::to_string(&variant).unwrap();
assert_eq!(json, expected);
let back: KeyType = serde_json::from_str(&json).unwrap();
assert_eq!(back, variant);
}
}
#[test]
fn test_key_data_single_roundtrip() {
let original = vec![1u8, 2, 3, 4];
let kd = KeyData::from_raw(&original);
let decoded = kd.decode_raw().unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_key_data_composite_roundtrip() {
let pq = vec![0xAA; 32];
let cl = vec![0xBB; 32];
let kd = KeyData::from_composite(&pq, &cl);
let (pq2, cl2) = kd.decode_composite().unwrap();
assert_eq!(pq2, pq);
assert_eq!(cl2, cl);
}
#[test]
fn test_key_data_single_decode_composite_fails() {
let kd = KeyData::from_raw(&[1, 2, 3]);
assert!(kd.decode_composite().is_err());
}
#[test]
fn test_key_data_composite_decode_raw_fails() {
let kd = KeyData::from_composite(&[1], &[2]);
assert!(kd.decode_raw().is_err());
}
#[test]
fn test_key_data_debug_redacts_secret_content_succeeds() {
let kd = KeyData::from_raw(&[0xDE, 0xAD]);
let debug = format!("{:?}", kd);
assert!(!debug.contains("3q0"), "Debug should not contain base64 key material");
assert!(debug.contains("[...]"));
}
#[test]
fn test_json_roundtrip_ml_kem_768_public_roundtrip() {
let key = PortableKey::new(
KeyAlgorithm::MlKem768,
KeyType::Public,
KeyData::from_raw(&vec![0xCC; 1184]),
);
let json = key.to_json().unwrap();
let restored = PortableKey::from_json(&json).unwrap();
assert_eq!(restored.version(), 1);
assert_eq!(restored.algorithm(), KeyAlgorithm::MlKem768);
assert_eq!(restored.key_type(), KeyType::Public);
assert_eq!(restored.key_data().decode_raw().unwrap().len(), 1184);
}
#[test]
fn test_json_roundtrip_aes_symmetric_roundtrip() {
let key = PortableKey::new(
KeyAlgorithm::Aes256,
KeyType::Symmetric,
KeyData::from_raw(&[0u8; 32]),
);
let json = key.to_json().unwrap();
let restored = PortableKey::from_json(&json).unwrap();
assert_eq!(restored.algorithm(), KeyAlgorithm::Aes256);
assert_eq!(restored.key_type(), KeyType::Symmetric);
}
#[test]
fn test_json_roundtrip_hybrid_kem_roundtrip() {
let key = PortableKey::new(
KeyAlgorithm::HybridMlKem768X25519,
KeyType::Secret,
KeyData::from_composite(&vec![0xAA; 2400], &vec![0xBB; 32]),
);
let json = key.to_json().unwrap();
let restored = PortableKey::from_json(&json).unwrap();
assert_eq!(restored.algorithm(), KeyAlgorithm::HybridMlKem768X25519);
let (pq, cl) = restored.key_data().decode_composite().unwrap();
assert_eq!(pq.len(), 2400);
assert_eq!(cl.len(), 32);
}
#[test]
fn test_cbor_roundtrip_ml_kem_768_roundtrip() {
let key = PortableKey::new(
KeyAlgorithm::MlKem768,
KeyType::Public,
KeyData::from_raw(&vec![0xCC; 1184]),
);
let cbor = key.to_cbor().unwrap();
let restored = PortableKey::from_cbor(&cbor).unwrap();
assert_eq!(restored.version(), 1);
assert_eq!(restored.algorithm(), KeyAlgorithm::MlKem768);
assert_eq!(restored.key_data().decode_raw().unwrap().len(), 1184);
}
#[test]
fn test_cbor_roundtrip_hybrid_sig_roundtrip() {
let key = PortableKey::new(
KeyAlgorithm::HybridMlDsa65Ed25519,
KeyType::Secret,
KeyData::from_composite(&vec![0xCC; 1952], &vec![0xDD; 32]),
);
let cbor = key.to_cbor().unwrap();
let restored = PortableKey::from_cbor(&cbor).unwrap();
assert_eq!(restored.algorithm(), KeyAlgorithm::HybridMlDsa65Ed25519);
assert_eq!(restored.key_type(), KeyType::Secret);
}
#[test]
fn test_cbor_smaller_than_json_is_correct() {
let key = PortableKey::new(
KeyAlgorithm::MlKem768,
KeyType::Public,
KeyData::from_raw(&vec![0xAA; 1184]),
);
let json_bytes = key.to_json().unwrap().len();
let cbor_bytes = key.to_cbor().unwrap().len();
assert!(
cbor_bytes < json_bytes,
"CBOR ({cbor_bytes}) should be smaller than JSON ({json_bytes})"
);
}
#[test]
fn test_cbor_json_cross_format_consistency_roundtrip() {
let key = PortableKey::new(
KeyAlgorithm::MlDsa65,
KeyType::Public,
KeyData::from_raw(&vec![0xBB; 1952]),
);
let json = key.to_json().unwrap();
let cbor = key.to_cbor().unwrap();
let from_json = PortableKey::from_json(&json).unwrap();
let from_cbor = PortableKey::from_cbor(&cbor).unwrap();
assert_eq!(from_json.algorithm(), from_cbor.algorithm());
assert_eq!(from_json.key_type(), from_cbor.key_type());
assert_eq!(
from_json.key_data().decode_raw().unwrap(),
from_cbor.key_data().decode_raw().unwrap()
);
}
#[test]
fn test_validate_symmetric_wrong_key_type_fails() {
let key =
PortableKey::new(KeyAlgorithm::Aes256, KeyType::Public, KeyData::from_raw(&[0u8; 32]));
assert!(key.validate().is_err());
}
#[test]
fn test_validate_non_symmetric_with_symmetric_type_fails() {
let key = PortableKey::new(
KeyAlgorithm::MlKem768,
KeyType::Symmetric,
KeyData::from_raw(&vec![0u8; 1184]),
);
assert!(key.validate().is_err());
}
#[test]
fn test_validate_hybrid_with_single_data_fails() {
let key = PortableKey::new(
KeyAlgorithm::HybridMlKem768X25519,
KeyType::Public,
KeyData::from_raw(&[0u8; 32]),
);
assert!(key.validate().is_err());
}
#[test]
fn test_validate_non_hybrid_with_composite_data_fails() {
let key = PortableKey::new(
KeyAlgorithm::MlKem768,
KeyType::Public,
KeyData::from_composite(&[0u8; 32], &[0u8; 32]),
);
assert!(key.validate().is_err());
}
#[test]
fn test_validate_bad_base64_fails() {
let key = PortableKey {
version: 1,
use_case: None,
security_level: None,
algorithm: KeyAlgorithm::Aes256,
key_type: KeyType::Symmetric,
key_data: KeyData::Single { raw: "not-valid-base64!!!".to_string() },
created: Utc::now(),
metadata: BTreeMap::new(),
};
assert!(key.validate().is_err());
}
#[test]
fn test_debug_redacts_secret_key_content_succeeds() {
let key = PortableKey::new(
KeyAlgorithm::MlDsa65,
KeyType::Secret,
KeyData::from_raw(&[0xDE, 0xAD, 0xBE, 0xEF]),
);
let debug = format!("{:?}", key);
assert!(debug.contains("REDACTED"));
assert!(!debug.contains("3q2+7w"));
}
#[test]
fn test_debug_shows_public_key_type_in_output_succeeds() {
let key = PortableKey::new(
KeyAlgorithm::MlDsa65,
KeyType::Public,
KeyData::from_raw(&[0xDE, 0xAD]),
);
let debug = format!("{:?}", key);
assert!(debug.contains("[key data]"));
assert!(!debug.contains("REDACTED"));
}
#[test]
fn test_metadata_roundtrip_via_json_roundtrip() {
let mut key = PortableKey::new(
KeyAlgorithm::Aes256,
KeyType::Symmetric,
KeyData::from_raw(&[0u8; 32]),
);
key.set_label("Production signing key");
key.set_metadata("custom_field".to_string(), serde_json::json!(42));
let json = key.to_json().unwrap();
let restored = PortableKey::from_json(&json).unwrap();
assert_eq!(restored.label(), Some("Production signing key"));
assert_eq!(restored.metadata().get("custom_field"), Some(&serde_json::json!(42)));
}
#[test]
fn test_metadata_omitted_when_empty_in_json_succeeds() {
let key =
PortableKey::new(KeyAlgorithm::Ed25519, KeyType::Public, KeyData::from_raw(&[0u8; 32]));
let json = key.to_json().unwrap();
assert!(!json.contains("metadata"));
}
#[test]
fn test_json_file_roundtrip_via_disk_roundtrip() {
let dir = std::env::temp_dir().join("latticearc_key_format_test");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("test_key.json");
let key = PortableKey::new(
KeyAlgorithm::MlKem768,
KeyType::Public,
KeyData::from_raw(&vec![0xAA; 1184]),
);
key.write_to_file(&path).unwrap();
let restored = PortableKey::read_from_file(&path).unwrap();
assert_eq!(restored.algorithm(), KeyAlgorithm::MlKem768);
assert_eq!(restored.key_data().decode_raw().unwrap().len(), 1184);
let _ = std::fs::remove_file(&path);
let _ = std::fs::remove_dir(&dir);
}
#[test]
fn test_cbor_file_roundtrip_via_disk_roundtrip() {
let dir = std::env::temp_dir().join("latticearc_key_cbor_test");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("test_key.cbor");
let key = PortableKey::new(
KeyAlgorithm::MlKem768,
KeyType::Public,
KeyData::from_raw(&vec![0xAA; 1184]),
);
key.write_cbor_to_file(&path).unwrap();
let restored = PortableKey::read_cbor_from_file(&path).unwrap();
assert_eq!(restored.algorithm(), KeyAlgorithm::MlKem768);
let _ = std::fs::remove_file(&path);
let _ = std::fs::remove_dir(&dir);
}
#[cfg(unix)]
#[test]
fn test_file_permissions_secret_key_are_restricted_succeeds() {
use std::os::unix::fs::PermissionsExt;
let dir = std::env::temp_dir().join("latticearc_key_perms_test");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("secret_key.json");
let key = PortableKey::new(
KeyAlgorithm::Aes256,
KeyType::Symmetric,
KeyData::from_raw(&[0u8; 32]),
);
key.write_to_file(&path).unwrap();
let perms = std::fs::metadata(&path).unwrap().permissions();
assert_eq!(perms.mode() & 0o777, 0o600);
let _ = std::fs::remove_file(&path);
let _ = std::fs::remove_dir(&dir);
}
#[test]
fn test_from_legacy_json_succeeds() {
let legacy = r#"{
"algorithm": "ML-DSA-65",
"key_type": "public",
"key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"label": "Test key"
}"#;
let key = PortableKey::from_legacy_json(legacy).unwrap();
assert_eq!(key.algorithm(), KeyAlgorithm::MlDsa65);
assert_eq!(key.key_type(), KeyType::Public);
assert_eq!(key.label(), Some("Test key"));
}
#[test]
fn test_from_legacy_json_secret_succeeds() {
let legacy = r#"{
"algorithm": "ed25519",
"key_type": "private",
"key": "AQIDBA=="
}"#;
let key = PortableKey::from_legacy_json(legacy).unwrap();
assert_eq!(key.key_type(), KeyType::Secret);
}
#[test]
fn test_from_legacy_json_unknown_algorithm_fails() {
let legacy = r#"{"algorithm":"UNKNOWN-999","key_type":"public","key":"AQID"}"#;
assert!(PortableKey::from_legacy_json(legacy).is_err());
}
#[test]
fn test_from_legacy_json_unknown_key_type_fails() {
let legacy = r#"{"algorithm":"ed25519","key_type":"unknown","key":"AQID"}"#;
assert!(PortableKey::from_legacy_json(legacy).is_err());
}
#[test]
fn test_every_single_algorithm_roundtrip() {
let single_algorithms = [
(KeyAlgorithm::MlKem512, KeyType::Public),
(KeyAlgorithm::MlKem768, KeyType::Public),
(KeyAlgorithm::MlKem1024, KeyType::Secret),
(KeyAlgorithm::MlDsa44, KeyType::Public),
(KeyAlgorithm::MlDsa65, KeyType::Secret),
(KeyAlgorithm::MlDsa87, KeyType::Public),
(KeyAlgorithm::SlhDsaShake128s, KeyType::Public),
(KeyAlgorithm::SlhDsaShake256f, KeyType::Secret),
(KeyAlgorithm::FnDsa512, KeyType::Public),
(KeyAlgorithm::FnDsa1024, KeyType::Secret),
(KeyAlgorithm::Ed25519, KeyType::Public),
(KeyAlgorithm::X25519, KeyType::Secret),
(KeyAlgorithm::Aes256, KeyType::Symmetric),
(KeyAlgorithm::ChaCha20, KeyType::Symmetric),
];
for (alg, kt) in &single_algorithms {
let key = PortableKey::new(*alg, *kt, KeyData::from_raw(&[0x42; 32]));
let json = key.to_json().unwrap();
let from_json = PortableKey::from_json(&json).unwrap();
assert_eq!(from_json.algorithm(), *alg);
let cbor = key.to_cbor().unwrap();
let from_cbor = PortableKey::from_cbor(&cbor).unwrap();
assert_eq!(from_cbor.algorithm(), *alg);
assert_eq!(from_cbor.key_type(), *kt);
}
}
#[test]
fn test_every_hybrid_algorithm_roundtrip() {
let hybrid_algorithms = [
(KeyAlgorithm::HybridMlKem512X25519, KeyType::Public),
(KeyAlgorithm::HybridMlKem768X25519, KeyType::Secret),
(KeyAlgorithm::HybridMlKem1024X25519, KeyType::Public),
(KeyAlgorithm::HybridMlDsa44Ed25519, KeyType::Public),
(KeyAlgorithm::HybridMlDsa65Ed25519, KeyType::Secret),
(KeyAlgorithm::HybridMlDsa87Ed25519, KeyType::Public),
];
for (alg, kt) in &hybrid_algorithms {
let key =
PortableKey::new(*alg, *kt, KeyData::from_composite(&[0xAA; 64], &[0xBB; 32]));
let json = key.to_json().unwrap();
let from_json = PortableKey::from_json(&json).unwrap();
assert_eq!(from_json.algorithm(), *alg);
let cbor = key.to_cbor().unwrap();
let from_cbor = PortableKey::from_cbor(&cbor).unwrap();
assert_eq!(from_cbor.algorithm(), *alg);
assert_eq!(from_cbor.key_type(), *kt);
}
}
#[test]
fn test_from_json_invalid_json_fails() {
assert!(PortableKey::from_json("not json").is_err());
}
#[test]
fn test_from_cbor_invalid_data_fails() {
assert!(PortableKey::from_cbor(&[0xFF, 0xFF]).is_err());
}
#[test]
fn test_from_json_missing_fields_fails() {
assert!(PortableKey::from_json(r#"{"version":1}"#).is_err());
}
#[test]
fn test_read_nonexistent_file_fails() {
assert!(
PortableKey::read_from_file(std::path::Path::new("/nonexistent/path.json")).is_err()
);
}
#[test]
fn test_version_is_current_format_has_correct_size() {
let key =
PortableKey::new(KeyAlgorithm::Ed25519, KeyType::Public, KeyData::from_raw(&[0u8; 32]));
assert_eq!(key.version(), PortableKey::CURRENT_VERSION);
}
#[test]
fn test_with_created_sets_timestamp_succeeds() {
let ts = DateTime::parse_from_rfc3339("2026-01-01T00:00:00Z").unwrap().with_timezone(&Utc);
let key = PortableKey::with_created(
KeyAlgorithm::Ed25519,
KeyType::Public,
KeyData::from_raw(&[0u8; 32]),
ts,
);
assert_eq!(*key.created(), ts);
}
#[test]
fn test_pretty_json_contains_newlines_and_indentation_is_correct() {
let key =
PortableKey::new(KeyAlgorithm::Ed25519, KeyType::Public, KeyData::from_raw(&[0u8; 32]));
let pretty = key.to_json_pretty().unwrap();
assert!(pretty.contains('\n'));
}
#[test]
fn test_for_use_case_file_storage_is_correct() {
use crate::types::types::UseCase;
let key = PortableKey::for_use_case(
UseCase::FileStorage,
KeyType::Public,
KeyData::from_raw(&vec![0xAA; 1568]),
);
assert_eq!(key.use_case(), Some(UseCase::FileStorage));
assert!(key.security_level().is_none());
assert_eq!(key.algorithm(), KeyAlgorithm::HybridMlKem1024X25519);
}
#[test]
fn test_for_use_case_iot_is_correct() {
use crate::types::types::UseCase;
let key = PortableKey::for_use_case(
UseCase::IoTDevice,
KeyType::Public,
KeyData::from_raw(&vec![0xBB; 800]),
);
assert_eq!(key.use_case(), Some(UseCase::IoTDevice));
assert_eq!(key.algorithm(), KeyAlgorithm::HybridMlKem512X25519);
}
#[test]
fn test_for_use_case_secure_messaging_is_correct() {
use crate::types::types::UseCase;
let key = PortableKey::for_use_case(
UseCase::SecureMessaging,
KeyType::Public,
KeyData::from_raw(&vec![0xCC; 1184]),
);
assert_eq!(key.algorithm(), KeyAlgorithm::HybridMlKem768X25519);
}
#[test]
fn test_for_security_level_high_is_correct() {
use crate::types::types::SecurityLevel;
let key = PortableKey::for_security_level(
SecurityLevel::High,
KeyType::Public,
KeyData::from_raw(&vec![0xDD; 1184]),
);
assert!(key.use_case().is_none());
assert_eq!(key.security_level(), Some(SecurityLevel::High));
assert_eq!(key.algorithm(), KeyAlgorithm::HybridMlKem768X25519);
}
#[test]
fn test_for_security_level_quantum_is_correct() {
use crate::types::types::SecurityLevel;
let key = PortableKey::for_security_level(
SecurityLevel::Quantum,
KeyType::Public,
KeyData::from_raw(&vec![0xEE; 1568]),
);
assert_eq!(key.security_level(), Some(SecurityLevel::Quantum));
assert_eq!(key.algorithm(), KeyAlgorithm::MlKem1024);
}
#[test]
fn test_for_use_case_with_level_security_takes_precedence_succeeds() {
use crate::types::types::{SecurityLevel, UseCase};
let key = PortableKey::for_use_case_with_level(
UseCase::IoTDevice,
SecurityLevel::Maximum,
KeyType::Public,
KeyData::from_raw(&vec![0xFF; 1568]),
);
assert_eq!(key.use_case(), Some(UseCase::IoTDevice));
assert_eq!(key.security_level(), Some(SecurityLevel::Maximum));
assert_eq!(key.algorithm(), KeyAlgorithm::HybridMlKem1024X25519);
}
#[test]
fn test_for_use_case_json_includes_use_case_field_succeeds() {
use crate::types::types::UseCase;
let key = PortableKey::for_use_case(
UseCase::DatabaseEncryption,
KeyType::Public,
KeyData::from_raw(&[0u8; 32]),
);
let json = key.to_json().unwrap();
assert!(json.contains("use_case"));
assert!(json.contains("database-encryption"));
}
#[test]
fn test_for_security_level_json_includes_security_level_field_succeeds() {
use crate::types::types::SecurityLevel;
let key = PortableKey::for_security_level(
SecurityLevel::Standard,
KeyType::Public,
KeyData::from_raw(&[0u8; 32]),
);
let json = key.to_json().unwrap();
assert!(json.contains("security_level"));
assert!(json.contains("standard"));
}
#[test]
fn test_for_use_case_cbor_roundtrip() {
use crate::types::types::UseCase;
let key = PortableKey::for_use_case(
UseCase::HealthcareRecords,
KeyType::Secret,
KeyData::from_composite(&[0xAA; 64], &[0xBB; 32]),
);
let cbor = key.to_cbor().unwrap();
let restored = PortableKey::from_cbor(&cbor).unwrap();
assert_eq!(restored.use_case(), Some(UseCase::HealthcareRecords));
assert_eq!(restored.algorithm(), KeyAlgorithm::HybridMlKem1024X25519);
}
#[test]
fn proof_e2e_file_storage_two_process() {
use crate::hybrid::kem_hybrid;
use crate::types::types::UseCase;
let (pk, sk) = kem_hybrid::generate_keypair().unwrap();
let (portable_pk, portable_sk) =
PortableKey::from_hybrid_kem_keypair(UseCase::FileStorage, &pk, &sk).unwrap();
let pk_json = portable_pk.to_json().unwrap();
let pk_json_len = pk_json.len();
let sk_json = portable_sk.to_json().unwrap();
let sk_json_len = sk_json.len();
drop(pk);
drop(sk);
drop(portable_pk);
drop(portable_sk);
let sender_pk = PortableKey::from_json(&pk_json).unwrap();
let sender_hybrid_pk = sender_pk.to_hybrid_public_key().unwrap();
let encapsulated = kem_hybrid::encapsulate(&sender_hybrid_pk).unwrap();
let sender_shared_secret = encapsulated.shared_secret().to_vec();
let receiver_sk_portable = PortableKey::from_json(&sk_json).unwrap();
let receiver_sk = receiver_sk_portable.to_hybrid_secret_key().unwrap();
let receiver_shared_secret = kem_hybrid::decapsulate(&receiver_sk, &encapsulated).unwrap();
let secrets_match = receiver_shared_secret.as_slice() == sender_shared_secret.as_slice();
let uc_preserved = receiver_sk_portable.use_case() == Some(UseCase::FileStorage);
assert!(secrets_match, "Shared secrets must match across processes");
assert!(uc_preserved);
println!(
"[PROOF] {{\"test\":\"e2e_file_storage_two_process\",\
\"category\":\"key-format\",\
\"use_case\":\"file-storage\",\
\"algorithm\":\"hybrid-ml-kem-768-x25519\",\
\"format\":\"json\",\
\"pk_json_bytes\":{pk_json_len},\
\"sk_json_bytes\":{sk_json_len},\
\"shared_secret_bytes\":{},\
\"cross_process_kem_match\":{secrets_match},\
\"use_case_preserved\":{uc_preserved},\
\"status\":\"PASS\"}}",
receiver_shared_secret.len(),
);
}
#[test]
fn proof_e2e_secure_messaging_cbor_two_process() {
use crate::hybrid::kem_hybrid;
use crate::types::types::UseCase;
let (pk, sk) = kem_hybrid::generate_keypair().unwrap();
let (portable_pk, portable_sk) =
PortableKey::from_hybrid_kem_keypair(UseCase::SecureMessaging, &pk, &sk).unwrap();
let pk_cbor = portable_pk.to_cbor().unwrap();
let pk_cbor_len = pk_cbor.len();
let sk_cbor = portable_sk.to_cbor().unwrap();
let sk_cbor_len = sk_cbor.len();
let pk_json_len = portable_pk.to_json().unwrap().len();
drop(pk);
drop(sk);
drop(portable_pk);
drop(portable_sk);
let sender_pk = PortableKey::from_cbor(&pk_cbor).unwrap();
let sender_hybrid_pk = sender_pk.to_hybrid_public_key().unwrap();
let encapsulated = kem_hybrid::encapsulate(&sender_hybrid_pk).unwrap();
let sender_ss = encapsulated.shared_secret().to_vec();
let receiver_sk_portable = PortableKey::from_cbor(&sk_cbor).unwrap();
let receiver_sk = receiver_sk_portable.to_hybrid_secret_key().unwrap();
let receiver_ss = kem_hybrid::decapsulate(&receiver_sk, &encapsulated).unwrap();
let secrets_match = receiver_ss.as_slice() == sender_ss.as_slice();
assert!(secrets_match);
assert!(pk_cbor_len < pk_json_len);
println!(
"[PROOF] {{\"test\":\"e2e_secure_messaging_cbor_two_process\",\
\"category\":\"key-format\",\
\"use_case\":\"secure-messaging\",\
\"format\":\"cbor\",\
\"pk_cbor_bytes\":{pk_cbor_len},\
\"sk_cbor_bytes\":{sk_cbor_len},\
\"pk_json_bytes\":{pk_json_len},\
\"cbor_savings_pct\":{:.1},\
\"cross_process_kem_match\":{secrets_match},\
\"status\":\"PASS\"}}",
(1.0 - (pk_cbor_len as f64 / pk_json_len as f64)) * 100.0,
);
}
#[test]
fn proof_e2e_legal_document_signing_two_process() {
use crate::hybrid::sig_hybrid;
use crate::types::types::UseCase;
let (pk, sk) = sig_hybrid::generate_keypair().unwrap();
let (portable_pk, _portable_sk) =
PortableKey::from_hybrid_sig_keypair(UseCase::LegalDocuments, &pk, &sk).unwrap();
let pk_json = portable_pk.to_json().unwrap();
let message = b"WHEREAS the parties agree to the following terms and conditions...";
let signature = sig_hybrid::sign(&sk, message).unwrap();
let sig_bytes = signature.ml_dsa_sig().len() + signature.ed25519_sig().len();
drop(pk);
drop(sk);
drop(portable_pk);
let verifier_pk = PortableKey::from_json(&pk_json).unwrap();
let verifier_hybrid_pk = verifier_pk.to_hybrid_sig_public_key().unwrap();
let valid = sig_hybrid::verify(&verifier_hybrid_pk, message, &signature).unwrap();
let uc_ok = verifier_pk.use_case() == Some(UseCase::LegalDocuments);
let alg_ok = verifier_pk.algorithm() == KeyAlgorithm::HybridMlDsa65Ed25519;
assert!(valid, "Signature must verify with JSON-restored PK");
assert!(uc_ok);
assert!(alg_ok);
println!(
"[PROOF] {{\"test\":\"e2e_legal_document_signing_two_process\",\
\"category\":\"key-format\",\
\"use_case\":\"legal-documents\",\
\"algorithm\":\"hybrid-ml-dsa-65-ed25519\",\
\"message_len\":{},\
\"total_sig_bytes\":{sig_bytes},\
\"cross_process_verify\":{valid},\
\"use_case_preserved\":{uc_ok},\
\"status\":\"PASS\"}}",
message.len(),
);
}
#[test]
fn proof_e2e_key_file_persistence_two_process() {
use crate::hybrid::kem_hybrid;
use crate::types::types::UseCase;
let dir = std::env::temp_dir().join("latticearc_proof_key_file_e2e");
std::fs::create_dir_all(&dir).unwrap();
let pk_json_path = dir.join("cloud.pub.json");
let sk_json_path = dir.join("cloud.sec.json");
let pk_cbor_path = dir.join("cloud.pub.cbor");
let (pk, sk) = kem_hybrid::generate_keypair().unwrap();
let (portable_pk, portable_sk) =
PortableKey::from_hybrid_kem_keypair(UseCase::CloudStorage, &pk, &sk).unwrap();
portable_pk.write_to_file(&pk_json_path).unwrap();
portable_pk.write_cbor_to_file(&pk_cbor_path).unwrap();
portable_sk.write_to_file(&sk_json_path).unwrap();
drop(pk);
drop(sk);
drop(portable_pk);
drop(portable_sk);
let sender_pk =
PortableKey::read_from_file(&pk_json_path).unwrap().to_hybrid_public_key().unwrap();
let encapsulated = kem_hybrid::encapsulate(&sender_pk).unwrap();
let sender_ss = encapsulated.shared_secret().to_vec();
let receiver_sk_portable = PortableKey::read_from_file(&sk_json_path).unwrap();
let receiver_sk = receiver_sk_portable.to_hybrid_secret_key().unwrap();
let receiver_ss = kem_hybrid::decapsulate(&receiver_sk, &encapsulated).unwrap();
let json_match = receiver_ss.as_slice() == sender_ss.as_slice();
let cbor_pk = PortableKey::read_cbor_from_file(&pk_cbor_path)
.unwrap()
.to_hybrid_public_key()
.unwrap();
let enc2 = kem_hybrid::encapsulate(&cbor_pk).unwrap();
let dec2 = kem_hybrid::decapsulate(&receiver_sk, &enc2).unwrap();
let cbor_match = dec2.as_slice() == enc2.shared_secret();
let json_size = std::fs::metadata(&pk_json_path).unwrap().len();
let cbor_size = std::fs::metadata(&pk_cbor_path).unwrap().len();
assert!(json_match);
assert!(cbor_match);
println!(
"[PROOF] {{\"test\":\"e2e_key_file_persistence_two_process\",\
\"category\":\"key-format\",\
\"use_case\":\"cloud-storage\",\
\"json_file_bytes\":{json_size},\
\"cbor_file_bytes\":{cbor_size},\
\"json_cross_process_kem\":{json_match},\
\"cbor_cross_process_kem\":{cbor_match},\
\"status\":\"PASS\"}}",
);
let _ = std::fs::remove_file(&pk_json_path);
let _ = std::fs::remove_file(&sk_json_path);
let _ = std::fs::remove_file(&pk_cbor_path);
let _ = std::fs::remove_dir(&dir);
}
#[test]
fn proof_e2e_cross_format_consistency() {
use crate::hybrid::kem_hybrid;
use crate::types::types::UseCase;
let (pk, sk) = kem_hybrid::generate_keypair().unwrap();
let (portable_pk, portable_sk) =
PortableKey::from_hybrid_kem_keypair(UseCase::DatabaseEncryption, &pk, &sk).unwrap();
let json = portable_pk.to_json().unwrap();
let cbor = portable_pk.to_cbor().unwrap();
let sk_json = portable_sk.to_json().unwrap();
drop(pk);
drop(sk);
drop(portable_pk);
drop(portable_sk);
let pk_from_json = PortableKey::from_json(&json).unwrap().to_hybrid_public_key().unwrap();
let pk_from_cbor = PortableKey::from_cbor(&cbor).unwrap().to_hybrid_public_key().unwrap();
let sk_restored = PortableKey::from_json(&sk_json).unwrap().to_hybrid_secret_key().unwrap();
let keys_match = pk_from_json.ml_kem_pk() == pk_from_cbor.ml_kem_pk()
&& pk_from_json.ecdh_pk() == pk_from_cbor.ecdh_pk();
let enc1 = kem_hybrid::encapsulate(&pk_from_json).unwrap();
let dec1 = kem_hybrid::decapsulate(&sk_restored, &enc1).unwrap();
let json_kem_ok = dec1.as_slice() == enc1.shared_secret();
let enc2 = kem_hybrid::encapsulate(&pk_from_cbor).unwrap();
let dec2 = kem_hybrid::decapsulate(&sk_restored, &enc2).unwrap();
let cbor_kem_ok = dec2.as_slice() == enc2.shared_secret();
assert!(keys_match);
assert!(json_kem_ok);
assert!(cbor_kem_ok);
println!(
"[PROOF] {{\"test\":\"e2e_cross_format_consistency\",\
\"category\":\"key-format\",\
\"json_bytes\":{},\
\"cbor_bytes\":{},\
\"key_material_match\":{keys_match},\
\"json_kem_cross_process\":{json_kem_ok},\
\"cbor_kem_cross_process\":{cbor_kem_ok},\
\"status\":\"PASS\"}}",
json.len(),
cbor.len(),
);
}
#[test]
fn proof_e2e_enterprise_metadata_roundtrip() {
use crate::hybrid::kem_hybrid;
use crate::types::types::UseCase;
let (pk, sk) = kem_hybrid::generate_keypair().unwrap();
let (mut portable_pk, portable_sk) =
PortableKey::from_hybrid_kem_keypair(UseCase::HealthcareRecords, &pk, &sk).unwrap();
portable_pk.set_label("HIPAA-compliant DEK");
portable_pk.set_metadata(
"compliance".to_string(),
serde_json::json!({"standard": "HIPAA", "audit_id": "AUD-2026-0042"}),
);
portable_pk.set_metadata("department".to_string(), serde_json::json!("cardiology"));
let pk_json = portable_pk.to_json().unwrap();
let sk_json = portable_sk.to_json().unwrap();
let pk_cbor = portable_pk.to_cbor().unwrap();
drop(pk);
drop(sk);
drop(portable_pk);
drop(portable_sk);
let from_json = PortableKey::from_json(&pk_json).unwrap();
let label_ok = from_json.label() == Some("HIPAA-compliant DEK");
let compliance_ok = from_json
.metadata()
.get("compliance")
.and_then(|v| v.get("standard"))
.and_then(|v| v.as_str())
== Some("HIPAA");
let dept_ok =
from_json.metadata().get("department") == Some(&serde_json::json!("cardiology"));
let json_pk = from_json.to_hybrid_public_key().unwrap();
let json_sk = PortableKey::from_json(&sk_json).unwrap().to_hybrid_secret_key().unwrap();
let enc = kem_hybrid::encapsulate(&json_pk).unwrap();
let dec = kem_hybrid::decapsulate(&json_sk, &enc).unwrap();
let kem_ok = dec.as_slice() == enc.shared_secret();
let from_cbor = PortableKey::from_cbor(&pk_cbor).unwrap();
let cbor_label_ok = from_cbor.label() == Some("HIPAA-compliant DEK");
let cbor_audit_ok = from_cbor
.metadata()
.get("compliance")
.and_then(|v| v.get("audit_id"))
.and_then(|v| v.as_str())
== Some("AUD-2026-0042");
assert!(label_ok);
assert!(compliance_ok);
assert!(dept_ok);
assert!(cbor_label_ok);
assert!(cbor_audit_ok);
assert!(kem_ok);
let metadata_count = from_json.metadata().len();
println!(
"[PROOF] {{\"test\":\"e2e_enterprise_metadata_roundtrip\",\
\"category\":\"key-format\",\
\"use_case\":\"healthcare-records\",\
\"metadata_fields\":{metadata_count},\
\"json_label\":{label_ok},\
\"json_compliance\":{compliance_ok},\
\"cbor_label\":{cbor_label_ok},\
\"cbor_audit\":{cbor_audit_ok},\
\"cross_process_kem_with_metadata\":{kem_ok},\
\"status\":\"PASS\"}}",
);
}
#[test]
fn proof_e2e_security_level_precedence() {
use crate::types::types::{SecurityLevel, UseCase};
let key = PortableKey::for_use_case_with_level(
UseCase::IoTDevice,
SecurityLevel::Maximum,
KeyType::Public,
KeyData::from_composite(&[0x42; 1568], &[0x43; 32]),
);
let uc_algo = resolve_use_case_algorithm(UseCase::IoTDevice);
let sl_algo = resolve_security_level_algorithm(SecurityLevel::Maximum);
let actual_algo = key.algorithm();
let precedence_correct = actual_algo == sl_algo && actual_algo != uc_algo;
let json = key.to_json().unwrap();
let restored = PortableKey::from_json(&json).unwrap();
let uc_preserved = restored.use_case() == Some(UseCase::IoTDevice);
let sl_preserved = restored.security_level() == Some(SecurityLevel::Maximum);
let algo_preserved = restored.algorithm() == KeyAlgorithm::HybridMlKem1024X25519;
assert!(precedence_correct);
assert!(uc_preserved);
assert!(sl_preserved);
assert!(algo_preserved);
println!(
"[PROOF] {{\"test\":\"e2e_security_level_precedence\",\
\"category\":\"key-format\",\
\"use_case\":\"io-t-device\",\
\"security_level\":\"maximum\",\
\"use_case_would_select\":\"{uc_algo:?}\",\
\"security_level_selects\":\"{sl_algo:?}\",\
\"actual_algorithm\":\"{actual_algo:?}\",\
\"precedence_correct\":{precedence_correct},\
\"use_case_preserved\":{uc_preserved},\
\"security_level_preserved\":{sl_preserved},\
\"algorithm_preserved\":{algo_preserved},\
\"status\":\"PASS\"}}",
);
}
#[test]
fn test_to_hybrid_public_key_wrong_algorithm_fails() {
let key =
PortableKey::new(KeyAlgorithm::Ed25519, KeyType::Public, KeyData::from_raw(&[0u8; 32]));
assert!(key.to_hybrid_public_key().is_err());
}
#[test]
fn test_to_hybrid_sig_public_key_wrong_algorithm_fails() {
let key = PortableKey::new(
KeyAlgorithm::MlKem768,
KeyType::Public,
KeyData::from_raw(&[0u8; 32]),
);
assert!(key.to_hybrid_sig_public_key().is_err());
}
#[test]
fn test_all_use_cases_resolve_to_algorithm_is_correct() {
use crate::types::types::UseCase;
let all = [
UseCase::SecureMessaging,
UseCase::EmailEncryption,
UseCase::VpnTunnel,
UseCase::ApiSecurity,
UseCase::FileStorage,
UseCase::DatabaseEncryption,
UseCase::CloudStorage,
UseCase::BackupArchive,
UseCase::ConfigSecrets,
UseCase::Authentication,
UseCase::SessionToken,
UseCase::DigitalCertificate,
UseCase::KeyExchange,
UseCase::FinancialTransactions,
UseCase::LegalDocuments,
UseCase::BlockchainTransaction,
UseCase::HealthcareRecords,
UseCase::GovernmentClassified,
UseCase::PaymentCard,
UseCase::IoTDevice,
UseCase::FirmwareSigning,
UseCase::AuditLog,
];
for uc in &all {
let key =
PortableKey::for_use_case(*uc, KeyType::Public, KeyData::from_raw(&[0u8; 32]));
assert!(
key.algorithm().is_hybrid() || matches!(key.algorithm(), KeyAlgorithm::MlKem1024),
"UseCase {:?} resolved to unexpected algorithm {:?}",
uc,
key.algorithm()
);
}
}
#[test]
fn test_all_security_levels_resolve_to_algorithm_is_correct() {
use crate::types::types::SecurityLevel;
let levels = [
(SecurityLevel::Standard, KeyAlgorithm::HybridMlKem512X25519),
(SecurityLevel::High, KeyAlgorithm::HybridMlKem768X25519),
(SecurityLevel::Maximum, KeyAlgorithm::HybridMlKem1024X25519),
(SecurityLevel::Quantum, KeyAlgorithm::MlKem1024),
];
for (level, expected) in &levels {
let key = PortableKey::for_security_level(
*level,
KeyType::Public,
KeyData::from_raw(&[0u8; 32]),
);
assert_eq!(key.algorithm(), *expected, "Level {:?}", level);
}
}
}