Skip to main content

tecdsa_core/sign/
mod.rs

1//! Threshold signing (Protocol 3, spec §6.3): two rounds, S → C → S.
2//!
3//! 1. S → C `SignMsg1`: `R₂ = g^{k₂}`, `Y = X₁^{k₂}`.
4//! 2. C → S `SignMsg2`: `R₁`, `R`, `S = enc(u; ρ^{λ₀})·E^v` and `ψ_sig`.
5//! 3. S finishes: `r = 𝒳(R₁^{k₂+e})`, `σ = dec(S)·(k₂+e)^{-1}`, checked as a plain ECDSA signature,
6//!    low-s normalized, with a recovery id. Optionally `SignMsg3` carries the result to C.
7use k256::{
8    ProjectivePoint, Scalar, U256,
9    ecdsa::{
10        RecoveryId, Signature as EcdsaSignature, VerifyingKey, signature::hazmat::PrehashVerifier,
11    },
12    elliptic_curve::{ops::Reduce, point::AffineCoordinates, scalar::IsHigh},
13};
14use rand_core::CryptoRng;
15
16use crate::{
17    curve::{decode_scalar, encode_point, encode_scalar},
18    error::{Error, Result},
19    hd::HdPath,
20    params::Params,
21    ssid::Ssid,
22    transcript::Transcript,
23};
24
25pub mod client;
26pub mod server;
27
28pub use client::ClientSign;
29pub use server::{ServerSign0, ServerSign1};
30
31/// A finished ECDSA signature with low `s` and its recovery id.
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub struct Signature {
34    pub r: Scalar,
35    pub s: Scalar,
36    /// Bit 0: parity of `y(R)` (after low-s normalization); bit 1: `x(R) ≥ q`.
37    pub recid: u8,
38}
39
40impl Signature {
41    /// 64-byte `r ‖ s`.
42    pub fn to_bytes(&self) -> [u8; 64] {
43        let mut out = [0u8; 64];
44        out[..32].copy_from_slice(&encode_scalar(&self.r));
45        out[32..].copy_from_slice(&encode_scalar(&self.s));
46        out
47    }
48
49    pub fn to_ecdsa(&self) -> Result<EcdsaSignature> {
50        EcdsaSignature::from_scalars(self.r, self.s).map_err(|_| Error::InvalidEncoding)
51    }
52
53    pub fn recovery_id(&self) -> Result<RecoveryId> {
54        RecoveryId::from_byte(self.recid).ok_or(Error::InvalidEncoding)
55    }
56}
57
58/// Digest `m` as a scalar (`bits2int` for a 256-bit digest, reduced mod `q`).
59pub(crate) fn digest_scalar(digest: &[u8; 32]) -> Scalar {
60    <Scalar as Reduce<U256>>::reduce(&U256::from_be_slice(digest))
61}
62
63/// Rejects a digest that reduces to zero mod `q`. The reference implementation requires this
64/// (its comments cite Claim 7.15 of the BAM paper); the claim itself was not re-checked here.
65pub(crate) fn check_digest(digest: &[u8; 32]) -> Result<()> {
66    if bool::from(k256::elliptic_curve::Field::is_zero(&digest_scalar(digest))) {
67        return Err(Error::InvalidEncoding);
68    }
69    Ok(())
70}
71
72/// The key a signature is made under and the client-share shift: `(X + δ·G, δ)` for an HD
73/// path, `(X, 0)` without one.
74pub(crate) fn signing_key(
75    x_pub: &ProjectivePoint,
76    hd: Option<&HdPath>,
77) -> Result<(ProjectivePoint, Scalar)> {
78    match hd {
79        None => Ok((*x_pub, Scalar::ZERO)),
80        Some(h) => {
81            let delta = h.delta(x_pub)?;
82            Ok((*x_pub + ProjectivePoint::GENERATOR * delta, delta))
83        }
84    }
85}
86
87/// Base transcript of a signing session for `role` (`"S"` or `"C"`).
88pub(crate) fn tx_base(params: &Params, ssid: &Ssid, role: &str) -> Transcript {
89    let mut t = Transcript::new("sign");
90    t.append("params_id", &[params.id as u8]);
91    t.append("ssid", &ssid.encode());
92    t.append("role", role.as_bytes());
93    t
94}
95
96/// `e = RO(X, R₁, R, m)` (domain `ro/sign-nonce`, bound to the ssid as well).
97pub(crate) fn nonce_challenge(
98    params: &Params,
99    ssid: &Ssid,
100    x_pub: &ProjectivePoint,
101    r1: &ProjectivePoint,
102    r: &ProjectivePoint,
103    digest: &[u8; 32],
104) -> Scalar {
105    let mut t = Transcript::new("ro/sign-nonce");
106    t.append("params_id", &[params.id as u8]);
107    t.append("ssid", &ssid.encode());
108    t.append("X", &encode_point(x_pub));
109    t.append("R1", &encode_point(r1));
110    t.append("R", &encode_point(r));
111    t.append("m", digest);
112    t.challenge("e").scalar()
113}
114
115/// Hedged nonce `k = H(fresh randomness ‖ secret ‖ context) mod q`, never zero (FR-G4).
116pub(crate) fn hedged_nonce<R: CryptoRng + ?Sized>(
117    rng: &mut R,
118    label: &str,
119    secret: &Scalar,
120    context: &[u8],
121) -> Scalar {
122    let mut fresh = [0u8; 32];
123    for i in 0u32.. {
124        rng.fill_bytes(&mut fresh);
125        let mut t = Transcript::new("hedged-nonce");
126        t.append("label", label.as_bytes());
127        t.append("rng", &fresh);
128        t.append("secret", &encode_scalar(secret));
129        t.append("ctx", context);
130        t.append("ctr", &i.to_be_bytes());
131        let k = t.challenge("k").scalar();
132        if !bool::from(k256::elliptic_curve::Field::is_zero(&k)) {
133            return k;
134        }
135    }
136    Scalar::ONE
137}
138
139/// Plain ECDSA verification of `(r, s)` against `X` and the digest.
140pub(crate) fn ecdsa_verify(
141    x_pub: &ProjectivePoint,
142    digest: &[u8; 32],
143    sig: &Signature,
144) -> Result<()> {
145    let vk = VerifyingKey::from_affine(x_pub.to_affine()).map_err(|_| Error::VerificationFailed)?;
146    vk.verify_prehash(digest, &sig.to_ecdsa()?)
147        .map_err(|_| Error::VerificationFailed)
148}
149
150/// Low-s normalization and recovery id from the nonce point `R = (x, y)` (FR-G1, FR-G2).
151pub(crate) fn finalize(r_point: &ProjectivePoint, r: Scalar, s: Scalar) -> Signature {
152    let affine = r_point.to_affine();
153    let y_odd: bool = affine.y_is_odd().into();
154    let x_reduced = encode_scalar(&r) != affine.x().as_slice();
155    let mut recid = u8::from(y_odd) | (u8::from(x_reduced) << 1);
156    let mut s = s;
157    if bool::from(s.is_high()) {
158        s = -s;
159        recid ^= 1;
160    }
161    Signature { r, s, recid }
162}
163
164/// Decodes `r ‖ s` canonical scalars and a recovery id.
165pub(crate) fn decode_signature(r: &[u8], s: &[u8], recid: u8) -> Result<Signature> {
166    let sig = Signature {
167        r: decode_scalar(r)?,
168        s: decode_scalar(s)?,
169        recid,
170    };
171    sig.recovery_id()?;
172    Ok(sig)
173}
174
175#[cfg(test)]
176mod tests {
177    extern crate std;
178    use super::*;
179    use crate::hd::HdPath;
180    use crate::{
181        keygen::{ClientKeyShare, ServerKeyShare},
182        testutil::{fx, keygen, params},
183    };
184    use k256::ecdsa::VerifyingKey;
185    use rand_chacha::ChaCha8Rng;
186    use rand_core::{Rng, SeedableRng};
187    use std::{collections::BTreeSet, time::Instant, vec::Vec};
188
189    /// One complete signing session; returns the server's signature and `SignMsg3`.
190    fn sign_once(
191        rng: &mut ChaCha8Rng,
192        c: &ClientKeyShare,
193        s: &ServerKeyShare,
194        digest: &[u8; 32],
195    ) -> (Signature, Vec<u8>) {
196        let (s1, m1) =
197            ServerSign0::start(rng, &fx().server_view, s, b"client-1", digest, None).unwrap();
198        let m2 = ClientSign::respond(rng, &fx().setup, c, b"client-1", digest, &m1, None).unwrap();
199        s1.finish(&fx().server_view, fx().sk.paillier_sk(), &m2)
200            .unwrap()
201    }
202
203    fn digest(rng: &mut ChaCha8Rng) -> [u8; 32] {
204        let mut d = [0u8; 32];
205        rng.fill_bytes(&mut d);
206        d
207    }
208
209    #[test]
210    fn honest_signatures_verify_and_recover() {
211        let (c, s) = keygen(21);
212        let mut rng = ChaCha8Rng::seed_from_u64(1);
213        let vk = VerifyingKey::from_affine(c.public_key().to_affine()).unwrap();
214        let mut parities = BTreeSet::new();
215        for _ in 0..40 {
216            let d = digest(&mut rng);
217            let (sig, m3) = sign_once(&mut rng, &c, &s, &d);
218            // an ordinary ECDSA signature
219            vk.verify_prehash(&d, &sig.to_ecdsa().unwrap()).unwrap();
220            // low-s (BIP-62 / EIP-2)
221            assert!(!bool::from(sig.s.is_high()));
222            // the recovery id recovers X
223            let rec = VerifyingKey::recover_from_prehash(
224                &d,
225                &sig.to_ecdsa().unwrap(),
226                sig.recovery_id().unwrap(),
227            )
228            .unwrap();
229            assert_eq!(rec, vk);
230            // the client accepts the server's SignMsg3
231            assert_eq!(
232                ClientSign::verify_final(&params(), &c, &d, &m3, None).unwrap(),
233                sig
234            );
235            parities.insert(sig.recid & 1);
236        }
237        assert_eq!(parities.len(), 2, "both y parities must occur");
238    }
239
240    #[test]
241    fn same_digest_gives_fresh_nonces() {
242        let (c, s) = keygen(22);
243        let mut rng = ChaCha8Rng::seed_from_u64(2);
244        let d = digest(&mut rng);
245        let (a, _) = sign_once(&mut rng, &c, &s, &d);
246        let (b, _) = sign_once(&mut rng, &c, &s, &d);
247        assert_ne!(a.r, b.r);
248    }
249
250    #[test]
251    fn corrupted_messages_are_rejected() {
252        let (c, s) = keygen(23);
253        let mut rng = ChaCha8Rng::seed_from_u64(3);
254        let d = digest(&mut rng);
255        let (s1, m1) =
256            ServerSign0::start(&mut rng, &fx().server_view, &s, b"client-1", &d, None).unwrap();
257
258        for i in [4usize, 10, 30, 36, 50, m1.len() - 1] {
259            let mut bad = m1.clone();
260            bad[i] ^= 1;
261            assert!(
262                ClientSign::respond(&mut rng, &fx().setup, &c, b"client-1", &d, &bad, None)
263                    .is_err(),
264                "msg1 flip {i}"
265            );
266        }
267        let m2 =
268            ClientSign::respond(&mut rng, &fx().setup, &c, b"client-1", &d, &m1, None).unwrap();
269        let positions = [
270            4usize,
271            20,
272            40,
273            80,
274            120,
275            200,
276            400,
277            m2.len() / 2,
278            m2.len() - 60,
279            m2.len() - 1,
280        ];
281        for i in positions.iter().copied().filter(|i| *i < m2.len()) {
282            let mut bad = m2.clone();
283            bad[i] ^= 1;
284            let r = s1
285                .test_clone()
286                .finish(&fx().server_view, fx().sk.paillier_sk(), &bad);
287            assert!(r.is_err(), "msg2 flip {i}");
288        }
289        assert!(
290            s1.test_clone()
291                .finish(
292                    &fx().server_view,
293                    fx().sk.paillier_sk(),
294                    &m2[..m2.len() - 1]
295                )
296                .is_err()
297        );
298        assert!(
299            s1.finish(&fx().server_view, fx().sk.paillier_sk(), &m2)
300                .is_ok()
301        );
302    }
303
304    #[test]
305    fn session_binding() {
306        let (c, s) = keygen(24);
307        let (c_other, _) = keygen(25);
308        let mut rng = ChaCha8Rng::seed_from_u64(4);
309        let (d1, d2) = (digest(&mut rng), digest(&mut rng));
310
311        // client signs another digest than the server expects
312        let (s1, m1) =
313            ServerSign0::start(&mut rng, &fx().server_view, &s, b"client-1", &d1, None).unwrap();
314        let m2 =
315            ClientSign::respond(&mut rng, &fx().setup, &c, b"client-1", &d2, &m1, None).unwrap();
316        assert!(
317            s1.finish(&fx().server_view, fx().sk.paillier_sk(), &m2)
318                .is_err()
319        );
320
321        // client with another id
322        let (s1, m1) =
323            ServerSign0::start(&mut rng, &fx().server_view, &s, b"client-1", &d1, None).unwrap();
324        let m2 =
325            ClientSign::respond(&mut rng, &fx().setup, &c, b"client-X", &d1, &m1, None).unwrap();
326        assert!(
327            s1.finish(&fx().server_view, fx().sk.paillier_sk(), &m2)
328                .is_err()
329        );
330
331        // SignMsg2 of an earlier session
332        let (_, m1a) =
333            ServerSign0::start(&mut rng, &fx().server_view, &s, b"client-1", &d1, None).unwrap();
334        let m2a =
335            ClientSign::respond(&mut rng, &fx().setup, &c, b"client-1", &d1, &m1a, None).unwrap();
336        let (s1b, _) =
337            ServerSign0::start(&mut rng, &fx().server_view, &s, b"client-1", &d1, None).unwrap();
338        assert!(
339            s1b.finish(&fx().server_view, fx().sk.paillier_sk(), &m2a)
340                .is_err()
341        );
342
343        // a client holding another key rejects the server's SignMsg1 (Y ≠ R₂^{x₁})
344        let (_, m1) =
345            ServerSign0::start(&mut rng, &fx().server_view, &s, b"client-1", &d1, None).unwrap();
346        assert!(
347            ClientSign::respond(&mut rng, &fx().setup, &c_other, b"client-1", &d1, &m1, None)
348                .is_err()
349        );
350    }
351
352    fn hd(chain: u8, path: &[u32]) -> HdPath {
353        HdPath::new([chain; 32], path.to_vec()).unwrap()
354    }
355
356    #[test]
357    fn hd_signatures_verify_under_the_derived_key_only() {
358        let (c, s) = keygen(28);
359        let mut rng = ChaCha8Rng::seed_from_u64(7);
360        let x = VerifyingKey::from_affine(c.public_key().to_affine()).unwrap();
361        for path in [
362            hd(1, &[0]),
363            hd(2, &[44, 0, 0, 0, 7]),
364            hd(3, &[5, 6, 1_000_000]),
365        ] {
366            let d = digest(&mut rng);
367            let (st, m1) = ServerSign0::start(
368                &mut rng,
369                &fx().server_view,
370                &s,
371                b"client-1",
372                &d,
373                Some(&path),
374            )
375            .unwrap();
376            let m2 =
377                ClientSign::respond(&mut rng, &fx().setup, &c, b"client-1", &d, &m1, Some(&path))
378                    .unwrap();
379            let (sig, m3) = st
380                .finish(&fx().server_view, fx().sk.paillier_sk(), &m2)
381                .unwrap();
382
383            let derived = path.delta(c.public_key()).unwrap();
384            let derived_pk = VerifyingKey::from_affine(
385                (*c.public_key() + ProjectivePoint::GENERATOR * derived).to_affine(),
386            )
387            .unwrap();
388            let ecdsa = sig.to_ecdsa().unwrap();
389            derived_pk.verify_prehash(&d, &ecdsa).unwrap();
390            assert!(
391                x.verify_prehash(&d, &ecdsa).is_err(),
392                "must not verify under the master key"
393            );
394            let rec =
395                VerifyingKey::recover_from_prehash(&d, &ecdsa, sig.recovery_id().unwrap()).unwrap();
396            assert_eq!(rec, derived_pk);
397            assert_eq!(
398                ClientSign::verify_final(&params(), &c, &d, &m3, Some(&path)).unwrap(),
399                sig
400            );
401            assert!(ClientSign::verify_final(&params(), &c, &d, &m3, None).is_err());
402        }
403    }
404
405    #[test]
406    fn hd_paths_must_agree() {
407        let (c, s) = keygen(29);
408        let mut rng = ChaCha8Rng::seed_from_u64(8);
409        let d = digest(&mut rng);
410        let p = hd(1, &[1, 2]);
411        let (_, m1) =
412            ServerSign0::start(&mut rng, &fx().server_view, &s, b"client-1", &d, Some(&p)).unwrap();
413        // other path, other chain code, or none at all: the client rejects SignMsg1 (Y ≠ R₂^{x₁'})
414        for other in [Some(hd(1, &[2, 1])), Some(hd(9, &[1, 2])), None] {
415            let r = ClientSign::respond(
416                &mut rng,
417                &fx().setup,
418                &c,
419                b"client-1",
420                &d,
421                &m1,
422                other.as_ref(),
423            );
424            assert!(r.is_err());
425        }
426        // and the other way round
427        let (_, m1) =
428            ServerSign0::start(&mut rng, &fx().server_view, &s, b"client-1", &d, None).unwrap();
429        assert!(
430            ClientSign::respond(&mut rng, &fx().setup, &c, b"client-1", &d, &m1, Some(&p)).is_err()
431        );
432    }
433
434    #[test]
435    fn zero_digest_is_rejected_by_both_sides() {
436        let (c, s) = keygen(27);
437        let mut rng = ChaCha8Rng::seed_from_u64(6);
438        let zero = [0u8; 32];
439        assert!(
440            ServerSign0::start(&mut rng, &fx().server_view, &s, b"client-1", &zero, None).is_err()
441        );
442        // digest equal to the group order reduces to zero as well
443        let order = crate::curve::order();
444        let mut n = [0u8; 32];
445        let bytes = order.to_be_bytes();
446        n.copy_from_slice(&bytes[bytes.len() - 32..]);
447        assert!(
448            ServerSign0::start(&mut rng, &fx().server_view, &s, b"client-1", &n, None).is_err()
449        );
450        let d = digest(&mut rng);
451        let (_, m1) =
452            ServerSign0::start(&mut rng, &fx().server_view, &s, b"client-1", &d, None).unwrap();
453        assert!(
454            ClientSign::respond(&mut rng, &fx().setup, &c, b"client-1", &zero, &m1, None).is_err()
455        );
456    }
457
458    #[test]
459    fn signmsg3_is_checked_by_the_client() {
460        let (c, s) = keygen(26);
461        let mut rng = ChaCha8Rng::seed_from_u64(5);
462        let d = digest(&mut rng);
463        let (sig, m3) = sign_once(&mut rng, &c, &s, &d);
464        let p = params();
465        assert!(ClientSign::verify_final(&p, &c, &digest(&mut rng), &m3, None).is_err());
466        let mut bad = m3.clone();
467        let n = bad.len();
468        bad[n - 5] ^= 1; // inside s
469        assert!(ClientSign::verify_final(&p, &c, &d, &bad, None).is_err());
470        let wrong_recid = crate::messages::SignMsg3 {
471            r: encode_scalar(&sig.r).to_vec(),
472            s: encode_scalar(&sig.s).to_vec(),
473            recid: sig.recid ^ 1,
474        }
475        .encode(&p)
476        .unwrap();
477        assert!(ClientSign::verify_final(&p, &c, &d, &wrong_recid, None).is_err());
478    }
479
480    #[test]
481    #[ignore = "long: run with --release --ignored; N from TECDSA_SIGN_COUNT (default 10000)"]
482    fn many_signatures_reduced() {
483        let n: usize = std::env::var("TECDSA_SIGN_COUNT")
484            .ok()
485            .and_then(|v| v.parse().ok())
486            .unwrap_or(10_000);
487        let (c, s) = keygen(30);
488        let vk = VerifyingKey::from_affine(c.public_key().to_affine()).unwrap();
489        let mut rng = ChaCha8Rng::seed_from_u64(30);
490        let t = Instant::now();
491        for _ in 0..n {
492            let d = digest(&mut rng);
493            let (sig, _) = sign_once(&mut rng, &c, &s, &d);
494            vk.verify_prehash(&d, &sig.to_ecdsa().unwrap()).unwrap();
495            let rec = VerifyingKey::recover_from_prehash(
496                &d,
497                &sig.to_ecdsa().unwrap(),
498                sig.recovery_id().unwrap(),
499            )
500            .unwrap();
501            assert_eq!(rec, vk);
502        }
503        std::eprintln!("{n} signatures in {:?}", t.elapsed());
504    }
505
506    #[test]
507    #[ignore = "full-size: run with --release --ignored --nocapture"]
508    fn full_size_sign() {
509        use crate::{params::PARAMS_128, setup::server_setup};
510        let p = PARAMS_128;
511        let mut rng = ChaCha8Rng::seed_from_u64(77);
512        let (data, sk) = server_setup(&mut rng, &p, [1u8; 32], b"server").unwrap();
513        let client_view = data.verify(&p).unwrap().with_tables();
514        let server_view = data.assume_verified(&p).unwrap().with_tables();
515        std::eprintln!("tables: {} KiB", client_view.tables_size_bytes() / 1024);
516        let clock = crate::testutil::TestClock(core::cell::Cell::new(0));
517        let (s1, m1) = crate::keygen::ServerKeygen0::start(&mut rng, &p, &data, b"client").unwrap();
518        let (c1, m2) =
519            crate::keygen::ClientKeygen0::on_msg1(&mut rng, &clock, &client_view, b"client", &m1)
520                .unwrap();
521        let (s_share, m3) = s1.on_msg2(&mut rng, &m2).unwrap();
522        let c_share = c1.on_msg3(&clock, &m3).unwrap();
523        let (mut tc, mut ts1, mut ts3) = (Vec::new(), Vec::new(), Vec::new());
524        let mut size2 = 0;
525        let count: usize = std::env::var("TECDSA_FULL_SIGN_COUNT")
526            .ok()
527            .and_then(|v| v.parse().ok())
528            .unwrap_or(20);
529        for _ in 0..count {
530            let d = digest(&mut rng);
531            let t = Instant::now();
532            let (st, m1) =
533                ServerSign0::start(&mut rng, &server_view, &s_share, b"client", &d, None).unwrap();
534            ts1.push(t.elapsed());
535            let t = Instant::now();
536            let m2 =
537                ClientSign::respond(&mut rng, &client_view, &c_share, b"client", &d, &m1, None)
538                    .unwrap();
539            tc.push(t.elapsed());
540            size2 = m2.len();
541            let t = Instant::now();
542            let (sig, _) = st.finish(&server_view, sk.paillier_sk(), &m2).unwrap();
543            ts3.push(t.elapsed());
544            let vk = VerifyingKey::from_affine(c_share.public_key().to_affine()).unwrap();
545            vk.verify_prehash(&d, &sig.to_ecdsa().unwrap()).unwrap();
546        }
547        let med = |v: &mut Vec<core::time::Duration>| {
548            v.sort();
549            v[v.len() / 2]
550        };
551        std::eprintln!(
552            "sign (median of {count}): S1 {:?} | client {:?} (SignMsg2 {size2} B) | S3 {:?}",
553            med(&mut ts1),
554            med(&mut tc),
555            med(&mut ts3)
556        );
557    }
558}