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] 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}