use super::provenance::Signer;
use axon_csys::ed25519::{
keypair_from_seed, Ed25519Keypair, ED25519_PUBLICKEY_BYTES, ED25519_SIGNATURE_BYTES,
};
use axon_csys::mldsa::{MLDSA65_PUBLICKEY_BYTES, MLDSA65_SECRETKEY_BYTES, MLDSA65_SIGNATURE_BYTES};
pub const HYBRID_SIGNATURE_BYTES: usize = ED25519_SIGNATURE_BYTES + MLDSA65_SIGNATURE_BYTES;
#[derive(Clone)]
pub struct HybridPublicKeys {
pub ed25519: [u8; ED25519_PUBLICKEY_BYTES],
pub ml_dsa_65: [u8; MLDSA65_PUBLICKEY_BYTES],
}
pub struct HybridSigner {
ed: Ed25519Keypair,
mldsa_public: [u8; MLDSA65_PUBLICKEY_BYTES],
mldsa_secret: [u8; MLDSA65_SECRETKEY_BYTES],
}
impl HybridSigner {
pub fn generate() -> Option<Self> {
let mut seed = axon_csys::ed25519::generate_seed()?;
let ed = keypair_from_seed(&seed);
for b in seed.iter_mut() {
unsafe { core::ptr::write_volatile(b, 0) };
}
let (mldsa_public, mldsa_secret) = axon_csys::mldsa::keypair()?;
Some(HybridSigner { ed, mldsa_public, mldsa_secret })
}
pub fn public_keys(&self) -> HybridPublicKeys {
HybridPublicKeys {
ed25519: self.ed.public,
ml_dsa_65: self.mldsa_public,
}
}
pub fn verify_hybrid(message: &[u8], signature: &[u8], keys: &HybridPublicKeys) -> bool {
if signature.len() != HYBRID_SIGNATURE_BYTES {
return false;
}
let digest = axon_csys::crypto::sha256(message);
let (ed_half, mldsa_half) = signature.split_at(ED25519_SIGNATURE_BYTES);
axon_csys::ed25519::verify(ed_half, &digest, &keys.ed25519)
&& axon_csys::mldsa::verify(mldsa_half, &digest, &keys.ml_dsa_65)
}
}
impl Signer for HybridSigner {
fn algorithm(&self) -> &str {
"Ed25519+ML-DSA-65"
}
fn sign(&self, message: &[u8]) -> Vec<u8> {
let digest = axon_csys::crypto::sha256(message);
let ed_sig = axon_csys::ed25519::sign(&digest, &self.ed);
let mldsa_sig = axon_csys::mldsa::sign(&digest, &self.mldsa_secret)
.expect("OS RNG failed during ML-DSA signing — refusing to emit a partial bundle");
let mut bundle = Vec::with_capacity(HYBRID_SIGNATURE_BYTES);
bundle.extend_from_slice(&ed_sig);
bundle.extend_from_slice(&mldsa_sig);
bundle
}
fn verify(&self, message: &[u8], signature: &[u8]) -> bool {
Self::verify_hybrid(message, signature, &self.public_keys())
}
}
impl Drop for HybridSigner {
fn drop(&mut self) {
for b in self.ed.expanded.iter_mut() {
unsafe { core::ptr::write_volatile(b, 0) };
}
for b in self.mldsa_secret.iter_mut() {
unsafe { core::ptr::write_volatile(b, 0) };
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip_via_the_signer_trait() {
let signer = HybridSigner::generate().expect("OS RNG available");
let msg = b"evidencia bajo firma hibrida";
let sig = signer.sign(msg);
assert_eq!(sig.len(), HYBRID_SIGNATURE_BYTES, "64 + 3309 = 3373, fixed");
assert!(signer.verify(msg, &sig));
assert!(!signer.verify(b"otra evidencia", &sig));
}
#[test]
fn the_bundle_is_the_papers_formula() {
let signer = HybridSigner::generate().expect("rng");
let keys = signer.public_keys();
let msg = b"estructura, no solo roundtrip";
let sig = signer.sign(msg);
let digest = axon_csys::crypto::sha256(msg);
let (ed_half, mldsa_half) = sig.split_at(ED25519_SIGNATURE_BYTES);
assert!(
axon_csys::ed25519::verify(ed_half, &digest, &keys.ed25519),
"the first 64 bytes must be Ed25519 over SHA-256(message)"
);
assert!(
axon_csys::mldsa::verify(mldsa_half, &digest, &keys.ml_dsa_65),
"the remaining 3309 bytes must be ML-DSA-65 over the same digest"
);
}
#[test]
fn hybrid_means_both_not_either() {
let signer = HybridSigner::generate().expect("rng");
let msg = b"ambas mitades o ninguna";
let sig = signer.sign(msg);
let mut ed_corrupted = sig.clone();
ed_corrupted[0] ^= 1; assert!(
!signer.verify(msg, &ed_corrupted),
"a valid ML-DSA half must NOT carry a corrupted Ed25519 half"
);
let mut mldsa_corrupted = sig.clone();
mldsa_corrupted[ED25519_SIGNATURE_BYTES + 10] ^= 1; assert!(
!signer.verify(msg, &mldsa_corrupted),
"a valid Ed25519 half must NOT carry a corrupted ML-DSA half"
);
}
#[test]
fn wrong_lengths_are_refused_before_any_cryptography() {
let signer = HybridSigner::generate().expect("rng");
let msg = b"largo exacto o nada";
let sig = signer.sign(msg);
assert!(!signer.verify(msg, &sig[..HYBRID_SIGNATURE_BYTES - 1]));
assert!(!signer.verify(msg, &[]));
assert!(!signer.verify(msg, &sig[..ED25519_SIGNATURE_BYTES]));
}
#[test]
fn distinct_signers_reject_each_others_bundles() {
let a = HybridSigner::generate().expect("rng");
let b = HybridSigner::generate().expect("rng");
let msg = b"cada llave firma lo suyo";
let sig_a = a.sign(msg);
assert!(a.verify(msg, &sig_a));
assert!(!b.verify(msg, &sig_a));
}
#[test]
fn the_algorithm_label_is_stable() {
let signer = HybridSigner::generate().expect("rng");
assert_eq!(signer.algorithm(), "Ed25519+ML-DSA-65");
}
}