#![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";
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
)
}
}
#[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 {
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(),
}
}
}
impl Drop for KeyData {
fn drop(&mut self) {
match self {
Self::Single { raw } => {
raw.zeroize();
}
Self::Composite { pq, classical } => {
pq.zeroize();
classical.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(),
)),
}
}
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(),
)),
}
}
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}"))
})?;
}
}
Ok(())
}
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,
deprecated
)]
mod tests {
use super::*;
#[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);
}
}
}