#![deny(unsafe_code)]
#![deny(missing_docs)]
use crate::primitives::ec::ed25519::{Ed25519KeyPair, Ed25519Signature as Ed25519SignatureOps};
use crate::primitives::ec::traits::{EcKeyPair, EcSignature};
use crate::primitives::sig::ml_dsa::{
MlDsaParameterSet, MlDsaPublicKey, MlDsaSecretKey, MlDsaSignature,
};
pub struct HybridVerifyDummyParsed {
pub pq_pk: MlDsaPublicKey,
pub pq_sig: MlDsaSignature,
pub pq_test_message: Vec<u8>,
}
pub struct HybridVerifyDummy {
pub pq_pk: Vec<u8>,
pub pq_sig: Vec<u8>,
pub ed_pk: Vec<u8>,
pub ed_sig: Vec<u8>,
pub parsed: std::sync::Mutex<Option<HybridVerifyDummyParsed>>,
pub param_set: MlDsaParameterSet,
}
impl HybridVerifyDummy {
#[must_use]
pub fn parsed_or_init(&self) -> Option<HybridVerifyDummyParsed> {
let mut guard = match self.parsed.lock() {
Ok(g) => g,
Err(poisoned) => {
tracing::warn!(
target: "latticearc::hybrid::verify_equalizer",
"ML-DSA verify-equalizer parsed-cache mutex was poisoned; \
a previous init panicked. Recovering inner guard."
);
poisoned.into_inner()
}
};
if guard.is_none() {
*guard = try_init_parsed(self.param_set);
}
guard.as_ref().map(HybridVerifyDummyParsed::clone)
}
}
impl Clone for HybridVerifyDummyParsed {
fn clone(&self) -> Self {
Self {
pq_pk: self.pq_pk.clone(),
pq_sig: self.pq_sig.clone(),
pq_test_message: self.pq_test_message.clone(),
}
}
}
#[must_use]
pub fn hybrid_verify_dummy_material(param_set: MlDsaParameterSet) -> &'static HybridVerifyDummy {
use std::sync::OnceLock;
static M44: OnceLock<HybridVerifyDummy> = OnceLock::new();
static M65: OnceLock<HybridVerifyDummy> = OnceLock::new();
static M87: OnceLock<HybridVerifyDummy> = OnceLock::new();
let cell = match param_set {
MlDsaParameterSet::MlDsa44 => &M44,
MlDsaParameterSet::MlDsa65 => &M65,
MlDsaParameterSet::MlDsa87 => &M87,
};
cell.get_or_init(|| HybridVerifyDummy {
pq_pk: vec![0u8; param_set.public_key_size()],
pq_sig: vec![0u8; param_set.signature_size()],
ed_pk: vec![0u8; 32],
ed_sig: vec![0u8; 64],
parsed: std::sync::Mutex::new(try_init_parsed(param_set)),
param_set,
})
}
fn try_init_parsed(param_set: MlDsaParameterSet) -> Option<HybridVerifyDummyParsed> {
let pq_test_message: Vec<u8> = b"latticearc/verify-equalizer/dummy-message-v1".to_vec();
let (pq_pk, sk): (MlDsaPublicKey, MlDsaSecretKey) =
crate::primitives::sig::ml_dsa::generate_keypair(param_set).ok()?;
let pq_sig = sk.sign(&pq_test_message, &[]).ok()?;
drop(sk);
Some(HybridVerifyDummyParsed { pq_pk, pq_sig, pq_test_message })
}
pub struct Ed25519VerifyDummy {
pub parsed: std::sync::Mutex<Option<Ed25519VerifyDummyParsed>>,
}
pub struct Ed25519VerifyDummyParsed {
pub ed_pk: Vec<u8>,
pub ed_sig: Vec<u8>,
pub ed_test_message: Vec<u8>,
}
impl Clone for Ed25519VerifyDummyParsed {
fn clone(&self) -> Self {
Self {
ed_pk: self.ed_pk.clone(),
ed_sig: self.ed_sig.clone(),
ed_test_message: self.ed_test_message.clone(),
}
}
}
impl Ed25519VerifyDummy {
#[must_use]
pub fn parsed_or_init(&self) -> Option<Ed25519VerifyDummyParsed> {
let mut guard = match self.parsed.lock() {
Ok(g) => g,
Err(poisoned) => {
tracing::warn!(
target: "latticearc::hybrid::verify_equalizer",
"Ed25519 verify-equalizer parsed-cache mutex was poisoned; \
a previous init panicked. Recovering inner guard."
);
poisoned.into_inner()
}
};
if guard.is_none() {
*guard = try_init_ed25519_parsed();
}
guard.as_ref().map(Ed25519VerifyDummyParsed::clone)
}
}
#[must_use]
pub fn ed25519_verify_dummy_material() -> &'static Ed25519VerifyDummy {
use std::sync::OnceLock;
static CELL: OnceLock<Ed25519VerifyDummy> = OnceLock::new();
CELL.get_or_init(|| Ed25519VerifyDummy {
parsed: std::sync::Mutex::new(try_init_ed25519_parsed()),
})
}
fn try_init_ed25519_parsed() -> Option<Ed25519VerifyDummyParsed> {
let test_message: Vec<u8> = b"latticearc/verify-equalizer/dummy-ed25519-v1".to_vec();
let keypair = Ed25519KeyPair::generate().ok()?;
let signature = keypair.sign(&test_message).ok()?;
let ed_pk = keypair.public_key_bytes();
let ed_sig = Ed25519SignatureOps::signature_bytes(&signature);
drop(keypair);
Some(Ed25519VerifyDummyParsed { ed_pk, ed_sig, ed_test_message: test_message })
}
#[cfg(test)]
#[expect(clippy::panic, reason = "Tests panic on missing init material")]
mod tests {
use super::*;
#[test]
fn parsed_material_verifies_successfully_for_all_parameter_sets() {
for param_set in
[MlDsaParameterSet::MlDsa44, MlDsaParameterSet::MlDsa65, MlDsaParameterSet::MlDsa87]
{
let dummy = hybrid_verify_dummy_material(param_set);
let Some(parsed) = dummy.parsed_or_init() else {
panic!("init keygen + sign must succeed under test conditions for {param_set:?}");
};
let result = parsed.pq_pk.verify(&parsed.pq_test_message, &parsed.pq_sig, &[]);
assert!(
matches!(result, Ok(true)),
"pre-parsed equalizer material must self-verify for {:?}",
param_set,
);
}
}
#[test]
fn raw_byte_fields_preserve_legacy_contract() {
for param_set in
[MlDsaParameterSet::MlDsa44, MlDsaParameterSet::MlDsa65, MlDsaParameterSet::MlDsa87]
{
let dummy = hybrid_verify_dummy_material(param_set);
assert_eq!(dummy.pq_pk.len(), param_set.public_key_size());
assert_eq!(dummy.pq_sig.len(), param_set.signature_size());
assert!(dummy.pq_pk.iter().all(|&b| b == 0));
assert!(dummy.pq_sig.iter().all(|&b| b == 0));
assert_eq!(dummy.ed_pk.len(), 32);
assert_eq!(dummy.ed_sig.len(), 64);
assert!(dummy.ed_pk.iter().all(|&b| b == 0));
assert!(dummy.ed_sig.iter().all(|&b| b == 0));
}
}
}