use crate::constants;
use crate::error::{Error, Result};
use crate::primitives::{ed25519, mldsa, sha3_256, xwing};
use subtle::Choice;
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
#[derive(Clone, Eq)]
pub struct IdentityPublicKey(pub(crate) Vec<u8>);
impl PartialEq for IdentityPublicKey {
fn eq(&self, other: &Self) -> bool {
use subtle::ConstantTimeEq;
self.0.ct_eq(&other.0).into()
}
}
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct IdentitySecretKey(pub(crate) Vec<u8>);
#[derive(Clone, PartialEq, Eq)]
pub struct HybridSignature(pub(crate) Vec<u8>);
impl IdentityPublicKey {
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
pub fn from_bytes(bytes: Vec<u8>) -> Result<Self> {
if bytes.len() != constants::LO_PUBLIC_KEY_SIZE {
return Err(Error::InvalidLength {
expected: constants::LO_PUBLIC_KEY_SIZE,
got: bytes.len(),
});
}
Ok(Self(bytes))
}
pub fn x25519_pk(&self) -> &[u8] {
&self.0[..32]
}
pub fn mlkem_pk(&self) -> &[u8] {
&self.0[32..constants::XWING_PUBLIC_KEY_SIZE]
}
pub fn ed25519_pk(&self) -> &[u8] {
&self.0[constants::XWING_PUBLIC_KEY_SIZE..constants::XWING_PUBLIC_KEY_SIZE + 32]
}
pub fn mldsa_pk(&self) -> &[u8] {
&self.0[constants::XWING_PUBLIC_KEY_SIZE + 32..]
}
pub fn xwing_pk(&self) -> &[u8] {
&self.0[..constants::XWING_PUBLIC_KEY_SIZE]
}
pub fn fingerprint_hex(&self) -> String {
sha3_256::fingerprint_hex(&self.0)
}
pub fn fingerprint_raw(&self) -> [u8; 32] {
sha3_256::hash(&self.0)
}
}
impl IdentitySecretKey {
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
pub fn from_bytes(bytes: Vec<u8>) -> Result<Self> {
let mut bytes = Zeroizing::new(bytes);
if bytes.len() != constants::LO_SECRET_KEY_SIZE {
return Err(Error::InvalidLength {
expected: constants::LO_SECRET_KEY_SIZE,
got: bytes.len(),
});
}
Ok(Self(std::mem::take(&mut *bytes)))
}
pub fn x25519_sk(&self) -> &[u8] {
&self.0[..32]
}
pub fn xwing_sk(&self) -> &[u8] {
&self.0[..constants::XWING_SECRET_KEY_SIZE]
}
pub fn ed25519_sk(&self) -> &[u8] {
&self.0[constants::XWING_SECRET_KEY_SIZE..constants::XWING_SECRET_KEY_SIZE + 32]
}
pub fn mldsa_sk(&self) -> &[u8] {
&self.0[constants::XWING_SECRET_KEY_SIZE + 32..]
}
}
impl HybridSignature {
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
pub fn from_bytes(bytes: Vec<u8>) -> Result<Self> {
if bytes.len() != constants::HYBRID_SIGNATURE_SIZE {
return Err(Error::InvalidLength {
expected: constants::HYBRID_SIGNATURE_SIZE,
got: bytes.len(),
});
}
Ok(Self(bytes))
}
pub fn ed25519_sig(&self) -> &[u8] {
&self.0[..constants::ED25519_SIGNATURE_SIZE]
}
pub fn mldsa_sig(&self) -> &[u8] {
&self.0[constants::ED25519_SIGNATURE_SIZE..]
}
}
pub struct GeneratedIdentity {
pub public_key: IdentityPublicKey,
pub secret_key: IdentitySecretKey,
pub fingerprint_hex: String,
}
#[must_use = "identity key material must not be discarded"]
pub fn generate_identity() -> Result<GeneratedIdentity> {
let (xwing_pk, xwing_sk) = xwing::keygen()?;
assert_eq!(
xwing_sk.as_bytes().len(),
constants::XWING_SECRET_KEY_SIZE,
"X-Wing secret key size mismatch — update XWING_SECRET_KEY_SIZE in constants.rs"
);
let (ed_vk, ed_sk) = ed25519::keygen();
assert_eq!(ed_vk.as_bytes().len(), 32, "Ed25519 PK must be 32 bytes");
let (mldsa_pk, mldsa_sk) = mldsa::keygen()?;
assert_eq!(
mldsa_sk.as_bytes().len(),
32,
"ML-DSA-65 seed size mismatch — expected 32 bytes"
);
let mut pk = Vec::with_capacity(xwing_pk.as_bytes().len() + 32 + mldsa_pk.as_bytes().len());
pk.extend_from_slice(xwing_pk.as_bytes());
pk.extend_from_slice(ed_vk.as_bytes());
pk.extend_from_slice(mldsa_pk.as_bytes());
let mut sk = Zeroizing::new(Vec::with_capacity(
xwing_sk.as_bytes().len() + 32 + mldsa_sk.as_bytes().len(),
));
sk.extend_from_slice(xwing_sk.as_bytes());
sk.extend_from_slice(ed_sk.as_bytes());
sk.extend_from_slice(mldsa_sk.as_bytes());
let fingerprint = sha3_256::fingerprint_hex(&pk);
Ok(GeneratedIdentity {
public_key: IdentityPublicKey(pk),
secret_key: IdentitySecretKey(std::mem::take(&mut *sk)),
fingerprint_hex: fingerprint,
})
}
pub fn hybrid_sign(sk: &IdentitySecretKey, message: &[u8]) -> Result<HybridSignature> {
let ed_sk_bytes: &[u8; 32] = sk.ed25519_sk().try_into().map_err(|_| Error::Internal)?;
let signing_key = ed25519_dalek::SigningKey::from_bytes(ed_sk_bytes);
let sig_classical = ed25519::sign(&signing_key, message);
let mut sk_bytes = Zeroizing::new(sk.mldsa_sk().to_vec());
let mldsa_sk = mldsa::SecretKey::from_bytes(std::mem::take(&mut *sk_bytes))?;
let sig_pqc = mldsa::sign(&mldsa_sk, message)?;
debug_assert_eq!(sig_pqc.as_bytes().len(), constants::MLDSA_SIGNATURE_SIZE);
let mut sig = Vec::with_capacity(constants::ED25519_SIGNATURE_SIZE + sig_pqc.as_bytes().len());
sig.extend_from_slice(&sig_classical);
sig.extend_from_slice(sig_pqc.as_bytes());
Ok(HybridSignature(sig))
}
#[must_use = "signature verification result must be checked"]
pub fn hybrid_verify(pk: &IdentityPublicKey, message: &[u8], sig: &HybridSignature) -> Result<()> {
let sig_classical = sig.ed25519_sig();
let sig_pqc_bytes = sig.mldsa_sig();
let ed_sig: &[u8; 64] = sig_classical.try_into().map_err(|_| Error::Internal)?;
let ed_pk_bytes: &[u8; 32] = pk.ed25519_pk().try_into().map_err(|_| Error::Internal)?;
let r1 = ed25519_dalek::VerifyingKey::from_bytes(ed_pk_bytes)
.map_err(|_| Error::VerificationFailed)
.and_then(|vk| ed25519::verify(&vk, message, ed_sig));
let mldsa_pk = mldsa::PublicKey::from_bytes_unchecked(pk.mldsa_pk().to_vec());
let mldsa_sig = mldsa::Signature::from_bytes_unchecked(sig_pqc_bytes.to_vec());
let r2 = mldsa::verify(&mldsa_pk, message, &mldsa_sig);
let ok1 = Choice::from(u8::from(r1.is_ok()));
let ok2 = Choice::from(u8::from(r2.is_ok()));
if bool::from(ok1 & ok2) {
Ok(())
} else {
Err(Error::VerificationFailed)
}
}
pub fn encapsulate(pk: &IdentityPublicKey) -> Result<(xwing::Ciphertext, xwing::SharedSecret)> {
let xwing_pk = xwing::PublicKey::from_bytes_unchecked(pk.xwing_pk().to_vec());
xwing::encapsulate(&xwing_pk)
}
pub fn decapsulate(sk: &IdentitySecretKey, ct: &xwing::Ciphertext) -> Result<xwing::SharedSecret> {
let xwing_sk = xwing::SecretKey::from_bytes_unchecked(sk.xwing_sk().to_vec());
xwing::decapsulate(&xwing_sk, ct)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Error;
#[test]
fn generate_identity_sizes() {
let GeneratedIdentity {
public_key: pk,
secret_key: sk,
..
} = generate_identity().unwrap();
assert_eq!(pk.as_bytes().len(), constants::LO_PUBLIC_KEY_SIZE);
assert_eq!(sk.as_bytes().len(), constants::LO_SECRET_KEY_SIZE);
}
#[test]
fn fingerprint_format() {
let GeneratedIdentity {
public_key: pk,
fingerprint_hex: fp,
..
} = generate_identity().unwrap();
assert_eq!(fp.len(), 64);
assert!(
fp.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())
);
assert_eq!(pk.fingerprint_hex(), fp);
}
#[test]
fn fingerprint_matches_pk() {
let GeneratedIdentity {
public_key: pk,
fingerprint_hex: fp,
..
} = generate_identity().unwrap();
let expected = sha3_256::fingerprint_hex(pk.as_bytes());
assert_eq!(fp, expected);
}
#[test]
fn pk_component_extraction() {
let GeneratedIdentity { public_key: pk, .. } = generate_identity().unwrap();
let bytes = pk.as_bytes();
assert_eq!(pk.x25519_pk(), &bytes[..32]);
assert_eq!(pk.mlkem_pk(), &bytes[32..constants::XWING_PUBLIC_KEY_SIZE]);
assert_eq!(
pk.ed25519_pk(),
&bytes[constants::XWING_PUBLIC_KEY_SIZE..constants::XWING_PUBLIC_KEY_SIZE + 32]
);
assert_eq!(
pk.mldsa_pk(),
&bytes[constants::XWING_PUBLIC_KEY_SIZE + 32..]
);
assert_eq!(pk.x25519_pk().len(), 32);
assert_eq!(pk.mlkem_pk().len(), 1184);
assert_eq!(pk.ed25519_pk().len(), 32);
assert_eq!(pk.mldsa_pk().len(), 1952);
}
#[test]
fn xwing_pk_accessor() {
let GeneratedIdentity { public_key: pk, .. } = generate_identity().unwrap();
assert_eq!(
pk.xwing_pk(),
&pk.as_bytes()[..constants::XWING_PUBLIC_KEY_SIZE]
);
assert_eq!(pk.xwing_pk().len(), 1216);
}
#[test]
fn fingerprint_raw_accessor() {
let GeneratedIdentity { public_key: pk, .. } = generate_identity().unwrap();
let raw = pk.fingerprint_raw();
assert_eq!(raw.len(), 32);
assert_eq!(raw, sha3_256::hash(pk.as_bytes()));
}
#[test]
fn hybrid_sign_verify_round_trip() {
let GeneratedIdentity {
public_key: pk,
secret_key: sk,
..
} = generate_identity().unwrap();
let sig = hybrid_sign(&sk, b"test message").unwrap();
assert!(hybrid_verify(&pk, b"test message", &sig).is_ok());
}
#[test]
fn hybrid_sign_size() {
let GeneratedIdentity { secret_key: sk, .. } = generate_identity().unwrap();
let sig = hybrid_sign(&sk, b"test").unwrap();
assert_eq!(sig.as_bytes().len(), constants::HYBRID_SIGNATURE_SIZE);
assert_eq!(sig.as_bytes().len(), 3373);
}
#[test]
fn hybrid_verify_wrong_message() {
let GeneratedIdentity {
public_key: pk,
secret_key: sk,
..
} = generate_identity().unwrap();
let sig = hybrid_sign(&sk, b"message one").unwrap();
assert!(matches!(
hybrid_verify(&pk, b"message two", &sig),
Err(Error::VerificationFailed)
));
}
#[test]
fn hybrid_verify_wrong_key() {
let GeneratedIdentity { secret_key: sk, .. } = generate_identity().unwrap();
let GeneratedIdentity {
public_key: pk2, ..
} = generate_identity().unwrap();
let sig = hybrid_sign(&sk, b"test").unwrap();
assert!(matches!(
hybrid_verify(&pk2, b"test", &sig),
Err(Error::VerificationFailed)
));
}
#[test]
fn hybrid_verify_tampered_ed25519() {
let GeneratedIdentity {
public_key: pk,
secret_key: sk,
..
} = generate_identity().unwrap();
let sig = hybrid_sign(&sk, b"test").unwrap();
let mut bad = sig.as_bytes().to_vec();
bad[0] ^= 0xFF; let bad_sig = HybridSignature::from_bytes(bad).unwrap();
assert!(matches!(
hybrid_verify(&pk, b"test", &bad_sig),
Err(Error::VerificationFailed)
));
}
#[test]
fn hybrid_verify_tampered_mldsa() {
let GeneratedIdentity {
public_key: pk,
secret_key: sk,
..
} = generate_identity().unwrap();
let sig = hybrid_sign(&sk, b"test").unwrap();
let mut bad = sig.as_bytes().to_vec();
bad[64] ^= 0xFF; let bad_sig = HybridSignature::from_bytes(bad).unwrap();
assert!(matches!(
hybrid_verify(&pk, b"test", &bad_sig),
Err(Error::VerificationFailed)
));
}
#[test]
fn hybrid_verify_valid_ed25519_invalid_mldsa() {
let GeneratedIdentity {
public_key: pk,
secret_key: sk,
..
} = generate_identity().unwrap();
let sig = hybrid_sign(&sk, b"test").unwrap();
let mut franken = sig.as_bytes()[..64].to_vec();
franken.extend_from_slice(&vec![0u8; constants::MLDSA_SIGNATURE_SIZE]);
let bad_sig = HybridSignature::from_bytes(franken).unwrap();
assert!(matches!(
hybrid_verify(&pk, b"test", &bad_sig),
Err(Error::VerificationFailed)
));
}
#[test]
fn hybrid_verify_invalid_ed25519_valid_mldsa() {
let GeneratedIdentity {
public_key: pk,
secret_key: sk,
..
} = generate_identity().unwrap();
let sig = hybrid_sign(&sk, b"test").unwrap();
let mut franken = vec![0u8; 64];
franken.extend_from_slice(&sig.as_bytes()[64..]);
let bad_sig = HybridSignature::from_bytes(franken).unwrap();
assert!(matches!(
hybrid_verify(&pk, b"test", &bad_sig),
Err(Error::VerificationFailed)
));
}
#[test]
fn encapsulate_decapsulate() {
let GeneratedIdentity {
public_key: pk,
secret_key: sk,
..
} = generate_identity().unwrap();
let (ct, ss_enc) = encapsulate(&pk).unwrap();
let ss_dec = decapsulate(&sk, &ct).unwrap();
assert_eq!(ss_enc.as_bytes(), ss_dec.as_bytes());
}
#[test]
fn identity_public_key_from_bytes_wrong_size() {
assert!(matches!(
IdentityPublicKey::from_bytes(vec![0u8; 100]),
Err(Error::InvalidLength {
expected: 3200,
got: 100
})
));
}
#[test]
fn identity_secret_key_from_bytes_wrong_size() {
assert!(matches!(
IdentitySecretKey::from_bytes(vec![0u8; 100]),
Err(Error::InvalidLength {
expected: 2496,
got: 100
})
));
}
#[test]
fn hybrid_signature_from_bytes_wrong_size() {
assert!(matches!(
HybridSignature::from_bytes(vec![0u8; 100]),
Err(Error::InvalidLength {
expected: 3373,
got: 100
})
));
}
#[test]
fn hybrid_verify_cross_spliced_signatures_rejected() {
let GeneratedIdentity {
public_key: pk,
secret_key: sk,
..
} = generate_identity().unwrap();
let sig_a = hybrid_sign(&sk, b"message A").unwrap();
let sig_b = hybrid_sign(&sk, b"message B").unwrap();
let mut spliced = sig_a.as_bytes()[..constants::ED25519_SIGNATURE_SIZE].to_vec();
spliced.extend_from_slice(&sig_b.as_bytes()[constants::ED25519_SIGNATURE_SIZE..]);
let franken_sig = HybridSignature::from_bytes(spliced).unwrap();
assert!(matches!(
hybrid_verify(&pk, b"message A", &franken_sig),
Err(Error::VerificationFailed)
));
assert!(matches!(
hybrid_verify(&pk, b"message B", &franken_sig),
Err(Error::VerificationFailed)
));
}
#[test]
fn hybrid_sign_nondeterministic() {
let GeneratedIdentity { secret_key: sk, .. } = generate_identity().unwrap();
let msg = b"same message";
let sig1 = hybrid_sign(&sk, msg).unwrap();
let sig2 = hybrid_sign(&sk, msg).unwrap();
assert_eq!(&sig1.as_bytes()[..64], &sig2.as_bytes()[..64]);
assert_ne!(&sig1.as_bytes()[64..], &sig2.as_bytes()[64..]);
}
}