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 defence 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, MlDsa87, Signature as MlSignature,
83    SigningKey as MlSigningKey, 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: &'static [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    #[must_use]
312    pub fn get_pub_key(&self) -> &[u8] {
313        &self.composite_pk
314    }
315
316    /// Signs `message` using all three underlying schemes and returns the composite signature.
317    ///
318    /// The composite signature is the concatenation:
319    ///
320    /// ```text
321    /// [ Ed25519 sig (64) | ML-DSA-87 sig (4627) | SLH-DSA-SHAKE256f sig (49856) ]
322    /// ```
323    ///
324    /// Each sub-signature is computed over a Sha3-512 digest that binds
325    /// `context` and the composite verification key (see [`hash_message`]),
326    /// preventing cross-context and cross-key reuse.
327    ///
328    /// # Arguments
329    ///
330    /// * `message`     — Arbitrary-length payload to authenticate.
331    /// * `context`     — Application-defined domain tag (e.g. `b"my-app-v1"`).
332    ///   Use a distinct context for each logical operation to prevent
333    ///   cross-context signature reuse.
334    /// * `random_seed` — 64 bytes of fresh CSPRNG entropy.  The value is
335    ///   consumed and zeroized after use.  While SWing uses randomized
336    ///   signing to defend against side-channel attacks, reusing a seed
337    ///   against the same message/key remains secure (falling back to
338    ///   deterministic security).
339    ///
340    /// # Errors
341    ///
342    /// | Variant | Cause |
343    /// |---------|-------|
344    /// | [`Error::SigningFailed`]  | An underlying primitive returned an error |
345    /// | [`Error::InvalidFormat`]  | Internal slice conversion failed (should not occur) |
346    ///
347    /// # Example
348    ///
349    /// ```rust,no_run
350    /// # #[cfg(feature = "sign")] {
351    /// use b_wing::SWing;
352    ///
353    /// # let seed = [0u8; 160];
354    /// # let swing = SWing::from_seed(&seed).unwrap();
355    /// let mut random_seed = [0u8; 64];
356    /// getrandom::fill(&mut random_seed).expect("CSPRNG failed");
357    ///
358    /// let sig = swing.sign(b"hello", b"my-app-v1", random_seed).unwrap();
359    /// assert_eq!(sig.len(), SWing::SIGNATURE_SIZE);
360    /// # }
361    /// ```
362    /// # Memory Usage
363    ///
364    /// For the Ed25519 layer, this method performs a heap allocation
365    /// (`.concat()`) proportional to the size of the message.  When signing
366    /// gigabyte-scale payloads, ensure the system has sufficient memory or
367    /// pre-hash the payload before passing it to SWing.
368    pub fn sign(
369        &self,
370        message: &[u8],
371        context: &[u8],
372        random_seed: impl Into<Zeroizing<[u8; 64]>>,
373    ) -> Result<Vec<u8>, Error> {
374        let random_seed: Zeroizing<[u8; 64]> = random_seed.into();
375
376        let context_hash = hash_message(context, &self.get_pub_key());
377        let mut combined_sig = Vec::with_capacity(SWing::SIGNATURE_SIZE);
378
379        // 1. Ed25519
380        // NOTE: Appends message to context_hash.
381        combined_sig.extend_from_slice(
382            &self
383                .ed_sk
384                .sign(&[&context_hash, message].concat())
385                .to_bytes(),
386        );
387
388        // 2. ML-DSA-87
389        let mut ml_seed = Zeroizing::new([0u8; 32]);
390        ml_seed.copy_from_slice(&random_seed[32..64]);
391        let mut ml_rng = ChaCha20Rng::from_seed(*ml_seed);
392
393        let ml_signature = self
394            .ml_sk
395            .sign_randomized(&message, &context_hash, &mut ml_rng)
396            .map_err(|_| Error::SigningFailed)?;
397        combined_sig.extend_from_slice(&ml_signature.to_bytes());
398
399        // 3. SLH-DSA-SHAKE256f
400        let slh_rand: Zeroizing<[u8; 32]> = Zeroizing::new(
401            random_seed[0..32]
402                .try_into()
403                .map_err(|_| Error::InvalidFormat)?,
404        );
405        let slh_signature = Box::new(
406            self.slh_sk
407                .try_sign_with_context(&message, &context_hash, Some(&*slh_rand))
408                .map_err(|_| Error::SigningFailed)?,
409        );
410        combined_sig.extend_from_slice(&slh_signature.to_vec());
411
412        Ok(combined_sig)
413    }
414
415    /// Verifies a composite SWing signature against the provided verification key.
416    ///
417    /// Verification proceeds sequentially through all three sub-signatures in
418    /// the order **Ed25519 → ML-DSA-87 → SLH-DSA-SHAKE256f** and returns
419    /// [`Error::VerificationFailed`] as soon as any one of them fails
420    /// (fail-fast behaviour).  A return value of `Ok(true)` guarantees that
421    /// *all three* sub-signatures are valid.
422    ///
423    /// # Arguments
424    ///
425    /// * `vk`        — [`VERIFICATION_KEY_SIZE`][SWing::VERIFICATION_KEY_SIZE]-byte
426    ///   composite verification key obtained from [`get_pub_key`][SWing::get_pub_key].
427    /// * `message`   — The original payload that was signed.
428    /// * `context`   — The context tag used during signing (must match exactly).
429    /// * `signature` — [`SIGNATURE_SIZE`][SWing::SIGNATURE_SIZE]-byte composite
430    ///   signature produced by [`sign`][SWing::sign].
431    ///
432    /// # Errors
433    ///
434    /// | Variant | Cause |
435    /// |---------|-------|
436    /// | [`Error::InvalidFormat`]      | `vk` or `signature` has wrong length, or a sub-key cannot be parsed |
437    /// | [`Error::VerificationFailed`] | Any sub-signature is invalid |
438    ///
439    /// # Example
440    ///
441    /// ```rust,no_run
442    /// # #[cfg(feature = "sign")] {
443    /// use b_wing::SWing;
444    ///
445    /// # let seed = [0u8; 160];
446    /// # let swing = SWing::from_seed(&seed).unwrap();
447    /// # let vk = swing.get_pub_key().to_vec();
448    /// # let sig = swing.sign(b"hello", b"ctx", [3u8; 64]).unwrap();
449    /// let ok = SWing::verify(&vk, b"hello", b"ctx", &sig).unwrap();
450    /// assert!(ok);
451    ///
452    /// // Tampered message must fail:
453    /// assert!(SWing::verify(&vk, b"TAMPERED", b"ctx", &sig).is_err());
454    /// # }
455    /// ```
456    #[must_use]
457    pub fn verify(
458        vk: &[u8],
459        message: &[u8],
460        context: &[u8],
461        signature: &[u8],
462    ) -> Result<bool, Error> {
463        // Enforce strict size constraints prior to performing slice indexing
464        if vk.len() != Self::VERIFICATION_KEY_SIZE || signature.len() != Self::SIGNATURE_SIZE {
465            return Err(Error::InvalidFormat);
466        }
467
468        let context_hash = hash_message(context, vk);
469
470        let ed_vk_end = ED25519_VK_SIZE;
471        let ml_vk_end = ed_vk_end + ML_DSA_87_VK_SIZE;
472
473        let ed_sig_end = ED25519_SIG_SIZE;
474        let ml_sig_end = ed_sig_end + ML_DSA_87_SIG_SIZE;
475
476        // 1. Ed25519
477        let ed_vk = EdVerifyingKey::from_bytes(
478            vk[..ed_vk_end]
479                .try_into()
480                .map_err(|_| Error::InvalidFormat)?,
481        )
482        .map_err(|_| Error::InvalidFormat)?;
483
484        let ed_sig = EdSignature::from_bytes(
485            signature[..ed_sig_end]
486                .try_into()
487                .map_err(|_| Error::InvalidFormat)?,
488        );
489
490        // NOTE: Appends message to context_hash.
491        ed_vk
492            .verify_strict(&[&context_hash, message].concat(), &ed_sig)
493            .map_err(|_| Error::VerificationFailed)?;
494
495        // 2. ML-DSA-87
496        let ml_vk = MlVerifyingKey::<MlDsa87>::decode(
497            &MlEncodedVerifyingKey::<MlDsa87>::try_from(&vk[ed_vk_end..ml_vk_end])
498                .map_err(|_| Error::InvalidFormat)?,
499        );
500        let ml_sig = MlSignature::try_from(&signature[ed_sig_end..ml_sig_end])
501            .map_err(|_| Error::InvalidFormat)?;
502
503        if !ml_vk.verify_with_context(message, &context_hash, &ml_sig) {
504            return Err(Error::VerificationFailed);
505        }
506
507        // 3. SLH-DSA-SHAKE256f
508        let slh_vk = SlhVerifyingKey::<Shake256f>::try_from(&vk[ml_vk_end..])
509            .map_err(|_| Error::InvalidFormat)?;
510        let slh_sig = Box::new(
511            SlhSignature::try_from(&signature[ml_sig_end..]).map_err(|_| Error::InvalidFormat)?,
512        );
513
514        slh_vk
515            .try_verify_with_context(message, &context_hash, &slh_sig)
516            .map_err(|_| Error::VerificationFailed)?;
517
518        Ok(true)
519    }
520}
521
522// ======================================================================
523// Cryptographic Helpers (Internal)
524// ======================================================================
525
526/// Derives a deterministic 64-byte domain-separation hash for use in all sub-signatures.
527///
528/// The hash binds the caller-supplied `context` string and the composite
529/// verification key `composite_vk` to the fixed [`SWING_CONTEXT`] domain tag.
530/// All three sub-signatures are computed over a message that includes this
531/// hash, preventing:
532///
533/// * **Cross-context reuse:** A signature for `context = "login"` cannot be
534///   replayed as a signature for `context = "payment"`.
535/// * **Cross-key confusion:** A signature produced under key `A` cannot be
536///   presented as valid under key `B` because the key material is mixed into
537///   the digest.
538///
539/// # Construction
540///
541/// ```text
542/// H = SHA3-512( SWING_CONTEXT || composite_vk || len(context) || context )
543/// ```
544fn hash_message(context: &[u8], composite_vk: &[u8]) -> [u8; 64] {
545    let mut hasher = Sha3_512::new();
546    hasher.update(SWING_CONTEXT);
547    hasher.update(composite_vk);
548    hasher.update((context.len() as u64).to_le_bytes());
549    hasher.update(context);
550    hasher.finalize().into()
551}
552
553#[cfg(test)]
554mod tests {
555    use super::*;
556    use std::sync::LazyLock;
557
558    // ======================================================================
559    // Test Constants & Helpers
560    // ======================================================================
561
562    const TEST_MESSAGE: &[u8] = b"Paranoia is just a higher level of awareness.";
563    const TEST_CONTEXT: &[u8] = b"My-test-Prompt";
564
565    static MASTER_SEED: [u8; 160] = [0x42; 160];
566    static RANDOM_SEED: [u8; 64] = [0x84; 64];
567
568    static S_WING: LazyLock<SWing> = LazyLock::new(|| SWing::from_seed(&MASTER_SEED).unwrap());
569
570    static SHARED_SIG: LazyLock<Vec<u8>> = LazyLock::new(|| {
571        S_WING
572            .sign(TEST_MESSAGE, TEST_CONTEXT, RANDOM_SEED)
573            .unwrap()
574    });
575
576    // ======================================================================
577    // Happy Path & Determinism
578    // ======================================================================
579
580    #[test]
581    fn test_happy_path_round_trip() {
582        // 1. Generate Public Key
583        let vk = S_WING.get_pub_key();
584        assert_eq!(vk.len(), SWing::VERIFICATION_KEY_SIZE);
585
586        // 2. Sign
587        let signature = &*SHARED_SIG;
588        assert_eq!(signature.len(), SWing::SIGNATURE_SIZE);
589
590        // 3. Verify
591        let is_valid = SWing::verify(vk, TEST_MESSAGE, TEST_CONTEXT, signature)
592            .expect("Verification should succeed");
593        assert!(is_valid, "Valid signature must pass the gauntlet");
594    }
595
596    #[test]
597    fn test_strict_determinism() {
598        // 1. Key Generation Determinism
599        let binding = SWing::from_seed(&MASTER_SEED).unwrap();
600        let vk2 = binding.get_pub_key();
601        assert_eq!(
602            S_WING.get_pub_key(),
603            vk2,
604            "Verifying keys must be identical for the same master seed"
605        );
606
607        // 2. Signature Determinism
608        let sig2 = binding
609            .sign(TEST_MESSAGE, TEST_CONTEXT, RANDOM_SEED)
610            .unwrap();
611        assert_eq!(
612            *SHARED_SIG, sig2,
613            "Signatures must be identical for the same seeds and message"
614        );
615    }
616
617    // ======================================================================
618    // Formatting & Boundary Rejections
619    // ======================================================================
620
621    #[test]
622    fn test_invalid_lengths_rejected_immediately() {
623        let vk = S_WING.get_pub_key();
624        let sig = &*SHARED_SIG;
625
626        // 1. Bad VK Length
627        let bad_vk = vec![0u8; SWing::VERIFICATION_KEY_SIZE - 1];
628        let res_vk = SWing::verify(&bad_vk, TEST_MESSAGE, TEST_CONTEXT, sig);
629        assert_eq!(
630            res_vk,
631            Err(Error::InvalidFormat),
632            "Must reject invalid VK lengths"
633        );
634
635        // 2. Bad Signature Length
636        let bad_sig = vec![0u8; SWing::SIGNATURE_SIZE + 1];
637        let res_sig = SWing::verify(vk, TEST_MESSAGE, TEST_CONTEXT, &bad_sig);
638        assert_eq!(
639            res_sig,
640            Err(Error::InvalidFormat),
641            "Must reject invalid signature lengths"
642        );
643    }
644
645    // ======================================================================
646    // The Gauntlet (Tampering & Fail-Fast Logic)
647    // ======================================================================
648
649    #[test]
650    fn test_message_and_context_tampering_fails_fast() {
651        let vk = S_WING.get_pub_key();
652        let sig = &*SHARED_SIG;
653
654        // Tamper Message
655        let res_msg = SWing::verify(vk, b"I am an attacker", TEST_CONTEXT, sig);
656        assert_eq!(res_msg, Err(Error::VerificationFailed));
657
658        // Tamper Context
659        let res_ctx = SWing::verify(vk, TEST_MESSAGE, b"Wrong-Context", sig);
660        assert_eq!(res_ctx, Err(Error::VerificationFailed));
661    }
662
663    #[test]
664    fn test_wrong_public_key_rejection() {
665        let bob_seed = [0x99; 160];
666        let binding = SWing::from_seed(&bob_seed).unwrap();
667        let bob_vk = binding.get_pub_key();
668
669        let alice_sig = &*SHARED_SIG;
670
671        // Server tries to verify with Bob's public key
672        let result = SWing::verify(bob_vk, TEST_MESSAGE, TEST_CONTEXT, alice_sig);
673        assert_eq!(
674            result,
675            Err(Error::VerificationFailed),
676            "Signatures verified against the wrong key must fail"
677        );
678    }
679
680    #[test]
681    fn test_gauntlet_layer_1_ed25519_tampering() {
682        let vk = S_WING.get_pub_key();
683        let mut sig = SHARED_SIG.clone();
684
685        // Tamper with the Ed25519 section of the signature (Bytes 0..64)
686        sig[10] ^= 0xFF;
687
688        let result = SWing::verify(vk, TEST_MESSAGE, TEST_CONTEXT, &sig);
689        assert_eq!(
690            result,
691            Err(Error::VerificationFailed),
692            "Altering the first 64 bytes must trigger Ed25519 rejection"
693        );
694    }
695
696    #[test]
697    fn test_gauntlet_layer_2_ml_dsa_tampering() {
698        let vk = S_WING.get_pub_key();
699        let mut sig = SHARED_SIG.clone();
700
701        // Tamper with the ML-DSA section of the signature (Bytes 64..4691)
702        sig[1000] ^= 0xFF;
703
704        let result = SWing::verify(vk, TEST_MESSAGE, TEST_CONTEXT, &sig);
705
706        match result {
707            Err(Error::VerificationFailed) | Err(Error::InvalidFormat) => {}
708            _ => panic!(
709                "Expected ML-DSA to reject the tampered signature, got {:?}",
710                result
711            ),
712        }
713    }
714
715    #[test]
716    fn test_gauntlet_layer_3_slh_dsa_tampering() {
717        let vk = S_WING.get_pub_key();
718        let mut sig = SHARED_SIG.clone();
719
720        // Tamper with the SLH-DSA section of the signature (Bytes 4691..End)
721        sig[5000] ^= 0xFF;
722
723        let result = SWing::verify(vk, TEST_MESSAGE, TEST_CONTEXT, &sig);
724
725        match result {
726            Err(Error::VerificationFailed) | Err(Error::InvalidFormat) => {}
727            _ => panic!(
728                "Expected SLH-DSA to reject the tampered signature, got {:?}",
729                result
730            ),
731        }
732    }
733}