nautilus_lighter/signing/schnorr/
sig.rs1use crate::signing::{
37 curve::{Point, SCALAR_BYTES, Scalar},
38 field::Fp5,
39 hash::hash_two_to_quintic,
40};
41
42pub const SIG_BYTES: usize = 2 * SCALAR_BYTES;
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
49pub struct Signature {
50 pub s: Scalar,
52 pub e: Scalar,
54}
55
56impl Signature {
57 #[must_use]
59 pub fn to_le_bytes(&self) -> [u8; SIG_BYTES] {
60 let mut out = [0u8; SIG_BYTES];
61 out[..SCALAR_BYTES].copy_from_slice(&self.s.to_le_bytes());
62 out[SCALAR_BYTES..].copy_from_slice(&self.e.to_le_bytes());
63 out
64 }
65
66 #[must_use]
73 pub fn from_le_bytes_reduce(bytes: [u8; SIG_BYTES]) -> Self {
74 let mut s_buf = [0u8; SCALAR_BYTES];
75 let mut e_buf = [0u8; SCALAR_BYTES];
76 s_buf.copy_from_slice(&bytes[..SCALAR_BYTES]);
77 e_buf.copy_from_slice(&bytes[SCALAR_BYTES..]);
78
79 Self {
80 s: Scalar::from_le_bytes_reduce(s_buf),
81 e: Scalar::from_le_bytes_reduce(e_buf),
82 }
83 }
84
85 #[must_use]
87 pub fn is_canonical(&self) -> bool {
88 self.s.is_canonical() && self.e.is_canonical()
89 }
90}
91
92#[must_use]
96pub(super) fn sign(sk: Scalar, hashed_msg: Fp5, k: Scalar) -> Signature {
97 let r = Point::mulgen_ct(k).encode();
98 let e = Scalar::from_fp5(hash_two_to_quintic(r, hashed_msg));
99
100 Signature { s: k - e * sk, e }
101}
102
103#[must_use]
113pub(super) fn verify(pk_w: Fp5, hashed_msg: Fp5, sig: &Signature) -> bool {
114 if !sig.is_canonical() {
115 return false;
116 }
117
118 let pk = match Point::decode(pk_w) {
119 Some(p) => p,
120 None => return false,
121 };
122
123 let r_v = (Point::mulgen(sig.s) + pk.scalar_mul(sig.e)).encode();
124 let e_v = Scalar::from_fp5(hash_two_to_quintic(r_v, hashed_msg));
125
126 e_v == sig.e
127}
128
129#[cfg(test)]
130mod tests {
131 use proptest::prelude::*;
132 use rstest::rstest;
133 use serde::Deserialize;
134
135 use super::*;
136 use crate::signing::{
137 fixtures::{
138 arb_fp5, arb_scalar, bytes_to_hex, decode_fp5_bytes, decode_scalar_bytes,
139 decode_sig_bytes,
140 },
141 schnorr::{PrivateKey, PublicKey},
142 };
143
144 const VECTORS_JSON: &str = include_str!(concat!(
145 env!("CARGO_MANIFEST_DIR"),
146 "/test_data/signing_schnorr_vectors.json",
147 ));
148
149 #[derive(Debug, Deserialize)]
150 struct VectorsFile {
151 vectors: Vectors,
152 }
153
154 #[derive(Debug, Deserialize)]
155 struct Vectors {
156 keygen: Vec<KeyGenVector>,
157 sign: Vec<SignVector>,
158 }
159
160 #[derive(Debug, Deserialize)]
161 struct KeyGenVector {
162 sk: String,
163 pk: String,
164 }
165
166 #[derive(Debug, Deserialize)]
167 struct SignVector {
168 sk: String,
169 hashed_msg: String,
170 k: String,
171 sig: String,
172 }
173
174 #[rstest]
175 fn keygen_matches_go_reference_vectors() {
176 let suite: VectorsFile = serde_json::from_str(VECTORS_JSON).expect("parse vectors");
177 assert!(!suite.vectors.keygen.is_empty(), "keygen vectors empty");
178
179 for (i, v) in suite.vectors.keygen.iter().enumerate() {
180 let sk = PrivateKey::from_le_bytes_reduce(decode_scalar_bytes(&v.sk));
181 let pk = sk.public_key();
182 assert_eq!(
183 bytes_to_hex(&pk.to_le_bytes()),
184 v.pk,
185 "vector {i}: pk encoding diverged",
186 );
187 }
188 }
189
190 #[rstest]
191 fn sign_matches_go_reference_vectors() {
192 let suite: VectorsFile = serde_json::from_str(VECTORS_JSON).expect("parse vectors");
193 assert!(!suite.vectors.sign.is_empty(), "sign vectors empty");
194
195 for (i, v) in suite.vectors.sign.iter().enumerate() {
196 let sk = PrivateKey::from_le_bytes_reduce(decode_scalar_bytes(&v.sk));
197 let hashed_msg = decode_fp5_bytes(&v.hashed_msg);
198 let k = Scalar::from_le_bytes_reduce(decode_scalar_bytes(&v.k));
199
200 let sig = sk.sign(hashed_msg, k);
201 assert_eq!(
202 bytes_to_hex(&sig.to_le_bytes()),
203 v.sig,
204 "vector {i}: signature bytes diverged",
205 );
206
207 let pk = sk.public_key();
208 assert!(pk.verify(hashed_msg, &sig), "vector {i}: verify failed");
209 }
210 }
211
212 #[rstest]
213 fn signature_round_trip_through_bytes() {
214 let suite: VectorsFile = serde_json::from_str(VECTORS_JSON).expect("parse vectors");
215 let v = &suite.vectors.sign[0];
216
217 let bytes = decode_sig_bytes(&v.sig);
218 let sig = Signature::from_le_bytes_reduce(bytes);
219 assert!(sig.is_canonical(), "fixture sig must be canonical");
220 assert_eq!(sig.to_le_bytes(), bytes, "round trip diverged");
221 }
222
223 #[rstest]
224 fn verify_rejects_corrupted_signature() {
225 let suite: VectorsFile = serde_json::from_str(VECTORS_JSON).expect("parse vectors");
226 let v = &suite.vectors.sign[0];
227
228 let sk = PrivateKey::from_le_bytes_reduce(decode_scalar_bytes(&v.sk));
229 let hashed_msg = decode_fp5_bytes(&v.hashed_msg);
230 let k = Scalar::from_le_bytes_reduce(decode_scalar_bytes(&v.k));
231 let mut sig = sk.sign(hashed_msg, k);
232 let pk = sk.public_key();
233
234 sig.s += Scalar::ONE;
235 assert!(!pk.verify(hashed_msg, &sig), "tampered sig must not verify");
236 }
237
238 #[rstest]
239 fn verify_rejects_wrong_message() {
240 let suite: VectorsFile = serde_json::from_str(VECTORS_JSON).expect("parse vectors");
241 let v = &suite.vectors.sign[0];
242
243 let sk = PrivateKey::from_le_bytes_reduce(decode_scalar_bytes(&v.sk));
244 let hashed_msg = decode_fp5_bytes(&v.hashed_msg);
245 let k = Scalar::from_le_bytes_reduce(decode_scalar_bytes(&v.k));
246 let sig = sk.sign(hashed_msg, k);
247 let pk = sk.public_key();
248
249 let other_msg = Fp5::from_u64s_reduce([1, 2, 3, 4, 5]);
250 assert!(
251 !pk.verify(other_msg, &sig),
252 "verify must fail for a different message",
253 );
254 }
255
256 #[rstest]
257 fn verify_rejects_malformed_pubkey() {
258 let bad = Fp5::from_u64s_reduce([
260 13_557_832_913_345_268_708,
261 15_669_280_705_791_538_619,
262 8_534_654_657_267_986_396,
263 12_533_218_303_838_131_749,
264 5_058_070_698_878_426_028,
265 ]);
266 let pk = crate::signing::schnorr::PublicKey::from_fp5(bad);
267
268 let sig = Signature {
269 s: Scalar::ONE,
270 e: Scalar::ONE,
271 };
272 assert!(
273 !pk.verify(Fp5::ZERO, &sig),
274 "verify must fail when pk does not decode",
275 );
276 }
277
278 fn non_canonical_scalar() -> Scalar {
283 crate::signing::curve::ORDER
284 }
285
286 #[rstest]
287 #[case(true, true, true)]
288 #[case(false, true, false)]
289 #[case(true, false, false)]
290 #[case(false, false, false)]
291 fn signature_is_canonical_truth_table(
292 #[case] s_canonical: bool,
293 #[case] e_canonical: bool,
294 #[case] expected: bool,
295 ) {
296 let canon = Scalar::ONE;
297 let non_canon = non_canonical_scalar();
298 let sig = Signature {
299 s: if s_canonical { canon } else { non_canon },
300 e: if e_canonical { canon } else { non_canon },
301 };
302 assert_eq!(sig.is_canonical(), expected);
303 }
304
305 #[rstest]
306 fn verify_rejects_non_canonical_signature() {
307 let suite: VectorsFile = serde_json::from_str(VECTORS_JSON).expect("parse vectors");
308 let v = &suite.vectors.sign[0];
309
310 let sk = PrivateKey::from_le_bytes_reduce(decode_scalar_bytes(&v.sk));
311 let hashed_msg = decode_fp5_bytes(&v.hashed_msg);
312 let pk = sk.public_key();
313
314 let bad = non_canonical_scalar();
315 let sig = Signature { s: bad, e: bad };
316
317 assert!(
318 !pk.verify(hashed_msg, &sig),
319 "verify must reject non-canonical signature scalars",
320 );
321 }
322
323 proptest! {
324 #[rstest]
327 fn prop_sign_verify_round_trip(
328 sk in arb_scalar(),
329 k in arb_scalar(),
330 hashed_msg in arb_fp5(),
331 ) {
332 prop_assume!(!k.is_zero());
335 prop_assume!(!sk.is_zero());
339
340 let private_key = PrivateKey::from_scalar(sk);
341 let pk = private_key.public_key();
342 let sig = private_key.sign(hashed_msg, k);
343 prop_assert!(pk.verify(hashed_msg, &sig));
344 }
345
346 #[rstest]
349 fn prop_signature_bytes_round_trip(
350 sk in arb_scalar(),
351 k in arb_scalar(),
352 hashed_msg in arb_fp5(),
353 ) {
354 prop_assume!(!sk.is_zero());
355 prop_assume!(!k.is_zero());
356 let sig = PrivateKey::from_scalar(sk).sign(hashed_msg, k);
357 prop_assert!(sig.is_canonical());
358 let decoded = Signature::from_le_bytes_reduce(sig.to_le_bytes());
359 prop_assert_eq!(decoded, sig);
360 }
361 }
362
363 proptest! {
364 #![proptest_config(ProptestConfig {
368 cases: 64,
369 ..ProptestConfig::default()
370 })]
371
372 #[rstest]
374 fn prop_verify_rejects_single_bit_flip_in_s(
375 sk in arb_scalar(),
376 k in arb_scalar(),
377 hashed_msg in arb_fp5(),
378 byte_idx in 0usize..SCALAR_BYTES,
379 bit in 0u8..8,
380 ) {
381 prop_assume!(!sk.is_zero());
382 prop_assume!(!k.is_zero());
383 let private_key = PrivateKey::from_scalar(sk);
384 let pk = private_key.public_key();
385 let sig = private_key.sign(hashed_msg, k);
386
387 let mut bytes = sig.to_le_bytes();
388 bytes[byte_idx] ^= 1 << bit;
389 let tampered = Signature::from_le_bytes_reduce(bytes);
390 prop_assume!(tampered != sig);
391 prop_assert!(!pk.verify(hashed_msg, &tampered));
392 }
393
394 #[rstest]
396 fn prop_verify_rejects_single_bit_flip_in_e(
397 sk in arb_scalar(),
398 k in arb_scalar(),
399 hashed_msg in arb_fp5(),
400 byte_idx in SCALAR_BYTES..SIG_BYTES,
401 bit in 0u8..8,
402 ) {
403 prop_assume!(!sk.is_zero());
404 prop_assume!(!k.is_zero());
405 let private_key = PrivateKey::from_scalar(sk);
406 let pk = private_key.public_key();
407 let sig = private_key.sign(hashed_msg, k);
408
409 let mut bytes = sig.to_le_bytes();
410 bytes[byte_idx] ^= 1 << bit;
411 let tampered = Signature::from_le_bytes_reduce(bytes);
412 prop_assume!(tampered != sig);
413 prop_assert!(!pk.verify(hashed_msg, &tampered));
414 }
415
416 #[rstest]
421 fn prop_verify_rejects_perturbed_pubkey(
422 sk in arb_scalar(),
423 k in arb_scalar(),
424 hashed_msg in arb_fp5(),
425 offset in arb_fp5(),
426 ) {
427 prop_assume!(!sk.is_zero());
428 prop_assume!(!k.is_zero());
429 prop_assume!(!offset.is_zero());
430
431 let private_key = PrivateKey::from_scalar(sk);
432 let pk = private_key.public_key();
433 let sig = private_key.sign(hashed_msg, k);
434 prop_assert!(pk.verify(hashed_msg, &sig));
435
436 let perturbed = PublicKey::from_fp5(pk.as_fp5() + offset);
437 prop_assume!(perturbed.as_fp5() != pk.as_fp5());
440 prop_assert!(!perturbed.verify(hashed_msg, &sig));
441 }
442 }
443
444 #[rstest]
448 fn verify_under_neutral_pubkey_returns_false() {
449 let pk = PublicKey::from_fp5(Fp5::ZERO);
450 let sig = Signature {
451 s: Scalar::ONE,
452 e: Scalar::ONE,
453 };
454 assert!(
455 !pk.verify(Fp5::from_u64s_reduce([1, 2, 3, 4, 5]), &sig),
456 "verify under neutral pk must return false for an unrelated sig",
457 );
458 }
459
460 #[rstest]
461 fn from_le_bytes_reduce_normalizes_non_canonical_signature() {
462 let suite: VectorsFile = serde_json::from_str(VECTORS_JSON).expect("parse vectors");
463 let v = &suite.vectors.sign[0];
464
465 let sk = PrivateKey::from_le_bytes_reduce(decode_scalar_bytes(&v.sk));
466 let hashed_msg = decode_fp5_bytes(&v.hashed_msg);
467 let k = Scalar::from_le_bytes_reduce(decode_scalar_bytes(&v.k));
468 let canonical = sk.sign(hashed_msg, k);
469 let pk = sk.public_key();
470
471 let s_inflated = canonical.s.add_inner(crate::signing::curve::ORDER);
474 let e_inflated = canonical.e.add_inner(crate::signing::curve::ORDER);
475 let mut bytes = [0u8; SIG_BYTES];
476 bytes[..SCALAR_BYTES].copy_from_slice(&s_inflated.to_le_bytes());
477 bytes[SCALAR_BYTES..].copy_from_slice(&e_inflated.to_le_bytes());
478
479 let reduced = Signature::from_le_bytes_reduce(bytes);
480 assert!(
481 reduced.is_canonical(),
482 "reduced signature must be canonical",
483 );
484 assert_eq!(
485 reduced, canonical,
486 "reduction must recover the canonical sig"
487 );
488 assert!(
489 pk.verify(hashed_msg, &reduced),
490 "reduced signature must still verify",
491 );
492 }
493}