extern crate alloc;
use alloc::{format, vec::Vec};
use ed25519_dalek::{
Signature as Ed25519Signature, Signer, SigningKey as Ed25519SigningKey,
Verifier, VerifyingKey as Ed25519VerifyingKey,
};
use rand_core::{CryptoRng, RngCore};
use serde::{Deserialize, Serialize};
use zeroize::{Zeroize, ZeroizeOnDrop};
use crate::ctx::check_ctx;
use crate::error::{SigError, SigResult};
use crate::fips204::MlDsa65Keypair;
use crate::types::{
deserialize_bytes_base64url, serialize_bytes_base64url, SigAlgorithm, SigPublicKey, Signature,
};
pub const HYBRID_ALGORITHM: &str = "ed25519+ml_dsa_65";
pub const HYBRID_CTX_FRAME_DOMAIN: u8 = 0x00;
fn frame_classical_ctx(ctx: &[u8], message: &[u8]) -> Vec<u8> {
let mut framed = Vec::with_capacity(2 + ctx.len() + message.len());
framed.push(HYBRID_CTX_FRAME_DOMAIN);
framed.push(ctx.len() as u8);
framed.extend_from_slice(ctx);
framed.extend_from_slice(message);
framed
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HybridPublicKey {
#[serde(
serialize_with = "serialize_bytes_base64url",
deserialize_with = "deserialize_bytes_base64url"
)]
pub classical: Vec<u8>,
#[serde(
serialize_with = "serialize_bytes_base64url",
deserialize_with = "deserialize_bytes_base64url"
)]
pub pqc: Vec<u8>,
}
impl HybridPublicKey {
pub fn to_json(&self) -> SigResult<alloc::string::String> {
serde_json::to_string(self).map_err(|e| SigError::Serialization(format!("{}", e)))
}
pub fn from_json(json: &str) -> SigResult<Self> {
serde_json::from_str(json).map_err(|e| SigError::Serialization(format!("{}", e)))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HybridSignature {
#[serde(
serialize_with = "serialize_bytes_base64url",
deserialize_with = "deserialize_bytes_base64url"
)]
pub classical: Vec<u8>,
#[serde(
serialize_with = "serialize_bytes_base64url",
deserialize_with = "deserialize_bytes_base64url"
)]
pub pqc: Vec<u8>,
}
impl HybridSignature {
pub fn to_json(&self) -> SigResult<alloc::string::String> {
serde_json::to_string(self).map_err(|e| SigError::Serialization(format!("{}", e)))
}
pub fn from_json(json: &str) -> SigResult<Self> {
serde_json::from_str(json).map_err(|e| SigError::Serialization(format!("{}", e)))
}
}
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct HybridSecretKey {
pub ed25519_seed: [u8; 32],
pub ml_dsa_65: Vec<u8>,
}
impl core::fmt::Debug for HybridSecretKey {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"HybridSecretKey {{ ed25519_seed: [REDACTED 32 bytes], ml_dsa_65: [REDACTED {} bytes] }}",
self.ml_dsa_65.len()
)
}
}
pub struct HybridSigner {
ed25519_key: Ed25519SigningKey,
ml_dsa_key: MlDsa65Keypair,
}
impl HybridSigner {
pub fn generate<R: CryptoRng + RngCore>(rng: &mut R) -> SigResult<Self> {
let ed25519_key = Ed25519SigningKey::generate(rng);
let ml_dsa_key = MlDsa65Keypair::generate(rng)?;
Ok(Self { ed25519_key, ml_dsa_key })
}
pub fn from_ed25519_secret<R: CryptoRng + RngCore>(
rng: &mut R,
ed25519_secret: &[u8; 32],
) -> SigResult<Self> {
let ed25519_key = Ed25519SigningKey::from_bytes(ed25519_secret);
let ml_dsa_key = MlDsa65Keypair::generate(rng)?;
Ok(Self { ed25519_key, ml_dsa_key })
}
pub fn from_secret_key_bytes(
ed25519_secret: &[u8; 32],
ml_dsa_65_seed: &[u8],
) -> SigResult<Self> {
let ed25519_key = Ed25519SigningKey::from_bytes(ed25519_secret);
let ml_dsa_key = MlDsa65Keypair::from_secret_key_bytes(ml_dsa_65_seed)?;
Ok(Self { ed25519_key, ml_dsa_key })
}
pub fn secret_key(&self) -> HybridSecretKey {
HybridSecretKey {
ed25519_seed: self.ed25519_key.to_bytes(),
ml_dsa_65: self.ml_dsa_key.secret_key().bytes.clone(),
}
}
pub fn public_key(&self) -> HybridPublicKey {
HybridPublicKey {
classical: self.ed25519_key.verifying_key().to_bytes().to_vec(),
pqc: self.ml_dsa_key.public_key().bytes,
}
}
pub fn sign(&self, message: &[u8]) -> SigResult<HybridSignature> {
let classical = self.ed25519_key.sign(message).to_bytes().to_vec();
let pqc = self.ml_dsa_key.sign_deterministic(message)?.bytes;
Ok(HybridSignature { classical, pqc })
}
pub fn verify(message: &[u8], signature: &HybridSignature, public_key: &HybridPublicKey) -> SigResult<()> {
let vk_bytes: [u8; 32] = public_key.classical.as_slice().try_into().map_err(|_| {
SigError::InvalidPublicKey(format!(
"Ed25519 public key must be 32 bytes, got {}",
public_key.classical.len()
))
})?;
let ed25519_vk = Ed25519VerifyingKey::from_bytes(&vk_bytes)
.map_err(|e| SigError::InvalidPublicKey(format!("{}", e)))?;
let sig_bytes: [u8; 64] = signature.classical.as_slice().try_into().map_err(|_| {
SigError::InvalidSignature(format!(
"Ed25519 signature must be 64 bytes, got {}",
signature.classical.len()
))
})?;
let ed25519_sig = Ed25519Signature::from_bytes(&sig_bytes);
ed25519_vk
.verify(message, &ed25519_sig)
.map_err(|_| SigError::HybridClassicalFailed)?;
let pqc_pk = SigPublicKey::new(SigAlgorithm::MlDsa65, public_key.pqc.clone());
let pqc_sig = Signature::new(SigAlgorithm::MlDsa65, signature.pqc.clone());
MlDsa65Keypair::verify(&pqc_pk, message, &pqc_sig).map_err(|_| SigError::HybridPqcFailed)?;
Ok(())
}
pub fn sign_ctx<R: CryptoRng + RngCore>(
&self,
rng: &mut R,
ctx: &[u8],
message: &[u8],
) -> SigResult<HybridSignature> {
let _ = rng;
check_ctx(ctx)?;
let framed = frame_classical_ctx(ctx, message);
let classical = self.ed25519_key.sign(&framed).to_bytes().to_vec();
let pqc = self.ml_dsa_key.sign_ctx_deterministic(ctx, message)?.bytes;
Ok(HybridSignature { classical, pqc })
}
pub fn verify_ctx(
public_key: &HybridPublicKey,
ctx: &[u8],
message: &[u8],
signature: &HybridSignature,
) -> SigResult<()> {
check_ctx(ctx)?;
let vk_bytes: [u8; 32] = public_key.classical.as_slice().try_into().map_err(|_| {
SigError::InvalidPublicKey(format!(
"Ed25519 public key must be 32 bytes, got {}",
public_key.classical.len()
))
})?;
let ed25519_vk = Ed25519VerifyingKey::from_bytes(&vk_bytes)
.map_err(|e| SigError::InvalidPublicKey(format!("{}", e)))?;
let sig_bytes: [u8; 64] = signature.classical.as_slice().try_into().map_err(|_| {
SigError::InvalidSignature(format!(
"Ed25519 signature must be 64 bytes, got {}",
signature.classical.len()
))
})?;
let ed25519_sig = Ed25519Signature::from_bytes(&sig_bytes);
let framed = frame_classical_ctx(ctx, message);
ed25519_vk
.verify(&framed, &ed25519_sig)
.map_err(|_| SigError::HybridClassicalFailed)?;
let pqc_pk = SigPublicKey::new(SigAlgorithm::MlDsa65, public_key.pqc.clone());
let pqc_sig = Signature::new(SigAlgorithm::MlDsa65, signature.pqc.clone());
MlDsa65Keypair::verify_ctx(&pqc_pk, ctx, message, &pqc_sig)
.map_err(|_| SigError::HybridPqcFailed)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
use rand::rngs::OsRng;
#[test]
fn both_pass() {
let signer = HybridSigner::generate(&mut OsRng).unwrap();
let pk = signer.public_key();
let sig = signer.sign(b"hello world").unwrap();
HybridSigner::verify(b"hello world", &sig, &pk).expect("hybrid verify should pass");
}
#[test]
fn classical_only_fails() {
let signer = HybridSigner::generate(&mut OsRng).unwrap();
let pk = signer.public_key();
let mut sig = signer.sign(b"hello world").unwrap();
sig.classical[0] ^= 0xFF; let err = HybridSigner::verify(b"hello world", &sig, &pk).unwrap_err();
assert_eq!(err, SigError::HybridClassicalFailed);
}
#[test]
fn pqc_only_fails() {
let signer = HybridSigner::generate(&mut OsRng).unwrap();
let pk = signer.public_key();
let mut sig = signer.sign(b"hello world").unwrap();
sig.pqc[0] ^= 0xFF; let err = HybridSigner::verify(b"hello world", &sig, &pk).unwrap_err();
assert_eq!(err, SigError::HybridPqcFailed);
}
#[test]
fn both_fail() {
let signer = HybridSigner::generate(&mut OsRng).unwrap();
let pk = signer.public_key();
let mut sig = signer.sign(b"hello world").unwrap();
sig.classical[0] ^= 0xFF;
sig.pqc[0] ^= 0xFF;
let err = HybridSigner::verify(b"hello world", &sig, &pk).unwrap_err();
assert!(err == SigError::HybridClassicalFailed || err == SigError::HybridPqcFailed);
}
#[test]
fn wrong_message_fails() {
let signer = HybridSigner::generate(&mut OsRng).unwrap();
let pk = signer.public_key();
let sig = signer.sign(b"correct message").unwrap();
assert!(HybridSigner::verify(b"wrong message", &sig, &pk).is_err());
}
#[test]
fn json_round_trip() {
let signer = HybridSigner::generate(&mut OsRng).unwrap();
let pk = signer.public_key();
let sig = signer.sign(b"hello world").unwrap();
let pk2 = HybridPublicKey::from_json(&pk.to_json().unwrap()).unwrap();
let sig2 = HybridSignature::from_json(&sig.to_json().unwrap()).unwrap();
assert_eq!(pk, pk2);
assert_eq!(sig, sig2);
HybridSigner::verify(b"hello world", &sig2, &pk2).expect("round-tripped hybrid verify should pass");
}
#[test]
fn from_ed25519_secret_preserves_classical_key() {
let mut seed = [0u8; 32];
OsRng.fill_bytes(&mut seed);
let legacy_vk = Ed25519SigningKey::from_bytes(&seed).verifying_key();
let signer = HybridSigner::from_ed25519_secret(&mut OsRng, &seed).unwrap();
let pk = signer.public_key();
assert_eq!(pk.classical, legacy_vk.to_bytes().to_vec());
}
#[test]
fn from_ed25519_secret_signs_and_verifies() {
let mut seed = [0u8; 32];
OsRng.fill_bytes(&mut seed);
let signer = HybridSigner::from_ed25519_secret(&mut OsRng, &seed).unwrap();
let pk = signer.public_key();
let sig = signer.sign(b"bridged agent message").unwrap();
HybridSigner::verify(b"bridged agent message", &sig, &pk)
.expect("signature from a bridged Ed25519 key must verify");
}
#[test]
fn secret_key_round_trip_reproduces_public_key_and_signatures() {
let signer = HybridSigner::generate(&mut OsRng).unwrap();
let pk = signer.public_key();
let sk = signer.secret_key();
let restored = HybridSigner::from_secret_key_bytes(&sk.ed25519_seed, &sk.ml_dsa_65).unwrap();
let restored_pk = restored.public_key();
assert_eq!(pk, restored_pk, "restored signer must have an identical public key");
let sig = signer.sign(b"persisted identity").unwrap();
HybridSigner::verify(b"persisted identity", &sig, &restored_pk)
.expect("original signature must verify under the restored public key");
let sig2 = restored.sign(b"persisted identity").unwrap();
HybridSigner::verify(b"persisted identity", &sig2, &pk)
.expect("restored signature must verify under the original public key");
}
#[test]
fn secret_key_debug_is_redacted() {
let signer = HybridSigner::generate(&mut OsRng).unwrap();
let sk = signer.secret_key();
let debug = format!("{:?}", sk);
assert!(debug.contains("REDACTED"));
assert!(!debug.contains(&hex_encode(&sk.ed25519_seed)));
}
fn hex_encode(bytes: &[u8]) -> alloc::string::String {
use core::fmt::Write;
let mut s = alloc::string::String::with_capacity(bytes.len() * 2);
for b in bytes {
let _ = write!(s, "{:02x}", b);
}
s
}
#[test]
fn sign_ctx_round_trip() {
let signer = HybridSigner::generate(&mut OsRng).unwrap();
let pk = signer.public_key();
let sig = signer.sign_ctx(&mut OsRng, b"8gentz-agent-v1", b"hello").unwrap();
HybridSigner::verify_ctx(&pk, b"8gentz-agent-v1", b"hello", &sig)
.expect("sign_ctx/verify_ctx round-trip should pass");
}
#[test]
fn sign_ctx_wrong_ctx_fails() {
let signer = HybridSigner::generate(&mut OsRng).unwrap();
let pk = signer.public_key();
let sig = signer.sign_ctx(&mut OsRng, b"8gentz-agent-v1", b"hello").unwrap();
let err = HybridSigner::verify_ctx(&pk, b"8gentz-fabric-v1", b"hello", &sig).unwrap_err();
assert!(err == SigError::HybridClassicalFailed || err == SigError::HybridPqcFailed);
}
#[test]
fn sign_ctx_mode_separation_from_plain_sign() {
let signer = HybridSigner::generate(&mut OsRng).unwrap();
let pk = signer.public_key();
let plain_sig = signer.sign(b"hello").unwrap();
assert!(
HybridSigner::verify_ctx(&pk, &[], b"hello", &plain_sig).is_err(),
"plain sign() signature must not verify via verify_ctx(pk, &[], m, sig)"
);
let ctx_sig = signer.sign_ctx(&mut OsRng, &[], b"hello").unwrap();
assert!(
HybridSigner::verify(b"hello", &ctx_sig, &pk).is_err(),
"sign_ctx(&[]) signature must not verify via plain verify()"
);
}
#[test]
fn sign_ctx_too_long_context_rejected() {
let signer = HybridSigner::generate(&mut OsRng).unwrap();
let pk = signer.public_key();
let long_ctx = vec![0u8; 256];
let err = signer.sign_ctx(&mut OsRng, &long_ctx, b"hello").unwrap_err();
assert_eq!(err, SigError::ContextTooLong { len: 256 });
let sig = signer.sign_ctx(&mut OsRng, b"ok-ctx", b"hello").unwrap();
let err = HybridSigner::verify_ctx(&pk, &long_ctx, b"hello", &sig).unwrap_err();
assert_eq!(err, SigError::ContextTooLong { len: 256 });
}
#[test]
fn sign_ctx_classical_tamper_fails_classical() {
let signer = HybridSigner::generate(&mut OsRng).unwrap();
let pk = signer.public_key();
let mut sig = signer.sign_ctx(&mut OsRng, b"8gentz-agent-v1", b"hello").unwrap();
sig.classical[0] ^= 0xFF;
let err = HybridSigner::verify_ctx(&pk, b"8gentz-agent-v1", b"hello", &sig).unwrap_err();
assert_eq!(err, SigError::HybridClassicalFailed);
}
#[test]
fn sign_ctx_pqc_tamper_fails_pqc() {
let signer = HybridSigner::generate(&mut OsRng).unwrap();
let pk = signer.public_key();
let mut sig = signer.sign_ctx(&mut OsRng, b"8gentz-agent-v1", b"hello").unwrap();
sig.pqc[0] ^= 0xFF;
let err = HybridSigner::verify_ctx(&pk, b"8gentz-agent-v1", b"hello", &sig).unwrap_err();
assert_eq!(err, SigError::HybridPqcFailed);
}
}