diff --git a/Cargo.lock b/Cargo.lock index 32d7a52..43b5b29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,7 +49,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror", + "thiserror 2.0.18", "time", ] @@ -1020,7 +1020,7 @@ version = "0.1.0" dependencies = [ "quinn", "rustls", - "thiserror", + "thiserror 2.0.18", "wtransport", ] @@ -1038,6 +1038,7 @@ dependencies = [ "rand_core 0.6.4", "serde", "sha2 0.11.0", + "thiserror 1.0.69", "zeroize", ] @@ -1331,7 +1332,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -1353,7 +1354,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -1852,13 +1853,33 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2377,7 +2398,7 @@ dependencies = [ "rustls-pki-types", "sha2 0.11.0", "socket2", - "thiserror", + "thiserror 2.0.18", "time", "tokio", "tracing", @@ -2394,7 +2415,7 @@ checksum = "d5867c629e4252f7439d82315923daaf27f4fa442410d51b78ab93ef4c432a11" dependencies = [ "httlib-huffman", "octets", - "thiserror", + "thiserror 2.0.18", "url", ] @@ -2425,7 +2446,7 @@ dependencies = [ "oid-registry", "ring", "rusticata-macros", - "thiserror", + "thiserror 2.0.18", "time", ] diff --git a/crypto/Cargo.toml b/crypto/Cargo.toml index 867596b..26031c7 100644 --- a/crypto/Cargo.toml +++ b/crypto/Cargo.toml @@ -16,6 +16,7 @@ ed25519-dalek = { version = "2.2", optional = true, features = [ hkdf = { version = "0.13", optional = true } sha2 = { version = "0.11", optional = true } zeroize = { version = "1.9", features = ["derive"] } +thiserror = "1" rand_core = { version = "0.6", features = ["getrandom"] } getrandom = "0.4.3" mlkem-tls = { version = "0.2", optional = true } diff --git a/crypto/src/error.rs b/crypto/src/error.rs index 53ad891..c1a535f 100644 --- a/crypto/src/error.rs +++ b/crypto/src/error.rs @@ -1,38 +1,31 @@ -use std::fmt; +use thiserror::Error; -#[derive(Debug, Clone)] +#[derive(Error, Debug, Clone)] pub enum CryptoError { + #[error("encryption failed")] EncryptionFailed, + #[error("decryption failed")] DecryptionFailed, + #[error("invalid key length")] InvalidKeyLength, + #[error("invalid nonce length")] InvalidNonceLength, + #[error("invalid signature")] InvalidSignature, + #[error("signing failed")] SigningFailed, + #[error("verification failed")] VerificationFailed, + #[error("key generation failed")] KeyGenerationFailed, + #[error("KDF error")] KdfError, + #[error("KEM encapsulation failed")] KemEncapsulationFailed, + #[error("KEM decapsulation failed")] KemDecapsulationFailed, + #[error("unknown algorithm")] UnknownAlgorithm, + #[error("invalid hex encoding")] + InvalidHex, } - -impl fmt::Display for CryptoError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - CryptoError::EncryptionFailed => write!(f, "encryption failed"), - CryptoError::DecryptionFailed => write!(f, "decryption failed"), - CryptoError::InvalidKeyLength => write!(f, "invalid key length"), - CryptoError::InvalidNonceLength => write!(f, "invalid nonce length"), - CryptoError::InvalidSignature => write!(f, "invalid signature"), - CryptoError::SigningFailed => write!(f, "signing failed"), - CryptoError::VerificationFailed => write!(f, "verification failed"), - CryptoError::KeyGenerationFailed => write!(f, "key generation failed"), - CryptoError::KdfError => write!(f, "KDF error"), - CryptoError::KemEncapsulationFailed => write!(f, "KEM encapsulation failed"), - CryptoError::KemDecapsulationFailed => write!(f, "KEM decapsulation failed"), - CryptoError::UnknownAlgorithm => write!(f, "unknown algorithm"), - } - } -} - -impl std::error::Error for CryptoError {} diff --git a/crypto/src/keypair.rs b/crypto/src/keypair.rs index a5e80c5..7e0d922 100644 --- a/crypto/src/keypair.rs +++ b/crypto/src/keypair.rs @@ -1,5 +1,8 @@ +use std::fmt; use zeroize::{Zeroize, ZeroizeOnDrop}; +// --- Private key types --- + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(transparent))] #[derive(Zeroize, ZeroizeOnDrop)] @@ -9,12 +12,26 @@ impl EncryptionPrivateKey { pub fn new(bytes: Vec) -> Self { Self(bytes) } - pub fn as_bytes(&self) -> &[u8] { &self.0 } } +impl fmt::Debug for EncryptionPrivateKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("EncryptionPrivateKey") + .field("len", &self.0.len()) + .field("data", &"[REDACTED]") + .finish() + } +} + +impl AsRef<[u8]> for EncryptionPrivateKey { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + impl From> for EncryptionPrivateKey { fn from(bytes: Vec) -> Self { Self(bytes) @@ -27,6 +44,8 @@ impl From<&[u8]> for EncryptionPrivateKey { } } +// --- + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(transparent))] #[derive(Zeroize, ZeroizeOnDrop)] @@ -36,12 +55,26 @@ impl SignaturePrivateKey { pub fn new(bytes: Vec) -> Self { Self(bytes) } - pub fn as_bytes(&self) -> &[u8] { &self.0 } } +impl fmt::Debug for SignaturePrivateKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SignaturePrivateKey") + .field("len", &self.0.len()) + .field("data", &"[REDACTED]") + .finish() + } +} + +impl AsRef<[u8]> for SignaturePrivateKey { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + impl From> for SignaturePrivateKey { fn from(bytes: Vec) -> Self { Self(bytes) @@ -54,59 +87,7 @@ impl From<&[u8]> for SignaturePrivateKey { } } -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(transparent))] -#[derive(Clone)] -pub struct EncryptionPublicKey(Vec); - -impl EncryptionPublicKey { - pub fn new(bytes: Vec) -> Self { - Self(bytes) - } - - pub fn as_bytes(&self) -> &[u8] { - &self.0 - } -} - -impl From> for EncryptionPublicKey { - fn from(bytes: Vec) -> Self { - Self(bytes) - } -} - -impl From<&[u8]> for EncryptionPublicKey { - fn from(bytes: &[u8]) -> Self { - Self(bytes.to_vec()) - } -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(transparent))] -#[derive(Clone)] -pub struct SignaturePublicKey(Vec); - -impl SignaturePublicKey { - pub fn new(bytes: Vec) -> Self { - Self(bytes) - } - - pub fn as_bytes(&self) -> &[u8] { - &self.0 - } -} - -impl From> for SignaturePublicKey { - fn from(bytes: Vec) -> Self { - Self(bytes) - } -} - -impl From<&[u8]> for SignaturePublicKey { - fn from(bytes: &[u8]) -> Self { - Self(bytes.to_vec()) - } -} +// --- #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(transparent))] @@ -117,12 +98,26 @@ impl KemPrivateKey { pub fn new(bytes: Vec) -> Self { Self(bytes) } - pub fn as_bytes(&self) -> &[u8] { &self.0 } } +impl fmt::Debug for KemPrivateKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("KemPrivateKey") + .field("len", &self.0.len()) + .field("data", &"[REDACTED]") + .finish() + } +} + +impl AsRef<[u8]> for KemPrivateKey { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + impl From> for KemPrivateKey { fn from(bytes: Vec) -> Self { Self(bytes) @@ -135,6 +130,179 @@ impl From<&[u8]> for KemPrivateKey { } } +// --- + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] +#[derive(Zeroize, ZeroizeOnDrop)] +pub struct SignaturePqPrivateKey(Vec); + +impl SignaturePqPrivateKey { + pub fn new(bytes: Vec) -> Self { + Self(bytes) + } + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } +} + +impl fmt::Debug for SignaturePqPrivateKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SignaturePqPrivateKey") + .field("len", &self.0.len()) + .field("data", &"[REDACTED]") + .finish() + } +} + +impl AsRef<[u8]> for SignaturePqPrivateKey { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl From> for SignaturePqPrivateKey { + fn from(bytes: Vec) -> Self { + Self(bytes) + } +} + +impl From<&[u8]> for SignaturePqPrivateKey { + fn from(bytes: &[u8]) -> Self { + Self(bytes.to_vec()) + } +} + +// --- Public key types --- + +fn bytes_to_hex(bytes: &[u8]) -> String { + bytes + .iter() + .fold(String::with_capacity(bytes.len() * 2), |mut s, b| { + use fmt::Write; + let _ = write!(s, "{:02x}", b); + s + }) +} + +fn hex_to_bytes(s: &str) -> Result, crate::error::CryptoError> { + if s.len() % 2 != 0 { + return Err(crate::error::CryptoError::InvalidHex); + } + (0..s.len()) + .step_by(2) + .map(|i| { + u8::from_str_radix(&s[i..i + 2], 16).map_err(|_| crate::error::CryptoError::InvalidHex) + }) + .collect() +} + +// --- + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] +#[derive(Clone)] +pub struct EncryptionPublicKey(Vec); + +impl EncryptionPublicKey { + pub fn new(bytes: Vec) -> Self { + Self(bytes) + } + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + pub fn to_hex(&self) -> String { + bytes_to_hex(&self.0) + } + pub fn from_hex(s: &str) -> Result { + hex_to_bytes(s).map(Self) + } +} + +impl fmt::Debug for EncryptionPublicKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "EncryptionPublicKey({})", self.to_hex()) + } +} + +impl AsRef<[u8]> for EncryptionPublicKey { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl From> for EncryptionPublicKey { + fn from(bytes: Vec) -> Self { + Self(bytes) + } +} + +impl From<&[u8]> for EncryptionPublicKey { + fn from(bytes: &[u8]) -> Self { + Self(bytes.to_vec()) + } +} + +impl From<&EncryptionPublicKey> for Vec { + fn from(key: &EncryptionPublicKey) -> Vec { + key.0.clone() + } +} + +// --- + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] +#[derive(Clone)] +pub struct SignaturePublicKey(Vec); + +impl SignaturePublicKey { + pub fn new(bytes: Vec) -> Self { + Self(bytes) + } + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + pub fn to_hex(&self) -> String { + bytes_to_hex(&self.0) + } + pub fn from_hex(s: &str) -> Result { + hex_to_bytes(s).map(Self) + } +} + +impl fmt::Debug for SignaturePublicKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "SignaturePublicKey({})", self.to_hex()) + } +} + +impl AsRef<[u8]> for SignaturePublicKey { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl From> for SignaturePublicKey { + fn from(bytes: Vec) -> Self { + Self(bytes) + } +} + +impl From<&[u8]> for SignaturePublicKey { + fn from(bytes: &[u8]) -> Self { + Self(bytes.to_vec()) + } +} + +impl From<&SignaturePublicKey> for Vec { + fn from(key: &SignaturePublicKey) -> Vec { + key.0.clone() + } +} + +// --- + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(transparent))] #[derive(Clone)] @@ -144,10 +312,27 @@ impl KemPublicKey { pub fn new(bytes: Vec) -> Self { Self(bytes) } - pub fn as_bytes(&self) -> &[u8] { &self.0 } + pub fn to_hex(&self) -> String { + bytes_to_hex(&self.0) + } + pub fn from_hex(s: &str) -> Result { + hex_to_bytes(s).map(Self) + } +} + +impl fmt::Debug for KemPublicKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "KemPublicKey({})", self.to_hex()) + } +} + +impl AsRef<[u8]> for KemPublicKey { + fn as_ref(&self) -> &[u8] { + &self.0 + } } impl From> for KemPublicKey { @@ -162,6 +347,14 @@ impl From<&[u8]> for KemPublicKey { } } +impl From<&KemPublicKey> for Vec { + fn from(key: &KemPublicKey) -> Vec { + key.0.clone() + } +} + +// --- + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(transparent))] #[derive(Clone)] @@ -171,10 +364,27 @@ impl SignaturePqPublicKey { pub fn new(bytes: Vec) -> Self { Self(bytes) } - pub fn as_bytes(&self) -> &[u8] { &self.0 } + pub fn to_hex(&self) -> String { + bytes_to_hex(&self.0) + } + pub fn from_hex(s: &str) -> Result { + hex_to_bytes(s).map(Self) + } +} + +impl fmt::Debug for SignaturePqPublicKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "SignaturePqPublicKey({})", self.to_hex()) + } +} + +impl AsRef<[u8]> for SignaturePqPublicKey { + fn as_ref(&self) -> &[u8] { + &self.0 + } } impl From> for SignaturePqPublicKey { @@ -189,32 +399,13 @@ impl From<&[u8]> for SignaturePqPublicKey { } } -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(transparent))] -#[derive(Zeroize, ZeroizeOnDrop)] -pub struct SignaturePqPrivateKey(Vec); - -impl SignaturePqPrivateKey { - pub fn new(bytes: Vec) -> Self { - Self(bytes) - } - - pub fn as_bytes(&self) -> &[u8] { - &self.0 +impl From<&SignaturePqPublicKey> for Vec { + fn from(key: &SignaturePqPublicKey) -> Vec { + key.0.clone() } } -impl From> for SignaturePqPrivateKey { - fn from(bytes: Vec) -> Self { - Self(bytes) - } -} - -impl From<&[u8]> for SignaturePqPrivateKey { - fn from(bytes: &[u8]) -> Self { - Self(bytes.to_vec()) - } -} +// --- Keyring --- #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(ZeroizeOnDrop)] @@ -252,12 +443,9 @@ impl Keyring { #[cfg(all(feature = "mlkem-tls", feature = "ml-dsa", feature = "ed25519-dalek"))] pub fn generate() -> Self { let (kem_sk, kem_pk) = crate::kem::HybridKem::generate_keypair(); - let (ed_signer, sig_cl_sk, sig_cl_pk) = crate::sign::Ed25519Signer::generate(); let (_ml_signer, sig_pq_sk, sig_pq_pk) = crate::sign::MlDsaSigner::generate(); - drop(ed_signer); - Self { kem_public_key: kem_pk, kem_secret_key: kem_sk, @@ -267,8 +455,98 @@ impl Keyring { sig_cl_secret_key: sig_cl_sk, } } + + pub fn public_key_bundle(&self) -> PublicKeyBundle { + PublicKeyBundle { + kem_public_key: self.kem_public_key.clone(), + sig_pq_public_key: self.sig_pq_public_key.clone(), + sig_cl_public_key: self.sig_cl_public_key.clone(), + } + } + + pub fn to_bytes(&self) -> Vec { + let fields: &[&[u8]] = &[ + self.kem_public_key.as_bytes(), + self.kem_secret_key.as_bytes(), + self.sig_pq_public_key.as_bytes(), + self.sig_pq_secret_key.as_bytes(), + self.sig_cl_public_key.as_bytes(), + self.sig_cl_secret_key.as_bytes(), + ]; + let mut out = Vec::new(); + for f in fields { + out.extend_from_slice(&(f.len() as u16).to_be_bytes()); + out.extend_from_slice(f); + } + out + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + use crate::error::CryptoError; + let mut offset = 0; + let read_key = |offset: &mut usize| -> Result, CryptoError> { + let len = u16::from_be_bytes( + bytes + .get(*offset..*offset + 2) + .ok_or(CryptoError::InvalidKeyLength)? + .try_into() + .expect("slice is 2 bytes, verified above"), + ) as usize; + *offset += 2; + let key = bytes + .get(*offset..*offset + len) + .ok_or(CryptoError::InvalidKeyLength)? + .to_vec(); + *offset += len; + Ok(key) + }; + + Ok(Self { + kem_public_key: KemPublicKey::new(read_key(&mut offset)?), + kem_secret_key: KemPrivateKey::new(read_key(&mut offset)?), + sig_pq_public_key: SignaturePqPublicKey::new(read_key(&mut offset)?), + sig_pq_secret_key: SignaturePqPrivateKey::new(read_key(&mut offset)?), + sig_cl_public_key: SignaturePublicKey::new(read_key(&mut offset)?), + sig_cl_secret_key: SignaturePrivateKey::new(read_key(&mut offset)?), + }) + } } +impl TryFrom<&[u8]> for Keyring { + type Error = crate::error::CryptoError; + fn try_from(bytes: &[u8]) -> Result { + Self::from_bytes(bytes) + } +} + +impl From<&Keyring> for Vec { + fn from(keyring: &Keyring) -> Vec { + keyring.to_bytes() + } +} + +impl fmt::Debug for Keyring { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Keyring") + .field("kem_public_key", &self.kem_public_key) + .field("kem_secret_key", &self.kem_secret_key) + .field("sig_pq_public_key", &self.sig_pq_public_key) + .field("sig_pq_secret_key", &self.sig_pq_secret_key) + .field("sig_cl_public_key", &self.sig_cl_public_key) + .field("sig_cl_secret_key", &self.sig_cl_secret_key) + .finish() + } +} + +// --- PublicKeyBundle --- + +/// Ed25519 public key is always 32 bytes. +pub const SIG_CL_PUBLIC_KEY_LEN: usize = 32; +/// ML-DSA-65 public key is always 1952 bytes. +pub const SIG_PQ_PUBLIC_KEY_LEN: usize = 1952; +/// Hybrid X25519 + ML-KEM-768 public key (32 + 1184 bytes). +pub const KEM_PUBLIC_KEY_LEN: usize = 1216; + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone)] pub struct PublicKeyBundle { @@ -290,6 +568,21 @@ impl PublicKeyBundle { } } + #[cfg(all(feature = "ed25519-dalek", feature = "ml-dsa", feature = "mlkem-tls"))] + pub fn validate(&self) -> Result<(), crate::error::CryptoError> { + use crate::error::CryptoError; + if self.sig_cl_public_key.as_bytes().len() != SIG_CL_PUBLIC_KEY_LEN { + return Err(CryptoError::InvalidKeyLength); + } + if self.sig_pq_public_key.as_bytes().len() != SIG_PQ_PUBLIC_KEY_LEN { + return Err(CryptoError::InvalidKeyLength); + } + if self.kem_public_key.as_bytes().len() != KEM_PUBLIC_KEY_LEN { + return Err(CryptoError::InvalidKeyLength); + } + Ok(()) + } + pub fn as_bytes(&self) -> Vec { let kem = self.kem_public_key.as_bytes(); let pq = self.sig_pq_public_key.as_bytes(); @@ -354,66 +647,137 @@ impl PublicKeyBundle { } } -impl Keyring { - pub fn public_key_bundle(&self) -> PublicKeyBundle { - PublicKeyBundle { - kem_public_key: self.kem_public_key.clone(), - sig_pq_public_key: self.sig_pq_public_key.clone(), - sig_cl_public_key: self.sig_cl_public_key.clone(), - } - } - - /* - * Serialize the full keyring (all six keys) into a byte vector. - * - * Format: for each key, a 2-byte length prefix followed by the key bytes, - * in the order: kem_pk, kem_sk, sig_pq_pk, sig_pq_sk, sig_cl_pk, sig_cl_sk. - */ - pub fn to_bytes(&self) -> Vec { - let fields: &[&[u8]] = &[ - self.kem_public_key.as_bytes(), - self.kem_secret_key.as_bytes(), - self.sig_pq_public_key.as_bytes(), - self.sig_pq_secret_key.as_bytes(), - self.sig_cl_public_key.as_bytes(), - self.sig_cl_secret_key.as_bytes(), - ]; - let mut out = Vec::new(); - for f in fields { - out.extend_from_slice(&(f.len() as u16).to_be_bytes()); - out.extend_from_slice(f); - } - out - } - - /// Deserialize a full keyring from bytes produced by [`Keyring::to_bytes`]. - pub fn from_bytes(bytes: &[u8]) -> Result { - use crate::error::CryptoError; - let mut offset = 0; - let read_key = |offset: &mut usize| -> Result, CryptoError> { - let len = u16::from_be_bytes( - bytes - .get(*offset..*offset + 2) - .ok_or(CryptoError::InvalidKeyLength)? - .try_into() - .expect("slice is 2 bytes, verified above"), - ) as usize; - *offset += 2; - let key = bytes - .get(*offset..*offset + len) - .ok_or(CryptoError::InvalidKeyLength)? - .to_vec(); - *offset += len; - Ok(key) - }; - - Ok(Self { - kem_public_key: KemPublicKey::new(read_key(&mut offset)?), - kem_secret_key: KemPrivateKey::new(read_key(&mut offset)?), - sig_pq_public_key: SignaturePqPublicKey::new(read_key(&mut offset)?), - sig_pq_secret_key: SignaturePqPrivateKey::new(read_key(&mut offset)?), - sig_cl_public_key: SignaturePublicKey::new(read_key(&mut offset)?), - sig_cl_secret_key: SignaturePrivateKey::new(read_key(&mut offset)?), - }) +impl TryFrom<&[u8]> for PublicKeyBundle { + type Error = crate::error::CryptoError; + fn try_from(bytes: &[u8]) -> Result { + Self::from_bytes(bytes) + } +} + +impl From<&PublicKeyBundle> for Vec { + fn from(bundle: &PublicKeyBundle) -> Vec { + bundle.as_bytes() + } +} + +impl fmt::Debug for PublicKeyBundle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PublicKeyBundle") + .field("kem_public_key", &self.kem_public_key) + .field("sig_pq_public_key", &self.sig_pq_public_key) + .field("sig_cl_public_key", &self.sig_cl_public_key) + .finish() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn public_key_bundle_roundtrip() { + let kem = KemPublicKey::new(vec![1u8; 32]); + let pq = SignaturePqPublicKey::new(vec![2u8; 64]); + let cl = SignaturePublicKey::new(vec![3u8; 32]); + + let bundle = PublicKeyBundle::new(kem, pq, cl); + let bytes = bundle.as_bytes(); + let recovered = PublicKeyBundle::from_bytes(&bytes).unwrap(); + + assert_eq!( + bundle.kem_public_key.as_bytes(), + recovered.kem_public_key.as_bytes() + ); + assert_eq!( + bundle.sig_pq_public_key.as_bytes(), + recovered.sig_pq_public_key.as_bytes() + ); + assert_eq!( + bundle.sig_cl_public_key.as_bytes(), + recovered.sig_cl_public_key.as_bytes() + ); + } + + #[test] + fn public_key_bundle_try_from_roundtrip() { + let bundle = PublicKeyBundle::new( + KemPublicKey::new(vec![0xABu8; 48]), + SignaturePqPublicKey::new(vec![0xCDu8; 96]), + SignaturePublicKey::new(vec![0xEFu8; 32]), + ); + let bytes: Vec = Vec::from(&bundle); + let recovered = PublicKeyBundle::try_from(bytes.as_slice()).unwrap(); + assert_eq!(bundle.as_bytes(), recovered.as_bytes()); + } + + #[test] + fn keyring_roundtrip() { + let keyring = Keyring::new( + KemPublicKey::new(vec![1u8; 32]), + KemPrivateKey::new(vec![2u8; 32]), + SignaturePqPublicKey::new(vec![3u8; 64]), + SignaturePqPrivateKey::new(vec![4u8; 64]), + SignaturePublicKey::new(vec![5u8; 32]), + SignaturePrivateKey::new(vec![6u8; 32]), + ); + let bytes = keyring.to_bytes(); + let recovered = Keyring::from_bytes(&bytes).unwrap(); + assert_eq!( + keyring.kem_public_key.as_bytes(), + recovered.kem_public_key.as_bytes() + ); + assert_eq!( + keyring.kem_secret_key.as_bytes(), + recovered.kem_secret_key.as_bytes() + ); + assert_eq!( + keyring.sig_cl_public_key.as_bytes(), + recovered.sig_cl_public_key.as_bytes() + ); + } + + #[test] + fn keyring_try_from_roundtrip() { + let keyring = Keyring::new( + KemPublicKey::new(vec![0u8; 16]), + KemPrivateKey::new(vec![1u8; 16]), + SignaturePqPublicKey::new(vec![2u8; 16]), + SignaturePqPrivateKey::new(vec![3u8; 16]), + SignaturePublicKey::new(vec![4u8; 16]), + SignaturePrivateKey::new(vec![5u8; 16]), + ); + let bytes: Vec = Vec::from(&keyring); + let recovered = Keyring::try_from(bytes.as_slice()).unwrap(); + assert_eq!(keyring.to_bytes(), recovered.to_bytes()); + } + + #[test] + fn hex_roundtrip() { + let key = KemPublicKey::new(vec![0xDE, 0xAD, 0xBE, 0xEF]); + let hex = key.to_hex(); + assert_eq!(hex, "deadbeef"); + let recovered = KemPublicKey::from_hex(&hex).unwrap(); + assert_eq!(key.as_bytes(), recovered.as_bytes()); + } + + #[test] + fn hex_invalid_returns_error() { + assert!(KemPublicKey::from_hex("xyz").is_err()); + assert!(KemPublicKey::from_hex("abc").is_err()); // odd length + } + + #[test] + fn debug_private_key_redacted() { + let key = KemPrivateKey::new(vec![0u8; 32]); + let s = format!("{:?}", key); + assert!(s.contains("REDACTED")); + assert!(!s.contains("00")); + } + + #[test] + fn debug_public_key_shows_hex() { + let key = KemPublicKey::new(vec![0xABu8; 4]); + let s = format!("{:?}", key); + assert!(s.contains("abababab")); } }