Skip to main content

ic_ec/nist/
gentable.rs

1//! A precomputed table for multiplying a NIST curve's generator.
2//!
3//! # Why
4//!
5//! [`Point::mul_scalar`][super::point::Point::mul_scalar] handles an arbitrary
6//! point: it builds `1..=8` times the point per call and walks the scalar four
7//! bits at a time, which is what ECDH needs. Every digit still costs four
8//! doublings, and for an arbitrary point nothing can remove them.
9//!
10//! The measurements below date from when `mul_scalar` was a
11//! double-and-add-always ladder, one addition per bit.
12//!
13//! ECDSA signing does not need it. `R = k*G` multiplies the generator, which is
14//! the same point in every signature, so its multiples can be computed once.
15//! Measured on P-256 before this existed: one scalar multiplication cost 146.8
16//! microseconds and a whole signature 159.5, so the ladder was essentially the
17//! entire operation and everything else -- RFC 6979, the inversion -- was
18//! thirteen microseconds of it.
19//!
20//! # The shape
21//!
22//! The same construction [`crate::ed25519`] uses, and the reasoning is there in
23//! full: signed radix-16 digits so only positive multiples are stored, one
24//! table per *pair* of digits with the odd ones scaled by four doublings
25//! applied once, and a lookup that reads every entry and selects with
26//! conditional moves rather than indexing -- a table indexed by a secret is a
27//! cache-timing channel.
28//!
29//! The differences are that a Weierstrass point negates in `y` rather than `x`
30//! and `t`, and that the scalar width varies by curve, so the digit count is
31//! computed from `C::SCALAR_BYTES` and the arrays are sized for the widest
32//! curve this crate has.
33//!
34//! # Where it lives
35//!
36//! Behind `std`, in a `static` per curve: one `OnceLock` per window, filled in
37//! order on first use, or earlier through [`crate::prepare`]. P-256's table is
38//! about 25 KiB and P-521's about 114 KiB, which is a reasonable trade on a
39//! host and a bad one on a microcontroller. `no_std` multiplies the generator
40//! with [`Point::mul_scalar`], which uses this module's [`Window`] and
41//! [`signed_digits`] against the point it is given, one window built per call:
42//! the same four bits per addition, without the storage.
43//!
44//! The storage is static, not heap. It used to be a `Vec`, and a caller that
45//! forbids allocation after start-up -- a TLS engine running in caller-owned
46//! memory -- met that allocation in the middle of its first handshake. A
47//! single `OnceLock` holding every window would avoid the heap, but its value
48//! is built on the stack and then moved in, which is 114 KiB of stack for
49//! P-521; that overflowed once, and is why the `Vec` was there. A lock per
50//! window needs neither: each window is built where it is stored, and at most
51//! one window's worth of stack is in use at a time. `tests/cold_tables.rs`
52//! checks that a first use allocates nothing.
53
54use ic_core::ct::Choice;
55
56#[cfg(feature = "std")]
57use super::arith::Field;
58use super::point::Curve;
59use super::point::Point;
60#[cfg(feature = "std")]
61use super::point::{AffinePoint, Projective};
62
63/// Digits for the widest curve here: P-521 has a 66-byte scalar, so 132
64/// nibbles, plus one for the carry out of the top. See `signed_digits`.
65const MAX_DIGITS: usize = 133;
66
67/// Entries per table: the multiples `1..=8`.
68const ENTRIES: usize = 8;
69
70/// `1..=8` times some fixed point: a multiple of the generator in the table,
71/// or the point being multiplied in [`Point::mul_scalar`].
72pub(super) struct Window<C: Curve>([Point<C>; ENTRIES]);
73
74impl<C: Curve> Window<C> {
75    pub(super) fn new(base: &Point<C>) -> Self {
76        let mut entries = [Point::identity(); ENTRIES];
77        entries[0] = *base;
78        for i in 1..ENTRIES {
79            entries[i] = entries[i - 1].add(base);
80        }
81        Self(entries)
82    }
83
84    /// `digit * base` for `digit` in `[-8, 8]`, without indexing by it.
85    pub(super) fn select(&self, digit: i8) -> Point<C> {
86        let negative = Choice::from_u8((digit as u8) >> 7);
87        let magnitude = ((digit as i16 ^ (digit as i16 >> 7)) - (digit as i16 >> 7)) as u8;
88
89        let mut out = Point::identity();
90        for (i, entry) in self.0.iter().enumerate() {
91            let hit = Choice::from_u8(u8::from(magnitude == (i as u8 + 1)));
92            Point::cmov(&mut out, entry, hit);
93        }
94        out.conditional_negate(negative);
95        out
96    }
97}
98
99/// `1..=8` times one power of the generator, normalized to affine, for
100/// [`Table`]: an affine entry makes the accumulator's addition one
101/// multiplication cheaper. Normalizing costs an inversion per entry, once,
102/// when the table is built.
103#[cfg(feature = "std")]
104pub struct AffineWindow<C: Curve>([AffinePoint<C>; ENTRIES]);
105
106#[cfg(feature = "std")]
107impl<C: Curve> AffineWindow<C> {
108    /// `None` only if a multiple of `base` is the identity, which `1..=8`
109    /// times a power of a generator of prime order above 8 never is.
110    fn new(base: &Point<C>) -> Option<Self> {
111        let window = Window::new(base);
112        let mut entries = [AffinePoint {
113            x: C::Field::ZERO,
114            y: C::Field::ZERO,
115        }; ENTRIES];
116        for (out, entry) in entries.iter_mut().zip(window.0.iter()) {
117            *out = entry.to_affine()?;
118        }
119        Some(Self(entries))
120    }
121
122    /// `acc + digit * base` for `digit` in `[-8, 8]`, without indexing by it.
123    ///
124    /// A zero digit selects the identity, which has no affine form, so the
125    /// addition is made anyway -- on an arbitrary entry -- and its result
126    /// discarded by a conditional move. The work is the same for every digit.
127    fn add_to(&self, acc: &Projective<C>, digit: i8) -> Projective<C> {
128        let negative = Choice::from_u8((digit as u8) >> 7);
129        let magnitude = ((digit as i16 ^ (digit as i16 >> 7)) - (digit as i16 >> 7)) as u8;
130
131        let mut x = self.0[0].x;
132        let mut y = self.0[0].y;
133        for (i, entry) in self.0.iter().enumerate() {
134            let hit = Choice::from_u8(u8::from(magnitude == (i as u8 + 1)));
135            C::Field::cmov(&mut x, &entry.x, hit);
136            C::Field::cmov(&mut y, &entry.y, hit);
137        }
138        let ny = y.neg();
139        C::Field::cmov(&mut y, &ny, negative);
140
141        let mut out = acc.add_affine(&x, &y);
142        Projective::cmov(&mut out, acc, Choice::from_u8(u8::from(magnitude == 0)));
143        out
144    }
145}
146
147/// One curve's windows: `16^(2i) * G` and its multiples `1..=8`, for each `i`.
148#[cfg(feature = "std")]
149pub type Windows<C> = [std::sync::OnceLock<AffineWindow<C>>];
150
151/// The window for `base`, which is always a nonzero multiple of the generator.
152#[cfg(feature = "std")]
153fn window_for<C: Curve>(base: &Point<C>) -> AffineWindow<C> {
154    match AffineWindow::new(base) {
155        Some(window) => window,
156        None => unreachable!("a multiple of the generator below its order is the identity"),
157    }
158}
159
160/// Fill every window, in order, sharing the doublings between them.
161///
162/// Costs a little over one scalar multiplication, once. `built` makes it run
163/// once: a second caller waits for the first rather than repeating the work.
164#[cfg(feature = "std")]
165pub fn prepare<C: Curve>(windows: &Windows<C>, built: &std::sync::OnceLock<()>) {
166    built.get_or_init(|| {
167        let mut base = Point::<C>::generator();
168        for (i, slot) in windows.iter().enumerate() {
169            if i > 0 {
170                // times 16^2 = eight doublings.
171                for _ in 0..8 {
172                    base = base.double();
173                }
174            }
175            slot.get_or_init(|| window_for(&base));
176        }
177    });
178}
179
180/// `scalar * G`.
181#[cfg(feature = "std")]
182pub fn mul<C: Curve>(
183    windows: &Windows<C>,
184    built: &std::sync::OnceLock<()>,
185    scalar: &C::Scalar,
186) -> Point<C> {
187    prepare(windows, built);
188    // Every window is filled by now. Each read still goes through
189    // `get_or_init`, building that one window by itself if it somehow were
190    // not, which is slower and gives the same answer rather than a panic.
191    let window = |i: usize| {
192        windows[i].get_or_init(|| {
193            let mut base = Point::<C>::generator();
194            for _ in 0..8 * i {
195                base = base.double();
196            }
197            window_for(&base)
198        })
199    };
200
201    let bytes = scalar.to_bytes();
202    let digits = signed_digits(bytes.as_ref());
203    // Every nibble, plus the carry digit above them.
204    let n = bytes.as_ref().len() * 2 + 1;
205    debug_assert!(n.div_ceil(2) <= windows.len());
206
207    // Accumulated projectively; see `Projective` for why.
208    let mut acc = Projective::identity();
209    for i in (1..n).step_by(2) {
210        acc = window(i / 2).add_to(&acc, digits[i]);
211    }
212    for _ in 0..4 {
213        acc = acc.double();
214    }
215    for i in (0..n).step_by(2) {
216        acc = window(i / 2).add_to(&acc, digits[i]);
217    }
218    acc.to_jacobian()
219}
220
221/// The scalar as signed radix-16 digits, each in `[-8, 8]`.
222///
223/// `bytes` is big-endian, as `Field::to_bytes` produces; the digits are
224/// little-endian, least significant first, because that is the order the
225/// accumulation wants.
226///
227/// Every nibble is recoded and the carry out of the top gets a digit of its
228/// own, which is where this differs from the Ed25519 version.
229///
230/// There, scalars are below `2^255`, so the most significant nibble is at most
231/// 7, one carry takes it to 8, and 8 is a legal digit -- the last nibble can
232/// absorb the carry and no extra digit is needed. Here the group orders are
233/// close to `2^(8*SCALAR_BYTES)`, so the top nibble can be 15; a carry would
234/// make it 16, which is outside `[-8, 8]`, and `select` would match no entry
235/// and silently return the identity. Hence the extra digit, which is 0 or 1.
236///
237/// That is exactly how this failed first: the Ed25519 recoding was reused
238/// unchanged and every RFC 6979 vector rejected it.
239pub(super) fn signed_digits(bytes: &[u8]) -> [i8; MAX_DIGITS] {
240    let mut nibbles = [0i8; MAX_DIGITS];
241    let n = bytes.len() * 2;
242    for (i, byte) in bytes.iter().rev().enumerate() {
243        nibbles[i * 2] = (byte & 0x0f) as i8;
244        nibbles[i * 2 + 1] = (byte >> 4) as i8;
245    }
246
247    for i in 0..n {
248        let carry = (nibbles[i] + 8) >> 4;
249        nibbles[i] -= carry << 4;
250        nibbles[i + 1] += carry;
251    }
252    debug_assert!(nibbles[n] == 0 || nibbles[n] == 1);
253    nibbles
254}
255
256/// A curve that knows how to multiply its own generator.
257///
258/// The trait exists on every target; only the implementation differs. Under
259/// `std` it is the table; under `no_std` it is [`Point::mul_scalar`] on the
260/// generator, four bits at a time with no stored window, so callers need no
261/// conditional bound and the generic code has one shape.
262///
263/// It is separate from [`Curve`] because the table's storage is a `static`,
264/// which has to live in a concrete function body: a `static` inside a generic
265/// function is shared across every instantiation, which would hand P-384 the
266/// P-256 table.
267pub trait HasGeneratorTable: Curve + Sized + 'static {
268    /// `scalar * G`.
269    fn mul_generator(scalar: &Self::Scalar) -> super::point::Point<Self>;
270
271    /// Build the table now rather than on first use. Nothing under `no_std`.
272    fn prepare_generator_table();
273}
274
275/// Implement [`HasGeneratorTable`] for a curve, with its own storage.
276macro_rules! generator_table_for {
277    ($curve:ty) => {
278        /// This curve's generator table, and whether it has been filled.
279        #[cfg(feature = "std")]
280        fn generator_table() -> (
281            &'static $crate::nist::gentable::Windows<$curve>,
282            &'static std::sync::OnceLock<()>,
283        ) {
284            const USED: usize = <$curve as $crate::nist::point::Curve>::SCALAR_BYTES + 1;
285            #[allow(clippy::declare_interior_mutable_const)]
286            const EMPTY: std::sync::OnceLock<$crate::nist::gentable::AffineWindow<$curve>> =
287                std::sync::OnceLock::new();
288            static WINDOWS: [std::sync::OnceLock<$crate::nist::gentable::AffineWindow<$curve>>;
289                USED] = [EMPTY; USED];
290            static BUILT: std::sync::OnceLock<()> = std::sync::OnceLock::new();
291            (&WINDOWS, &BUILT)
292        }
293
294        impl $crate::nist::gentable::HasGeneratorTable for $curve {
295            #[cfg(feature = "std")]
296            fn mul_generator(
297                scalar: &<Self as $crate::nist::point::Curve>::Scalar,
298            ) -> $crate::nist::point::Point<Self> {
299                let (windows, built) = generator_table();
300                $crate::nist::gentable::mul(windows, built, scalar)
301            }
302
303            #[cfg(not(feature = "std"))]
304            fn mul_generator(
305                scalar: &<Self as $crate::nist::point::Curve>::Scalar,
306            ) -> $crate::nist::point::Point<Self> {
307                $crate::nist::point::Point::<Self>::generator().mul_scalar(scalar)
308            }
309
310            fn prepare_generator_table() {
311                #[cfg(feature = "std")]
312                {
313                    let (windows, built) = generator_table();
314                    $crate::nist::gentable::prepare(windows, built);
315                }
316            }
317        }
318    };
319}
320
321pub(crate) use generator_table_for;