use sha3::{Digest, Sha3_512};
use zeroize::Zeroizing;
use rand_chacha::ChaCha20Rng;
use rand_core::SeedableRng;
use ed25519_dalek::{
Signature as EdSignature, Signer as EdSigner, SigningKey as EdSigningKey,
VerifyingKey as EdVerifyingKey,
};
use ml_dsa::{
EncodedVerifyingKey as MlEncodedVerifyingKey, MlDsa87, Signature as MlSignature,
ExpandedSigningKey as MlSigningKey, VerifyingKey as MlVerifyingKey,
signature::SignatureEncoding as MlSignatureEncoding,
};
use slh_dsa::{
Shake256f, Signature as SlhSignature, SigningKey as SlhSigningKey,
VerifyingKey as SlhVerifyingKey, signature::Keypair,
};
const ED25519_VK_SIZE: usize = 32;
const ED25519_SIG_SIZE: usize = 64;
const ML_DSA_87_VK_SIZE: usize = 2592;
const ML_DSA_87_SIG_SIZE: usize = 4627;
const SLH_DSA_256F_VK_SIZE: usize = 64;
const SLH_DSA_256F_SIG_SIZE: usize = 49856;
pub const MASTER_SEED_SIZE: usize = 160;
const SWING_CONTEXT: &'static [u8; 64] = &[
43, 229, 51, 244, 223, 83, 139, 19, 204, 237, 234, 148, 235, 203, 225, 75, 40, 130, 210, 79,
216, 63, 240, 161, 160, 1, 159, 249, 38, 28, 76, 151, 177, 69, 13, 232, 211, 26, 66, 140, 75,
147, 59, 224, 39, 119, 19, 82, 246, 79, 191, 160, 125, 101, 184, 224, 43, 78, 246, 110, 192,
156, 169, 14,
];
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Error {
SigningFailed,
InvalidFormat,
VerificationFailed,
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Error::SigningFailed => write!(f, "Signing operation failed"),
Error::InvalidFormat => write!(f, "Invalid format or size"),
Error::VerificationFailed => write!(f, "Signature verification failed"),
}
}
}
pub struct SWing {
ed_sk: EdSigningKey,
ml_sk: Box<MlSigningKey<MlDsa87>>,
slh_sk: SlhSigningKey<Shake256f>,
composite_pk: Vec<u8>,
}
impl SWing {
pub const VERIFICATION_KEY_SIZE: usize =
ED25519_VK_SIZE + ML_DSA_87_VK_SIZE + SLH_DSA_256F_VK_SIZE;
pub const SIGNATURE_SIZE: usize = ED25519_SIG_SIZE + ML_DSA_87_SIG_SIZE + SLH_DSA_256F_SIG_SIZE;
pub fn from_seed(master_seed: &[u8; MASTER_SEED_SIZE]) -> Result<Self, Error> {
let ed_sk = EdSigningKey::from_bytes(
master_seed[0..32]
.try_into()
.map_err(|_| Error::InvalidFormat)?,
);
let ml_sk = Box::new(MlSigningKey::<MlDsa87>::from_seed(
master_seed[32..64]
.try_into()
.map_err(|_| Error::InvalidFormat)?,
));
let sk_seed = &master_seed[64..96];
let sk_prf = &master_seed[96..128];
let pk_seed = &master_seed[128..160];
let slh_sk = SlhSigningKey::<Shake256f>::slh_keygen_internal(sk_seed, sk_prf, pk_seed);
let mut composite_pk = Vec::with_capacity(SWing::VERIFICATION_KEY_SIZE);
composite_pk.extend_from_slice(EdVerifyingKey::from(&ed_sk).as_bytes());
composite_pk.extend_from_slice(&ml_sk.verifying_key().encode());
composite_pk.extend_from_slice(&slh_sk.verifying_key().to_bytes());
Ok(Self {
ed_sk,
ml_sk,
slh_sk,
composite_pk,
})
}
#[must_use]
pub fn get_pub_key(&self) -> &[u8] {
&self.composite_pk
}
pub fn sign(
&self,
message: &[u8],
context: &[u8],
random_seed: impl Into<Zeroizing<[u8; 64]>>,
) -> Result<Vec<u8>, Error> {
let random_seed: Zeroizing<[u8; 64]> = random_seed.into();
let context_hash = hash_message(context, &self.get_pub_key());
let mut combined_sig = Vec::with_capacity(SWing::SIGNATURE_SIZE);
combined_sig.extend_from_slice(
&self
.ed_sk
.sign(&[&context_hash, message].concat())
.to_bytes(),
);
let mut ml_seed = Zeroizing::new([0u8; 32]);
ml_seed.copy_from_slice(&random_seed[32..64]);
let mut ml_rng = ChaCha20Rng::from_seed(*ml_seed);
let ml_signature = self
.ml_sk
.sign_randomized(&message, &context_hash, &mut ml_rng)
.map_err(|_| Error::SigningFailed)?;
combined_sig.extend_from_slice(&ml_signature.to_bytes());
let slh_rand: Zeroizing<[u8; 32]> = Zeroizing::new(
random_seed[0..32]
.try_into()
.map_err(|_| Error::InvalidFormat)?,
);
let slh_signature = Box::new(
self.slh_sk
.try_sign_with_context(&message, &context_hash, Some(&*slh_rand))
.map_err(|_| Error::SigningFailed)?,
);
combined_sig.extend_from_slice(&slh_signature.to_vec());
Ok(combined_sig)
}
#[must_use]
pub fn verify(
vk: &[u8],
message: &[u8],
context: &[u8],
signature: &[u8],
) -> Result<bool, Error> {
if vk.len() != Self::VERIFICATION_KEY_SIZE || signature.len() != Self::SIGNATURE_SIZE {
return Err(Error::InvalidFormat);
}
let context_hash = hash_message(context, vk);
let ed_vk_end = ED25519_VK_SIZE;
let ml_vk_end = ed_vk_end + ML_DSA_87_VK_SIZE;
let ed_sig_end = ED25519_SIG_SIZE;
let ml_sig_end = ed_sig_end + ML_DSA_87_SIG_SIZE;
let ed_vk = EdVerifyingKey::from_bytes(
vk[..ed_vk_end]
.try_into()
.map_err(|_| Error::InvalidFormat)?,
)
.map_err(|_| Error::InvalidFormat)?;
let ed_sig = EdSignature::from_bytes(
signature[..ed_sig_end]
.try_into()
.map_err(|_| Error::InvalidFormat)?,
);
ed_vk
.verify_strict(&[&context_hash, message].concat(), &ed_sig)
.map_err(|_| Error::VerificationFailed)?;
let ml_vk = MlVerifyingKey::<MlDsa87>::decode(
&MlEncodedVerifyingKey::<MlDsa87>::try_from(&vk[ed_vk_end..ml_vk_end])
.map_err(|_| Error::InvalidFormat)?,
);
let ml_sig = MlSignature::try_from(&signature[ed_sig_end..ml_sig_end])
.map_err(|_| Error::InvalidFormat)?;
if !ml_vk.verify_with_context(message, &context_hash, &ml_sig) {
return Err(Error::VerificationFailed);
}
let slh_vk = SlhVerifyingKey::<Shake256f>::try_from(&vk[ml_vk_end..])
.map_err(|_| Error::InvalidFormat)?;
let slh_sig = Box::new(
SlhSignature::try_from(&signature[ml_sig_end..]).map_err(|_| Error::InvalidFormat)?,
);
slh_vk
.try_verify_with_context(message, &context_hash, &slh_sig)
.map_err(|_| Error::VerificationFailed)?;
Ok(true)
}
}
fn hash_message(context: &[u8], composite_vk: &[u8]) -> [u8; 64] {
let mut hasher = Sha3_512::new();
hasher.update(SWING_CONTEXT);
hasher.update(composite_vk);
hasher.update((context.len() as u64).to_le_bytes());
hasher.update(context);
hasher.finalize().into()
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::LazyLock;
const TEST_MESSAGE: &[u8] = b"Paranoia is just a higher level of awareness.";
const TEST_CONTEXT: &[u8] = b"My-test-Prompt";
static MASTER_SEED: [u8; 160] = [0x42; 160];
static RANDOM_SEED: [u8; 64] = [0x84; 64];
static S_WING: LazyLock<SWing> = LazyLock::new(|| SWing::from_seed(&MASTER_SEED).unwrap());
static SHARED_SIG: LazyLock<Vec<u8>> = LazyLock::new(|| {
S_WING
.sign(TEST_MESSAGE, TEST_CONTEXT, RANDOM_SEED)
.unwrap()
});
#[test]
fn test_happy_path_round_trip() {
let vk = S_WING.get_pub_key();
assert_eq!(vk.len(), SWing::VERIFICATION_KEY_SIZE);
let signature = &*SHARED_SIG;
assert_eq!(signature.len(), SWing::SIGNATURE_SIZE);
let is_valid = SWing::verify(vk, TEST_MESSAGE, TEST_CONTEXT, signature)
.expect("Verification should succeed");
assert!(is_valid, "Valid signature must pass the gauntlet");
}
#[test]
fn test_strict_determinism() {
let binding = SWing::from_seed(&MASTER_SEED).unwrap();
let vk2 = binding.get_pub_key();
assert_eq!(
S_WING.get_pub_key(),
vk2,
"Verifying keys must be identical for the same master seed"
);
let sig2 = binding
.sign(TEST_MESSAGE, TEST_CONTEXT, RANDOM_SEED)
.unwrap();
assert_eq!(
*SHARED_SIG, sig2,
"Signatures must be identical for the same seeds and message"
);
}
#[test]
fn test_invalid_lengths_rejected_immediately() {
let vk = S_WING.get_pub_key();
let sig = &*SHARED_SIG;
let bad_vk = vec![0u8; SWing::VERIFICATION_KEY_SIZE - 1];
let res_vk = SWing::verify(&bad_vk, TEST_MESSAGE, TEST_CONTEXT, sig);
assert_eq!(
res_vk,
Err(Error::InvalidFormat),
"Must reject invalid VK lengths"
);
let bad_sig = vec![0u8; SWing::SIGNATURE_SIZE + 1];
let res_sig = SWing::verify(vk, TEST_MESSAGE, TEST_CONTEXT, &bad_sig);
assert_eq!(
res_sig,
Err(Error::InvalidFormat),
"Must reject invalid signature lengths"
);
}
#[test]
fn test_message_and_context_tampering_fails_fast() {
let vk = S_WING.get_pub_key();
let sig = &*SHARED_SIG;
let res_msg = SWing::verify(vk, b"I am an attacker", TEST_CONTEXT, sig);
assert_eq!(res_msg, Err(Error::VerificationFailed));
let res_ctx = SWing::verify(vk, TEST_MESSAGE, b"Wrong-Context", sig);
assert_eq!(res_ctx, Err(Error::VerificationFailed));
}
#[test]
fn test_wrong_public_key_rejection() {
let bob_seed = [0x99; 160];
let binding = SWing::from_seed(&bob_seed).unwrap();
let bob_vk = binding.get_pub_key();
let alice_sig = &*SHARED_SIG;
let result = SWing::verify(bob_vk, TEST_MESSAGE, TEST_CONTEXT, alice_sig);
assert_eq!(
result,
Err(Error::VerificationFailed),
"Signatures verified against the wrong key must fail"
);
}
#[test]
fn test_gauntlet_layer_1_ed25519_tampering() {
let vk = S_WING.get_pub_key();
let mut sig = SHARED_SIG.clone();
sig[10] ^= 0xFF;
let result = SWing::verify(vk, TEST_MESSAGE, TEST_CONTEXT, &sig);
assert_eq!(
result,
Err(Error::VerificationFailed),
"Altering the first 64 bytes must trigger Ed25519 rejection"
);
}
#[test]
fn test_gauntlet_layer_2_ml_dsa_tampering() {
let vk = S_WING.get_pub_key();
let mut sig = SHARED_SIG.clone();
sig[1000] ^= 0xFF;
let result = SWing::verify(vk, TEST_MESSAGE, TEST_CONTEXT, &sig);
match result {
Err(Error::VerificationFailed) | Err(Error::InvalidFormat) => {}
_ => panic!(
"Expected ML-DSA to reject the tampered signature, got {:?}",
result
),
}
}
#[test]
fn test_gauntlet_layer_3_slh_dsa_tampering() {
let vk = S_WING.get_pub_key();
let mut sig = SHARED_SIG.clone();
sig[5000] ^= 0xFF;
let result = SWing::verify(vk, TEST_MESSAGE, TEST_CONTEXT, &sig);
match result {
Err(Error::VerificationFailed) | Err(Error::InvalidFormat) => {}
_ => panic!(
"Expected SLH-DSA to reject the tampered signature, got {:?}",
result
),
}
}
}