Skip to main content

tecdsa_core/keygen/
mod.rs

1//! Distributed key generation (Protocol 2, spec §6.2): four messages, S–C.
2//!
3//! 1. S → C `KgMsg1`: nonce and commitment `B = RO(ssid, S, X₂)`.
4//! 2. C → S `KgMsg2`: `X₁`, ephemeral DF tuple, `ψ_df*`, `ψ_dlog`.
5//! 3. S → C `KgMsg3`: `X₂`, `E`, `ψ_dlenc`.
6//! 4. C verifies and outputs.
7use alloc::vec::Vec;
8use crypto_bigint::BoxedUint;
9use k256::{ProjectivePoint, Scalar};
10use sha2::{Digest, Sha256};
11use zeroize::{ZeroizeOnDrop, Zeroizing};
12
13use crate::{
14    arith::monty::Modulus,
15    curve::{decode_point, decode_scalar, encode_point, encode_scalar},
16    error::{Error, Result},
17    params::Params,
18    ssid::Ssid,
19    transcript::{Transcript, uint_from_be, uint_to_be},
20    zk::df::DfStatement,
21};
22
23pub mod client;
24pub mod server;
25
26pub use client::{ClientKeygen0, ClientKeygen1};
27pub use server::{ServerKeygen0, ServerKeygen1};
28
29/// The client must finish within this time after sending `KgMsg2` (spec §6.2, step 2).
30pub const KEYGEN_TIMEOUT_MS: u64 = 60_000;
31
32/// Monotonic millisecond clock supplied by the application (the core is `no_std`).
33pub trait Clock {
34    fn now_ms(&self) -> u64;
35}
36
37/// Base transcript of one keygen session for `role` (`"S"` or `"C"`).
38pub(crate) fn tx_base(params: &Params, ssid: &Ssid, role: &str) -> Transcript {
39    let mut t = Transcript::new("keygen");
40    t.append("params_id", &[params.id as u8]);
41    t.append("ssid", &ssid.encode());
42    t.append("role", role.as_bytes());
43    t
44}
45
46/// `B = RO(ssid, S, X₂)` (domain `ro/kg-commit`, spec §10.3).
47pub(crate) fn commitment(params: &Params, ssid: &Ssid, x2_pub: &ProjectivePoint) -> [u8; 32] {
48    let mut t = Transcript::new("ro/kg-commit");
49    t.append("params_id", &[params.id as u8]);
50    t.append("ssid", &ssid.encode());
51    t.append("S", ssid.server_id());
52    t.append("X2", &encode_point(x2_pub));
53    t.challenge("B").seed()
54}
55
56/// Fixed-width element `< bound` (canonical) as an integer.
57pub(crate) fn canonical_uint(bytes: &[u8], len: usize, bound: &BoxedUint) -> Result<BoxedUint> {
58    if bytes.len() != len {
59        return Err(Error::InvalidEncoding);
60    }
61    let x = uint_from_be(bytes, bound.bits_precision())?;
62    let b = crate::arith::sbig::resized(bound, x.bits_precision());
63    if x.cmp_vartime(&b) != core::cmp::Ordering::Less {
64        return Err(Error::OutOfRange);
65    }
66    Ok(x)
67}
68
69/// Decodes the client's ephemeral tuple `(M̂, v, u1, u2)` with strict length and range checks.
70pub(crate) fn decode_client_tuple(
71    params: &Params,
72    m_hat: &[u8],
73    v: &[u8],
74    u1: &[u8],
75    u2: &[u8],
76) -> Result<DfStatement> {
77    let len = params.n_client as usize / 8;
78    if m_hat.len() != len {
79        return Err(Error::InvalidEncoding);
80    }
81    let m = uint_from_be(m_hat, params.n_client)?;
82    if m.bits_vartime() != params.n_client {
83        return Err(Error::OutOfRange);
84    }
85    Modulus::new(m.clone())?;
86    let vv = canonical_uint(v, len, &m)?;
87    let a = canonical_uint(u1, len, &m)?;
88    let b = canonical_uint(u2, len, &m)?;
89    DfStatement::client(&m, &vv, &a, &b)
90}
91
92pub(crate) fn encode_client_tuple(params: &Params, st: &DfStatement) -> Result<[Vec<u8>; 4]> {
93    let len = params.n_client as usize / 8;
94    let enc = |r: &crate::arith::monty::Residue| uint_to_be(&r.retrieve(), len);
95    Ok([
96        uint_to_be(st.n_hat.modulus().as_ref(), len)?,
97        enc(&st.t)?,
98        enc(&st.s1)?,
99        enc(&st.s2)?,
100    ])
101}
102
103fn key_id(x_pub: &ProjectivePoint, e: &[u8]) -> [u8; 32] {
104    let mut h = Sha256::new();
105    h.update(encode_point(x_pub));
106    h.update(e);
107    h.finalize().into()
108}
109
110/// Domain separator of [`chain_code`].
111const CHAIN_CODE_DOMAIN: &[u8] = b"tecdsa-1ole/hd-chain-code/v1";
112
113/// Public BIP32 chain code of a key: `SHA-256(domain ‖ X ‖ E)`. Both sides derive the same
114/// value from the shared keygen output, so no extra round is needed. It is public, as in
115/// BIP32, and does not depend on any secret.
116fn chain_code(x_pub: &ProjectivePoint, e: &[u8]) -> [u8; 32] {
117    let mut h = Sha256::new();
118    h.update(CHAIN_CODE_DOMAIN);
119    h.update(encode_point(x_pub));
120    h.update(e);
121    h.finalize().into()
122}
123
124const SHARE_VERSION: u8 = 0x01;
125
126macro_rules! key_share {
127    ($name:ident, $field:ident, $doc:literal) => {
128        #[doc = $doc]
129        #[derive(ZeroizeOnDrop)]
130        pub struct $name {
131            $field: Scalar,
132            #[zeroize(skip)]
133            x_pub: ProjectivePoint,
134            /// `E` as fixed-width big-endian (`⌈2·n_rsa/8⌉` bytes).
135            #[zeroize(skip)]
136            e: Vec<u8>,
137        }
138
139        impl core::fmt::Debug for $name {
140            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
141                f.write_str(concat!(stringify!($name), "(***)"))
142            }
143        }
144
145        impl $name {
146            pub(crate) fn new($field: Scalar, x_pub: ProjectivePoint, e: Vec<u8>) -> Self {
147                Self { $field, x_pub, e }
148            }
149            /// Joint public key `X = X₁·X₂`.
150            pub fn public_key(&self) -> &ProjectivePoint {
151                &self.x_pub
152            }
153            /// The Paillier ciphertext `E` (fixed-width bytes).
154            pub fn e_bytes(&self) -> &[u8] {
155                &self.e
156            }
157            /// Public chain code for HD derivation, identical on both sides.
158            pub fn chain_code(&self) -> [u8; 32] {
159                chain_code(&self.x_pub, &self.e)
160            }
161            /// `SHA-256(X ‖ E)`, the `key_id` of sign sessions.
162            pub fn key_id(&self) -> [u8; 32] {
163                key_id(&self.x_pub, &self.e)
164            }
165            #[allow(dead_code)]
166            pub(crate) fn secret(&self) -> &Scalar {
167                &self.$field
168            }
169            /// Explicit export for a `SecretStore`: `version ‖ share(32) ‖ X(33) ‖ E`.
170            pub fn export_secret(&self) -> Zeroizing<Vec<u8>> {
171                let mut out = Zeroizing::new(Vec::with_capacity(1 + 32 + 33 + self.e.len()));
172                out.push(SHARE_VERSION);
173                out.extend_from_slice(&encode_scalar(&self.$field));
174                out.extend_from_slice(&encode_point(&self.x_pub));
175                out.extend_from_slice(&self.e);
176                out
177            }
178            pub fn import_secret(bytes: &[u8], params: &Params) -> Result<Self> {
179                let elen = (2 * params.n_rsa as usize).div_ceil(8);
180                if bytes.len() != 1 + 32 + 33 + elen || bytes[0] != SHARE_VERSION {
181                    return Err(Error::InvalidEncoding);
182                }
183                Ok(Self {
184                    $field: decode_scalar(&bytes[1..33])?,
185                    x_pub: decode_point(&bytes[33..66])?,
186                    e: bytes[66..].to_vec(),
187                })
188            }
189        }
190    };
191}
192
193key_share!(
194    ClientKeyShare,
195    x1,
196    "Client output of keygen: `x₁`, `X`, `E`."
197);
198key_share!(
199    ServerKeyShare,
200    x2,
201    "Server output of keygen: `x₂ = x₂′ mod q`, `X`, `E`."
202);
203
204#[cfg(test)]
205mod tests {
206    extern crate std;
207    use super::*;
208    use crate::{
209        arith::sbig::SBig,
210        params::PARAMS_128,
211        testutil::{TestClock, fx, params},
212    };
213    use core::cell::Cell;
214    use k256::ProjectivePoint;
215    use rand_chacha::ChaCha8Rng;
216    use rand_core::SeedableRng;
217
218    fn run(seed: u64) -> (ClientKeyShare, ServerKeyShare) {
219        let p = params();
220        let mut rng = ChaCha8Rng::seed_from_u64(seed);
221        let clock = TestClock(Cell::new(1_000));
222        let (s1, m1) = ServerKeygen0::start(&mut rng, &p, &fx().data, b"client-1").unwrap();
223        let (c1, m2) =
224            ClientKeygen0::on_msg1(&mut rng, &clock, &fx().setup, b"client-1", &m1).unwrap();
225        let (server_share, m3) = s1.on_msg2(&mut rng, &m2).unwrap();
226        let client_share = c1.on_msg3(&clock, &m3).unwrap();
227        (client_share, server_share)
228    }
229
230    #[test]
231    fn chain_code_is_shared_and_key_bound() {
232        let (c, s) = run(5);
233        assert_eq!(c.chain_code(), s.chain_code());
234        let (c2, _) = run(6);
235        assert_ne!(c.chain_code(), c2.chain_code());
236        assert_ne!(c.chain_code(), c.key_id());
237    }
238
239    #[test]
240    fn honest_keygen() {
241        let (c, s) = run(5);
242        assert_eq!(c.public_key(), s.public_key());
243        assert_eq!(c.e_bytes(), s.e_bytes());
244        assert_eq!(c.key_id(), s.key_id());
245        let sum = *c.secret() + *s.secret();
246        assert_eq!(ProjectivePoint::GENERATOR * sum, *c.public_key());
247        // E decrypts to x₂′ (mod N), which reduces to x₂ mod q
248        let pk = fx().data.paillier_pk().unwrap();
249        let e = pk
250            .check_ciphertext(&uint_from_be(s.e_bytes(), 1024).unwrap())
251            .unwrap();
252        let m = fx().sk.paillier_sk().dec(&e).unwrap();
253        // signed interpretation: values above N/2 are negative
254        let n = pk.n().clone();
255        let half = n.shr_vartime(1).unwrap();
256        let signed = if m.cmp_vartime(&half) == core::cmp::Ordering::Greater {
257            SBig::from_uint(crate::arith::sbig::resized(&m, n.bits_precision()))
258                .sub(&SBig::from_uint(n))
259        } else {
260            SBig::from_uint(m)
261        };
262        assert_eq!(crate::curve::sbig_to_scalar(&signed).unwrap(), *s.secret());
263        assert_eq!(std::format!("{c:?}"), "ClientKeyShare(***)");
264        // export / import
265        let back = ServerKeyShare::import_secret(&s.export_secret(), &params()).unwrap();
266        assert_eq!(back.key_id(), s.key_id());
267        assert_eq!(*back.secret(), *s.secret());
268        assert!(ClientKeyShare::import_secret(&c.export_secret()[1..], &params()).is_err());
269    }
270
271    #[test]
272    fn two_sessions_differ() {
273        let (a, _) = run(5);
274        let (b, _) = run(6);
275        assert_ne!(a.public_key(), b.public_key());
276    }
277
278    #[test]
279    fn tampered_messages_are_rejected() {
280        let p = params();
281        let clock = TestClock(Cell::new(0));
282        let mut rng = ChaCha8Rng::seed_from_u64(9);
283        let (s1, m1) = ServerKeygen0::start(&mut rng, &p, &fx().data, b"client-1").unwrap();
284        let (c1, m2) =
285            ClientKeygen0::on_msg1(&mut rng, &clock, &fx().setup, b"client-1", &m1).unwrap();
286
287        // KgMsg2 corruption: server must reject (decode or verification).
288        let positions = [
289            4,
290            20,
291            60,
292            100,
293            200,
294            300,
295            m2.len() / 2,
296            m2.len() - 40,
297            m2.len() - 1,
298        ];
299        for &i in positions.iter().filter(|i| **i < m2.len()) {
300            let mut bad = m2.clone();
301            bad[i] ^= 0x01;
302            let s = clone_server_state(&s1);
303            assert!(s.on_msg2(&mut rng, &bad).is_err(), "msg2 flip at {i}");
304        }
305        // truncated / extended / wrong envelope
306        assert!(
307            clone_server_state(&s1)
308                .on_msg2(&mut rng, &m2[..m2.len() - 1])
309                .is_err()
310        );
311        let mut longer = m2.clone();
312        longer.push(0);
313        assert!(clone_server_state(&s1).on_msg2(&mut rng, &longer).is_err());
314        let mut wrong_type = m2.clone();
315        wrong_type[3] = 3;
316        assert!(
317            clone_server_state(&s1)
318                .on_msg2(&mut rng, &wrong_type)
319                .is_err()
320        );
321
322        let (_, m3) = s1.on_msg2(&mut rng, &m2).unwrap();
323        // KgMsg3 corruption: client must reject.
324        let positions = [
325            4,
326            10,
327            40,
328            100,
329            200,
330            300,
331            m3.len() / 2,
332            m3.len() - 100,
333            m3.len() - 1,
334        ];
335        for &i in positions.iter().filter(|i| **i < m3.len()) {
336            let mut bad = m3.clone();
337            bad[i] ^= 0x01;
338            assert!(
339                clone_client_state(&c1).on_msg3(&clock, &bad).is_err(),
340                "msg3 flip at {i}"
341            );
342        }
343        assert!(c1.on_msg3(&clock, &m3).is_ok());
344    }
345
346    #[test]
347    fn timer_and_session_binding() {
348        let p = params();
349        let mut rng = ChaCha8Rng::seed_from_u64(10);
350        let clock = TestClock(Cell::new(0));
351        let (s1, m1) = ServerKeygen0::start(&mut rng, &p, &fx().data, b"client-1").unwrap();
352        let (c1, m2) =
353            ClientKeygen0::on_msg1(&mut rng, &clock, &fx().setup, b"client-1", &m1).unwrap();
354        let (_, m3) = s1.on_msg2(&mut rng, &m2).unwrap();
355
356        // exactly at the limit passes, one millisecond later fails
357        clock.0.set(KEYGEN_TIMEOUT_MS);
358        assert!(clone_client_state(&c1).on_msg3(&clock, &m3).is_ok());
359        clock.0.set(KEYGEN_TIMEOUT_MS + 1);
360        assert_eq!(
361            clone_client_state(&c1).on_msg3(&clock, &m3).unwrap_err(),
362            Error::Timeout
363        );
364
365        // a client with another id cannot use the session's messages (ssid mismatch)
366        clock.0.set(0);
367        let (s2, m1b) = ServerKeygen0::start(&mut rng, &p, &fx().data, b"client-1").unwrap();
368        let (c2, m2b) =
369            ClientKeygen0::on_msg1(&mut rng, &clock, &fx().setup, b"client-OTHER", &m1b).unwrap();
370        assert!(s2.on_msg2(&mut rng, &m2b).is_err());
371        drop(c2);
372
373        // KgMsg3 of another session is rejected by the client
374        let (s3, m1c) = ServerKeygen0::start(&mut rng, &p, &fx().data, b"client-1").unwrap();
375        let (c3, m2c) =
376            ClientKeygen0::on_msg1(&mut rng, &clock, &fx().setup, b"client-1", &m1c).unwrap();
377        let (_, m3c) = s3.on_msg2(&mut rng, &m2c).unwrap();
378        assert!(c3.on_msg3(&clock, &m3).is_err());
379        let _ = m3c;
380    }
381
382    // States are consumed by value in the API; tests need copies to try many corruptions.
383    fn clone_server_state(s: &ServerKeygen1) -> ServerKeygen1 {
384        s.test_clone()
385    }
386    fn clone_client_state(s: &ClientKeygen1) -> ClientKeygen1 {
387        s.test_clone()
388    }
389
390    #[test]
391    #[ignore = "full-size: run with --release --ignored --nocapture"]
392    fn full_size_keygen() {
393        use crate::setup::server_setup;
394        use std::time::Instant;
395        let p = PARAMS_128;
396        let mut rng = ChaCha8Rng::seed_from_u64(77);
397        let (data, _sk) = server_setup(&mut rng, &p, [1u8; 32], b"server").unwrap();
398        let setup = data.verify(&p).unwrap();
399        let clock = TestClock(Cell::new(0));
400        let t = Instant::now();
401        let (s1, m1) = ServerKeygen0::start(&mut rng, &p, &data, b"client").unwrap();
402        let t_s1 = t.elapsed();
403        let t = Instant::now();
404        let (c1, m2) = ClientKeygen0::on_msg1(&mut rng, &clock, &setup, b"client", &m1).unwrap();
405        let t_c = t.elapsed();
406        let t = Instant::now();
407        let (ss, m3) = s1.on_msg2(&mut rng, &m2).unwrap();
408        let t_s2 = t.elapsed();
409        let t = Instant::now();
410        let cs = c1.on_msg3(&clock, &m3).unwrap();
411        let t_c2 = t.elapsed();
412        assert_eq!(cs.public_key(), ss.public_key());
413        std::eprintln!(
414            "keygen: S1 {t_s1:?} ({} B) | C step 2 {t_c:?} ({} B) | S step 3 {t_s2:?} ({} B) | C step 4 {t_c2:?}",
415            m1.len(),
416            m2.len(),
417            m3.len()
418        );
419    }
420}