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] is a
6//! double-and-add-always ladder: every bit doubles and adds, and a conditional
7//! move decides whether the addition counts. That is the right algorithm for an
8//! arbitrary point and it is what ECDH needs.
9//!
10//! ECDSA signing does not need it. `R = k*G` multiplies the generator, which is
11//! the same point in every signature, so its multiples can be computed once.
12//! Measured on P-256 before this existed: one scalar multiplication cost 146.8
13//! microseconds and a whole signature 159.5, so the ladder was essentially the
14//! entire operation and everything else -- RFC 6979, the inversion -- was
15//! thirteen microseconds of it.
16//!
17//! # The shape
18//!
19//! The same construction [`crate::ed25519`] uses, and the reasoning is there in
20//! full: signed radix-16 digits so only positive multiples are stored, one
21//! table per *pair* of digits with the odd ones scaled by four doublings
22//! applied once, and a lookup that reads every entry and selects with
23//! conditional moves rather than indexing -- a table indexed by a secret is a
24//! cache-timing channel.
25//!
26//! The differences are that a Weierstrass point negates in `y` rather than `x`
27//! and `t`, and that the scalar width varies by curve, so the digit count is
28//! computed from `C::SCALAR_BYTES` and the arrays are sized for the widest
29//! curve this crate has.
30//!
31//! # Where it lives
32//!
33//! Behind `std`, built once into a `OnceLock`. P-256's table is about 25 KiB
34//! and P-521's about 114 KiB, which is a reasonable trade on a host and a bad
35//! one on a microcontroller; `no_std` keeps the ladder, which needs no storage.
36
37#[cfg(feature = "std")]
38use ic_core::ct::Choice;
39
40#[cfg(feature = "std")]
41use super::arith::Field;
42use super::point::Curve;
43#[cfg(feature = "std")]
44use super::point::Point;
45
46#[cfg(feature = "std")]
47/// Digits for the widest curve here: P-521 has a 66-byte scalar, so 132
48/// nibbles, plus one for the carry out of the top. See `signed_digits`.
49const MAX_DIGITS: usize = 133;
50
51#[cfg(feature = "std")]
52/// Entries per table: the multiples `1..=8`.
53const ENTRIES: usize = 8;
54
55#[cfg(feature = "std")]
56/// `1..=8` times some fixed multiple of the generator.
57struct Window<C: Curve>([Point<C>; ENTRIES]);
58
59#[cfg(feature = "std")]
60impl<C: Curve> Window<C> {
61 fn new(base: &Point<C>) -> Self {
62 let mut entries = [Point::identity(); ENTRIES];
63 entries[0] = *base;
64 for i in 1..ENTRIES {
65 entries[i] = entries[i - 1].add(base);
66 }
67 Self(entries)
68 }
69
70 /// `digit * base` for `digit` in `[-8, 8]`, without indexing by it.
71 fn select(&self, digit: i8) -> Point<C> {
72 let negative = Choice::from_u8((digit as u8) >> 7);
73 let magnitude = ((digit as i16 ^ (digit as i16 >> 7)) - (digit as i16 >> 7)) as u8;
74
75 let mut out = Point::identity();
76 for (i, entry) in self.0.iter().enumerate() {
77 let hit = Choice::from_u8(u8::from(magnitude == (i as u8 + 1)));
78 Point::cmov(&mut out, entry, hit);
79 }
80 out.conditional_negate(negative);
81 out
82 }
83}
84
85/// Every multiple of the generator this algorithm needs.
86///
87/// The windows live on the heap and are pushed one at a time. An array sized
88/// for the widest curve would be about 116 KiB for P-521, and building it as a
89/// stack temporary before moving it into the `OnceLock` overflowed the stack --
90/// which is how this first failed, in the FIPS self-test doctests. A `Vec` also
91/// sizes each curve to what it actually uses rather than to P-521.
92#[cfg(feature = "std")]
93pub struct Table<C: Curve> {
94 windows: std::vec::Vec<Window<C>>,
95}
96
97#[cfg(feature = "std")]
98impl<C: Curve> Table<C> {
99 /// Build it. Costs a little over one scalar multiplication, once.
100 pub fn build() -> Self {
101 // One window per pair of digits, and there are `2*SCALAR_BYTES + 1`
102 // digits once the carry digit is counted.
103 let used = C::SCALAR_BYTES + 1;
104 let mut windows = std::vec::Vec::with_capacity(used);
105 let mut base = Point::<C>::generator();
106 for i in 0..used {
107 if i > 0 {
108 // times 16^2 = eight doublings.
109 for _ in 0..8 {
110 base = base.double();
111 }
112 }
113 windows.push(Window::new(&base));
114 }
115 Self { windows }
116 }
117
118 /// `scalar * G`.
119 pub fn mul(&self, scalar: &C::Scalar) -> Point<C> {
120 let bytes = scalar.to_bytes();
121 let digits = signed_digits(bytes.as_ref());
122 // Every nibble, plus the carry digit above them.
123 let n = bytes.as_ref().len() * 2 + 1;
124 debug_assert!(n.div_ceil(2) <= self.windows.len());
125
126 let mut acc = Point::identity();
127 for i in (1..n).step_by(2) {
128 acc = acc.add(&self.windows[i / 2].select(digits[i]));
129 }
130 for _ in 0..4 {
131 acc = acc.double();
132 }
133 for i in (0..n).step_by(2) {
134 acc = acc.add(&self.windows[i / 2].select(digits[i]));
135 }
136 acc
137 }
138}
139
140/// The scalar as signed radix-16 digits, each in `[-8, 8]`.
141///
142/// `bytes` is big-endian, as `Field::to_bytes` produces; the digits are
143/// little-endian, least significant first, because that is the order the
144/// accumulation wants.
145///
146/// Every nibble is recoded and the carry out of the top gets a digit of its
147/// own, which is where this differs from the Ed25519 version.
148///
149/// There, scalars are below `2^255`, so the most significant nibble is at most
150/// 7, one carry takes it to 8, and 8 is a legal digit -- the last nibble can
151/// absorb the carry and no extra digit is needed. Here the group orders are
152/// close to `2^(8*SCALAR_BYTES)`, so the top nibble can be 15; a carry would
153/// make it 16, which is outside `[-8, 8]`, and `select` would match no entry
154/// and silently return the identity. Hence the extra digit, which is 0 or 1.
155///
156/// That is exactly how this failed first: the Ed25519 recoding was reused
157/// unchanged and every RFC 6979 vector rejected it.
158#[cfg(feature = "std")]
159fn signed_digits(bytes: &[u8]) -> [i8; MAX_DIGITS] {
160 let mut nibbles = [0i8; MAX_DIGITS];
161 let n = bytes.len() * 2;
162 for (i, byte) in bytes.iter().rev().enumerate() {
163 nibbles[i * 2] = (byte & 0x0f) as i8;
164 nibbles[i * 2 + 1] = (byte >> 4) as i8;
165 }
166
167 for i in 0..n {
168 let carry = (nibbles[i] + 8) >> 4;
169 nibbles[i] -= carry << 4;
170 nibbles[i + 1] += carry;
171 }
172 debug_assert!(nibbles[n] == 0 || nibbles[n] == 1);
173 nibbles
174}
175
176/// A curve that knows how to multiply its own generator.
177///
178/// The trait exists on every target; only the implementation differs. Under
179/// `std` it is the table; under `no_std` it is the ladder, so callers need no
180/// conditional bound and the generic code has one shape.
181///
182/// It is separate from [`Curve`] because the table's storage is a `static`,
183/// which has to live in a concrete function body: a `static` inside a generic
184/// function is shared across every instantiation, which would hand P-384 the
185/// P-256 table.
186pub trait HasGeneratorTable: Curve + Sized + 'static {
187 /// `scalar * G`.
188 fn mul_generator(scalar: &Self::Scalar) -> super::point::Point<Self>;
189}
190
191/// Implement [`HasGeneratorTable`] for a curve, with its own storage.
192#[macro_export]
193macro_rules! generator_table_for {
194 ($curve:ty) => {
195 impl $crate::nist::gentable::HasGeneratorTable for $curve {
196 #[cfg(feature = "std")]
197 fn mul_generator(
198 scalar: &<Self as $crate::nist::point::Curve>::Scalar,
199 ) -> $crate::nist::point::Point<Self> {
200 static TABLE: std::sync::OnceLock<$crate::nist::gentable::Table<$curve>> =
201 std::sync::OnceLock::new();
202 TABLE
203 .get_or_init($crate::nist::gentable::Table::build)
204 .mul(scalar)
205 }
206
207 #[cfg(not(feature = "std"))]
208 fn mul_generator(
209 scalar: &<Self as $crate::nist::point::Curve>::Scalar,
210 ) -> $crate::nist::point::Point<Self> {
211 use $crate::nist::point::Curve as _;
212 $crate::nist::point::Point::<Self>::generator().mul_scalar(scalar)
213 }
214 }
215 };
216}