Skip to main content

tecdsa_core/zk/
df.rs

1//! Π_df (paper Protocol 8): the Damgård–Fujisaki parameters `(N̂, t, s1, s2)` satisfy
2//! `s_i = t^{λ_i}` with `λ_i < 2^{2ℓ}`. `r_df` parallel repetitions share one Fiat–Shamir challenge
3//! (2 bits per repetition). Compact proof: the challenge bits and the responses `z_i`.
4//!
5//! Also hosts the statement/witness types and the shared `t`-power helper used by Π_df*.
6use alloc::vec::Vec;
7use crypto_bigint::BoxedUint;
8use rand_core::CryptoRng;
9use zeroize::{Zeroize, ZeroizeOnDrop};
10
11use crate::{
12    arith::{
13        fixed_base::FixedBase,
14        monty::{Modulus, Residue},
15        sbig::{SBig, SRange},
16    },
17    error::{Error, Result},
18    params::Params,
19    rsa::tough::DfSecret,
20    transcript::Transcript,
21};
22
23const WINDOW: u32 = 4;
24const MAX_RESTARTS: usize = 64;
25
26/// Public DF statement `(N̂, t, s1, s2)`.
27#[derive(Clone, Debug)]
28pub struct DfStatement {
29    pub n_hat: Modulus,
30    pub t: Residue,
31    pub s1: Residue,
32    pub s2: Residue,
33}
34
35impl DfStatement {
36    pub fn new(n_hat: Modulus, t: Residue, s1: Residue, s2: Residue) -> Self {
37        Self { n_hat, t, s1, s2 }
38    }
39
40    /// From plain integers (reduced modulo `N̂`).
41    pub fn from_uints(
42        n_hat: &BoxedUint,
43        t: &BoxedUint,
44        s1: &BoxedUint,
45        s2: &BoxedUint,
46    ) -> Result<Self> {
47        let m = Modulus::new(n_hat.clone())?;
48        let (t, s1, s2) = (m.residue(t), m.residue(s1), m.residue(s2));
49        Ok(Self {
50            n_hat: m,
51            t,
52            s1,
53            s2,
54        })
55    }
56
57    /// Client role: `(M̂, v, u1, u2)` ↦ `(N̂, t, s1, s2)`.
58    pub fn client(
59        m_hat: &BoxedUint,
60        v: &BoxedUint,
61        u1: &BoxedUint,
62        u2: &BoxedUint,
63    ) -> Result<Self> {
64        Self::from_uints(m_hat, v, u1, u2)
65    }
66
67    /// Appends the statement with fixed widths.
68    pub(crate) fn append_to(&self, tx: &mut Transcript) -> Result<()> {
69        let len = (self.n_hat.modulus().bits() as usize).div_ceil(8);
70        tx.append_uint("N", self.n_hat.modulus().as_ref(), len)?;
71        tx.append_residue("t", &self.n_hat, &self.t)?;
72        tx.append_residue("s1", &self.n_hat, &self.s1)?;
73        tx.append_residue("s2", &self.n_hat, &self.s2)?;
74        Ok(())
75    }
76
77    /// Inverses of `t`, `s1`, `s2`; fails if one is not a unit.
78    pub(crate) fn inverses(&self) -> Result<(Residue, Residue)> {
79        self.n_hat.invert(&self.t)?;
80        Ok((self.n_hat.invert(&self.s1)?, self.n_hat.invert(&self.s2)?))
81    }
82}
83
84/// Secret exponents `λ1, λ2`.
85#[derive(ZeroizeOnDrop)]
86pub struct DfWitness {
87    pub lambda1: BoxedUint,
88    pub lambda2: BoxedUint,
89}
90
91impl core::fmt::Debug for DfWitness {
92    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
93        f.write_str("DfWitness(***)")
94    }
95}
96
97impl DfWitness {
98    pub fn new(lambda1: BoxedUint, lambda2: BoxedUint) -> Self {
99        Self { lambda1, lambda2 }
100    }
101
102    pub fn from_secret(s: &DfSecret) -> Self {
103        Self {
104            lambda1: s.lambda1.clone(),
105            lambda2: s.lambda2.clone(),
106        }
107    }
108
109    /// Both exponents as signed values, checking `λ_i < 2^{bits}`.
110    pub(crate) fn as_sbig(&self, bits: u32) -> Result<(SBig, SBig)> {
111        if self.lambda1.bits() > bits || self.lambda2.bits() > bits {
112            return Err(Error::OutOfRange);
113        }
114        Ok((
115            SBig::from_uint(self.lambda1.clone()),
116            SBig::from_uint(self.lambda2.clone()),
117        ))
118    }
119}
120
121/// Powers of `t` with signed exponents in `±2^n`, via a fixed-base table.
122/// Uses `t^z = t^{z+K} · (t^K)^{-1}` with `K = 2^{n−1}` so that no per-call inversion is needed.
123pub(crate) struct TBase {
124    fb: FixedBase,
125    shift: SBig,
126    t_inv_shift: Residue,
127}
128
129impl TBase {
130    pub(crate) fn new(m: &Modulus, t: &Residue, n: u32) -> Result<Self> {
131        let fb = FixedBase::new(m, t, n + 1, WINDOW);
132        let k =
133            SBig::from_uint(BoxedUint::one_with_precision(n.next_multiple_of(64) + 64).shl(n - 1));
134        let t_k = fb.pow_vartime(&k)?;
135        let t_inv_shift = m.invert(&t_k)?;
136        Ok(Self {
137            fb,
138            shift: k,
139            t_inv_shift,
140        })
141    }
142
143    /// Secret exponent.
144    pub(crate) fn pow_ct(&self, z: &SBig) -> Result<Residue> {
145        let mut s = z.add(&self.shift);
146        let r = self.fb.pow_ct(&s).map(|x| x.mul(&self.t_inv_shift));
147        s.zeroize();
148        r
149    }
150
151    /// Public exponent.
152    pub(crate) fn pow_vartime(&self, z: &SBig) -> Result<Residue> {
153        Ok(self
154            .fb
155            .pow_vartime(&z.add(&self.shift))?
156            .mul(&self.t_inv_shift))
157    }
158}
159
160/// Packs 2-bit values little-endian within each byte.
161fn pack_bits(vals: &[u8]) -> Vec<u8> {
162    let mut out = alloc::vec![0u8; (2 * vals.len()).div_ceil(8)];
163    for (i, v) in vals.iter().enumerate() {
164        out[i / 4] |= (v & 3) << (2 * (i % 4));
165    }
166    out
167}
168
169fn unpack_bits(bytes: &[u8], r: usize) -> Result<Vec<u8>> {
170    if bytes.len() != (2 * r).div_ceil(8) {
171        return Err(Error::InvalidEncoding);
172    }
173    let out: Vec<u8> = (0..r)
174        .map(|i| (bytes[i / 4] >> (2 * (i % 4))) & 3)
175        .collect();
176    // padding bits must be zero (canonical)
177    if pack_bits(&out) != bytes {
178        return Err(Error::InvalidEncoding);
179    }
180    Ok(out)
181}
182
183fn z_range(params: &Params) -> SRange {
184    SRange::pm_pow2(2 * params.ell + params.nu)
185}
186
187fn z_len(params: &Params) -> usize {
188    1 + ((2 * params.ell + params.nu - 1) as usize).div_ceil(8)
189}
190
191fn base_tx(tx: &Transcript, st: &DfStatement, params: &Params) -> Result<Transcript> {
192    let mut t = tx.clone();
193    t.append("zk", b"df");
194    st.append_to(&mut t)?;
195    t.append_u64("r", u64::from(params.r_df));
196    Ok(t)
197}
198
199fn derive_bits(t: &Transcript, a: &[Residue], st: &DfStatement) -> Result<Vec<u8>> {
200    let mut t = t.clone();
201    for ai in a {
202        t.append_residue("A", &st.n_hat, ai)?;
203    }
204    let mut ch = t.challenge("e");
205    Ok((0..a.len()).map(|_| ch.small(2) as u8).collect())
206}
207
208/// Proof of correct DF parameters. `e[i] = e1 | e2 << 1`.
209#[derive(Clone, Debug, PartialEq, Eq)]
210pub struct DfProof {
211    pub e: Vec<u8>,
212    pub z: Vec<SBig>,
213}
214
215impl DfProof {
216    pub fn prove<R: CryptoRng + ?Sized>(
217        rng: &mut R,
218        tx: &Transcript,
219        params: &Params,
220        st: &DfStatement,
221        w: &DfWitness,
222    ) -> Result<Self> {
223        let r = params.r_df as usize;
224        let n = 2 * params.ell + params.nu;
225        let (l1, l2) = {
226            let (a, b) = w.as_sbig(2 * params.ell)?;
227            (a, b)
228        };
229        let t0 = base_tx(tx, st, params)?;
230        let tb = TBase::new(&st.n_hat, &st.t, n)?;
231        let range = z_range(params);
232        for _ in 0..MAX_RESTARTS {
233            let mut alphas: Vec<SBig> = (0..r).map(|_| SBig::random_in(rng, range)).collect();
234            let a: Result<Vec<Residue>> = alphas.iter().map(|al| tb.pow_ct(al)).collect();
235            let a = match a {
236                Ok(a) => a,
237                Err(e) => {
238                    alphas.zeroize();
239                    return Err(e);
240                }
241            };
242            let e = derive_bits(&t0, &a, st)?;
243            let mut z = Vec::with_capacity(r);
244            let mut ok = true;
245            for (al, ei) in alphas.iter().zip(&e) {
246                let mut zi = al.clone();
247                if ei & 1 != 0 {
248                    zi = zi.add(&l1);
249                }
250                if ei & 2 != 0 {
251                    zi = zi.add(&l2);
252                }
253                ok &= zi.in_range(range);
254                z.push(zi);
255            }
256            alphas.zeroize();
257            if ok {
258                return Ok(Self { e, z });
259            }
260            z.zeroize();
261        }
262        Err(Error::OutOfRange)
263    }
264
265    pub fn verify(&self, tx: &Transcript, params: &Params, st: &DfStatement) -> Result<()> {
266        let r = params.r_df as usize;
267        let n = 2 * params.ell + params.nu;
268        if self.e.len() != r || self.z.len() != r || self.e.iter().any(|v| *v > 3) {
269            return Err(Error::InvalidEncoding);
270        }
271        let range = z_range(params);
272        if self.z.iter().any(|z| !z.in_range(range)) {
273            return Err(Error::OutOfRange);
274        }
275        let (s1i, s2i) = st.inverses()?;
276        let both = s1i.mul(&s2i);
277        let one = st.n_hat.one();
278        let tb = TBase::new(&st.n_hat, &st.t, n)?;
279        let mut a = Vec::with_capacity(r);
280        for (zi, ei) in self.z.iter().zip(&self.e) {
281            let c = match ei {
282                0 => &one,
283                1 => &s1i,
284                2 => &s2i,
285                _ => &both,
286            };
287            a.push(tb.pow_vartime(zi)?.mul(c));
288        }
289        let t0 = base_tx(tx, st, params)?;
290        if derive_bits(&t0, &a, st)? == self.e {
291            Ok(())
292        } else {
293            Err(Error::VerificationFailed)
294        }
295    }
296
297    pub fn encoded_len(params: &Params) -> usize {
298        (2 * params.r_df as usize).div_ceil(8) + params.r_df as usize * z_len(params)
299    }
300
301    /// Packed challenge bits ‖ `z_i` (sign byte + ⌈(2ℓ+ν−1)/8⌉ bytes each).
302    pub fn encode(&self, params: &Params) -> Result<Vec<u8>> {
303        if self.e.len() != params.r_df as usize || self.z.len() != self.e.len() {
304            return Err(Error::InvalidEncoding);
305        }
306        let mut out = pack_bits(&self.e);
307        for z in &self.z {
308            out.extend_from_slice(&z.to_bytes(z_range(params))?);
309        }
310        Ok(out)
311    }
312
313    pub fn decode(bytes: &[u8], params: &Params) -> Result<Self> {
314        if bytes.len() != Self::encoded_len(params) {
315            return Err(Error::InvalidEncoding);
316        }
317        let r = params.r_df as usize;
318        let eb = (2 * r).div_ceil(8);
319        let e = unpack_bits(&bytes[..eb], r)?;
320        let z = bytes[eb..]
321            .chunks(z_len(params))
322            .map(|c| SBig::from_bytes(c, z_range(params)))
323            .collect::<Result<Vec<_>>>()?;
324        Ok(Self { e, z })
325    }
326}
327
328#[cfg(test)]
329pub(crate) mod tests {
330    use super::*;
331    use crate::{
332        params::{PARAMS_128, ParamsId},
333        rsa::tough::{ToughSpec, sample_df_with},
334    };
335    use rand_chacha::ChaCha8Rng;
336    use rand_core::SeedableRng;
337
338    /// Reduced profile: ℓ = 16, ν = 16, 512-bit modulus.
339    pub(crate) fn small_params() -> Params {
340        Params {
341            id: ParamsId::P128,
342            ell: 16,
343            nu: 16,
344            r_df: 16,
345            r_df_star: 4,
346            ell0: 8,
347            ..PARAMS_128
348        }
349    }
350
351    pub(crate) fn small_stmt(seed: u64, p: &Params) -> (DfStatement, DfWitness, ChaCha8Rng) {
352        let mut rng = ChaCha8Rng::seed_from_u64(seed);
353        let d = sample_df_with(&mut rng, 512, ToughSpec { count: 4, bits: 64 }, p.ell).unwrap();
354        let st = DfStatement::from_uints(&d.n_hat, &d.t, &d.s1, &d.s2).unwrap();
355        (st, DfWitness::from_secret(&d.secret), rng)
356    }
357
358    #[test]
359    fn honest_roundtrip() {
360        let p = small_params();
361        let (st, w, mut rng) = small_stmt(1, &p);
362        let tx = Transcript::new("test");
363        for _ in 0..5 {
364            let pr = DfProof::prove(&mut rng, &tx, &p, &st, &w).unwrap();
365            pr.verify(&tx, &p, &st).unwrap();
366            let enc = pr.encode(&p).unwrap();
367            assert_eq!(enc.len(), DfProof::encoded_len(&p));
368            let q = DfProof::decode(&enc, &p).unwrap();
369            assert_eq!(pr, q);
370            q.verify(&tx, &p, &st).unwrap();
371            assert!(DfProof::decode(&enc[1..], &p).is_err());
372            let mut longer = enc.clone();
373            longer.push(0);
374            assert!(DfProof::decode(&longer, &p).is_err());
375        }
376    }
377
378    #[test]
379    fn tampering_fails() {
380        let p = small_params();
381        let (st, w, mut rng) = small_stmt(2, &p);
382        let tx = Transcript::new("test");
383        let pr = DfProof::prove(&mut rng, &tx, &p, &st, &w).unwrap();
384        // flip every challenge position
385        for i in 0..pr.e.len() {
386            let mut b = pr.clone();
387            b.e[i] ^= 1;
388            assert!(b.verify(&tx, &p, &st).is_err());
389            let mut b = pr.clone();
390            b.e[i] ^= 2;
391            assert!(b.verify(&tx, &p, &st).is_err());
392        }
393        // z ± 1
394        for i in 0..pr.z.len() {
395            for d in [1i64, -1] {
396                let mut b = pr.clone();
397                b.z[i] = b.z[i].add(&SBig::from_i64(d));
398                assert!(b.verify(&tx, &p, &st).is_err());
399            }
400        }
401        // z boundary: 2^{n-1} + 1 is out of range
402        let n = 2 * p.ell + p.nu;
403        let mut b = pr.clone();
404        b.z[0] = SBig::from_uint(BoxedUint::one_with_precision(n + 64).shl(n - 1))
405            .add(&SBig::from_i64(1));
406        assert_eq!(b.verify(&tx, &p, &st), Err(Error::OutOfRange));
407        // lengths
408        let mut b = pr.clone();
409        b.z.pop();
410        assert!(b.verify(&tx, &p, &st).is_err());
411        // statement fields
412        let m = &st.n_hat;
413        let one = m.one();
414        for which in 0..3 {
415            let mut s2 = st.clone();
416            match which {
417                0 => s2.t = s2.t.mul(&s2.t),
418                1 => s2.s1 = s2.s1.mul(&s2.t),
419                _ => s2.s2 = s2.s2.mul(&s2.t),
420            }
421            assert!(pr.verify(&tx, &p, &s2).is_err());
422        }
423        // non-units
424        let mut s3 = st.clone();
425        s3.s1 = m.residue(&BoxedUint::zero_with_precision(64));
426        assert!(pr.verify(&tx, &p, &s3).is_err());
427        let _ = one;
428        // transcript
429        let mut tx2 = Transcript::new("test");
430        tx2.append("x", b"y");
431        assert!(pr.verify(&tx2, &p, &st).is_err());
432    }
433
434    #[test]
435    fn wrong_witness_fails() {
436        let p = small_params();
437        let (st, w, mut rng) = small_stmt(3, &p);
438        let tx = Transcript::new("test");
439        let bad = DfWitness::new(
440            w.lambda1
441                .wrapping_add(BoxedUint::one_with_precision(w.lambda1.bits_precision())),
442            w.lambda2.clone(),
443        );
444        let pr = DfProof::prove(&mut rng, &tx, &p, &st, &bad).unwrap();
445        assert!(pr.verify(&tx, &p, &st).is_err());
446        // witness too large
447        let big = DfWitness::new(
448            BoxedUint::one_with_precision(128).shl(2 * p.ell),
449            w.lambda2.clone(),
450        );
451        assert!(DfProof::prove(&mut rng, &tx, &p, &st, &big).is_err());
452    }
453
454    #[test]
455    fn decode_rejects_bad() {
456        let p = small_params();
457        let (st, w, mut rng) = small_stmt(4, &p);
458        let tx = Transcript::new("test");
459        let enc = DfProof::prove(&mut rng, &tx, &p, &st, &w)
460            .unwrap()
461            .encode(&p)
462            .unwrap();
463        let eb = (2 * p.r_df as usize).div_ceil(8);
464        // bad sign byte
465        let mut b = enc.clone();
466        b[eb] = 2;
467        assert!(DfProof::decode(&b, &p).is_err());
468        // magnitude out of range
469        let mut b = enc.clone();
470        b[eb] = 0;
471        b[eb + 1] = 0xff;
472        assert!(DfProof::decode(&b, &p).is_err());
473        // non-canonical padding: r=16 fills the bytes exactly, so use r=15
474        let mut p15 = p;
475        p15.r_df = 15;
476        let mut e15 = alloc::vec![0u8; DfProof::encoded_len(&p15)];
477        e15[3] = 0x40; // padding bit set
478        for i in 0..15 {
479            e15[4 + i * z_len(&p15)] = 0;
480        }
481        assert!(DfProof::decode(&e15, &p15).is_err());
482    }
483
484    #[test]
485    #[ignore]
486    fn full_size_timing() {
487        extern crate std;
488        let p = PARAMS_128;
489        let mut rng = ChaCha8Rng::seed_from_u64(9);
490        let d = crate::rsa::tough::sample_df(&mut rng, &p).unwrap();
491        let st = DfStatement::from_uints(&d.n_hat, &d.t, &d.s1, &d.s2).unwrap();
492        let w = DfWitness::from_secret(&d.secret);
493        let tx = Transcript::new("test");
494        let t0 = std::time::Instant::now();
495        let pr = DfProof::prove(&mut rng, &tx, &p, &st, &w).unwrap();
496        let t1 = std::time::Instant::now();
497        pr.verify(&tx, &p, &st).unwrap();
498        let t2 = std::time::Instant::now();
499        std::println!(
500            "df prove {:?} verify {:?} size {}",
501            t1 - t0,
502            t2 - t1,
503            pr.encode(&p).unwrap().len()
504        );
505    }
506}