Skip to main content

sqisign_verify/
formats.rs

1//!
2//! Three wire formats with different size/speed tradeoffs:
3//!
4//! | Format | Level 1 | Description |
5//! |--------|---------|-------------|
6//! | Standard | 148 B | Default: 2×2 matrix + hints |
7//! | Expanded | 212 B | Pre-evaluated kernel points, faster verify |
8//! | Compressed | 129 B | 3 of 4 matrix entries, 4th via determinant |
9//!
10//! The standard format is the NIST v2.0 wire format and is the only one
11//! validated against KAT vectors. The other two are defined by the
12//! SQIsign specification as optional compression levels.
13
14use crate::ec::basis::{
15    difference_point, difference_point_with_hint, ec_curve_to_basis_2f_to_hint,
16};
17use crate::ec::pairing::{fp2_dlog_2e_pub, weil};
18use crate::ec::point::{ec_dbl_iter_basis, xadd};
19use crate::ec::{EcBasis, EcPoint};
20use crate::fp::{Fp2, FpBackend};
21use crate::params::{Level1, SecurityLevel};
22use crate::precomp::LevelPrecomp;
23use crate::theta::HD_EXTRA_TORSION;
24use hybrid_array::typenum::Unsigned;
25use hybrid_array::Array;
26
27use crate::hash::hash_to_challenge;
28use crate::types::{
29    decode_digits, encode_digits, fmt_fp2, fmt_hex, fmt_scalar, PublicKey, Scalar, Signature,
30};
31use crate::verify::{
32    basis_from_hint, check_canonical_basis_change_matrix, compute_challenge_curve,
33    compute_commitment_curve_verify, matrix_scalar_application_even_basis, mp_compare, mp_is_even,
34    protocols_verify, two_response_isogeny_verify_inner,
35};
36use crate::Error;
37
38/// Identifies which wire format a signature uses.
39///
40/// Format detection is purely length-based: each format has a unique
41/// wire size at every security level, so no prefix byte is needed.
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub enum SignatureFormat {
44    Expanded,
45    Standard,
46    Compressed,
47}
48
49/// Expanded signature with pre-evaluated kernel points.
50///
51/// Instead of storing the 2×2 basis change matrix, stores the resulting
52/// x-coordinates of P_chl and Q_chl directly. The difference point
53/// P_chl - Q_chl is recomputed during verification via `difference_point`.
54/// This may yield faster verification at the cost of a larger wire format;
55/// the actual speedup varies depending on the security level and platform.
56///
57/// Wire size: 212 bytes (Level 1), 316 bytes (Level 3), 420 bytes (Level 5).
58///
59/// # Create from a standard signature
60///
61/// ```no_run
62/// use sqisign_verify::{PublicKey, Signature, ExpandedSignature, Verifier};
63///
64/// # fn example(pk: &PublicKey, sig: &Signature) -> Result<(), sqisign_verify::Error> {
65/// let expanded = sig.expand(pk)?;
66/// pk.verify(b"message", &expanded)?;
67///
68/// // Serialize / deserialize
69/// let wire = expanded.to_bytes();
70/// let decoded: ExpandedSignature = ExpandedSignature::from_bytes(&wire)?;
71/// # Ok(())
72/// # }
73/// ```
74#[derive(Clone)]
75pub struct ExpandedSignature<L: SecurityLevel = Level1> {
76    /// Montgomery A-coefficient of the auxiliary curve E_aux.
77    pub(crate) e_aux_a: Fp2<L>,
78    /// Number of backtracking steps in the response isogeny.
79    /// Wire encoding packs flags into the high bits of this byte:
80    /// bit 7 = kernel_is_q, bit 6 = pmq_sign_hint, bits 0–5 = backtracking.
81    pub(crate) backtracking: u8,
82    /// Length of the initial 2-isogeny chain in the response.
83    pub(crate) two_resp_length: u8,
84    /// Challenge coefficient (LAMBDA bits).
85    pub(crate) chall_coeff: Scalar<L>,
86    /// x-coordinate of the first basis image under the matrix action.
87    pub(crate) p_chl_x: Fp2<L>,
88    /// x-coordinate of the second basis image under the matrix action.
89    pub(crate) q_chl_x: Fp2<L>,
90    /// If true, the kernel generator for the small 2-isogeny chain is
91    /// `Q_chl`; otherwise it is `P_chl`.
92    pub(crate) kernel_is_q: bool,
93    /// Sign hint for reconstructing P_chl - Q_chl via `difference_point`.
94    /// When true, the verifier negates the discriminant to get the correct root.
95    pub(crate) pmq_sign_hint: bool,
96    /// Torsion basis hint for the auxiliary curve.
97    pub(crate) hint_aux: u8,
98    /// Torsion basis hint for the challenge curve.
99    pub(crate) hint_chall: u8,
100}
101
102impl<L: FpBackend> core::fmt::Debug for ExpandedSignature<L> {
103    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
104        f.write_str("ExpandedSignature { e_aux_a: ")?;
105        fmt_fp2(f, &self.e_aux_a)?;
106        write!(
107            f,
108            ", bt: {}, trl: {}, chall: ",
109            self.backtracking, self.two_resp_length
110        )?;
111        fmt_scalar(f, &self.chall_coeff)?;
112        f.write_str(", p_chl_x: ")?;
113        fmt_fp2(f, &self.p_chl_x)?;
114        f.write_str(", q_chl_x: ")?;
115        fmt_fp2(f, &self.q_chl_x)?;
116        write!(
117            f,
118            ", kernel_is_q: {}, pmq_sign_hint: {}, hint_aux: 0x{:02x}, hint_chall: 0x{:02x} }}",
119            self.kernel_is_q, self.pmq_sign_hint, self.hint_aux, self.hint_chall
120        )
121    }
122}
123
124impl<L: FpBackend> ExpandedSignature<L> {
125    /// Wire size in bytes.
126    ///
127    /// Layout: `Fp2 (e_aux) | backtracking_and_flags | two_resp_length |
128    ///          LAMBDA/8 (challenge) | Fp2 (P_chl_x) | Fp2 (Q_chl_x) |
129    ///          hint_aux | hint_chall`
130    ///
131    /// The `kernel_is_q` flag is packed into bit 7 of the backtracking byte.
132    pub const WIRE_BYTES: usize = <L as SecurityLevel>::Fp2EncodedBytes::USIZE * 3
133        + <L as SecurityLevel>::LAMBDA as usize / 8
134        + 4;
135
136    fn chall_coeff_bytes() -> usize {
137        L::LAMBDA as usize / 8
138    }
139
140    /// Decode from bytes.
141    pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
142        if bytes.len() != Self::WIRE_BYTES {
143            return Err(Error::InvalidLength);
144        }
145
146        let fp2_len = <L as SecurityLevel>::Fp2EncodedBytes::USIZE;
147        let mut pos = 0;
148
149        let e_aux_a = Fp2::<L>::decode(&bytes[pos..pos + fp2_len]).ok_or(Error::MalformedInput)?;
150        pos += fp2_len;
151
152        let bt_byte = bytes[pos];
153        pos += 1;
154        let kernel_is_q = (bt_byte & 0x80) != 0;
155        let pmq_sign_hint = (bt_byte & 0x40) != 0;
156        let backtracking = bt_byte & 0x3F;
157
158        let two_resp_length = bytes[pos];
159        pos += 1;
160
161        let chall_bytes = Self::chall_coeff_bytes();
162        let mut chall_coeff = Scalar::<L>::default();
163        decode_digits(
164            chall_coeff.digits.as_mut_slice(),
165            &bytes[pos..],
166            chall_bytes,
167        );
168        pos += chall_bytes;
169
170        let p_chl_x = Fp2::<L>::decode(&bytes[pos..pos + fp2_len]).ok_or(Error::MalformedInput)?;
171        pos += fp2_len;
172
173        let q_chl_x = Fp2::<L>::decode(&bytes[pos..pos + fp2_len]).ok_or(Error::MalformedInput)?;
174        pos += fp2_len;
175
176        let hint_aux = bytes[pos];
177        pos += 1;
178        let hint_chall = bytes[pos];
179        pos += 1;
180        debug_assert_eq!(pos, Self::WIRE_BYTES);
181
182        Ok(Self {
183            e_aux_a,
184            backtracking,
185            two_resp_length,
186            chall_coeff,
187            p_chl_x,
188            q_chl_x,
189            kernel_is_q,
190            pmq_sign_hint,
191            hint_aux,
192            hint_chall,
193        })
194    }
195
196    /// Encode to bytes.
197    pub fn to_bytes(&self) -> Array<u8, L::ExpandedSigLen> {
198        let mut buf = Array::<u8, L::ExpandedSigLen>::default();
199        let fp2_len = <L as SecurityLevel>::Fp2EncodedBytes::USIZE;
200        let mut pos = 0;
201
202        let enc = self.e_aux_a.encode();
203        buf[pos..pos + fp2_len].copy_from_slice(&enc);
204        pos += fp2_len;
205
206        buf[pos] = self.backtracking
207            | if self.kernel_is_q { 0x80 } else { 0 }
208            | if self.pmq_sign_hint { 0x40 } else { 0 };
209        pos += 1;
210        buf[pos] = self.two_resp_length;
211        pos += 1;
212
213        let chall_bytes = Self::chall_coeff_bytes();
214        encode_digits(
215            &mut buf[pos..],
216            self.chall_coeff.digits.as_slice(),
217            chall_bytes,
218        );
219        pos += chall_bytes;
220
221        let enc = self.p_chl_x.encode();
222        buf[pos..pos + fp2_len].copy_from_slice(&enc);
223        pos += fp2_len;
224
225        let enc = self.q_chl_x.encode();
226        buf[pos..pos + fp2_len].copy_from_slice(&enc);
227        pos += fp2_len;
228
229        buf[pos] = self.hint_aux;
230        pos += 1;
231        buf[pos] = self.hint_chall;
232        pos += 1;
233        debug_assert_eq!(pos, Self::WIRE_BYTES);
234
235        buf
236    }
237}
238
239impl<L: FpBackend> TryFrom<&[u8]> for ExpandedSignature<L> {
240    type Error = Error;
241
242    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
243        Self::from_bytes(bytes)
244    }
245}
246
247impl<L: FpBackend> From<ExpandedSignature<L>> for Array<u8, L::ExpandedSigLen> {
248    fn from(sig: ExpandedSignature<L>) -> Self {
249        sig.to_bytes()
250    }
251}
252
253impl<L: FpBackend> signature::SignatureEncoding for ExpandedSignature<L>
254where
255    Array<u8, L::ExpandedSigLen>: Send + Sync,
256{
257    type Repr = Array<u8, L::ExpandedSigLen>;
258}
259
260impl<L: FpBackend + LevelPrecomp> signature::Verifier<ExpandedSignature<L>> for PublicKey<L> {
261    fn verify(&self, msg: &[u8], sig: &ExpandedSignature<L>) -> Result<(), signature::Error> {
262        verify_expanded(self, msg, sig).map_err(|_| signature::Error::new())
263    }
264}
265
266impl<L: FpBackend> core::fmt::Display for ExpandedSignature<L> {
267    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
268        fmt_hex(f, &self.to_bytes())
269    }
270}
271
272/// Compute a canonical 2ᶠ-torsion basis and its hint byte from a curve.
273fn basis_to_hint<L: FpBackend + LevelPrecomp>(
274    curve: &mut crate::ec::EcCurve<L>,
275    f: u32,
276) -> Result<(EcBasis<L>, u8), Error> {
277    ec_curve_to_basis_2f_to_hint(
278        curve,
279        f,
280        L::basis_e0_px_bytes(),
281        L::basis_e0_qx_bytes(),
282        L::p_cofactor_for_2f(),
283        L::p_cofactor_for_2f_bitlength() as usize,
284        L::torsion_even_power(),
285    )
286    .map_err(|()| Error::MalformedInput)
287}
288
289/// Multiply `a * b`, truncated to `out.len()` words, then mask to `mod_bits`.
290fn mp_mul_mod(out: &mut [u64], a: &[u64], b: &[u64], mod_bits: usize) {
291    let n = out.len();
292    out.fill(0);
293    for i in 0..a.len().min(n) {
294        let mut carry: u64 = 0;
295        for j in 0..b.len().min(n - i) {
296            let prod = (a[i] as u128) * (b[j] as u128) + (out[i + j] as u128) + (carry as u128);
297            out[i + j] = prod as u64;
298            carry = (prod >> 64) as u64;
299        }
300    }
301    mp_mod_2exp(out, mod_bits);
302}
303
304/// Right-shift a digit array by `shift` bits.
305fn mp_shiftr(a: &mut [u64], shift: usize) {
306    let n = a.len();
307    let word_shift = shift / 64;
308    let bit_shift = shift % 64;
309
310    if word_shift >= n {
311        a.fill(0);
312        return;
313    }
314
315    if bit_shift == 0 {
316        for i in 0..n - word_shift {
317            a[i] = a[i + word_shift];
318        }
319    } else {
320        for i in 0..n - word_shift - 1 {
321            a[i] = (a[i + word_shift] >> bit_shift) | (a[i + word_shift + 1] << (64 - bit_shift));
322        }
323        a[n - word_shift - 1] = a[n - 1] >> bit_shift;
324    }
325    a[n - word_shift..n].fill(0);
326}
327
328/// Mask a digit array to `e` bits.
329fn mp_mod_2exp(a: &mut [u64], e: usize) {
330    let q = e / 64;
331    let r = e % 64;
332    if q < a.len() {
333        if r != 0 {
334            a[q] &= (1u64 << r) - 1;
335        } else {
336            a[q] = 0;
337        }
338        a[q + 1..].fill(0);
339    }
340}
341
342/// Compute a⁻¹ mod 2ᵉ via Newton/Hensel lifting.
343/// Requires `a` to be odd.
344fn hensel_inv_mod_2e(out: &mut [u64], a: &[u64], e: usize) {
345    let n = out.len();
346    out.fill(0);
347    out[0] = 1;
348
349    let mut ax = [0u64; 8];
350    let mut factor = [0u64; 8];
351    let mut x_copy = [0u64; 8];
352
353    let mut prec = 1usize;
354    while prec < e {
355        let next = (prec * 2).min(e);
356
357        ax[..n].fill(0);
358        for i in 0..n {
359            let mut carry: u64 = 0;
360            for j in 0..n.min(n - i) {
361                if i + j >= n {
362                    break;
363                }
364                let prod =
365                    (a[i] as u128) * (out[j] as u128) + (ax[i + j] as u128) + (carry as u128);
366                ax[i + j] = prod as u64;
367                carry = (prod >> 64) as u64;
368            }
369        }
370        mp_mod_2exp(&mut ax[..n], next);
371
372        // factor = 2 - ax = !ax + 3 (two's complement: -ax + 2 = !ax + 1 + 2)
373        let mut carry = 3u128;
374        for i in 0..n {
375            let val = (!ax[i]) as u128 + carry;
376            factor[i] = val as u64;
377            carry = val >> 64;
378        }
379        mp_mod_2exp(&mut factor[..n], next);
380
381        x_copy[..n].copy_from_slice(&out[..n]);
382        out.fill(0);
383        for i in 0..n {
384            let mut carry_mul: u64 = 0;
385            for j in 0..n.min(n - i) {
386                if i + j >= n {
387                    break;
388                }
389                let prod = (x_copy[i] as u128) * (factor[j] as u128)
390                    + (out[i + j] as u128)
391                    + (carry_mul as u128);
392                out[i + j] = prod as u64;
393                carry_mul = (prod >> 64) as u64;
394            }
395        }
396        mp_mod_2exp(out, next);
397
398        prec = next;
399    }
400}
401
402/// Set bits `[start_bit..start_bit+n_bits)` from a hint byte.
403fn set_high_bits(digits: &mut [u64], hint: u8, start_bit: usize, n_bits: usize) {
404    for b in 0..n_bits {
405        if hint & (1u8 << b) != 0 {
406            let pos = start_bit + b;
407            let limb = pos / 64;
408            let bit = pos % 64;
409            if limb < digits.len() {
410                digits[limb] |= 1u64 << bit;
411            }
412        }
413    }
414}
415
416/// Compressed signature: 3 of 4 matrix coefficients stored.
417///
418/// One entry from the second row is dropped and recovered during
419/// decompression via the Weil pairing determinant relation. Which entry
420/// is dropped depends on M₀₀ parity:
421///
422/// - **M₀₀ odd**: drop M₁₁, store M₁₀ as `mat_var`.
423///   Recover M₁₁ = (det + M₀₁·M₁₀) · M₀₀⁻¹.
424/// - **M₀₀ even**: drop M₁₀, store M₁₁ as `mat_var`.
425///   Recover M₁₀ = (M₀₀·M₁₁ − det) · M₀₁⁻¹.
426///
427/// The Weil pairing determinant gives `E_RSP - bt` bits of precision,
428/// leaving exactly 2 unknown bits (HD_EXTRA_TORSION). These are packed
429/// into bits 2-3 of the backtracking byte on the wire (129 bytes at
430/// Level 1).
431///
432/// The canonical basis hints for E_chall and E_aux are not stored;
433/// they are recomputed from the curves during decompression.
434///
435/// Wire size: 129 bytes (Level 1), 196 bytes (Level 3), 257 bytes (Level 5).
436///
437/// # Create from a standard signature
438///
439/// ```no_run
440/// use sqisign_verify::{PublicKey, Signature, CompressedSignature, Verifier};
441///
442/// # fn example(pk: &PublicKey, sig: &Signature) -> Result<(), sqisign_verify::Error> {
443/// let compressed = sig.compress();
444/// pk.verify(b"message", &compressed)?;
445///
446/// // Serialize / deserialize
447/// let wire = compressed.to_bytes();
448/// let decoded: CompressedSignature = CompressedSignature::from_bytes(&wire)?;
449/// # Ok(())
450/// # }
451/// ```
452#[derive(Clone)]
453pub struct CompressedSignature<L: SecurityLevel = Level1> {
454    /// Montgomery A-coefficient of the auxiliary curve E_aux.
455    pub(crate) e_aux_a: Fp2<L>,
456    /// Number of backtracking steps (0–3).
457    pub(crate) backtracking: u8,
458    /// Length of the initial 2-isogeny chain in the response.
459    pub(crate) two_resp_length: u8,
460    /// Matrix entry M₀₀.
461    pub(crate) mat_00: Scalar<L>,
462    /// Matrix entry M₀₁.
463    pub(crate) mat_01: Scalar<L>,
464    /// Third matrix entry: M₁₀ when M₀₀ is odd, M₁₁ when even.
465    pub(crate) mat_var: Scalar<L>,
466    /// Challenge coefficient (LAMBDA bits).
467    pub(crate) chall_coeff: Scalar<L>,
468    /// 2-bit hint: bits of the dropped entry above what the Weil pairing
469    /// determinant recovers.
470    pub(crate) det_hint: u8,
471}
472
473impl<L: FpBackend> core::fmt::Debug for CompressedSignature<L> {
474    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
475        f.write_str("CompressedSignature { e_aux_a: ")?;
476        fmt_fp2(f, &self.e_aux_a)?;
477        write!(
478            f,
479            ", bt: {}, trl: {}, mat_00: ",
480            self.backtracking, self.two_resp_length
481        )?;
482        fmt_scalar(f, &self.mat_00)?;
483        f.write_str(", mat_01: ")?;
484        fmt_scalar(f, &self.mat_01)?;
485        f.write_str(", mat_var: ")?;
486        fmt_scalar(f, &self.mat_var)?;
487        f.write_str(", chall: ")?;
488        fmt_scalar(f, &self.chall_coeff)?;
489        write!(f, ", det_hint: 0x{:02x} }}", self.det_hint)
490    }
491}
492
493impl<L: SecurityLevel> CompressedSignature<L> {
494    /// Pack backtracking (2 bits), det_hint (2 bits), and
495    /// two_resp_length (4 bits) into one metadata byte.
496    ///
497    /// Layout: `[trl:4 | det_hint:2 | bt:2]` (LSB first).
498    fn pack_meta(&self) -> u8 {
499        (self.backtracking & 0x03) | ((self.det_hint & 0x03) << 2) | (self.two_resp_length << 4)
500    }
501
502    /// Unpack the metadata byte into (backtracking, det_hint, two_resp_length).
503    fn unpack_meta(packed: u8) -> (u8, u8, u8) {
504        let backtracking = packed & 0x03;
505        let det_hint = (packed >> 2) & 0x03;
506        let two_resp_length = (packed >> 4) & 0x0F;
507        (backtracking, det_hint, two_resp_length)
508    }
509}
510
511impl<L: FpBackend> CompressedSignature<L> {
512    /// Wire size in bytes.
513    ///
514    /// Layout: `Fp2 (e_aux) | packed_meta |
515    ///          3 × matrix_entry_bytes | LAMBDA/8 (challenge)`
516    ///
517    /// The packed metadata byte holds backtracking (bits 0–1),
518    /// det_hint (bits 2–3), and two_resp_length (bits 4–7).
519    /// Canonical basis hints are not stored.
520    pub const WIRE_BYTES: usize = <L as SecurityLevel>::Fp2EncodedBytes::USIZE
521        + 3 * ((<L as SecurityLevel>::E_RSP as usize + 9) / 8)
522        + <L as SecurityLevel>::LAMBDA as usize / 8
523        + 1;
524
525    fn matrix_entry_bytes() -> usize {
526        (L::E_RSP as usize + 9) / 8
527    }
528
529    fn chall_coeff_bytes() -> usize {
530        L::LAMBDA as usize / 8
531    }
532
533    /// Decode from bytes.
534    pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
535        if bytes.len() != Self::WIRE_BYTES {
536            return Err(Error::InvalidLength);
537        }
538
539        let fp2_len = <L as SecurityLevel>::Fp2EncodedBytes::USIZE;
540        let mat_bytes = Self::matrix_entry_bytes();
541        let chall_bytes = Self::chall_coeff_bytes();
542        let mut pos = 0;
543
544        let e_aux_a = Fp2::<L>::decode(&bytes[pos..pos + fp2_len]).ok_or(Error::MalformedInput)?;
545        pos += fp2_len;
546
547        let (backtracking, det_hint, two_resp_length) = Self::unpack_meta(bytes[pos]);
548        pos += 1;
549
550        let mut mat_00 = Scalar::<L>::default();
551        decode_digits(mat_00.digits.as_mut_slice(), &bytes[pos..], mat_bytes);
552        pos += mat_bytes;
553
554        let mut mat_01 = Scalar::<L>::default();
555        decode_digits(mat_01.digits.as_mut_slice(), &bytes[pos..], mat_bytes);
556        pos += mat_bytes;
557
558        let mut mat_var = Scalar::<L>::default();
559        decode_digits(mat_var.digits.as_mut_slice(), &bytes[pos..], mat_bytes);
560        pos += mat_bytes;
561
562        let mut chall_coeff = Scalar::<L>::default();
563        decode_digits(
564            chall_coeff.digits.as_mut_slice(),
565            &bytes[pos..],
566            chall_bytes,
567        );
568        pos += chall_bytes;
569
570        debug_assert_eq!(pos, Self::WIRE_BYTES);
571
572        Ok(Self {
573            e_aux_a,
574            backtracking,
575            two_resp_length,
576            mat_00,
577            mat_01,
578            mat_var,
579            chall_coeff,
580            det_hint,
581        })
582    }
583
584    /// Encode to bytes.
585    pub fn to_bytes(&self) -> Array<u8, L::CompressedSigLen> {
586        let mut buf = Array::<u8, L::CompressedSigLen>::default();
587        let fp2_len = <L as SecurityLevel>::Fp2EncodedBytes::USIZE;
588        let mat_bytes = Self::matrix_entry_bytes();
589        let chall_bytes = Self::chall_coeff_bytes();
590        let mut pos = 0;
591
592        let enc = self.e_aux_a.encode();
593        buf[pos..pos + fp2_len].copy_from_slice(&enc);
594        pos += fp2_len;
595
596        buf[pos] = self.pack_meta();
597        pos += 1;
598
599        encode_digits(&mut buf[pos..], self.mat_00.digits.as_slice(), mat_bytes);
600        pos += mat_bytes;
601        encode_digits(&mut buf[pos..], self.mat_01.digits.as_slice(), mat_bytes);
602        pos += mat_bytes;
603        encode_digits(&mut buf[pos..], self.mat_var.digits.as_slice(), mat_bytes);
604        pos += mat_bytes;
605
606        encode_digits(
607            &mut buf[pos..],
608            self.chall_coeff.digits.as_slice(),
609            chall_bytes,
610        );
611        pos += chall_bytes;
612
613        debug_assert_eq!(pos, Self::WIRE_BYTES);
614
615        buf
616    }
617
618    /// Decompress into a standard signature by recovering the dropped entry.
619    ///
620    /// The Weil pairing determinant gives `pow_dim2 + trl` bits of det(M).
621    /// The remaining 2 bits (HD_EXTRA_TORSION) come from `det_hint`.
622    pub fn decompress(&self, pk: &PublicKey<L>) -> Result<Signature<L>, Error>
623    where
624        L: LevelPrecomp,
625    {
626        let pow_dim2 = L::E_RSP as i32 - self.two_resp_length as i32 - self.backtracking as i32;
627        // SECURITY: reject pow_dim2 <= 0 (auxiliary curve unbound, breaks SUF-CMA).
628        if pow_dim2 <= 1 {
629            return Err(Error::InvalidSignature);
630        }
631        let pow_dim2 = pow_dim2 as usize;
632        let trl = self.two_resp_length as usize;
633        let det_precision = pow_dim2 + trl;
634        let f = det_precision + HD_EXTRA_TORSION as usize;
635        let g = pow_dim2 + HD_EXTRA_TORSION as usize;
636        let nw = L::MpLimbs::USIZE;
637
638        let m00_odd = self.mat_00.digits[0] & 1 != 0;
639
640        let pivot = if m00_odd {
641            &self.mat_00
642        } else {
643            if self.mat_01.digits[0] & 1 == 0 {
644                return Err(Error::InvalidSignature);
645            }
646            &self.mat_01
647        };
648
649        // Compute det(M) mod 2^det_precision via Weil pairing --
650        // Basis hints are not stored; recompute them from the curves.
651
652        let mut e_chall =
653            compute_challenge_curve(&self.chall_coeff, self.backtracking, &pk.curve, pk.hint_pk)
654                .ok_or(Error::InvalidSignature)?;
655        let (b_chall, hint_chall) = basis_to_hint(&mut e_chall, L::F_CHR)?;
656        let b_chall = ec_dbl_iter_basis(&b_chall, L::F_CHR as usize - f, &mut e_chall);
657        let ppq_chall = xadd(&b_chall.p, &b_chall.q, &b_chall.pmq);
658        let omega_f = weil::<L>(f as u32, &b_chall.p, &b_chall.q, &ppq_chall, &mut e_chall);
659
660        let mut e_aux =
661            crate::ec::EcCurve::<L>::from_a(&self.e_aux_a).ok_or(Error::InvalidSignature)?;
662        let (b_aux, hint_aux) = basis_to_hint(&mut e_aux, L::F_CHR)?;
663        let b_aux = ec_dbl_iter_basis(&b_aux, L::F_CHR as usize - g, &mut e_aux);
664        let ppq_aux = xadd(&b_aux.p, &b_aux.q, &b_aux.pmq);
665        let omega_aux = weil::<L>(g as u32, &b_aux.p, &b_aux.q, &ppq_aux, &mut e_aux);
666
667        let omega_f_inv = omega_f.inv();
668        let omega_aux_inv = omega_aux.inv();
669        let mut det_digits = [0u64; 8];
670        fp2_dlog_2e_pub::<L>(
671            &mut det_digits[..nw],
672            &omega_aux_inv,
673            &omega_f_inv,
674            f as u32,
675        )
676        .ok_or(Error::InvalidSignature)?;
677        mp_mod_2exp(&mut det_digits[..nw], det_precision);
678
679        // Recover the dropped entry mod 2^det_precision, then set 2 hint bits
680
681        let mut inv_pivot = Scalar::<L>::default();
682        hensel_inv_mod_2e(
683            inv_pivot.digits.as_mut_slice(),
684            pivot.digits.as_slice(),
685            det_precision,
686        );
687
688        let (mat_10, mat_11) = if m00_odd {
689            let mut product = Scalar::<L>::default();
690            mp_mul_mod(
691                product.digits.as_mut_slice(),
692                self.mat_01.digits.as_slice(),
693                self.mat_var.digits.as_slice(),
694                det_precision,
695            );
696            let mut numerator = Scalar::<L>::default();
697            let mut carry: u64 = 0;
698            for (i, &det_limb) in det_digits.iter().enumerate().take(nw) {
699                let sum = (det_limb as u128) + (product.digits[i] as u128) + (carry as u128);
700                numerator.digits[i] = sum as u64;
701                carry = (sum >> 64) as u64;
702            }
703            mp_mod_2exp(numerator.digits.as_mut_slice(), det_precision);
704
705            let mut recovered = Scalar::<L>::default();
706            mp_mul_mod(
707                recovered.digits.as_mut_slice(),
708                numerator.digits.as_slice(),
709                inv_pivot.digits.as_slice(),
710                det_precision,
711            );
712            set_high_bits(
713                recovered.digits.as_mut_slice(),
714                self.det_hint,
715                det_precision,
716                HD_EXTRA_TORSION as usize,
717            );
718            (self.mat_var.clone(), recovered)
719        } else {
720            let mut product = Scalar::<L>::default();
721            mp_mul_mod(
722                product.digits.as_mut_slice(),
723                self.mat_00.digits.as_slice(),
724                self.mat_var.digits.as_slice(),
725                det_precision,
726            );
727            let mut numerator = Scalar::<L>::default();
728            let mut borrow: u64 = 0;
729            for (i, &det_limb) in det_digits.iter().enumerate().take(nw) {
730                let (diff, b1) = product.digits[i].overflowing_sub(det_limb);
731                let (diff2, b2) = diff.overflowing_sub(borrow);
732                numerator.digits[i] = diff2;
733                borrow = (b1 as u64) + (b2 as u64);
734            }
735            mp_mod_2exp(numerator.digits.as_mut_slice(), det_precision);
736
737            let mut recovered = Scalar::<L>::default();
738            mp_mul_mod(
739                recovered.digits.as_mut_slice(),
740                numerator.digits.as_slice(),
741                inv_pivot.digits.as_slice(),
742                det_precision,
743            );
744            set_high_bits(
745                recovered.digits.as_mut_slice(),
746                self.det_hint,
747                det_precision,
748                HD_EXTRA_TORSION as usize,
749            );
750            (recovered, self.mat_var.clone())
751        };
752
753        Ok(Signature {
754            e_aux_a: self.e_aux_a.clone(),
755            backtracking: self.backtracking,
756            two_resp_length: self.two_resp_length,
757            mat: [[self.mat_00.clone(), self.mat_01.clone()], [mat_10, mat_11]],
758            chall_coeff: self.chall_coeff.clone(),
759            hint_aux,
760            hint_chall,
761        })
762    }
763}
764
765impl<L: FpBackend> TryFrom<&[u8]> for CompressedSignature<L> {
766    type Error = Error;
767
768    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
769        Self::from_bytes(bytes)
770    }
771}
772
773impl<L: FpBackend> From<CompressedSignature<L>> for Array<u8, L::CompressedSigLen> {
774    fn from(sig: CompressedSignature<L>) -> Self {
775        sig.to_bytes()
776    }
777}
778
779impl<L: FpBackend> signature::SignatureEncoding for CompressedSignature<L>
780where
781    Array<u8, L::CompressedSigLen>: Send + Sync,
782{
783    type Repr = Array<u8, L::CompressedSigLen>;
784}
785
786impl<L: FpBackend + LevelPrecomp> signature::Verifier<CompressedSignature<L>> for PublicKey<L> {
787    fn verify(&self, msg: &[u8], sig: &CompressedSignature<L>) -> Result<(), signature::Error> {
788        verify_compressed(self, msg, sig).map_err(|_| signature::Error::new())
789    }
790}
791
792impl<L: FpBackend + LevelPrecomp> signature::Verifier<AnySignature<L>> for PublicKey<L> {
793    fn verify(&self, msg: &[u8], sig: &AnySignature<L>) -> Result<(), signature::Error> {
794        match sig {
795            AnySignature::Standard(s) => crate::verify::protocols_verify(self, msg, s),
796            AnySignature::Expanded(s) => verify_expanded(self, msg, s),
797            AnySignature::Compressed(s) => verify_compressed(self, msg, s),
798        }
799        .map_err(|_| signature::Error::new())
800    }
801}
802
803impl<L: FpBackend> core::fmt::Display for CompressedSignature<L> {
804    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
805        fmt_hex(f, &self.to_bytes())
806    }
807}
808
809/// Any signature format, auto-detected from wire length.
810///
811/// Each format has a unique wire size at every security level, so no
812/// prefix byte is needed. Use [`AnySignature::from_bytes`] to parse a
813/// signature of unknown format, then verify with
814/// [`pk.verify(msg, &sig)`](signature::Verifier::verify).
815#[derive(Clone)]
816pub enum AnySignature<L: SecurityLevel = Level1> {
817    Expanded(ExpandedSignature<L>),
818    Standard(Signature<L>),
819    Compressed(CompressedSignature<L>),
820}
821
822impl<L: FpBackend> core::fmt::Debug for AnySignature<L> {
823    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
824        match self {
825            AnySignature::Expanded(s) => core::fmt::Debug::fmt(s, f),
826            AnySignature::Standard(s) => core::fmt::Debug::fmt(s, f),
827            AnySignature::Compressed(s) => core::fmt::Debug::fmt(s, f),
828        }
829    }
830}
831
832impl<L: FpBackend> AnySignature<L> {
833    /// Parse a signature, detecting the format from its byte length.
834    pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
835        let len = bytes.len();
836        if len == ExpandedSignature::<L>::WIRE_BYTES {
837            Ok(AnySignature::Expanded(ExpandedSignature::from_bytes(
838                bytes,
839            )?))
840        } else if len == L::SigLen::USIZE {
841            let sig = Signature::<L>::from_bytes(bytes)?;
842            Ok(AnySignature::Standard(sig))
843        } else if len == CompressedSignature::<L>::WIRE_BYTES {
844            Ok(AnySignature::Compressed(CompressedSignature::from_bytes(
845                bytes,
846            )?))
847        } else {
848            Err(Error::MalformedInput)
849        }
850    }
851
852    /// The format of this signature.
853    pub fn format(&self) -> SignatureFormat {
854        match self {
855            AnySignature::Expanded(_) => SignatureFormat::Expanded,
856            AnySignature::Standard(_) => SignatureFormat::Standard,
857            AnySignature::Compressed(_) => SignatureFormat::Compressed,
858        }
859    }
860}
861
862impl<L: FpBackend> core::fmt::Display for AnySignature<L> {
863    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
864        match self {
865            AnySignature::Expanded(s) => core::fmt::Display::fmt(s, f),
866            AnySignature::Standard(s) => core::fmt::Display::fmt(s, f),
867            AnySignature::Compressed(s) => core::fmt::Display::fmt(s, f),
868        }
869    }
870}
871
872impl<L: FpBackend + LevelPrecomp> Signature<L> {
873    /// Expand by pre-evaluating the matrix action to get kernel points.
874    ///
875    /// The expanded form may verify faster because the verifier
876    /// skips the biscalar multiplication step; the actual speedup
877    /// varies depending on the security level and platform.
878    ///
879    /// Requires the public key because the matrix action is evaluated
880    /// on the challenge curve, which is derived from the public key.
881    pub fn expand(&self, pk: &PublicKey<L>) -> Result<ExpandedSignature<L>, Error> {
882        let pow_dim2_deg_resp =
883            L::E_RSP as i32 - self.two_resp_length as i32 - self.backtracking as i32;
884        // SECURITY: reject pow_dim2_deg_resp <= 0 (auxiliary curve unbound, breaks SUF-CMA).
885        if pow_dim2_deg_resp <= 1 {
886            return Err(Error::InvalidSignature);
887        }
888
889        check_canonical_basis_change_matrix(self).ok_or(Error::InvalidSignature)?;
890
891        let mut e_chall =
892            compute_challenge_curve(&self.chall_coeff, self.backtracking, &pk.curve, pk.hint_pk)
893                .ok_or(Error::InvalidSignature)?;
894
895        let mut b_chall_can = basis_from_hint(&mut e_chall, L::F_CHR, self.hint_chall)
896            .ok_or(Error::InvalidSignature)?;
897
898        let dbl_chall = L::F_CHR as usize
899            - pow_dim2_deg_resp as usize
900            - HD_EXTRA_TORSION as usize
901            - self.two_resp_length as usize;
902        b_chall_can = crate::ec::point::ec_dbl_iter_basis(&b_chall_can, dbl_chall, &mut e_chall);
903
904        let f =
905            pow_dim2_deg_resp as usize + HD_EXTRA_TORSION as usize + self.two_resp_length as usize;
906        matrix_scalar_application_even_basis(&mut b_chall_can, &e_chall, &self.mat, f)
907            .ok_or(Error::InvalidSignature)?;
908
909        let kernel_is_q = self.two_resp_length > 0
910            && mp_is_even::<L>(&self.mat[0][0])
911            && mp_is_even::<L>(&self.mat[1][0]);
912
913        let p_aff = b_chall_can.p.x.mul(&b_chall_can.p.z.inv());
914        let q_aff = b_chall_can.q.x.mul(&b_chall_can.q.z.inv());
915
916        // Determine whether difference_point's default sqrt sign gives
917        // the correct P-Q. If not, the verifier must negate the discriminant.
918        let p_pt = EcPoint::new(p_aff.clone(), Fp2::one());
919        let q_pt = EcPoint::new(q_aff.clone(), Fp2::one());
920        let candidate = difference_point(&p_pt, &q_pt, &e_chall);
921        let known_pmq = &b_chall_can.pmq;
922        let cross1 = candidate.x.mul(&known_pmq.z);
923        let cross2 = candidate.z.mul(&known_pmq.x);
924        let pmq_sign_hint = !bool::from(cross1.ct_equal(&cross2));
925
926        Ok(ExpandedSignature {
927            e_aux_a: self.e_aux_a.clone(),
928            backtracking: self.backtracking,
929            two_resp_length: self.two_resp_length,
930            chall_coeff: self.chall_coeff.clone(),
931            p_chl_x: p_aff,
932            q_chl_x: q_aff,
933            kernel_is_q,
934            pmq_sign_hint,
935            hint_aux: self.hint_aux,
936            hint_chall: self.hint_chall,
937        })
938    }
939
940    /// Compress by dropping one second-row entry based on M₀₀ parity.
941    ///
942    /// The Weil pairing recovers `det_precision = pow_dim2 + trl` bits of
943    /// the dropped entry. The remaining 2 bits are stored as `det_hint`,
944    /// packed into the backtracking byte on the wire.
945    pub fn compress(&self) -> CompressedSignature<L> {
946        let pow_dim2 =
947            L::E_RSP as usize - self.two_resp_length as usize - self.backtracking as usize;
948        let det_precision = pow_dim2 + self.two_resp_length as usize;
949
950        let m00_odd = self.mat[0][0].digits[0] & 1 != 0;
951        let (kept, dropped) = if m00_odd {
952            (&self.mat[1][0], &self.mat[1][1])
953        } else {
954            (&self.mat[1][1], &self.mat[1][0])
955        };
956
957        let mut dropped_shifted = dropped.clone();
958        mp_shiftr(dropped_shifted.digits.as_mut_slice(), det_precision);
959        let det_hint = dropped_shifted.digits[0] as u8 & 0x03;
960
961        CompressedSignature {
962            e_aux_a: self.e_aux_a.clone(),
963            backtracking: self.backtracking,
964            two_resp_length: self.two_resp_length,
965            mat_00: self.mat[0][0].clone(),
966            mat_01: self.mat[0][1].clone(),
967            mat_var: kept.clone(),
968            chall_coeff: self.chall_coeff.clone(),
969            det_hint,
970        }
971    }
972}
973
974/// Verify an expanded signature (skips matrix action).
975///
976/// Uses the pre-evaluated kernel point x-coordinates stored in the
977/// expanded signature, bypassing the three biscalar multiplications
978/// that the standard format requires.
979pub(crate) fn verify_expanded<L: FpBackend + LevelPrecomp>(
980    pk: &PublicKey<L>,
981    msg: &[u8],
982    sig: &ExpandedSignature<L>,
983) -> Result<(), Error> {
984    let pow_dim2_deg_resp = L::E_RSP as i32 - sig.two_resp_length as i32 - sig.backtracking as i32;
985
986    // SECURITY: reject pow_dim2_deg_resp <= 0 (auxiliary curve unbound, breaks SUF-CMA).
987    if pow_dim2_deg_resp <= 1 {
988        return Err(Error::InvalidSignature);
989    }
990
991    if !crate::ec::EcCurve::<L>::verify_a(&pk.curve.a) {
992        return Err(Error::InvalidSignature);
993    }
994
995    let mut e_aux = crate::ec::EcCurve::<L>::from_a(&sig.e_aux_a).ok_or(Error::InvalidSignature)?;
996
997    if !crate::verify::verify_canonical_hint::<L>(&mut e_aux, sig.hint_aux) {
998        return Err(Error::InvalidSignature);
999    }
1000
1001    let mut e_chall =
1002        compute_challenge_curve(&sig.chall_coeff, sig.backtracking, &pk.curve, pk.hint_pk)
1003            .ok_or(Error::InvalidSignature)?;
1004
1005    // Validate that hint_chall matches the canonical hint for this curve.
1006    // basis_to_hint normalizes e_chall internally, satisfying the precondition
1007    // for difference_point_with_hint below.
1008    let (_, expected_hint_chall) = basis_to_hint(&mut e_chall, L::F_CHR)?;
1009    if sig.hint_chall != expected_hint_chall {
1010        return Err(Error::InvalidSignature);
1011    }
1012
1013    // Reconstruct b_chall_can from stored affine x-coordinates (Z = 1).
1014    // P - Q is recomputed via the quadratic formula on the challenge curve,
1015    // using the sign hint to select the correct root.
1016    let p_pt = EcPoint::new(sig.p_chl_x.clone(), Fp2::one());
1017    let q_pt = EcPoint::new(sig.q_chl_x.clone(), Fp2::one());
1018    let pmq_pt = difference_point_with_hint(&p_pt, &q_pt, &e_chall, sig.pmq_sign_hint)
1019        .ok_or(Error::InvalidSignature)?;
1020    let mut b_chall_can = EcBasis {
1021        p: p_pt,
1022        q: q_pt,
1023        pmq: pmq_pt,
1024    };
1025
1026    // Auxiliary basis (still requires hint-based generation).
1027    let b_aux_can =
1028        basis_from_hint(&mut e_aux, L::F_CHR, sig.hint_aux).ok_or(Error::InvalidSignature)?;
1029    let dbl_aux = L::F_CHR as usize - pow_dim2_deg_resp as usize - HD_EXTRA_TORSION as usize;
1030    let b_aux_can = crate::ec::point::ec_dbl_iter_basis(&b_aux_can, dbl_aux, &mut e_aux);
1031
1032    // Canonical encoding: unused hint bits must be zero to prevent malleability.
1033    if sig.two_resp_length == 0 && sig.kernel_is_q {
1034        return Err(Error::InvalidSignature);
1035    }
1036
1037    if sig.two_resp_length > 0 {
1038        two_response_isogeny_verify_inner(
1039            &mut e_chall,
1040            &mut b_chall_can,
1041            sig.kernel_is_q,
1042            sig.two_resp_length,
1043            pow_dim2_deg_resp,
1044        )
1045        .ok_or(Error::InvalidSignature)?;
1046    }
1047
1048    // Theta chain and commitment curve.
1049    let e_com = compute_commitment_curve_verify(
1050        &b_chall_can,
1051        &b_aux_can,
1052        &e_chall,
1053        &e_aux,
1054        pow_dim2_deg_resp,
1055    )
1056    .ok_or(Error::InvalidSignature)?;
1057
1058    // Final hash check.
1059    let chk_chall = hash_to_challenge(pk, &e_com, msg)?;
1060    if mp_compare::<L>(&sig.chall_coeff, &chk_chall) != 0 {
1061        return Err(Error::InvalidSignature);
1062    }
1063
1064    Ok(())
1065}
1066
1067/// Verify a compressed signature (reconstructs the dropped entry then verifies).
1068pub(crate) fn verify_compressed<L: FpBackend + LevelPrecomp>(
1069    pk: &PublicKey<L>,
1070    msg: &[u8],
1071    sig: &CompressedSignature<L>,
1072) -> Result<(), Error> {
1073    let standard = sig.decompress(pk)?;
1074    protocols_verify(pk, msg, &standard)
1075}
1076
1077#[cfg(test)]
1078mod tests {
1079    extern crate std;
1080
1081    use super::*;
1082    use crate::params::{Level1, Level3, Level5};
1083
1084    #[test]
1085    fn standard_sizes() {
1086        assert_eq!(<Level1 as SecurityLevel>::SigLen::USIZE, 148);
1087        assert_eq!(<Level3 as SecurityLevel>::SigLen::USIZE, 224);
1088        assert_eq!(<Level5 as SecurityLevel>::SigLen::USIZE, 292);
1089    }
1090
1091    #[test]
1092    fn expanded_sizes() {
1093        // 3 × Fp2EncodedBytes + LAMBDA/8 + 4
1094        assert_eq!(ExpandedSignature::<Level1>::WIRE_BYTES, 212);
1095        assert_eq!(ExpandedSignature::<Level3>::WIRE_BYTES, 316);
1096        assert_eq!(ExpandedSignature::<Level5>::WIRE_BYTES, 420);
1097    }
1098
1099    #[test]
1100    fn compressed_sizes() {
1101        assert_eq!(CompressedSignature::<Level1>::WIRE_BYTES, 129);
1102        assert_eq!(CompressedSignature::<Level3>::WIRE_BYTES, 196);
1103        assert_eq!(CompressedSignature::<Level5>::WIRE_BYTES, 257);
1104    }
1105
1106    #[test]
1107    #[allow(clippy::assertions_on_constants)]
1108    fn size_ordering() {
1109        assert!(
1110            CompressedSignature::<Level1>::WIRE_BYTES < <Level1 as SecurityLevel>::SigLen::USIZE
1111        );
1112        assert!(<Level1 as SecurityLevel>::SigLen::USIZE < ExpandedSignature::<Level1>::WIRE_BYTES);
1113    }
1114
1115    #[test]
1116    fn standard_signature_backward_compatible() {
1117        let sig = Signature::<Level1>::default();
1118        let bytes = sig.to_bytes();
1119        let decoded = Signature::<Level1>::from_bytes(&bytes);
1120        assert!(decoded.is_ok());
1121    }
1122
1123    #[test]
1124    fn any_signature_standard_roundtrip() {
1125        let sig = Signature::<Level1>::default();
1126        let bytes = sig.to_bytes();
1127        let decoded = AnySignature::<Level1>::from_bytes(&bytes);
1128        assert!(decoded.is_ok());
1129        match decoded.unwrap() {
1130            AnySignature::Standard(_) => {}
1131            _ => panic!("expected Standard variant"),
1132        }
1133    }
1134
1135    #[test]
1136    fn any_signature_rejects_wrong_length() {
1137        let bad = [0u8; 200];
1138        assert!(AnySignature::<Level1>::from_bytes(&bad).is_err());
1139    }
1140
1141    #[test]
1142    fn any_signature_rejects_empty() {
1143        assert!(AnySignature::<Level1>::from_bytes(&[]).is_err());
1144    }
1145
1146    #[test]
1147    fn any_signature_format_accessor() {
1148        let sig = Signature::<Level1>::default();
1149        let any = AnySignature::Standard(sig);
1150        assert_eq!(any.format(), SignatureFormat::Standard);
1151    }
1152
1153    #[test]
1154    fn expanded_from_bytes_too_short() {
1155        let data = [0u8; 100];
1156        assert!(matches!(
1157            ExpandedSignature::<Level1>::from_bytes(&data),
1158            Err(Error::InvalidLength)
1159        ));
1160    }
1161
1162    #[test]
1163    fn expanded_serialization_roundtrip() {
1164        let sig = ExpandedSignature::<Level1> {
1165            e_aux_a: Fp2::zero(),
1166            backtracking: 3,
1167            two_resp_length: 1,
1168            chall_coeff: Scalar::default(),
1169            p_chl_x: Fp2::zero(),
1170            q_chl_x: Fp2::zero(),
1171            kernel_is_q: true,
1172            pmq_sign_hint: true,
1173            hint_aux: 0xAB,
1174            hint_chall: 0xCD,
1175        };
1176        let buf = sig.to_bytes();
1177        let decoded = ExpandedSignature::<Level1>::from_bytes(
1178            &buf[..ExpandedSignature::<Level1>::WIRE_BYTES],
1179        )
1180        .expect("roundtrip decode failed");
1181        assert_eq!(decoded.backtracking, 3);
1182        assert_eq!(decoded.two_resp_length, 1);
1183        assert!(decoded.kernel_is_q);
1184        assert!(decoded.pmq_sign_hint);
1185        assert_eq!(decoded.hint_aux, 0xAB);
1186        assert_eq!(decoded.hint_chall, 0xCD);
1187    }
1188
1189    #[test]
1190    fn expanded_kernel_flag_packing() {
1191        let sig = ExpandedSignature::<Level1> {
1192            e_aux_a: Fp2::zero(),
1193            backtracking: 5,
1194            two_resp_length: 0,
1195            chall_coeff: Scalar::default(),
1196            p_chl_x: Fp2::zero(),
1197            q_chl_x: Fp2::zero(),
1198            kernel_is_q: false,
1199            pmq_sign_hint: false,
1200            hint_aux: 0,
1201            hint_chall: 0,
1202        };
1203        let buf = sig.to_bytes();
1204        let decoded = ExpandedSignature::<Level1>::from_bytes(
1205            &buf[..ExpandedSignature::<Level1>::WIRE_BYTES],
1206        )
1207        .unwrap();
1208        assert_eq!(decoded.backtracking, 5);
1209        assert!(!decoded.kernel_is_q);
1210    }
1211
1212    #[test]
1213    fn compressed_from_bytes_too_short() {
1214        let data = [0u8; 100];
1215        assert!(matches!(
1216            CompressedSignature::<Level1>::from_bytes(&data),
1217            Err(Error::InvalidLength)
1218        ));
1219    }
1220
1221    #[test]
1222    fn compressed_serialization_roundtrip() {
1223        let sig = CompressedSignature::<Level1> {
1224            e_aux_a: Fp2::zero(),
1225            backtracking: 2,
1226            two_resp_length: 1,
1227            mat_00: Scalar::default(),
1228            mat_01: Scalar::default(),
1229            mat_var: Scalar::default(),
1230            chall_coeff: Scalar::default(),
1231            det_hint: 0x03,
1232        };
1233        let buf = sig.to_bytes();
1234        let decoded = CompressedSignature::<Level1>::from_bytes(
1235            &buf[..CompressedSignature::<Level1>::WIRE_BYTES],
1236        )
1237        .expect("roundtrip decode failed");
1238        assert_eq!(decoded.backtracking, 2);
1239        assert_eq!(decoded.two_resp_length, 1);
1240        assert_eq!(decoded.det_hint, 0x03);
1241    }
1242
1243    #[test]
1244    fn expand_does_not_panic_on_default_inputs() {
1245        let sig = Signature::<Level1>::default();
1246        let pk = PublicKey::<Level1>::default();
1247        let _ = sig.expand(&pk);
1248    }
1249
1250    #[test]
1251    fn compress_default_signature() {
1252        let sig = Signature::<Level1>::default();
1253        let compressed = sig.compress();
1254        assert_eq!(compressed.backtracking, 0);
1255        assert_eq!(compressed.two_resp_length, 0);
1256    }
1257
1258    #[test]
1259    fn hensel_inverse_basic() {
1260        let mut out = [0u64; 4];
1261        let a = [3u64, 0, 0, 0];
1262        hensel_inv_mod_2e(&mut out, &a, 64);
1263        // Verify: a * out ≡ 1 mod 2^64
1264        let product = (3u128) * (out[0] as u128);
1265        assert_eq!(product as u64, 1);
1266    }
1267
1268    #[test]
1269    fn hensel_inverse_multiword() {
1270        let mut out = [0u64; 4];
1271        let a = [7u64, 0, 0, 0];
1272        hensel_inv_mod_2e(&mut out, &a, 125);
1273        // Verify: a * out ≡ 1 mod 2^125
1274        let mut check = [0u64; 4];
1275        mp_mul_mod(&mut check, &a, &out, 125);
1276        assert_eq!(check[0], 1);
1277        assert_eq!(check[1], 0);
1278    }
1279}