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;