b_wing/signing.rs
1//! # SWing: Hybrid Digital Signature Scheme
2//!
3//! SWing is a paranoia-grade, triple-layered hybrid digital signature scheme
4//! that provides **NIST Level 5 (256-bit)** post-quantum security. It follows
5//! a **strict redundancy** policy: a composite signature is only accepted if
6//! **all three** underlying sub-signatures verify successfully.
7//!
8//! ## Security Composition
9//!
10//! | Layer | Algorithm | Security assumption |
11//! |-------|-----------|---------------------|
12//! | 1 | **Ed25519** | Classical ECDSA — Edwards-curve discrete log |
13//! | 2 | **ML-DSA-87** (Dilithium) | Module-lattice MLWE (NIST PQ Level 5) |
14//! | 3 | **SLH-DSA-SHAKE256f** (Sphincs+) | Hash-based — conservative PQ |
15//!
16//! This triple-layer defense ensures that authentication remains secure even
17//! after a complete break of two out of three primitives, whether by a quantum
18//! adversary, lattice cryptanalysis, or classical side-channel attack.
19//!
20//! ## Signature and Key Sizes
21//!
22//! | Item | Bytes |
23//! |------|-------|
24//! | Composite verification key | 2,688 |
25//! | Composite signature | 54,547 |
26//! | Master seed | 160 |
27//!
28//! ## Design Principles
29//!
30//! * **Domain-separated binding.** Every sub-signature is computed over a
31//! Sha3-512 digest that binds the caller-supplied `context`, the composite
32//! verification key, and the domain constant [`SWING_CONTEXT`]. This
33//! prevents cross-key and cross-context signature reuse.
34//! * **Caller-supplied randomness.** The 64-byte `random_seed` passed to
35//! [`SWing::sign`] is the only source of non-determinism. The API is
36//! suitable for `no_std`, WASM, and embedded targets.
37//! * **Heap-cached keys.** [`SWing`] pre-computes the composite verification
38//! key once, amortising key-derivation costs across many sign/verify calls.
39//! * **Strict size validation.** [`SWing::verify`] checks both the
40//! verification-key length and the signature length before any cryptographic
41//! work, returning [`Error::InvalidFormat`] on mismatch.
42//!
43//! ## Usage
44//!
45//! ```rust,no_run
46//! # #[cfg(feature = "sign")] {
47//! use b_wing::{SWing, SignError};
48//!
49//! // --- Key generation (signer) ---------------------------------------
50//! // Fill from a CSPRNG in production (e.g. `getrandom::fill`).
51//! let master_seed = [0u8; 160];
52//! let signer = SWing::from_seed(&master_seed).unwrap();
53//! let vk = signer.get_pub_key(); // publish / distribute to verifiers
54//!
55//! // --- Signing --------------------------------------------------------
56//! let message = b"Authenticated payload";
57//! let context = b"my-app-v1";
58//! // MUST be freshly generated for every signature — never reuse!
59//! let random_seed = [1u8; 64];
60//! let signature = signer.sign(message, context, random_seed).unwrap();
61//!
62//! // --- Verification --------------------------------------------------
63//! // Succeeds only if all three sub-signatures are valid.
64//! let ok = SWing::verify(vk, message, context, &signature).unwrap();
65//! assert!(ok);
66//! # }
67//! ```
68
69use sha3::{Digest, Sha3_512};
70use zeroize::Zeroizing;
71
72// RNG Imports for PQ tricking
73use rand_chacha::ChaCha20Rng;
74use rand_core::SeedableRng;
75
76// Cryptographic Primitives
77use ed25519_dalek::{
78 Signature as EdSignature, Signer as EdSigner, SigningKey as EdSigningKey,
79 VerifyingKey as EdVerifyingKey,
80};
81use ml_dsa::{
82 EncodedVerifyingKey as MlEncodedVerifyingKey, ExpandedSigningKey as MlSigningKey, MlDsa87,
83 Signature as MlSignature, VerifyingKey as MlVerifyingKey,
84 signature::SignatureEncoding as MlSignatureEncoding,
85};
86use slh_dsa::{
87 Shake256f, Signature as SlhSignature, SigningKey as SlhSigningKey,
88 VerifyingKey as SlhVerifyingKey, signature::Keypair,
89};
90
91// ======================================================================
92// Constants & Error Types
93// ======================================================================
94
95/// Byte size of the Ed25519 verification key (compressed Edwards point).
96const ED25519_VK_SIZE: usize = 32;
97/// Byte size of an Ed25519 signature.
98const ED25519_SIG_SIZE: usize = 64;
99
100/// Byte size of the ML-DSA-87 verification key.
101const ML_DSA_87_VK_SIZE: usize = 2592;
102/// Byte size of an ML-DSA-87 signature.
103const ML_DSA_87_SIG_SIZE: usize = 4627;
104
105/// Byte size of the SLH-DSA-SHAKE256f verification key.
106const SLH_DSA_256F_VK_SIZE: usize = 64;
107/// Byte size of an SLH-DSA-SHAKE256f signature.
108const SLH_DSA_256F_SIG_SIZE: usize = 49856;
109
110/// Required byte length of the master seed passed to [`SWing::from_seed`].
111///
112/// The 160 bytes are partitioned deterministically as follows:
113///
114/// | Bytes | Usage |
115/// |-------|-------|
116/// | `[0..32]` | Ed25519 signing-key seed |
117/// | `[32..64]` | ML-DSA-87 keygen seed |
118/// | `[64..96]` | SLH-DSA SK seed (`sk_seed`) |
119/// | `[96..128]` | SLH-DSA SK PRF (`sk_prf`) |
120/// | `[128..160]`| SLH-DSA PK seed (`pk_seed`) |
121pub const MASTER_SEED_SIZE: usize = 160;
122
123/// Fixed 64-byte domain-separation tag prepended to every `hash_message` call.
124///
125/// Binds all three sub-signatures to the SWing construction, preventing
126/// cross-protocol confusion. This constant MUST NOT change across versions;
127/// doing so would silently invalidate all previously issued signatures.
128const SWING_CONTEXT: &[u8; 64] = &[
129 43, 229, 51, 244, 223, 83, 139, 19, 204, 237, 234, 148, 235, 203, 225, 75, 40, 130, 210, 79,
130 216, 63, 240, 161, 160, 1, 159, 249, 38, 28, 76, 151, 177, 69, 13, 232, 211, 26, 66, 140, 75,
131 147, 59, 224, 39, 119, 19, 82, 246, 79, 191, 160, 125, 101, 184, 224, 43, 78, 246, 110, 192,
132 156, 169, 14,
133];
134
135/// Errors that can occur during SWing signing or verification.
136///
137/// All variants implement [`core::fmt::Display`] for ergonomic error propagation.
138///
139/// # Example
140///
141/// ```rust,no_run
142/// # #[cfg(feature = "sign")] {
143/// use b_wing::{SWing, SignError};
144/// let bad_vk = vec![0u8; 10]; // wrong size
145/// assert_eq!(
146/// SWing::verify(&bad_vk, b"msg", b"ctx", &vec![0u8; SWing::SIGNATURE_SIZE]),
147/// Err(SignError::InvalidFormat),
148/// );
149/// # }
150/// ```
151#[derive(Debug, Copy, Clone, PartialEq, Eq)]
152pub enum Error {
153 /// One of the three underlying signing primitives returned an error.
154 ///
155 /// This is unexpected under normal operation; it may indicate an issue
156 /// with the caller-supplied `random_seed` or an upstream library fault.
157 SigningFailed,
158 /// A key, signature, or seed was the wrong length or could not be parsed.
159 ///
160 /// Expected sizes: [`SWing::VERIFICATION_KEY_SIZE`] and [`SWing::SIGNATURE_SIZE`].
161 InvalidFormat,
162 /// The composite signature failed cryptographic verification.
163 ///
164 /// This is returned as soon as *any* sub-signature fails; the remaining
165 /// layers are not evaluated (fail-fast behaviour).
166 VerificationFailed,
167}
168
169impl core::fmt::Display for Error {
170 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
171 match self {
172 Error::SigningFailed => write!(f, "Signing operation failed"),
173 Error::InvalidFormat => write!(f, "Invalid format or size"),
174 Error::VerificationFailed => write!(f, "Signature verification failed"),
175 }
176 }
177}
178
179// ======================================================================
180// Expanded SWing Key (Stateful / High-Throughput)
181// ======================================================================
182
183/// A stateful, high-throughput SWing key holder for signing.
184///
185/// `SWing` derives all three component signing keys from a single 160-byte
186/// master seed via [`from_seed`][SWing::from_seed] and caches the composite
187/// verification key in heap-allocated memory. Subsequent calls to
188/// [`sign`][SWing::sign] reuse the cached keys without any additional
189/// key-derivation overhead.
190///
191/// # Key Sizes
192///
193/// | Constant | Bytes | Components |
194/// |----------|-------|------------|
195/// | [`VERIFICATION_KEY_SIZE`][SWing::VERIFICATION_KEY_SIZE] | 2,688 | `Ed25519(32) ‖ ML-DSA-87(2592) ‖ SLH-DSA(64)` |
196/// | [`SIGNATURE_SIZE`][SWing::SIGNATURE_SIZE] | 54,547 | `Ed25519(64) ‖ ML-DSA-87(4627) ‖ SLH-DSA(49856)` |
197///
198/// # Security Note
199///
200/// The verification key returned by [`get_pub_key`][SWing::get_pub_key] is
201/// safe to distribute publicly. Secret key material is stored in fields that
202/// implement `zeroize::ZeroizeOnDrop` (via the underlying crates) and will be
203/// erased from memory when `SWing` is dropped.
204///
205/// # Example
206///
207/// ```rust,no_run
208/// # #[cfg(feature = "sign")] {
209/// use b_wing::SWing;
210///
211/// let master_seed = [0u8; 160]; // use a real CSPRNG in production
212/// let swing = SWing::from_seed(&master_seed).unwrap();
213///
214/// let vk = swing.get_pub_key();
215/// assert_eq!(vk.len(), SWing::VERIFICATION_KEY_SIZE);
216/// # }
217/// ```
218pub struct SWing {
219 ed_sk: EdSigningKey,
220 ml_sk: Box<MlSigningKey<MlDsa87>>,
221 slh_sk: SlhSigningKey<Shake256f>,
222 composite_pk: Vec<u8>,
223}
224
225impl SWing {
226 /// Byte length of the composite verification key: **2,688 bytes**.
227 ///
228 /// Memory layout:
229 /// ```text
230 /// [ Ed25519 vk (32) | ML-DSA-87 vk (2592) | SLH-DSA-SHAKE256f vk (64) ]
231 /// ```
232 pub const VERIFICATION_KEY_SIZE: usize =
233 ED25519_VK_SIZE + ML_DSA_87_VK_SIZE + SLH_DSA_256F_VK_SIZE;
234
235 /// Byte length of the composite signature: **54,547 bytes**.
236 ///
237 /// Memory layout:
238 /// ```text
239 /// [ Ed25519 sig (64) | ML-DSA-87 sig (4627) | SLH-DSA-SHAKE256f sig (49856) ]
240 /// ```
241 pub const SIGNATURE_SIZE: usize = ED25519_SIG_SIZE + ML_DSA_87_SIG_SIZE + SLH_DSA_256F_SIG_SIZE;
242
243 /// Expands a 160-byte master seed into a fully initialised `SWing` key holder.
244 ///
245 /// The seed is partitioned according to [`MASTER_SEED_SIZE`] into the three
246 /// component signing keys. All derivation is deterministic.
247 ///
248 /// # Security Requirements
249 ///
250 /// * `master_seed` **must** be generated by a cryptographically secure
251 /// random number generator (CSPRNG) such as `getrandom`.
252 /// * Treat the master seed with the same care as a private key — it must
253 /// never be logged, transmitted, or stored in plaintext.
254 /// * If the seed is compromised, all signatures produced by this instance
255 /// can be forged. Rotate by generating a new seed and re-publishing the
256 /// corresponding verification key.
257 ///
258 /// # Errors
259 ///
260 /// Returns [`Error::InvalidFormat`] if an internal slice conversion fails
261 /// (impossible for a correctly-sized input).
262 ///
263 /// # Example
264 ///
265 /// ```rust,no_run
266 /// # #[cfg(feature = "sign")] {
267 /// use b_wing::SWing;
268 ///
269 /// let mut seed = [0u8; 160];
270 /// getrandom::fill(&mut seed).expect("CSPRNG failed");
271 /// let swing = SWing::from_seed(&seed).expect("key generation failed");
272 /// # }
273 /// ```
274 pub fn from_seed(master_seed: &[u8; MASTER_SEED_SIZE]) -> Result<Self, Error> {
275 let ed_sk = EdSigningKey::from_bytes(
276 master_seed[0..32]
277 .try_into()
278 .map_err(|_| Error::InvalidFormat)?,
279 );
280 let ml_sk = Box::new(MlSigningKey::<MlDsa87>::from_seed(
281 master_seed[32..64]
282 .try_into()
283 .map_err(|_| Error::InvalidFormat)?,
284 ));
285
286 let sk_seed = &master_seed[64..96];
287 let sk_prf = &master_seed[96..128];
288 let pk_seed = &master_seed[128..160];
289
290 // Note: Using an internal method. Upgrading `slh-dsa` might break this API.
291 let slh_sk = SlhSigningKey::<Shake256f>::slh_keygen_internal(sk_seed, sk_prf, pk_seed);
292
293 let mut composite_pk = Vec::with_capacity(SWing::VERIFICATION_KEY_SIZE);
294 composite_pk.extend_from_slice(EdVerifyingKey::from(&ed_sk).as_bytes());
295 composite_pk.extend_from_slice(&ml_sk.verifying_key().encode());
296 composite_pk.extend_from_slice(&slh_sk.verifying_key().to_bytes());
297
298 Ok(Self {
299 ed_sk,
300 ml_sk,
301 slh_sk,
302 composite_pk,
303 })
304 }
305
306 /// Returns a reference to the cached composite verification key.
307 ///
308 /// The returned slice is [`VERIFICATION_KEY_SIZE`][SWing::VERIFICATION_KEY_SIZE]
309 /// bytes long and is safe to distribute publicly. Pass it to
310 /// [`verify`][SWing::verify] on the verifier's side.
311 pub fn get_pub_key(&self) -> &[u8] {
312 &self.composite_pk
313 }
314
315 /// Signs `message` using all three underlying schemes and returns the composite signature.
316 ///
317 /// The composite signature is the concatenation:
318 ///
319 /// ```text
320 /// [ Ed25519 sig (64) | ML-DSA-87 sig (4627) | SLH-DSA-SHAKE256f sig (49856) ]
321 /// ```
322 ///
323 /// Each sub-signature is computed over a Sha3-512 digest that binds
324 /// `context` and the composite verification key (see [`hash_message`]),
325 /// preventing cross-context and cross-key reuse.
326 ///
327 /// # Arguments
328 ///
329 /// * `message` — Arbitrary-length payload to authenticate.
330 /// * `context` — Application-defined domain tag (e.g. `b"my-app-v1"`).
331 /// Use a distinct context for each logical operation to prevent
332 /// cross-context signature reuse.
333 /// * `random_seed` — 64 bytes of fresh CSPRNG entropy. The value is
334 /// consumed and zeroized after use. While SWing uses randomized
335 /// signing to defend against side-channel attacks, reusing a seed
336 /// against the same message/key remains secure (falling back to
337 /// deterministic security).
338 ///
339 /// # Errors
340 ///
341 /// | Variant | Cause |
342 /// |---------|-------|
343 /// | [`Error::SigningFailed`] | An underlying primitive returned an error |
344 /// | [`Error::InvalidFormat`] | Internal slice conversion failed (should not occur) |
345 ///
346 /// # Example
347 ///
348 /// ```rust,no_run
349 /// # #[cfg(feature = "sign")] {
350 /// use b_wing::SWing;
351 ///
352 /// # let seed = [0u8; 160];
353 /// # let swing = SWing::from_seed(&seed).unwrap();
354 /// let mut random_seed = [0u8; 64];
355 /// getrandom::fill(&mut random_seed).expect("CSPRNG failed");
356 ///
357 /// let sig = swing.sign(b"hello", b"my-app-v1", random_seed).unwrap();
358 /// assert_eq!(sig.len(), SWing::SIGNATURE_SIZE);
359 /// # }
360 /// ```
361 /// # Memory Usage
362 ///
363 /// For the Ed25519 layer, this method performs a heap allocation
364 /// (`.concat()`) proportional to the size of the message. When signing
365 /// gigabyte-scale payloads, ensure the system has sufficient memory or
366 /// pre-hash the payload before passing it to SWing.
367 pub fn sign(
368 &self,
369 message: &[u8],
370 context: &[u8],
371 random_seed: impl Into<Zeroizing<[u8; 64]>>,
372 ) -> Result<Vec<u8>, Error> {
373 let random_seed: Zeroizing<[u8; 64]> = random_seed.into();
374
375 let context_hash = hash_message(context, self.get_pub_key());
376 let mut combined_sig = Vec::with_capacity(SWing::SIGNATURE_SIZE);
377
378 // 1. Ed25519
379 // NOTE: Appends message to context_hash.
380 combined_sig.extend_from_slice(
381 &self
382 .ed_sk
383 .sign(&[&context_hash, message].concat())
384 .to_bytes(),
385 );
386
387 // 2. ML-DSA-87
388 let mut ml_seed = Zeroizing::new([0u8; 32]);
389 ml_seed.copy_from_slice(&random_seed[32..64]);
390 let mut ml_rng = ChaCha20Rng::from_seed(*ml_seed);
391
392 let ml_signature = self
393 .ml_sk
394 .sign_randomized(message, &context_hash, &mut ml_rng)
395 .map_err(|_| Error::SigningFailed)?;
396 combined_sig.extend_from_slice(&ml_signature.to_bytes());
397
398 // 3. SLH-DSA-SHAKE256f
399 let slh_rand: Zeroizing<[u8; 32]> = Zeroizing::new(
400 random_seed[0..32]
401 .try_into()
402 .map_err(|_| Error::InvalidFormat)?,
403 );
404 let slh_signature = Box::new(
405 self.slh_sk
406 .try_sign_with_context(message, &context_hash, Some(&*slh_rand))
407 .map_err(|_| Error::SigningFailed)?,
408 );
409 combined_sig.extend_from_slice(&slh_signature.to_vec());
410
411 Ok(combined_sig)
412 }
413
414 /// Verifies a composite SWing signature against the provided verification key.
415 ///
416 /// Verification proceeds sequentially through all three sub-signatures in
417 /// the order **Ed25519 → ML-DSA-87 → SLH-DSA-SHAKE256f** and returns
418 /// [`Error::VerificationFailed`] as soon as any one of them fails
419 /// (fail-fast behaviour). A return value of `Ok(true)` guarantees that
420 /// *all three* sub-signatures are valid.
421 ///
422 /// # Arguments
423 ///
424 /// * `vk` — [`VERIFICATION_KEY_SIZE`][SWing::VERIFICATION_KEY_SIZE]-byte
425 /// composite verification key obtained from [`get_pub_key`][SWing::get_pub_key].
426 /// * `message` — The original payload that was signed.
427 /// * `context` — The context tag used during signing (must match exactly).
428 /// * `signature` — [`SIGNATURE_SIZE`][SWing::SIGNATURE_SIZE]-byte composite
429 /// signature produced by [`sign`][SWing::sign].
430 ///
431 /// # Errors
432 ///
433 /// | Variant | Cause |
434 /// |---------|-------|
435 /// | [`Error::InvalidFormat`] | `vk` or `signature` has wrong length, or a sub-key cannot be parsed |
436 /// | [`Error::VerificationFailed`] | Any sub-signature is invalid |
437 ///
438 /// # Example
439 ///
440 /// ```rust,no_run
441 /// # #[cfg(feature = "sign")] {
442 /// use b_wing::SWing;
443 ///
444 /// # let seed = [0u8; 160];
445 /// # let swing = SWing::from_seed(&seed).unwrap();
446 /// # let vk = swing.get_pub_key().to_vec();
447 /// # let sig = swing.sign(b"hello", b"ctx", [3u8; 64]).unwrap();
448 /// let ok = SWing::verify(&vk, b"hello", b"ctx", &sig).unwrap();
449 /// assert!(ok);
450 ///
451 /// // Tampered message must fail:
452 /// assert!(SWing::verify(&vk, b"TAMPERED", b"ctx", &sig).is_err());
453 /// # }
454 /// ```
455 pub fn verify(
456 vk: &[u8],
457 message: &[u8],
458 context: &[u8],
459 signature: &[u8],
460 ) -> Result<bool, Error> {
461 // Enforce strict size constraints prior to performing slice indexing
462 if vk.len() != Self::VERIFICATION_KEY_SIZE || signature.len() != Self::SIGNATURE_SIZE {
463 return Err(Error::InvalidFormat);
464 }
465
466 let context_hash = hash_message(context, vk);
467
468 let ed_vk_end = ED25519_VK_SIZE;
469 let ml_vk_end = ed_vk_end + ML_DSA_87_VK_SIZE;
470
471 let ed_sig_end = ED25519_SIG_SIZE;
472 let ml_sig_end = ed_sig_end + ML_DSA_87_SIG_SIZE;
473
474 // 1. Ed25519
475 let ed_vk = EdVerifyingKey::from_bytes(
476 vk[..ed_vk_end]
477 .try_into()
478 .map_err(|_| Error::InvalidFormat)?,
479 )
480 .map_err(|_| Error::InvalidFormat)?;
481
482 let ed_sig = EdSignature::from_bytes(
483 signature[..ed_sig_end]
484 .try_into()
485 .map_err(|_| Error::InvalidFormat)?,
486 );
487
488 // NOTE: Appends message to context_hash.
489 ed_vk
490 .verify_strict(&[&context_hash, message].concat(), &ed_sig)
491 .map_err(|_| Error::VerificationFailed)?;
492
493 // 2. ML-DSA-87
494 let ml_vk = MlVerifyingKey::<MlDsa87>::decode(
495 &MlEncodedVerifyingKey::<MlDsa87>::try_from(&vk[ed_vk_end..ml_vk_end])
496 .map_err(|_| Error::InvalidFormat)?,
497 );
498 let ml_sig = MlSignature::try_from(&signature[ed_sig_end..ml_sig_end])
499 .map_err(|_| Error::InvalidFormat)?;
500
501 if !ml_vk.verify_with_context(message, &context_hash, &ml_sig) {
502 return Err(Error::VerificationFailed);
503 }
504
505 // 3. SLH-DSA-SHAKE256f
506 let slh_vk = SlhVerifyingKey::<Shake256f>::try_from(&vk[ml_vk_end..])
507 .map_err(|_| Error::InvalidFormat)?;
508 let slh_sig = Box::new(
509 SlhSignature::try_from(&signature[ml_sig_end..]).map_err(|_| Error::InvalidFormat)?,
510 );
511
512 slh_vk
513 .try_verify_with_context(message, &context_hash, &slh_sig)
514 .map_err(|_| Error::VerificationFailed)?;
515
516 Ok(true)
517 }
518}
519
520// ======================================================================
521// Cryptographic Helpers (Internal)
522// ======================================================================
523
524/// Derives a deterministic 64-byte domain-separation hash for use in all sub-signatures.
525///
526/// The hash binds the caller-supplied `context` string and the composite
527/// verification key `composite_vk` to the fixed [`SWING_CONTEXT`] domain tag.
528/// All three sub-signatures are computed over a message that includes this
529/// hash, preventing:
530///
531/// * **Cross-context reuse:** A signature for `context = "login"` cannot be
532/// replayed as a signature for `context = "payment"`.
533/// * **Cross-key confusion:** A signature produced under key `A` cannot be
534/// presented as valid under key `B` because the key material is mixed into
535/// the digest.
536///
537/// # Construction
538///
539/// ```text
540/// H = SHA3-512( SWING_CONTEXT || composite_vk || len(context) || context )
541/// ```
542fn hash_message(context: &[u8], composite_vk: &[u8]) -> [u8; 64] {
543 let mut hasher = Sha3_512::new();
544 hasher.update(SWING_CONTEXT);
545 hasher.update(composite_vk);
546 hasher.update((context.len() as u64).to_le_bytes());
547 hasher.update(context);
548 hasher.finalize().into()
549}
550
551#[cfg(test)]
552mod tests {
553 use super::*;
554 use std::sync::LazyLock;
555
556 // ======================================================================
557 // Test Constants & Helpers
558 // ======================================================================
559
560 const TEST_MESSAGE: &[u8] = b"Paranoia is just a higher level of awareness.";
561 const TEST_CONTEXT: &[u8] = b"My-test-Prompt";
562
563 static MASTER_SEED: [u8; 160] = [0x42; 160];
564 static RANDOM_SEED: [u8; 64] = [0x84; 64];
565
566 static S_WING: LazyLock<SWing> = LazyLock::new(|| SWing::from_seed(&MASTER_SEED).unwrap());
567
568 static SHARED_SIG: LazyLock<Vec<u8>> = LazyLock::new(|| {
569 S_WING
570 .sign(TEST_MESSAGE, TEST_CONTEXT, RANDOM_SEED)
571 .unwrap()
572 });
573
574 // ======================================================================
575 // Happy Path & Determinism
576 // ======================================================================
577
578 #[test]
579 fn test_happy_path_round_trip() {
580 // 1. Generate Public Key
581 let vk = S_WING.get_pub_key();
582 assert_eq!(vk.len(), SWing::VERIFICATION_KEY_SIZE);
583
584 // 2. Sign
585 let signature = &*SHARED_SIG;
586 assert_eq!(signature.len(), SWing::SIGNATURE_SIZE);
587
588 // 3. Verify
589 let is_valid = SWing::verify(vk, TEST_MESSAGE, TEST_CONTEXT, signature)
590 .expect("Verification should succeed");
591 assert!(is_valid, "Valid signature must pass the gauntlet");
592 }
593
594 #[test]
595 fn test_strict_determinism() {
596 // 1. Key Generation Determinism
597 let binding = SWing::from_seed(&MASTER_SEED).unwrap();
598 let vk2 = binding.get_pub_key();
599 assert_eq!(
600 S_WING.get_pub_key(),
601 vk2,
602 "Verifying keys must be identical for the same master seed"
603 );
604
605 // 2. Signature Determinism
606 let sig2 = binding
607 .sign(TEST_MESSAGE, TEST_CONTEXT, RANDOM_SEED)
608 .unwrap();
609 assert_eq!(
610 *SHARED_SIG, sig2,
611 "Signatures must be identical for the same seeds and message"
612 );
613 }
614
615 // ======================================================================
616 // Formatting & Boundary Rejections
617 // ======================================================================
618
619 #[test]
620 fn test_invalid_lengths_rejected_immediately() {
621 let vk = S_WING.get_pub_key();
622 let sig = &*SHARED_SIG;
623
624 // 1. Bad VK Length
625 let bad_vk = vec![0u8; SWing::VERIFICATION_KEY_SIZE - 1];
626 let res_vk = SWing::verify(&bad_vk, TEST_MESSAGE, TEST_CONTEXT, sig);
627 assert_eq!(
628 res_vk,
629 Err(Error::InvalidFormat),
630 "Must reject invalid VK lengths"
631 );
632
633 // 2. Bad Signature Length
634 let bad_sig = vec![0u8; SWing::SIGNATURE_SIZE + 1];
635 let res_sig = SWing::verify(vk, TEST_MESSAGE, TEST_CONTEXT, &bad_sig);
636 assert_eq!(
637 res_sig,
638 Err(Error::InvalidFormat),
639 "Must reject invalid signature lengths"
640 );
641 }
642
643 // ======================================================================
644 // The Gauntlet (Tampering & Fail-Fast Logic)
645 // ======================================================================
646
647 #[test]
648 fn test_message_and_context_tampering_fails_fast() {
649 let vk = S_WING.get_pub_key();
650 let sig = &*SHARED_SIG;
651
652 // Tamper Message
653 let res_msg = SWing::verify(vk, b"I am an attacker", TEST_CONTEXT, sig);
654 assert_eq!(res_msg, Err(Error::VerificationFailed));
655
656 // Tamper Context
657 let res_ctx = SWing::verify(vk, TEST_MESSAGE, b"Wrong-Context", sig);
658 assert_eq!(res_ctx, Err(Error::VerificationFailed));
659 }
660
661 #[test]
662 fn test_wrong_public_key_rejection() {
663 let bob_seed = [0x99; 160];
664 let binding = SWing::from_seed(&bob_seed).unwrap();
665 let bob_vk = binding.get_pub_key();
666
667 let alice_sig = &*SHARED_SIG;
668
669 // Server tries to verify with Bob's public key
670 let result = SWing::verify(bob_vk, TEST_MESSAGE, TEST_CONTEXT, alice_sig);
671 assert_eq!(
672 result,
673 Err(Error::VerificationFailed),
674 "Signatures verified against the wrong key must fail"
675 );
676 }
677
678 #[test]
679 fn test_gauntlet_layer_1_ed25519_tampering() {
680 let vk = S_WING.get_pub_key();
681 let mut sig = SHARED_SIG.clone();
682
683 // Tamper with the Ed25519 section of the signature (Bytes 0..64)
684 sig[10] ^= 0xFF;
685
686 let result = SWing::verify(vk, TEST_MESSAGE, TEST_CONTEXT, &sig);
687 assert_eq!(
688 result,
689 Err(Error::VerificationFailed),
690 "Altering the first 64 bytes must trigger Ed25519 rejection"
691 );
692 }
693
694 #[test]
695 fn test_gauntlet_layer_2_ml_dsa_tampering() {
696 let vk = S_WING.get_pub_key();
697 let mut sig = SHARED_SIG.clone();
698
699 // Tamper with the ML-DSA section of the signature (Bytes 64..4691)
700 sig[1000] ^= 0xFF;
701
702 let result = SWing::verify(vk, TEST_MESSAGE, TEST_CONTEXT, &sig);
703
704 match result {
705 Err(Error::VerificationFailed) | Err(Error::InvalidFormat) => {}
706 _ => panic!(
707 "Expected ML-DSA to reject the tampered signature, got {:?}",
708 result
709 ),
710 }
711 }
712
713 #[test]
714 fn test_gauntlet_layer_3_slh_dsa_tampering() {
715 let vk = S_WING.get_pub_key();
716 let mut sig = SHARED_SIG.clone();
717
718 // Tamper with the SLH-DSA section of the signature (Bytes 4691..End)
719 sig[5000] ^= 0xFF;
720
721 let result = SWing::verify(vk, TEST_MESSAGE, TEST_CONTEXT, &sig);
722
723 match result {
724 Err(Error::VerificationFailed) | Err(Error::InvalidFormat) => {}
725 _ => panic!(
726 "Expected SLH-DSA to reject the tampered signature, got {:?}",
727 result
728 ),
729 }
730 }
731}