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}