Source
stdlib/p256/ec_core.hpp
1
// Copyright (c) 2026 BigBrain LLC. MIT-licensed (see LICENSE).2
// Original work; see ACKNOWLEDGMENTS.md for the open-source ideas we build upon.3
#pragma once5
// ec_core.hpp — the width-generic short-Weierstrass ECDSA machinery shared by the `p256`6
// and `p384` modules. NOT a cheatah module itself: an internal implementation header the7
// two curve .cpp files include (purrc resolves modules by `<module>.hpp` name only, so8
// this file is invisible to `import`).9
//10
// Everything is templated on a WeierstrassCurve traits struct carrying the limb count and11
// the curve constants; the field/scalar Montgomery contexts are still DERIVED from the12
// modulus at startup (no hand-transcribed Montgomery magic). A value is uint64_t[kLimbs],13
// LEAST-significant limb first; points are Jacobian (X:Y:Z) with the curve a = -3 (true14
// of every NIST prime curve). The algorithms are limb-count-independent copies of the15
// battle-tested p256 versions — only bounds changed, from 4/256/32 to16
// kLimbs/kBits/kBytes.18
#include <array>19
#include <concepts>20
#include <cstddef>21
#include <cstdint>22
#include <cstring>23
#include <string>24
#include <type_traits>26
namespace cheatah::ec {28
using u64 = std::uint64_t;29
using u128 = unsigned __int128;31
// The curve-traits concept every template below is constrained by: a NIST-style32
// short-Weierstrass curve (a = -3) over a prime field, its size in 64-bit limbs plus the33
// field prime P, group order N, coefficient B and base point (GX, GY) as little-endian34
// limb arrays.35
template <class C>36
concept WeierstrassCurve = requires {37
{ C::kLimbs } -> std::convertible_to<std::size_t>;38
requires C::kLimbs >= 4 && C::kLimbs <= 8;39
requires std::same_as<std::remove_cvref_t<decltype(C::P)>, std::array<u64, C::kLimbs>>;40
requires std::same_as<std::remove_cvref_t<decltype(C::N)>, std::array<u64, C::kLimbs>>;41
requires std::same_as<std::remove_cvref_t<decltype(C::B)>, std::array<u64, C::kLimbs>>;42
requires std::same_as<std::remove_cvref_t<decltype(C::GX)>, std::array<u64, C::kLimbs>>;43
requires std::same_as<std::remove_cvref_t<decltype(C::GY)>, std::array<u64, C::kLimbs>>;44
};46
template <WeierstrassCurve C>47
using fe = std::array<u64, C::kLimbs>; // one field/scalar value, limb[0] = least significant49
template <WeierstrassCurve C>50
inline constexpr std::size_t kBits = C::kLimbs * 64; // scalar size in bits (256 / 384)51
template <WeierstrassCurve C>52
inline constexpr std::size_t kBytes = C::kLimbs * 8; // big-endian byte size (32 / 48)54
// ---- plain multi-limb helpers -----------------------------------------------------------------55
/**56
* Whether every limb of @p a is zero (an OR-accumulate over all limbs, no early exit).57
* @tparam C the curve traits.58
* @param a the value to test.59
* @return true iff @p a == 0.60
* @complexity O(1) — kLimbs limb reads on a fixed-width value.61
* @alloc none.62
* @test CheatahP256.VerifyRejectsOutOfRangeAndInfinity63
*/64
template <WeierstrassCurve C>65
bool is_zero(const fe<C>& a) {66
u64 acc = 0;67
for (const u64 limb : a) acc |= limb;68
return acc == 0;69
}70
/**71
* Branch-free boolean-to-mask: false -> 0, true -> all-ones.72
* @param c the condition.73
* @return the 64-bit mask.74
* @complexity O(1).75
* @alloc none.76
* @test CheatahP256.ConstantTimePointOpsMatchReference77
*/78
inline u64 ct_mask(bool c) { return u64(0) - static_cast<u64>(c); }80
/**81
* Constant-time conditional move over a field element: r = m ? a : r, per limb, no branch.82
* @tparam C the curve traits.83
* @param r the destination (kept when @p m is 0).84
* @param a the source (copied when @p m is all-ones).85
* @param m the ct_mask (0 or all-ones).86
* @complexity O(1).87
* @alloc none.88
* @test CheatahP256.ConstantTimePointOpsMatchReference89
*/90
template <WeierstrassCurve C>91
inline void fe_cmov(fe<C>& r, const fe<C>& a, u64 m) {92
for (std::size_t i = 0; i < C::kLimbs; ++i) r[i] = (r[i] & ~m) | (a[i] & m);93
}95
/**96
* r = a - b (mod 2^kBits), returns the borrow.97
* @tparam C the curve traits.98
* @param r receives the difference.99
* @param a minuend.100
* @param b subtrahend.101
* @return the final borrow: 1 iff a < b, else 0.102
* @complexity O(1) — one pass over kLimbs limbs.103
* @alloc none.104
* @test CheatahP256.VerifyKnownVector105
*/106
template <WeierstrassCurve C>107
u64 sub_borrow(fe<C>& r, const fe<C>& a, const fe<C>& b) {108
u128 br = 0;109
for (std::size_t i = 0; i < C::kLimbs; ++i) {110
u128 d = (u128)a[i] - b[i] - br;111
r[i] = (u64)d;112
br = (d >> 64) & 1;113
}114
return (u64)br;115
}116
/**117
* r = a + b (mod 2^kBits), returns the carry.118
* @tparam C the curve traits.119
* @param r receives the sum.120
* @param a first addend.121
* @param b second addend.122
* @return the final carry out of the top limb (0 or 1).123
* @complexity O(1) — one pass over kLimbs limbs.124
* @alloc none.125
* @test CheatahP256.VerifyKnownVector126
*/127
template <WeierstrassCurve C>128
u64 add_carry(fe<C>& r, const fe<C>& a, const fe<C>& b) {129
u128 c = 0;130
for (std::size_t i = 0; i < C::kLimbs; ++i) {131
u128 s = (u128)a[i] + b[i] + c;132
r[i] = (u64)s;133
c = s >> 64;134
}135
return (u64)c;136
}138
/**139
* Multi-limb unsigned compare, in constant time.140
* @tparam C the curve traits.141
* @param a left operand.142
* @param b right operand.143
* @return true iff a >= b.144
* @complexity O(1) — one full-width subtract, always every limb.145
* @alloc none.146
* @test CheatahP256.VerifyRejectsOutOfRangeAndInfinity147
*/148
template <WeierstrassCurve C>149
bool geq(const fe<C>& a, const fe<C>& b) { // a >= b150
// A full-width subtract, not a most-significant-limb-first scan. The scan returned at the151
// FIRST differing limb, so its running time revealed how many high limbs the two shared —152
// and this is called directly on the private key and on the RFC 6979 nonce. `a >= b` is153
// exactly "the subtraction did not borrow", and sub_borrow is already branch-free.154
fe<C> scratch{};155
return sub_borrow<C>(scratch, a, b) == 0;156
}158
// ---- Montgomery context for one modulus (R = 2^kBits) ------------------------------------------159
/// Montgomery context for one modulus (R = 2^kBits) — all constants derived at startup.160
template <WeierstrassCurve C>161
struct Mont {162
fe<C> m; ///< the modulus163
fe<C> rr; ///< R^2 mod m164
fe<C> one; ///< R mod m (Montgomery form of 1)165
u64 n0; ///< -m^{-1} mod 2^64166
};168
/**169
* CIOS Montgomery multiplication: r = a*b*R^-1 mod m.170
* @tparam C the curve traits.171
* @param r receives the product.172
* @param a first factor (Montgomery form).173
* @param b second factor (Montgomery form).174
* @param M the Montgomery context.175
* @complexity O(1) — kLimbs^2 limb multiplies on a fixed-width value.176
* @alloc none.177
* @test CheatahP256.VerifyKnownVector178
*/179
template <WeierstrassCurve C>180
void mont_mul(fe<C>& r, const fe<C>& a, const fe<C>& b, const Mont<C>& M) {181
constexpr std::size_t L = C::kLimbs;182
u64 t[L + 1] = {};183
for (std::size_t i = 0; i < L; ++i) {184
// t += a * b[i]185
u128 carry = 0;186
for (std::size_t j = 0; j < L; ++j) {187
u128 p = (u128)a[j] * b[i] + t[j] + carry;188
t[j] = (u64)p;189
carry = p >> 64;190
}191
u128 s = (u128)t[L] + carry;192
t[L] = (u64)s;193
u64 top = (u64)(s >> 64);194
// m_mul = t[0] * n0 mod 2^64; t += m_mul * m; then shift right one limb195
u64 mmul = (u64)((u128)t[0] * M.n0);196
carry = 0;197
{198
u128 p = (u128)mmul * M.m[0] + t[0];199
carry = p >> 64; // low limb becomes 0200
}201
for (std::size_t j = 1; j < L; ++j) {202
u128 p = (u128)mmul * M.m[j] + t[j] + carry;203
t[j - 1] = (u64)p;204
carry = p >> 64;205
}206
u128 s2 = (u128)t[L] + carry;207
t[L - 1] = (u64)s2;208
t[L] = top + (u64)(s2 >> 64);209
}210
fe<C> res;211
for (std::size_t i = 0; i < L; ++i) res[i] = t[i];212
// Final conditional subtraction (t may be in [0, 2m)) — computed ALWAYS and selected with a213
// mask. The subtraction's own borrow is the comparison: no borrow means res >= m. The old214
// form branched, and its `||` short-circuited, so the top-word case skipped the compare215
// entirely — two data-dependent timing signals in the hot loop of every secret-scalar216
// multiply.217
fe<C> reduced;218
const u64 borrow = sub_borrow<C>(reduced, res, M.m);219
fe_cmov<C>(res, reduced, ~ct_mask(borrow != 0) | ct_mask(t[L] != 0));220
r = res;221
}222
/**223
* Modular addition: r = a + b mod m (add, then one conditional subtract of m).224
* @tparam C the curve traits.225
* @param r receives the sum.226
* @param a first addend.227
* @param b second addend.228
* @param M the Montgomery context (only its modulus is used).229
* @complexity O(1).230
* @alloc none.231
* @test CheatahP256.VerifyKnownVector232
*/233
template <WeierstrassCurve C>234
void mont_add(fe<C>& r, const fe<C>& a, const fe<C>& b, const Mont<C>& M) {235
fe<C> s;236
const u64 c = add_carry<C>(s, a, b);237
// Subtract m when the sum overflowed (c) or when it is already >= m — the latter being238
// exactly "the subtraction did not borrow". Both branches computed, one selected.239
fe<C> reduced;240
const u64 borrow = sub_borrow<C>(reduced, s, M.m);241
fe_cmov<C>(s, reduced, ct_mask(c != 0) | ~ct_mask(borrow != 0));242
r = s;243
}244
/**245
* Modular subtraction: r = a - b mod m (subtract, then one conditional add of m on borrow).246
* @tparam C the curve traits.247
* @param r receives the difference.248
* @param a minuend.249
* @param b subtrahend.250
* @param M the Montgomery context (only its modulus is used).251
* @complexity O(1).252
* @alloc none.253
* @test CheatahP256.VerifyKnownVector254
*/255
template <WeierstrassCurve C>256
void mont_sub(fe<C>& r, const fe<C>& a, const fe<C>& b, const Mont<C>& M) {257
fe<C> d;258
const u64 br = sub_borrow<C>(d, a, b);259
// Add m back on a borrow — computed unconditionally, selected by the borrow's mask.260
fe<C> wrapped;261
add_carry<C>(wrapped, d, M.m);262
fe_cmov<C>(d, wrapped, ct_mask(br != 0));263
r = d;264
}265
/**266
* Convert @p a into Montgomery form: r = a*R mod m (one mont_mul by R^2).267
* @tparam C the curve traits.268
* @param r receives the Montgomery form.269
* @param a the plain value.270
* @param M the Montgomery context.271
* @complexity O(1) — one mont_mul.272
* @alloc none.273
* @test CheatahP256.VerifyKnownVector274
*/275
template <WeierstrassCurve C>276
void to_mont(fe<C>& r, const fe<C>& a, const Mont<C>& M) {277
mont_mul<C>(r, a, M.rr, M);278
}279
/**280
* Convert @p a out of Montgomery form: r = a*R^-1 mod m (one mont_mul by 1).281
* @tparam C the curve traits.282
* @param r receives the plain value.283
* @param a the Montgomery-form value.284
* @param M the Montgomery context.285
* @complexity O(1) — one mont_mul.286
* @alloc none.287
* @test CheatahP256.VerifyKnownVector288
*/289
template <WeierstrassCurve C>290
void from_mont(fe<C>& r, const fe<C>& a, const Mont<C>& M) {291
fe<C> one{};292
one[0] = 1;293
mont_mul<C>(r, a, one, M);294
}295
/**296
* r = a^-1 mod m, via Fermat: a^(m-2). (m is prime for both p and n.)297
* @tparam C the curve traits.298
* @param r receives the inverse (Montgomery form).299
* @param a the value to invert (Montgomery form, nonzero).300
* @param M the Montgomery context.301
* @complexity O(1) — a fixed kBits-step square-and-multiply ladder.302
* @alloc none.303
* @test CheatahP256.VerifyKnownVector304
*/305
template <WeierstrassCurve C>306
void mont_inv(fe<C>& r, const fe<C>& a, const Mont<C>& M) {307
fe<C> two{};308
two[0] = 2;309
fe<C> exp;310
sub_borrow<C>(exp, M.m, two); // m - 2311
fe<C> result = M.one; // Montgomery 1312
fe<C> base = a;313
for (std::size_t i = 0; i < kBits<C>; ++i) {314
if ((exp[i / 64] >> (i % 64)) & 1) mont_mul<C>(result, result, base, M);315
mont_mul<C>(base, base, base, M);316
}317
r = result;318
}320
/**321
* a^-1 mod 2^64 (@p a odd), by Newton's iteration.322
* @param a the odd value to invert.323
* @return the inverse mod 2^64.324
* @complexity O(1) — five fixed Newton steps.325
* @alloc none.326
* @test CheatahP256.VerifyKnownVector327
*/328
inline u64 inv64(u64 a) { // starts correct to 3 bits, doubles per step329
u64 x = a; // correct to 3 bits330
for (int i = 0; i < 5; ++i) x *= 2 - a * x;331
return x;332
}333
/**334
* Build the Montgomery context for modulus @p m — every constant (n0, R^2 mod m, R mod m)335
* derived at startup, no hand-transcribed Montgomery magic.336
* @tparam C the curve traits.337
* @param m the (odd, prime) modulus.338
* @return the derived context.339
* @complexity O(1) — 2*kBits fixed doubling steps to derive R^2 mod m.340
* @alloc none.341
* @test CheatahP256.VerifyKnownVector342
*/343
template <WeierstrassCurve C>344
Mont<C> make_mont(const fe<C>& m) {345
Mont<C> M{};346
M.m = m;347
M.n0 = 0 - inv64(m[0]);348
// rr = 2^(2*kBits) mod m, by 2*kBits doublings of 1 with conditional subtract.349
fe<C> x{};350
x[0] = 1;351
for (std::size_t i = 0; i < 2 * kBits<C>; ++i) {352
fe<C> d;353
u64 c = add_carry<C>(d, x, x);354
if (c || geq<C>(d, m)) {355
fe<C> t;356
sub_borrow<C>(t, d, m);357
d = t;358
}359
x = d;360
}361
M.rr = x;362
// one = R mod m = 2^kBits mod m -> to_mont(1)363
fe<C> oneN{};364
oneN[0] = 1;365
mont_mul<C>(M.one, oneN, M.rr, M);366
return M;367
}369
// ---- the two per-curve field contexts (built once per instantiation) ---------------------------370
/**371
* The curve's field context: the Montgomery context for the prime P, built once per372
* instantiation (function-local static).373
* @tparam C the curve traits.374
* @return the context mod C::P.375
* @complexity O(1) after the one-time static make_mont on first use.376
* @alloc none — static storage.377
* @test CheatahP256.VerifyKnownVector378
*/379
template <WeierstrassCurve C>380
const Mont<C>& Fp() {381
static const Mont<C> m = make_mont<C>(C::P);382
return m;383
}384
/**385
* The curve's scalar context: the Montgomery context for the group order N, built once per386
* instantiation (function-local static).387
* @tparam C the curve traits.388
* @return the context mod C::N.389
* @complexity O(1) after the one-time static make_mont on first use.390
* @alloc none — static storage.391
* @test CheatahP256.VerifyKnownVector392
*/393
template <WeierstrassCurve C>394
const Mont<C>& Fn() {395
static const Mont<C> m = make_mont<C>(C::N);396
return m;397
}399
// ---- bytes <-> limbs (kBytes big-endian bytes) --------------------------------------------------400
/**401
* Load kBytes big-endian bytes into a little-endian limb array.402
* @tparam C the curve traits.403
* @param b pointer to kBytes bytes, most significant first.404
* @return the value.405
* @complexity O(1) — kBytes byte reads.406
* @alloc none.407
* @test CheatahP256.VerifyKnownVector408
*/409
template <WeierstrassCurve C>410
fe<C> be_to_fe(const unsigned char* b) {411
fe<C> r{};412
for (std::size_t limb = 0; limb < C::kLimbs; ++limb) {413
u64 v = 0;414
const unsigned char* p = b + (C::kLimbs - 1 - limb) * 8; // most-significant 8 bytes last415
for (int k = 0; k < 8; ++k) v = (v << 8) | p[k];416
r[limb] = v;417
}418
return r;419
}420
/**421
* Store a limb array as kBytes big-endian bytes (the inverse of be_to_fe).422
* @tparam C the curve traits.423
* @param out receives kBytes bytes, most significant first.424
* @param a the value to serialize.425
* @complexity O(1) — kBytes byte writes.426
* @alloc none.427
* @test CheatahP256.SignKnownVector428
*/429
template <WeierstrassCurve C>430
void fe_to_be(unsigned char* out, const fe<C>& a) {431
for (std::size_t limb = 0; limb < C::kLimbs; ++limb) {432
u64 v = a[limb];433
unsigned char* p = out + (C::kLimbs - 1 - limb) * 8;434
for (int k = 7; k >= 0; --k) {435
p[k] = (unsigned char)(v & 0xFF);436
v >>= 8;437
}438
}439
}441
// ---- Jacobian points (coordinates in Montgomery form, mod p) ------------------------------------442
/// A curve point in Jacobian projective coordinates (x = X/Z^2, y = Y/Z^3), Montgomery form.443
template <WeierstrassCurve C>444
struct Jac {445
fe<C> X; ///< projective X446
fe<C> Y; ///< projective Y447
fe<C> Z; ///< projective Z (0 also encodes the point at infinity)448
bool inf; ///< explicit point-at-infinity flag449
};450
/**451
* The point at infinity (the group identity): Z = 0 with the explicit flag set.452
* @tparam C the curve traits.453
* @return the identity point.454
* @complexity O(1).455
* @alloc none.456
* @test CheatahP256.VerifyHitsGroupLawSpecialCases457
*/458
template <WeierstrassCurve C>459
Jac<C> jac_infinity() {460
return Jac<C>{Fp<C>().one, Fp<C>().one, fe<C>{}, true};461
}463
/**464
* Jacobian point doubling, r = 2q, using the a = -3 formulas (true of every NIST prime curve).465
* Branchy (early-returns on infinity): for PUBLIC data only — the secret path uses jac_double_ct.466
* @tparam C the curve traits.467
* @param r receives the doubled point.468
* @param q the point to double.469
* @complexity O(1) — a fixed count of field operations.470
* @alloc none.471
* @test CheatahP256.VerifyKnownVector472
*/473
template <WeierstrassCurve C>474
void jac_double(Jac<C>& r, const Jac<C>& q) {475
const Mont<C>& F = Fp<C>();476
if (q.inf || is_zero<C>(q.Z)) {477
r = jac_infinity<C>();478
return;479
}480
fe<C> A, B, Cc, D, t1, t2;481
mont_mul<C>(A, q.X, q.X, F); // X^2482
mont_mul<C>(B, q.Y, q.Y, F); // Y^2483
mont_mul<C>(Cc, B, B, F); // Y^4484
// D = 2*((X+B)^2 - A - C)485
mont_add<C>(t1, q.X, B, F);486
mont_mul<C>(t1, t1, t1, F);487
mont_sub<C>(t1, t1, A, F);488
mont_sub<C>(t1, t1, Cc, F);489
mont_add<C>(D, t1, t1, F);490
// ZZ = Z^2 ; E = 3*(X - ZZ)*(X + ZZ) [uses a = -3]491
fe<C> ZZ;492
mont_mul<C>(ZZ, q.Z, q.Z, F);493
mont_sub<C>(t1, q.X, ZZ, F);494
mont_add<C>(t2, q.X, ZZ, F);495
mont_mul<C>(t1, t1, t2, F);496
fe<C> E;497
mont_add<C>(E, t1, t1, F);498
mont_add<C>(E, E, t1, F); // 3*(...)499
// F2 = E^2 ; X3 = F2 - 2D500
fe<C> X3;501
mont_mul<C>(X3, E, E, F);502
mont_sub<C>(X3, X3, D, F);503
mont_sub<C>(X3, X3, D, F);504
// Y3 = E*(D - X3) - 8C505
fe<C> Y3, eight;506
mont_sub<C>(t1, D, X3, F);507
mont_mul<C>(Y3, E, t1, F);508
mont_add<C>(eight, Cc, Cc, F);509
mont_add<C>(eight, eight, eight, F);510
mont_add<C>(eight, eight, eight, F); // 8C511
mont_sub<C>(Y3, Y3, eight, F);512
// Z3 = 2*Y*Z513
fe<C> Z3;514
mont_mul<C>(Z3, q.Y, q.Z, F);515
mont_add<C>(Z3, Z3, Z3, F);516
r = Jac<C>{X3, Y3, Z3, false};517
}519
/**520
* Jacobian point addition, r = a + b, with branchy special cases (either operand infinity,521
* a == b -> double, a == -b -> infinity). For PUBLIC data only — the secret path uses jac_add_ct.522
* @tparam C the curve traits.523
* @param r receives the sum.524
* @param a first point.525
* @param b second point.526
* @complexity O(1) — a fixed count of field operations.527
* @alloc none.528
* @test CheatahP256.VerifyHitsGroupLawSpecialCases529
*/530
template <WeierstrassCurve C>531
void jac_add(Jac<C>& r, const Jac<C>& a, const Jac<C>& b) {532
const Mont<C>& F = Fp<C>();533
if (a.inf || is_zero<C>(a.Z)) {534
r = b;535
return;536
}537
if (b.inf || is_zero<C>(b.Z)) {538
r = a;539
return;540
}541
fe<C> Z1Z1, Z2Z2, U1, U2, S1, S2;542
mont_mul<C>(Z1Z1, a.Z, a.Z, F);543
mont_mul<C>(Z2Z2, b.Z, b.Z, F);544
mont_mul<C>(U1, a.X, Z2Z2, F);545
mont_mul<C>(U2, b.X, Z1Z1, F);546
fe<C> t;547
mont_mul<C>(t, b.Z, Z2Z2, F);548
mont_mul<C>(S1, a.Y, t, F);549
mont_mul<C>(t, a.Z, Z1Z1, F);550
mont_mul<C>(S2, b.Y, t, F);551
fe<C> H, Rr;552
mont_sub<C>(H, U2, U1, F);553
mont_sub<C>(Rr, S2, S1, F);554
if (is_zero<C>(H)) {555
if (is_zero<C>(Rr)) {556
jac_double<C>(r, a);557
return;558
}559
r = jac_infinity<C>();560
return;561
}562
fe<C> HH, HHH, V;563
mont_mul<C>(HH, H, H, F);564
mont_mul<C>(HHH, HH, H, F);565
mont_mul<C>(V, U1, HH, F);566
fe<C> X3;567
mont_mul<C>(X3, Rr, Rr, F);568
mont_sub<C>(X3, X3, HHH, F);569
mont_sub<C>(X3, X3, V, F);570
mont_sub<C>(X3, X3, V, F);571
fe<C> Y3;572
mont_sub<C>(t, V, X3, F);573
mont_mul<C>(Y3, Rr, t, F);574
fe<C> s1hhh;575
mont_mul<C>(s1hhh, S1, HHH, F);576
mont_sub<C>(Y3, Y3, s1hhh, F);577
fe<C> Z3;578
mont_mul<C>(Z3, a.Z, b.Z, F);579
mont_mul<C>(Z3, Z3, H, F);580
r = Jac<C>{X3, Y3, Z3, false};581
}583
/**584
* Strauss-Shamir: u1*A + u2*B with ONE doubling chain (kBits doublings total)585
* instead of two separate scalar multiplications. A 2-bit window over both586
* scalars uses a 16-entry combined table [i*A + j*B] so it also halves the adds.587
* @tparam C the curve traits.588
* @param r receives u1*A + u2*B.589
* @param u1 first (public) scalar.590
* @param A first point.591
* @param u2 second (public) scalar.592
* @param B second point.593
* @complexity O(1) — kBits doublings plus at most kBits/2 adds.594
* @alloc none — the 16-entry window table lives on the stack.595
* @test CheatahP256.VerifyKnownVector596
*/597
template <WeierstrassCurve C>598
void jac_double_mul(Jac<C>& r, const fe<C>& u1, const Jac<C>& A, const fe<C>& u2, const Jac<C>& B) {599
Jac<C> tbl[4][4]; // tbl[i][j] = i*A + j*B, i,j in {0..3}600
tbl[0][0] = jac_infinity<C>();601
tbl[1][0] = A;602
jac_double<C>(tbl[2][0], A);603
jac_add<C>(tbl[3][0], tbl[2][0], A);604
tbl[0][1] = B;605
jac_double<C>(tbl[0][2], B);606
jac_add<C>(tbl[0][3], tbl[0][2], B);607
for (int i = 1; i < 4; ++i)608
for (int j = 1; j < 4; ++j) jac_add<C>(tbl[i][j], tbl[i][0], tbl[0][j]);610
Jac<C> acc = jac_infinity<C>();611
for (int i = static_cast<int>(kBits<C>) - 2; i >= 0; i -= 2) { // kBits is even612
Jac<C> t{};613
jac_double<C>(t, acc);614
acc = t;615
jac_double<C>(t, acc);616
acc = t;617
const unsigned a = (u1[i / 64] >> (i % 64)) & 0x3;618
const unsigned b = (u2[i / 64] >> (i % 64)) & 0x3;619
if (a || b) {620
jac_add<C>(t, acc, tbl[a][b]);621
acc = t;622
}623
}624
r = acc;625
}627
/**628
* The affine x-coordinate (normal form) of a Jacobian point: x = X / Z^2.629
* @tparam C the curve traits.630
* @param q the point (not infinity: Z must be invertible).631
* @return x out of Montgomery form.632
* @complexity O(1) — dominated by one mont_inv (a fixed Fermat ladder).633
* @alloc none.634
* @test CheatahP256.VerifyKnownVector635
*/636
template <WeierstrassCurve C>637
fe<C> jac_affine_x(const Jac<C>& q) {638
const Mont<C>& F = Fp<C>();639
fe<C> zinv, zinv2, x;640
mont_inv<C>(zinv, q.Z, F);641
mont_mul<C>(zinv2, zinv, zinv, F);642
mont_mul<C>(x, q.X, zinv2, F);643
fe<C> out;644
from_mont<C>(out, x, F);645
return out;646
}648
/**649
* Lift an affine point into Jacobian Montgomery form (Z = 1).650
* @tparam C the curve traits.651
* @param x the affine x-coordinate (plain form).652
* @param y the affine y-coordinate (plain form).653
* @return the Jacobian point.654
* @complexity O(1) — two to_mont conversions.655
* @alloc none.656
* @test CheatahP256.VerifyKnownVector657
*/658
template <WeierstrassCurve C>659
Jac<C> affine_to_jac(const fe<C>& x, const fe<C>& y) {660
const Mont<C>& F = Fp<C>();661
Jac<C> p{};662
to_mont<C>(p.X, x, F);663
to_mont<C>(p.Y, y, F);664
p.Z = F.one;665
p.inf = false;666
return p;667
}668
/**669
* The curve base point G, lifted to Jacobian form once (function-local static).670
* @tparam C the curve traits.671
* @return G.672
* @complexity O(1) after the one-time static lift on first use.673
* @alloc none — static storage.674
* @test CheatahP256.VerifyKnownVector675
*/676
template <WeierstrassCurve C>677
const Jac<C>& base_point() {678
static const Jac<C> g = affine_to_jac<C>(C::GX, C::GY);679
return g;680
}682
/**683
* Fixed-base comb for k*G. G is constant, so we precompute (once) the 2^kLimbs-entry684
* table T[s] = sum over set bits i of s of (2^(64*i) * G). Then k*G is just 64685
* doublings + 64 adds (vs kBits doublings for a generic window) — the big win for686
* the per-message signing path. Selector at step j is bit j of each 64-bit limb.687
* @tparam C the curve traits.688
* @return the comb table.689
* @complexity O(1) after the one-time static build ((kLimbs-1)*64 doublings plus the subset sums).690
* @alloc none — the table is a function-local static std::array.691
* @test CheatahP256.SignKnownVector692
*/693
template <WeierstrassCurve C>694
const std::array<Jac<C>, (1u << C::kLimbs)>& g_comb() {695
static const std::array<Jac<C>, (1u << C::kLimbs)> tbl = [] {696
constexpr std::size_t L = C::kLimbs;697
Jac<C> gi[L];698
gi[0] = affine_to_jac<C>(C::GX, C::GY);699
for (std::size_t i = 1; i < L; ++i) {700
Jac<C> acc = gi[i - 1];701
for (int b = 0; b < 64; ++b) { // gi[i] = 2^64 * gi[i-1]702
Jac<C> t{};703
jac_double<C>(t, acc);704
acc = t;705
}706
gi[i] = acc;707
}708
std::array<Jac<C>, (1u << L)> t{};709
t[0] = jac_infinity<C>();710
for (unsigned s = 1; s < (1u << L); ++s) {711
Jac<C> acc = jac_infinity<C>();712
for (std::size_t i = 0; i < L; ++i)713
if (s & (1u << i)) {714
Jac<C> r{};715
jac_add<C>(r, acc, gi[i]);716
acc = r;717
}718
t[s] = acc;719
}720
return t;721
}();722
return tbl;723
}725
// ---- constant-time point ops for the SECRET-scalar path (signing k*G, keygen d*G) --------------726
// jac_double_mul (verify) operates on PUBLIC data and stays branchy; the fixed-base comb below,727
// which multiplies the secret nonce/key, must not branch or index on secret bits. These helpers728
// give it branch-free doubling, addition, and table selection. They are differentially tested729
// against the branchy jac_double/jac_add over general + edge inputs730
// (CheatahP256.ConstantTimePointOpsMatchReference).732
/**733
* Constant-time conditional move over a Jacobian point (all three coordinates via fe_cmov;734
* the inf flag is recomputed from Z, which encodes infinity throughout the CT path).735
* @tparam C the curve traits.736
* @param r the destination point.737
* @param a the source point.738
* @param m the ct_mask (0 or all-ones).739
* @complexity O(1).740
* @alloc none.741
* @test CheatahP256.ConstantTimePointOpsMatchReference742
*/743
template <WeierstrassCurve C>744
inline void jac_cmov(Jac<C>& r, const Jac<C>& a, u64 m) {745
fe_cmov<C>(r.X, a.X, m);746
fe_cmov<C>(r.Y, a.Y, m);747
fe_cmov<C>(r.Z, a.Z, m);748
r.inf = is_zero<C>(r.Z); // infinity is encoded by Z==0 throughout the CT path749
}751
/**752
* Point doubling WITHOUT the is-infinity early return: the formula's Z3 = 2*Y*Z is already 0 when753
* the input is infinity (Z==0), so it self-encodes infinity, and a prime-order curve has no754
* finite 2-torsion point that could double TO infinity — so no branch is needed.755
* @tparam C the curve traits.756
* @param r receives 2q.757
* @param q the point to double.758
* @complexity O(1) — the same fixed field-operation count for every input.759
* @alloc none.760
* @test CheatahP256.ConstantTimePointOpsMatchReference761
*/762
template <WeierstrassCurve C>763
void jac_double_ct(Jac<C>& r, const Jac<C>& q) {764
const Mont<C>& F = Fp<C>();765
fe<C> A, B, Cc, D, t1, t2;766
mont_mul<C>(A, q.X, q.X, F);767
mont_mul<C>(B, q.Y, q.Y, F);768
mont_mul<C>(Cc, B, B, F);769
mont_add<C>(t1, q.X, B, F);770
mont_mul<C>(t1, t1, t1, F);771
mont_sub<C>(t1, t1, A, F);772
mont_sub<C>(t1, t1, Cc, F);773
mont_add<C>(D, t1, t1, F);774
fe<C> ZZ;775
mont_mul<C>(ZZ, q.Z, q.Z, F);776
mont_sub<C>(t1, q.X, ZZ, F);777
mont_add<C>(t2, q.X, ZZ, F);778
mont_mul<C>(t1, t1, t2, F);779
fe<C> E;780
mont_add<C>(E, t1, t1, F);781
mont_add<C>(E, E, t1, F);782
fe<C> X3;783
mont_mul<C>(X3, E, E, F);784
mont_sub<C>(X3, X3, D, F);785
mont_sub<C>(X3, X3, D, F);786
fe<C> Y3, eight;787
mont_sub<C>(t1, D, X3, F);788
mont_mul<C>(Y3, E, t1, F);789
mont_add<C>(eight, Cc, Cc, F);790
mont_add<C>(eight, eight, eight, F);791
mont_add<C>(eight, eight, eight, F);792
mont_sub<C>(Y3, Y3, eight, F);793
fe<C> Z3;794
mont_mul<C>(Z3, q.Y, q.Z, F);795
mont_add<C>(Z3, Z3, Z3, F);796
r = Jac<C>{X3, Y3, Z3, is_zero<C>(Z3)};797
}799
/**800
* Point addition, branch-free. It always computes the general add formula, then constant-time-801
* selects the correct result over the special cases via masks: a==inf -> b, b==inf -> a,802
* a==b -> double(a), a==-b -> infinity. Precedence is enforced by cmov ORDER (a==inf last / highest).803
* @tparam C the curve traits.804
* @param r receives a + b.805
* @param a first point.806
* @param b second point.807
* @complexity O(1) — the same fixed field-operation count for every input (the double is always computed).808
* @alloc none.809
* @test CheatahP256.ConstantTimePointOpsMatchReference810
*/811
template <WeierstrassCurve C>812
void jac_add_ct(Jac<C>& r, const Jac<C>& a, const Jac<C>& b) {813
const Mont<C>& F = Fp<C>();814
fe<C> Z1Z1, Z2Z2, U1, U2, S1, S2;815
mont_mul<C>(Z1Z1, a.Z, a.Z, F);816
mont_mul<C>(Z2Z2, b.Z, b.Z, F);817
mont_mul<C>(U1, a.X, Z2Z2, F);818
mont_mul<C>(U2, b.X, Z1Z1, F);819
fe<C> t;820
mont_mul<C>(t, b.Z, Z2Z2, F);821
mont_mul<C>(S1, a.Y, t, F);822
mont_mul<C>(t, a.Z, Z1Z1, F);823
mont_mul<C>(S2, b.Y, t, F);824
fe<C> H, Rr;825
mont_sub<C>(H, U2, U1, F);826
mont_sub<C>(Rr, S2, S1, F);827
fe<C> HH, HHH, V;828
mont_mul<C>(HH, H, H, F);829
mont_mul<C>(HHH, HH, H, F);830
mont_mul<C>(V, U1, HH, F);831
fe<C> X3;832
mont_mul<C>(X3, Rr, Rr, F);833
mont_sub<C>(X3, X3, HHH, F);834
mont_sub<C>(X3, X3, V, F);835
mont_sub<C>(X3, X3, V, F);836
fe<C> Y3;837
mont_sub<C>(t, V, X3, F);838
mont_mul<C>(Y3, Rr, t, F);839
fe<C> s1hhh;840
mont_mul<C>(s1hhh, S1, HHH, F);841
mont_sub<C>(Y3, Y3, s1hhh, F);842
fe<C> Z3;843
mont_mul<C>(Z3, a.Z, b.Z, F);844
mont_mul<C>(Z3, Z3, H, F);845
r = Jac<C>{X3, Y3, Z3, false}; // start = the general-case result847
const u64 ma = ct_mask(is_zero<C>(a.Z)); // a is infinity848
const u64 mb = ct_mask(is_zero<C>(b.Z)); // b is infinity849
const u64 hz = ct_mask(is_zero<C>(H));850
const u64 rz = ct_mask(is_zero<C>(Rr));851
Jac<C> dbl{};852
jac_double_ct<C>(dbl, a);853
const Jac<C> infp = jac_infinity<C>();854
jac_cmov<C>(r, dbl, hz & rz); // a == b -> 2a855
jac_cmov<C>(r, infp, hz & ~rz); // a == -b -> infinity856
jac_cmov<C>(r, a, mb); // b == infinity -> a857
jac_cmov<C>(r, b, ma); // a == infinity -> b (highest precedence, applied last)858
r.inf = is_zero<C>(r.Z);859
}861
/**862
* Constant-time table lookup: scan every entry, copying the one whose index == sel via a mask, so863
* the memory-access pattern (and timing) is independent of the secret selector.864
* @tparam C the curve traits.865
* @tparam N the table size.866
* @param out receives tbl[sel].867
* @param tbl the table.868
* @param sel the (secret) index.869
* @complexity O(N) — every entry is scanned by design.870
* @alloc none.871
* @test CheatahP256.SignKnownVector872
*/873
template <WeierstrassCurve C, std::size_t N>874
void ct_select(Jac<C>& out, const std::array<Jac<C>, N>& tbl, unsigned sel) {875
out = jac_infinity<C>();876
for (unsigned i = 0; i < N; ++i) jac_cmov<C>(out, tbl[i], ct_mask(i == sel));877
}879
/**880
* k*G for a SECRET scalar k, in constant time: 64 doublings + 64 unconditional adds over the881
* fixed-base comb table. The old form skipped the add when the window was zero and indexed the882
* table by the secret selector — both leaked bits of k. Here every step does the same work883
* (branch-free double, masked table select, unconditional branch-free add — add of the T[0]=infinity884
* entry when the window is zero is a no-op via the CT add's masks). The field arithmetic beneath is885
* branch-free as well: every modular reduction is computed and then selected with a mask, so no886
* step's timing depends on the values flowing through it.887
* @tparam C the curve traits.888
* @param r receives k*G.889
* @param k the secret scalar.890
* @complexity O(1) — exactly 64 CT doublings, 64 CT table scans, and 64 CT adds.891
* @alloc none.892
* @test CheatahP256.SignKnownVector893
*/894
template <WeierstrassCurve C>895
void jac_mul_base(Jac<C>& r, const fe<C>& k) {896
const auto& T = g_comb<C>();897
Jac<C> acc = jac_infinity<C>();898
for (int j = 63; j >= 0; --j) {899
Jac<C> t{};900
jac_double_ct<C>(t, acc);901
acc = t;902
unsigned sel = 0;903
for (std::size_t i = 0; i < C::kLimbs; ++i) sel |= static_cast<unsigned>((k[i] >> j) & 1u) << i;904
Jac<C> add{};905
ct_select<C>(add, T, sel);906
jac_add_ct<C>(t, acc, add);907
acc = t;908
}909
r = acc;910
}912
/**913
* Differentially validate the branch-free FIELD arithmetic against an independent reference.914
*915
* mont_add, mont_sub and mont_mul each end in a conditional reduction that is now computed916
* unconditionally and selected with a mask. @ref ct_add_selfcheck cannot police that: both sides917
* of its comparison call the same field ops, so an error there cancels. This checks the reductions918
* directly — the boundaries (0, 1, m-1) and a deterministic sweep whose intermediate sums and919
* differences land in `[m, 2m)`, the band the RFC 6979 vector never reaches — against a reference920
* written the obvious way.921
* @tparam C the curve traits.922
* @return true iff every case agrees.923
* @complexity O(1) — a fixed number of fixed-width operations.924
* @alloc none.925
* @test CheatahP256.ConstantTimeFieldOpsMatchReference926
*/927
template <WeierstrassCurve C>928
bool ct_field_selfcheck() {929
const Mont<C>& F = Fp<C>();930
// Reference add/sub: the same mathematics, written with branches. Correctness here is easy to931
// see by eye, which is the point — it is the oracle, not the fast path.932
auto ref_add = [&](const fe<C>& a, const fe<C>& b) {933
fe<C> s{};934
const u64 c = add_carry<C>(s, a, b);935
fe<C> t{};936
const u64 br = sub_borrow<C>(t, s, F.m);937
if (c != 0 || br == 0) return t; // overflowed, or already >= m938
return s;939
};940
auto ref_sub = [&](const fe<C>& a, const fe<C>& b) {941
fe<C> d{};942
const u64 br = sub_borrow<C>(d, a, b);943
if (br != 0) {944
fe<C> t{};945
add_carry<C>(t, d, F.m);946
return t;947
}948
return d;949
};951
fe<C> zero{};952
fe<C> one{};953
one[0] = 1;954
fe<C> mm1{}; // m - 1: the top of the field, where a sum must reduce955
sub_borrow<C>(mm1, F.m, one);957
std::array<fe<C>, 5> seeds{zero, one, mm1, F.m, F.rr};958
for (const fe<C>& a : seeds) {959
for (const fe<C>& b : seeds) {960
fe<C> got{};961
mont_add<C>(got, a, b, F);962
if (got != ref_add(a, b)) return false;963
mont_sub<C>(got, a, b, F);964
if (got != ref_sub(a, b)) return false;965
}966
}968
// A deterministic sweep (a 64-bit xorshift, so the case list is identical on every run and on969
// every machine) driving operands across the whole range rather than the few the vectors hit.970
u64 st = 0x9E3779B97F4A7C15ULL;971
auto next = [&]() {972
st ^= st << 13;973
st ^= st >> 7;974
st ^= st << 17;975
return st;976
};977
for (int iter = 0; iter < 512; ++iter) {978
fe<C> a{};979
fe<C> b{};980
for (std::size_t i = 0; i < C::kLimbs; ++i) {981
a[i] = next();982
b[i] = next();983
}984
// Bring both into the field first, so the operands are the shape the real path sees.985
fe<C> ar{};986
fe<C> br2{};987
mont_mul<C>(ar, a, F.one, F);988
mont_mul<C>(br2, b, F.one, F);989
fe<C> got{};990
mont_add<C>(got, ar, br2, F);991
if (got != ref_add(ar, br2)) return false;992
mont_sub<C>(got, ar, br2, F);993
if (got != ref_sub(ar, br2)) return false;994
// A product must land in [0, m): the reduction is what this is really testing.995
fe<C> prod{};996
mont_mul<C>(prod, ar, br2, F);997
if (geq<C>(prod, F.m)) return false;998
}999
return true;1000
}1002
/**1003
* Differential self-check for the constant-time point ops. A TEMPLATE, instantiated ONLY by the1004
* p256/p384 test seam (so there is no such code in a production build), it confirms jac_add_ct /1005
* jac_double_ct agree with the branchy reference jac_add / jac_double on the general case AND every1006
* special case — a==b, a==-b, and infinity operands — which the signing path exercises rarely or1007
* never, so this both proves correctness and drives those branches for coverage.1008
* @tparam C the curve traits.1009
* @return true iff every CT result matches the branchy reference.1010
* @complexity O(1) — a fixed handful of point operations.1011
* @alloc none.1012
* @test CheatahP256.ConstantTimePointOpsMatchReference1013
*/1014
template <WeierstrassCurve C>1015
bool ct_add_selfcheck() {1016
const Mont<C>& F = Fp<C>();1017
auto affine_eq = [&](const Jac<C>& u, const Jac<C>& v) -> bool {1018
const bool ui = is_zero<C>(u.Z), vi = is_zero<C>(v.Z);1019
if (ui || vi) return ui == vi; // both infinity, or neither1020
auto affine = [&](const Jac<C>& p, fe<C>& x, fe<C>& y) {1021
fe<C> zi, zi2, zi3, xm, ym;1022
mont_inv<C>(zi, p.Z, F);1023
mont_mul<C>(zi2, zi, zi, F);1024
mont_mul<C>(zi3, zi2, zi, F);1025
mont_mul<C>(xm, p.X, zi2, F);1026
mont_mul<C>(ym, p.Y, zi3, F);1027
from_mont<C>(x, xm, F);1028
from_mont<C>(y, ym, F);1029
};1030
fe<C> ux, uy, vx, vy;1031
affine(u, ux, uy);1032
affine(v, vx, vy);1033
return ux == vx && uy == vy;1034
};1035
// Reference points via the branchy ops: P = 3G, Q = 5G, and -P.1036
const Jac<C>& G = base_point<C>();1037
Jac<C> P{}, Q{}, tmp{};1038
jac_double<C>(tmp, G); // 2G1039
jac_add<C>(P, tmp, G); // 3G1040
jac_double<C>(tmp, tmp); // 4G1041
jac_add<C>(Q, tmp, G); // 5G1042
fe<C> zero{};1043
Jac<C> negP = P;1044
mont_sub<C>(negP.Y, zero, P.Y, F); // -P = (X, -Y, Z)1045
const Jac<C> inf = jac_infinity<C>();1047
Jac<C> ct{}, ref{};1048
bool ok = true;1049
jac_add_ct<C>(ct, P, Q); jac_add<C>(ref, P, Q); ok &= affine_eq(ct, ref); // general1050
jac_add_ct<C>(ct, P, P); jac_double<C>(ref, P); ok &= affine_eq(ct, ref); // a == b1051
jac_add_ct<C>(ct, P, negP); ok &= is_zero<C>(ct.Z); // a == -b -> infinity1052
jac_add_ct<C>(ct, inf, P); ok &= affine_eq(ct, P); // a == infinity1053
jac_add_ct<C>(ct, P, inf); ok &= affine_eq(ct, P); // b == infinity1054
jac_add_ct<C>(ct, inf, inf); ok &= is_zero<C>(ct.Z); // inf + inf1055
jac_double_ct<C>(ct, P); jac_double<C>(ref, P); ok &= affine_eq(ct, ref); // double general1056
jac_double_ct<C>(ct, inf); ok &= is_zero<C>(ct.Z); // double infinity1057
return ok;1058
}1060
/**1061
* Reduce a scalar already known to be < 2n into [0, n): a single conditional1062
* subtraction of the group order n. Used for the FIPS 186-4 hash truncation and1063
* for folding a curve x-coordinate (which lives in [0, p) < 2n) into a scalar.1064
* @tparam C the curve traits.1065
* @param v the value, < 2n.1066
* @return v mod n.1067
* @complexity O(1).1068
* @alloc none.1069
* @test CheatahP256.ReduceModNBoundary1070
*/1071
template <WeierstrassCurve C>1072
fe<C> reduce_mod_n(const fe<C>& v) {1073
if (geq<C>(v, C::N)) {1074
fe<C> t;1075
sub_borrow<C>(t, v, C::N);1076
return t;1077
}1078
return v;1079
}1081
/**1082
* Reduce a big-endian hash to a scalar in [0, n). A hash of at least kBytes keeps its1083
* leftmost kBytes (the FIPS 186-4 leftmost-bits truncation); a SHORTER hash is the whole1084
* value (X9.62 bits2int — right-aligned), e.g. a SHA-256 signature under a P-384 key.1085
* @tparam C the curve traits.1086
* @param h the digest bytes.1087
* @return the scalar in [0, n).1088
* @complexity O(1) — at most kBytes are copied regardless of the hash length.1089
* @alloc none — a stack buffer.1090
* @test CheatahP256.HashToScalarReducesWhenGreaterThanOrder1091
* @test CheatahP384.HashToScalarReducesWhenGreaterThanOrder1092
*/1093
template <WeierstrassCurve C>1094
fe<C> hash_to_scalar(const std::string& h) {1095
unsigned char buf[kBytes<C>] = {0};1096
if (h.size() >= kBytes<C>)1097
std::memcpy(buf, h.data(), kBytes<C>);1098
else1099
std::memcpy(buf + (kBytes<C> - h.size()), h.data(), h.size()); // NOLINT(bugprone-not-null-terminated-result): raw big-endian bytes, not a C string1100
return reduce_mod_n<C>(be_to_fe<C>(buf));1101
}1103
// ---- minimal DER helpers ------------------------------------------------------------------------1104
/**1105
* Parse SEQUENCE{INTEGER r, INTEGER s} -> kBytes big-endian r and s.1106
* Short-form lengths only: both curves' SEQUENCE stays under 128 bytes (P-384: <= ~104).1107
* @tparam C the curve traits.1108
* @param der the DER-encoded signature.1109
* @param r receives kBytes big-endian r.1110
* @param s receives kBytes big-endian s.1111
* @return false on any malformed encoding.1112
* @complexity O(1) — short-form DER caps the accepted input at 129 bytes (a longer @p der1113
* fails the exact-length check without being scanned).1114
* @alloc none.1115
* @test CheatahP256.VerifyDerWithLeadingZeroIntegers1116
*/1117
template <WeierstrassCurve C>1118
bool der_to_rs(const std::string& der, unsigned char* r, unsigned char* s) {1119
const auto* p = reinterpret_cast<const unsigned char*>(der.data());1120
std::size_t n = der.size(), i = 0;1121
auto read_int = [&](unsigned char* out) -> bool {1122
if (i >= n) return false;1123
const unsigned char int_tag = p[i++];1124
if (int_tag != 0x02) return false;1125
if (i >= n) return false;1126
std::size_t len = p[i++];1127
if (len & 0x80) return false; // curve-order ints are short-form1128
if (i + len > n || len == 0) return false;1129
const unsigned char* v = p + i;1130
// strip a leading zero (sign byte)1131
while (len > 1 && v[0] == 0) {1132
++v;1133
--len;1134
}1135
if (len > kBytes<C>) return false;1136
std::memset(out, 0, kBytes<C>);1137
std::memcpy(out + (kBytes<C> - len), v, len);1138
i += (std::size_t)(v - (p + i)) + len; // advance past the original field1139
return true;1140
};1141
if (i >= n) return false;1142
const unsigned char seq_tag = p[i++];1143
if (seq_tag != 0x30) return false;1144
if (i >= n) return false;1145
std::size_t seqlen = p[i++];1146
if (seqlen & 0x80) return false;1147
if (i + seqlen != n) return false;1148
return read_int(r) && read_int(s);1149
}1151
/**1152
* Is (x, y) on the curve y^2 = x^3 - 3x + b (mod p)? Rejects an off-curve /1153
* invalid-curve public key — SP 800-56A / FIPS 186 point validation, which the plain1154
* coordinate-range check (x,y < p) does not catch.1155
* @tparam C the curve traits.1156
* @param x the affine x-coordinate (plain form, < p).1157
* @param y the affine y-coordinate (plain form, < p).1158
* @return true iff the point satisfies the curve equation.1159
* @complexity O(1) — a fixed handful of field operations.1160
* @alloc none.1161
* @test CheatahP256.RejectsOffCurvePublicKey1162
* @test CheatahP384.RejectsOffCurvePublicKey1163
*/1164
template <WeierstrassCurve C>1165
bool on_curve(const fe<C>& x, const fe<C>& y) {1166
const Mont<C>& F = Fp<C>();1167
fe<C> xm, ym, x2, x3, tx, rhs, bm, y2;1168
to_mont<C>(xm, x, F);1169
to_mont<C>(ym, y, F);1170
mont_mul<C>(x2, xm, xm, F); // x^21171
mont_mul<C>(x3, x2, xm, F); // x^31172
mont_add<C>(tx, xm, xm, F); // 2x1173
mont_add<C>(tx, tx, xm, F); // 3x1174
mont_sub<C>(rhs, x3, tx, F); // x^3 - 3x1175
to_mont<C>(bm, C::B, F);1176
mont_add<C>(rhs, rhs, bm, F); // x^3 - 3x + b1177
mont_mul<C>(y2, ym, ym, F); // y^21178
return std::memcmp(y2.data(), rhs.data(), sizeof(fe<C>)) == 0;1179
}1181
/**1182
* ECDSA verification over raw byte forms: pubkey = 2*kBytes X||Y, sig = 2*kBytes r||s.1183
* @tparam C the curve traits.1184
* @param pubkey_xy the public key point, 2*kBytes X||Y big-endian.1185
* @param msg_hash the message digest (truncated/reduced by hash_to_scalar).1186
* @param sig_raw the signature, 2*kBytes r||s big-endian.1187
* @return true iff the signature verifies (range checks, on-curve check, and x == r all pass).1188
* @complexity O(1) — two scalar multiplications, computed as one Strauss-Shamir double chain.1189
* @alloc none.1190
* @test CheatahP256.VerifyKnownVector1191
* @test CheatahP384.VerifyKnownVector1192
*/1193
template <WeierstrassCurve C>1194
bool verify_raw(const std::string& pubkey_xy, const std::string& msg_hash,1195
const std::string& sig_raw) {1196
if (pubkey_xy.size() != 2 * kBytes<C> || sig_raw.size() != 2 * kBytes<C>) return false;1197
fe<C> r = be_to_fe<C>(reinterpret_cast<const unsigned char*>(sig_raw.data()));1198
fe<C> s = be_to_fe<C>(reinterpret_cast<const unsigned char*>(sig_raw.data()) + kBytes<C>);1199
if (is_zero<C>(r) || is_zero<C>(s) || geq<C>(r, C::N) || geq<C>(s, C::N)) return false;1201
const Mont<C>& Fnn = Fn<C>();1202
fe<C> e = hash_to_scalar<C>(msg_hash);1203
fe<C> sm, em, rm, w, u1, u2;1204
to_mont<C>(sm, s, Fnn);1205
mont_inv<C>(w, sm, Fnn); // w = s^-1 (Montgomery)1206
to_mont<C>(em, e, Fnn);1207
to_mont<C>(rm, r, Fnn);1208
fe<C> u1m, u2m;1209
mont_mul<C>(u1m, em, w, Fnn);1210
mont_mul<C>(u2m, rm, w, Fnn);1211
from_mont<C>(u1, u1m, Fnn);1212
from_mont<C>(u2, u2m, Fnn);1214
fe<C> qx = be_to_fe<C>(reinterpret_cast<const unsigned char*>(pubkey_xy.data()));1215
fe<C> qy = be_to_fe<C>(reinterpret_cast<const unsigned char*>(pubkey_xy.data()) + kBytes<C>);1216
if (geq<C>(qx, C::P) || geq<C>(qy, C::P)) return false;1217
if (!on_curve<C>(qx, qy)) return false; // reject off-curve / invalid-curve public keys1218
Jac<C> Q = affine_to_jac<C>(qx, qy);1220
Jac<C> R{};1221
jac_double_mul<C>(R, u1, base_point<C>(), u2, Q); // u1*G + u2*Q, one doubling chain1222
if (R.inf || is_zero<C>(R.Z)) return false;1223
fe<C> x = reduce_mod_n<C>(jac_affine_x<C>(R));1224
return std::memcmp(x.data(), r.data(), sizeof(fe<C>)) == 0;1225
}1227
/**1228
* ECDSA verification of the DER form (SEQUENCE{INTEGER r, INTEGER s} — TLS/X.509).1229
* @tparam C the curve traits.1230
* @param pubkey_xy the public key point, 2*kBytes X||Y big-endian.1231
* @param msg_hash the message digest.1232
* @param sig_der the DER-encoded signature.1233
* @return true iff the DER parses and the signature verifies.1234
* @complexity O(1) — der_to_rs plus one verify_raw.1235
* @alloc a temporary raw r||s signature string.1236
* @test CheatahP256.VerifyDerWithLeadingZeroIntegers1237
* @test CheatahP384.VerifyDerWithLeadingZeroIntegers1238
*/1239
template <WeierstrassCurve C>1240
bool verify_der(const std::string& pubkey_xy, const std::string& msg_hash,1241
const std::string& sig_der) {1242
unsigned char r[kBytes<C>], s[kBytes<C>];1243
if (!der_to_rs<C>(sig_der, r, s)) return false;1244
std::string raw;1245
raw.resize(2 * kBytes<C>);1246
std::memcpy(raw.data(), r, kBytes<C>);1247
std::memcpy(raw.data() + kBytes<C>, s, kBytes<C>);1248
return verify_raw<C>(pubkey_xy, msg_hash, raw);1249
}1251
/**1252
* Encode a raw r||s signature (2*kBytes big-endian bytes) as the DER1253
* `SEQUENCE{INTEGER r, INTEGER s}` that TLS CertificateVerify and X.509 carry — the1254
* exact inverse of @ref der_to_rs. Integers are minimal-form: leading zero bytes are1255
* stripped and a 0x00 sign byte is prepended when the top bit is set, so the output1256
* round-trips through any strict DER parser. The outer length always fits short form1257
* (max 2*(kBytes+3) = 102 bytes at P-384).1258
* @tparam C the curve traits.1259
* @param sig_raw the 2*kBytes r||s signature (e.g. sign_raw's output).1260
* @return the DER bytes, or "" if @p sig_raw has the wrong length or a zero integer1261
* (r = 0 / s = 0 is never a valid ECDSA signature).1262
* @complexity O(kBytes).1263
* @alloc the returned string plus the two integer temporaries.1264
* @test CheatahP256.RsToDerRoundTripsAndRejects1265
*/1266
template <WeierstrassCurve C>1267
std::string rs_to_der(const std::string& sig_raw) {1268
if (sig_raw.size() != 2 * kBytes<C>) return "";1269
const auto encode_int = [](const unsigned char* v) -> std::string {1270
std::size_t i = 0;1271
while (i < kBytes<C> - 1 && v[i] == 0) ++i; // strip leading zeros, keep >= 1 byte1272
if (v[i] == 0) return ""; // the integer is zero — not a signature1273
const bool sign = (v[i] & 0x80) != 0;1274
std::string out;1275
out.push_back(0x02);1276
out.push_back(static_cast<char>((kBytes<C> - i) + (sign ? 1 : 0)));1277
if (sign) out.push_back('\0');1278
out.append(reinterpret_cast<const char*>(v + i), kBytes<C> - i);1279
return out;1280
};1281
const std::string r = encode_int(reinterpret_cast<const unsigned char*>(sig_raw.data()));1282
const std::string s = encode_int(reinterpret_cast<const unsigned char*>(sig_raw.data()) + kBytes<C>);1283
if (r.empty() || s.empty()) return "";1284
std::string der;1285
der.push_back(0x30);1286
der.push_back(static_cast<char>(r.size() + s.size()));1287
return der + r + s;1288
}1290
} // namespace cheatah::ec