use ed25519_dalek::{Signer, Verifier};
use ml_dsa::{EncodedSignature, EncodedVerifyingKey, MlDsa65};
use zeroize::{Zeroize, Zeroizing};
use crate::{
error::{ExoError, Result},
types::{PqPublicKey, PqSecretKey, PublicKey, SecretKey, Signature},
};
pub struct KeyPair {
pub public: PublicKey,
secret: SecretKey,
}
impl KeyPair {
#[must_use]
pub fn generate() -> Self {
let (public, secret) = generate_keypair();
Self { public, secret }
}
pub fn from_secret_bytes(bytes: [u8; 32]) -> Result<Self> {
let signing_key = ed25519_dalek::SigningKey::from_bytes(&bytes);
let verifying_key = signing_key.verifying_key();
Ok(Self {
public: PublicKey::from_bytes(verifying_key.to_bytes()),
secret: SecretKey::from_bytes(bytes),
})
}
#[must_use]
pub fn sign(&self, message: &[u8]) -> Signature {
sign(message, &self.secret)
}
#[must_use]
pub fn verify(&self, message: &[u8], signature: &Signature) -> bool {
verify(message, signature, &self.public)
}
#[must_use]
pub fn public_key(&self) -> &PublicKey {
&self.public
}
#[must_use]
pub fn secret_key(&self) -> &SecretKey {
&self.secret
}
}
impl Drop for KeyPair {
fn drop(&mut self) {
self.secret.zeroize();
}
}
impl core::fmt::Debug for KeyPair {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("KeyPair")
.field("public", &self.public)
.field("secret", &"***")
.finish()
}
}
pub struct PqKeyPair {
pub public: PqPublicKey,
secret: PqSecretKey,
}
impl PqKeyPair {
#[must_use]
pub fn generate() -> Self {
let (public, secret) = generate_pq_keypair();
Self { public, secret }
}
pub fn sign(&self, message: &[u8]) -> Result<Signature> {
sign_pq(message, &self.secret)
}
#[must_use]
pub fn verify(&self, message: &[u8], signature: &Signature) -> bool {
verify_pq(message, signature, &self.public)
}
#[must_use]
pub fn public_key(&self) -> &PqPublicKey {
&self.public
}
}
impl core::fmt::Debug for PqKeyPair {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("PqKeyPair")
.field("public", &self.public)
.field("secret", &"***")
.finish()
}
}
#[must_use]
pub fn generate_keypair() -> (PublicKey, SecretKey) {
let mut csprng = rand::rngs::OsRng;
let signing_key = ed25519_dalek::SigningKey::generate(&mut csprng);
let verifying_key = signing_key.verifying_key();
(
PublicKey::from_bytes(verifying_key.to_bytes()),
SecretKey::from_bytes(signing_key.to_bytes()),
)
}
#[must_use]
pub fn sign(message: &[u8], secret: &SecretKey) -> Signature {
let signing_key = ed25519_dalek::SigningKey::from_bytes(secret.as_bytes());
let sig = signing_key.sign(message);
Signature::Ed25519(sig.to_bytes())
}
#[must_use]
pub fn verify(message: &[u8], signature: &Signature, public: &PublicKey) -> bool {
let sig_bytes = match signature {
Signature::Ed25519(b) => b,
Signature::Hybrid { .. } => return false,
Signature::PostQuantum(_) => return false,
Signature::Empty => return false,
};
let Ok(verifying_key) = ed25519_dalek::VerifyingKey::from_bytes(public.as_bytes()) else {
return false;
};
let Ok(sig) = ed25519_dalek::Signature::from_slice(sig_bytes) else {
return false;
};
verifying_key.verify(message, &sig).is_ok()
}
#[must_use]
pub fn generate_pq_keypair() -> (PqPublicKey, PqSecretKey) {
let mut seed_bytes = Zeroizing::new([0u8; 32]);
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut *seed_bytes);
let seed: ml_dsa::Seed = (*seed_bytes).into();
let sk = ml_dsa::SigningKey::<MlDsa65>::from_seed(&seed);
let vk = sk.verifying_key();
let vk_encoded: EncodedVerifyingKey<MlDsa65> = vk.encode();
let vk_bytes: Vec<u8> = AsRef::<[u8]>::as_ref(&vk_encoded).to_vec();
(
PqPublicKey::from_bytes(vk_bytes),
PqSecretKey::from_bytes(seed_bytes.to_vec()),
)
}
pub fn sign_pq(message: &[u8], secret: &PqSecretKey) -> Result<Signature> {
let seed_arr: [u8; 32] = secret
.as_bytes()
.try_into()
.map_err(|_| ExoError::CryptoError {
reason: format!(
"ML-DSA seed must be 32 bytes, got {}",
secret.as_bytes().len()
),
})?;
let seed: ml_dsa::Seed = seed_arr.into();
let sk = ml_dsa::SigningKey::<MlDsa65>::from_seed(&seed);
let pq_sig = sk
.sign_deterministic(message, &[])
.map_err(|e| ExoError::CryptoError {
reason: format!("ML-DSA-65 sign failed: {e}"),
})?;
let sig_encoded: EncodedSignature<MlDsa65> = pq_sig.encode();
Ok(Signature::PostQuantum(
AsRef::<[u8]>::as_ref(&sig_encoded).to_vec(),
))
}
#[must_use]
pub fn verify_pq(message: &[u8], signature: &Signature, public: &PqPublicKey) -> bool {
let Signature::PostQuantum(sig_bytes) = signature else {
return false;
};
let Ok(encoded_vk) = EncodedVerifyingKey::<MlDsa65>::try_from(public.as_bytes()) else {
return false;
};
let vk = ml_dsa::VerifyingKey::<MlDsa65>::decode(&encoded_vk);
let Ok(ml_sig) = ml_dsa::Signature::<MlDsa65>::try_from(sig_bytes.as_slice()) else {
return false;
};
vk.verify_with_context(message, &[], &ml_sig)
}
pub fn sign_hybrid(
message: &[u8],
classical_secret: &SecretKey,
pq_secret: &PqSecretKey,
) -> Result<Signature> {
let Signature::Ed25519(classical) = sign(message, classical_secret) else {
return Err(ExoError::CryptoError {
reason: "unexpected non-Ed25519 variant from sign()".into(),
});
};
let Signature::PostQuantum(pq) = sign_pq(message, pq_secret)? else {
return Err(ExoError::CryptoError {
reason: "unexpected non-PostQuantum variant from sign_pq()".into(),
});
};
Ok(Signature::Hybrid { classical, pq })
}
#[must_use]
pub fn verify_hybrid(
message: &[u8],
signature: &Signature,
classical_public: &PublicKey,
pq_public: &PqPublicKey,
) -> bool {
let Signature::Hybrid { classical, pq } = signature else {
return false;
};
let classical_ok = verify_ed25519_bytes(message, classical, classical_public);
let pq_ok = verify_pq(message, &Signature::PostQuantum(pq.clone()), pq_public);
classical_ok & pq_ok
}
fn verify_ed25519_bytes(message: &[u8], sig_bytes: &[u8; 64], public: &PublicKey) -> bool {
let Ok(verifying_key) = ed25519_dalek::VerifyingKey::from_bytes(public.as_bytes()) else {
return false;
};
let Ok(sig) = ed25519_dalek::Signature::from_slice(sig_bytes) else {
return false;
};
verifying_key.verify(message, &sig).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generate_keypair_produces_valid_pair() {
let (pk, sk) = generate_keypair();
let msg = b"test message";
let sig = sign(msg, &sk);
assert!(verify(msg, &sig, &pk));
}
#[test]
fn sign_verify_roundtrip() {
let (pk, sk) = generate_keypair();
let msg = b"hello exochain";
let sig = sign(msg, &sk);
assert!(verify(msg, &sig, &pk));
}
#[test]
fn verify_fails_wrong_message() {
let (pk, sk) = generate_keypair();
let sig = sign(b"original", &sk);
assert!(!verify(b"tampered", &sig, &pk));
}
#[test]
fn verify_fails_wrong_key() {
let (_pk1, sk1) = generate_keypair();
let (pk2, _sk2) = generate_keypair();
let sig = sign(b"msg", &sk1);
assert!(!verify(b"msg", &sig, &pk2));
}
#[test]
fn verify_fails_corrupt_signature() {
let (pk, sk) = generate_keypair();
let sig = sign(b"msg", &sk);
let corrupted = match sig {
Signature::Ed25519(mut b) => {
b[0] ^= 0xff;
Signature::Ed25519(b)
}
_ => panic!("expected Ed25519"),
};
assert!(!verify(b"msg", &corrupted, &pk));
}
#[test]
fn verify_rejects_empty_signature() {
let (pk, _) = generate_keypair();
assert!(!verify(b"msg", &Signature::Empty, &pk));
}
#[test]
fn verify_rejects_pq_signature_via_classical_path() {
let (pk, _) = generate_keypair();
assert!(!verify(b"msg", &Signature::PostQuantum(vec![1, 2, 3]), &pk));
}
#[test]
fn verify_rejects_hybrid_via_classical_path() {
let (pk, sk) = generate_keypair();
let classical = match sign(b"msg", &sk) {
Signature::Ed25519(b) => b,
_ => panic!("expected Ed25519"),
};
let hybrid = Signature::Hybrid {
classical,
pq: vec![0u8; 32],
};
assert!(
!verify(b"msg", &hybrid, &pk),
"verify() must not silently downgrade Hybrid to Ed25519-only"
);
}
#[test]
fn verify_fails_invalid_public_key() {
let (_, sk) = generate_keypair();
let sig = sign(b"msg", &sk);
let bad_pk = PublicKey::from_bytes([0u8; 32]);
assert!(!verify(b"msg", &sig, &bad_pk));
}
#[test]
fn keypair_generate_and_use() {
let kp = KeyPair::generate();
let msg = b"keypair test";
let sig = kp.sign(msg);
assert!(kp.verify(msg, &sig));
}
#[test]
fn keypair_from_secret_bytes() {
let (_, sk) = generate_keypair();
let kp = KeyPair::from_secret_bytes(*sk.as_bytes()).expect("valid");
let msg = b"from bytes";
let sig = kp.sign(msg);
assert!(kp.verify(msg, &sig));
}
#[test]
fn keypair_public_key_accessor() {
let kp = KeyPair::generate();
let pk = kp.public_key();
assert_eq!(*pk, kp.public);
}
#[test]
fn keypair_secret_key_accessor() {
let kp = KeyPair::generate();
let sk = kp.secret_key();
let sig = sign(b"test", sk);
assert!(verify(b"test", &sig, kp.public_key()));
}
#[test]
fn keypair_debug_redacts_secret() {
let kp = KeyPair::generate();
let dbg = format!("{kp:?}");
assert!(dbg.contains("***"));
assert!(dbg.contains("KeyPair"));
}
#[test]
fn keypair_deterministic_from_same_bytes() {
let (_, sk) = generate_keypair();
let bytes = *sk.as_bytes();
let kp1 = KeyPair::from_secret_bytes(bytes).expect("ok");
let kp2 = KeyPair::from_secret_bytes(bytes).expect("ok");
assert_eq!(kp1.public, kp2.public);
}
#[test]
fn signature_deterministic() {
let (_, sk) = generate_keypair();
let msg = b"determinism test";
let sig1 = sign(msg, &sk);
let sig2 = sign(msg, &sk);
assert_eq!(sig1, sig2);
}
#[test]
fn empty_message_sign_verify() {
let (pk, sk) = generate_keypair();
let sig = sign(b"", &sk);
assert!(verify(b"", &sig, &pk));
}
#[test]
fn large_message_sign_verify() {
let (pk, sk) = generate_keypair();
let msg = vec![0xab_u8; 10_000];
let sig = sign(&msg, &sk);
assert!(verify(&msg, &sig, &pk));
}
#[test]
fn pq_generate_keypair_produces_valid_sizes() {
let (pk, sk) = generate_pq_keypair();
assert_eq!(
pk.as_bytes().len(),
1952,
"PQ public key should be 1952 bytes"
);
assert_eq!(
sk.as_bytes().len(),
32,
"PQ secret key (seed) should be 32 bytes"
);
}
#[test]
fn generate_pq_keypair_zeroizes_stack_seed_buffer() {
let source = include_str!("crypto.rs");
let Some(production) = source.split("#[cfg(test)]").next() else {
panic!("production source section exists");
};
let Some(start) = production.find("pub fn generate_pq_keypair()") else {
panic!("generate_pq_keypair source exists");
};
let Some(end) = production[start..].find("/// Sign `message`") else {
panic!("sign_pq docs follow generate_pq_keypair");
};
let generate_pq_keypair_source = &production[start..start + end];
assert!(
generate_pq_keypair_source.contains("Zeroizing::new([0u8; 32])"),
"ML-DSA seed scratch buffer must be wrapped in Zeroizing"
);
assert!(
!generate_pq_keypair_source.contains("let mut seed_bytes = [0u8; 32];"),
"plain stack seed buffer can leave ML-DSA seed bytes behind after key generation"
);
}
#[test]
fn pq_sign_verify_roundtrip() {
let (pk, sk) = generate_pq_keypair();
let msg = b"hello post-quantum exochain";
let sig = sign_pq(msg, &sk).expect("sign_pq should succeed");
assert!(
verify_pq(msg, &sig, &pk),
"verify_pq should accept a valid PostQuantum signature"
);
}
#[test]
fn pq_verify_fails_wrong_message() {
let (pk, sk) = generate_pq_keypair();
let sig = sign_pq(b"original", &sk).expect("sign_pq");
assert!(!verify_pq(b"tampered", &sig, &pk));
}
#[test]
fn pq_verify_fails_wrong_key() {
let (_pk1, sk1) = generate_pq_keypair();
let (pk2, _sk2) = generate_pq_keypair();
let sig = sign_pq(b"msg", &sk1).expect("sign_pq");
assert!(!verify_pq(b"msg", &sig, &pk2));
}
#[test]
fn pq_verify_fails_corrupt_signature() {
let (pk, sk) = generate_pq_keypair();
let sig = sign_pq(b"msg", &sk).expect("sign_pq");
let corrupted = match sig {
Signature::PostQuantum(mut b) => {
b[0] ^= 0xff;
Signature::PostQuantum(b)
}
_ => panic!("expected PostQuantum"),
};
assert!(!verify_pq(b"msg", &corrupted, &pk));
}
#[test]
fn pq_verify_rejects_wrong_variant() {
let (pk, _) = generate_pq_keypair();
assert!(!verify_pq(b"msg", &Signature::Empty, &pk));
let (_, classical_sk) = generate_keypair();
let ed_sig = sign(b"msg", &classical_sk);
assert!(!verify_pq(b"msg", &ed_sig, &pk));
}
#[test]
fn pq_signature_has_correct_byte_length() {
let (_, sk) = generate_pq_keypair();
let sig = sign_pq(b"msg", &sk).expect("sign_pq");
let Signature::PostQuantum(bytes) = sig else {
panic!("expected PostQuantum variant");
};
assert_eq!(
bytes.len(),
3309,
"ML-DSA-65 signature should be 3309 bytes"
);
}
#[test]
fn pq_sign_is_deterministic() {
let (_, sk) = generate_pq_keypair();
let msg = b"determinism";
let sig1 = sign_pq(msg, &sk).expect("sign_pq");
let sig2 = sign_pq(msg, &sk).expect("sign_pq");
assert_eq!(
sig1, sig2,
"ML-DSA-65 deterministic signing must be reproducible"
);
}
#[test]
fn pq_keypair_struct_roundtrip() {
let kp = PqKeyPair::generate();
let msg = b"pq keypair test";
let sig = kp.sign(msg).expect("PqKeyPair::sign");
assert!(kp.verify(msg, &sig));
}
#[test]
fn pq_keypair_debug_redacts_secret() {
let kp = PqKeyPair::generate();
let dbg = format!("{kp:?}");
assert!(dbg.contains("***"));
assert!(dbg.contains("PqKeyPair"));
}
#[test]
fn pq_invalid_sk_bytes_returns_error() {
let bad_sk = PqSecretKey::from_bytes(vec![0u8; 8]); let result = sign_pq(b"msg", &bad_sk);
assert!(
result.is_err(),
"sign_pq with wrong-length seed should fail"
);
}
#[test]
fn hybrid_sign_verify_roundtrip() {
let (classical_pk, classical_sk) = generate_keypair();
let (pq_pk, pq_sk) = generate_pq_keypair();
let msg = b"hybrid dual-sign";
let sig = sign_hybrid(msg, &classical_sk, &pq_sk).expect("sign_hybrid");
assert!(
verify_hybrid(msg, &sig, &classical_pk, &pq_pk),
"verify_hybrid should accept a valid Hybrid signature"
);
}
#[test]
fn hybrid_verification_docs_do_not_overstate_component_timing_privacy() {
let source = include_str!("crypto.rs");
let Some(production) = source.split("#[cfg(test)]").next() else {
panic!("production source section exists");
};
assert!(
!production.contains("does not reveal which component failed"),
"hybrid verifier docs must not promise component-level timing indistinguishability"
);
assert!(
production.contains("component-level timing remains governed by the verifier crates"),
"hybrid verifier docs must state the remaining component-level timing boundary"
);
}
#[test]
fn hybrid_verify_fails_wrong_message() {
let (classical_pk, classical_sk) = generate_keypair();
let (pq_pk, pq_sk) = generate_pq_keypair();
let sig = sign_hybrid(b"original", &classical_sk, &pq_sk).expect("sign_hybrid");
assert!(!verify_hybrid(b"tampered", &sig, &classical_pk, &pq_pk));
}
#[test]
fn hybrid_verify_fails_wrong_classical_key() {
let (_classical_pk1, classical_sk1) = generate_keypair();
let (classical_pk2, _classical_sk2) = generate_keypair();
let (pq_pk, pq_sk) = generate_pq_keypair();
let sig = sign_hybrid(b"msg", &classical_sk1, &pq_sk).expect("sign_hybrid");
assert!(!verify_hybrid(b"msg", &sig, &classical_pk2, &pq_pk));
}
#[test]
fn hybrid_verify_fails_wrong_pq_key() {
let (classical_pk, classical_sk) = generate_keypair();
let (_pq_pk1, pq_sk1) = generate_pq_keypair();
let (pq_pk2, _pq_sk2) = generate_pq_keypair();
let sig = sign_hybrid(b"msg", &classical_sk, &pq_sk1).expect("sign_hybrid");
assert!(!verify_hybrid(b"msg", &sig, &classical_pk, &pq_pk2));
}
#[test]
fn hybrid_verify_fails_stripped_pq_component() {
let (classical_pk, classical_sk) = generate_keypair();
let (pq_pk, pq_sk) = generate_pq_keypair();
let sig = sign_hybrid(b"msg", &classical_sk, &pq_sk).expect("sign_hybrid");
let tampered = match sig {
Signature::Hybrid { classical, mut pq } => {
pq[0] ^= 0xff;
Signature::Hybrid { classical, pq }
}
_ => panic!("expected Hybrid"),
};
assert!(
!verify_hybrid(b"msg", &tampered, &classical_pk, &pq_pk),
"tampered PQ component must cause rejection (ExistentialSafeguard)"
);
}
#[test]
fn hybrid_verify_fails_stripped_classical_component() {
let (classical_pk, classical_sk) = generate_keypair();
let (pq_pk, pq_sk) = generate_pq_keypair();
let sig = sign_hybrid(b"msg", &classical_sk, &pq_sk).expect("sign_hybrid");
let tampered = match sig {
Signature::Hybrid { mut classical, pq } => {
classical[0] ^= 0xff;
Signature::Hybrid { classical, pq }
}
_ => panic!("expected Hybrid"),
};
assert!(
!verify_hybrid(b"msg", &tampered, &classical_pk, &pq_pk),
"tampered Ed25519 component must cause rejection (DualControl)"
);
}
#[test]
fn hybrid_verify_rejects_wrong_variant() {
let (classical_pk, _) = generate_keypair();
let (pq_pk, _) = generate_pq_keypair();
assert!(!verify_hybrid(
b"msg",
&Signature::Empty,
&classical_pk,
&pq_pk
));
assert!(!verify_hybrid(
b"msg",
&Signature::PostQuantum(vec![0u8; 32]),
&classical_pk,
&pq_pk
));
}
}