Skip to main content

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}