ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
curve25519.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file curve25519.cpp
6 * @brief Curve25519 field arithmetic (GF(2^255-19), radix-2^16) + X25519 (RFC 7748).
7 *
8 * The field element is sixteen int64 limbs of ~16 bits each; a full 16x16 schoolbook
9 * product fits in int64 (no 128-bit type, so it builds on 32-bit xtensa). Reduction
10 * folds anything at or above 2^256 back as *38 (2^256 = 2*2^255 = 2*19 mod p). The
11 * X25519 scalar multiplication is the RFC 7748 §5 Montgomery ladder with constant-time
12 * conditional swaps. Validated against the RFC 7748 §5.2 vectors (test_ed25519).
13 *
14 * On the ESP32-S3 (Arduino) X25519 has a second, byte-identical implementation that runs the
15 * ladder in canonical uint32[8] and does each field multiply as one 256-bit modular multiply on
16 * the RSA/MPI accelerator (~4.3x the software/PIE ladder); the field layer is in pc_fe25519.h (shared with
17 * Ed25519, active as PC_FE25519_MPI_HW). It shares the accelerator lock with mbedtls, so a scalar-mult is
18 * bracketed by pc_fe_hw_enable()/pc_fe_hw_disable() (esp_mpi_{enable,disable}_hardware_hw_op()).
19 */
20
22// On the S3, X25519 runs its whole Montgomery ladder in canonical uint32[8] and does each field multiply as
23// one 256-bit modular multiply on the RSA/MPI accelerator (~4.3x the software/PIE ladder). That field layer is
24// shared with Ed25519 (pc_ed25519.cpp) and defines PC_FE25519_MPI_HW when active (Arduino + S3).
26#ifdef ARDUINO
27#include "sdkconfig.h" // CONFIG_IDF_TARGET_ESP32S3 - selects the vector (PIE) field multiply
28#include <mbedtls/bignum.h> // ESP32: field inversion on the MPI/RSA hardware accelerator
29#endif
30#include "crypto/crypto_opt.h"
32
33// Small field constant (radix-2^16). Used only by the software X25519 ladder (the S3 MODMULT path carries its
34// own canonical a24), so it would be unused there.
35#ifndef PC_FE25519_MPI_HW
36static const pc_gf GF_121665 = {0xDB41, 1}; // 121665 = 0x1DB41 (Montgomery a24)
37#endif
38
39// Normalize each limb toward 16 bits, folding the carry above 2^256 back in as *38.
40// Two passes fully reduce a product's limbs; the +2^16 / -1 dance keeps it branch-free.
41static void gf_carry(pc_gf o)
42{
43 for (int i = 0; i < 16; i++)
44 {
45 o[i] += (int64_t)1 << 16;
46 int64_t c = o[i] >> 16;
47 o[(i + 1) * (i < 15)] += c - 1 + 37 * (c - 1) * (i == 15); // wrap: limb 15's carry *38 into limb 0
48 o[i] -= c << 16;
49 }
50}
51
52void pc_gf_copy(pc_gf out, const pc_gf in)
53{
54 for (int i = 0; i < 16; i++)
55 {
56 out[i] = in[i];
57 }
58}
59
60void pc_gf_add(pc_gf out, const pc_gf a, const pc_gf b)
61{
62 for (int i = 0; i < 16; i++)
63 {
64 out[i] = a[i] + b[i];
65 }
66}
67
68void pc_gf_sub(pc_gf out, const pc_gf a, const pc_gf b)
69{
70 for (int i = 0; i < 16; i++)
71 {
72 out[i] = a[i] - b[i];
73 }
74}
75
76#if defined(CONFIG_IDF_TARGET_ESP32S3) && CONFIG_IDF_TARGET_ESP32S3
77// ---- ESP32-S3 vector (PIE) field multiply --------------------------------------------------------
78// The S3 vector unit multiply-accumulates signed-16-bit lanes into a 40-bit accumulator
79// (ee.vmulas.s16.accx), but radix-2^16 limbs run to ~2^18 and go negative after a subtraction, so they
80// do not fit s16. Fix: balance each limb into signed-16-bit (a value-preserving carry redistribution
81// mod p), after which a*b is a pure s16xs16 convolution - exactly the vector MAC. Device-validated
82// byte-exact vs the scalar path (rig test, and test_gf_mul_s16_model_matches_scalar for the model);
83// ~1.55x the scalar multiply on hardware.
84
85// Balance a[16] into signed-16-bit limbs of the same value mod p (round-to-nearest carry; limb-15
86// overflow wraps *38 into limb 0 since 2^256 == 38; three passes settle the wrap).
87static void gf_balance_s16(int16_t o[16], const pc_gf a)
88{
89 // int32 throughout: limbs stay ~+-2^18 and carries ~+-2, so no value exceeds int32 - which avoids the
90 // emulated 64-bit carry-propagation math (48 steps per operand) that dominated the field multiply.
91 int32_t c[16];
92 for (int i = 0; i < 16; i++)
93 {
94 c[i] = (int32_t)a[i];
95 }
96 for (int pass = 0; pass < 3; pass++)
97 {
98 int32_t carry = 0;
99 for (int i = 0; i < 16; i++)
100 {
101 int32_t v = c[i] + carry;
102 carry = (v + 0x8000) >> 16;
103 c[i] = v - (carry << 16);
104 }
105 c[0] += 38 * carry;
106 }
107 for (int i = 0; i < 16; i++)
108 {
109 o[i] = (int16_t)c[i];
110 }
111}
112
113// One output limb t[k] = as[0..15] . window[0..15] on the ACCX. as is 16-byte aligned; the reversed-b
114// window w may be unaligned (loaded via ee.ld.128.usar + ee.src.q). ACCX is 40-bit: sign-extend bit 39.
115static inline int64_t gf_accx_dot_win(const int16_t *as, const int16_t *w)
116{
117 uint32_t lo, hi;
118 const int16_t *pa = as, *pw = w;
119 asm volatile("ee.zero.accx\n"
120 "ee.vld.128.ip q3, %[a], 16\n"
121 "ee.ld.128.usar.ip q0, %[w], 16\n"
122 "ee.ld.128.usar.ip q1, %[w], 16\n"
123 "ee.src.q q2, q0, q1\n"
124 "ee.vmulas.s16.accx q3, q2\n"
125 "ee.vld.128.ip q3, %[a], 16\n"
126 "ee.ld.128.usar.ip q0, %[w], 16\n"
127 "ee.src.q q2, q1, q0\n"
128 "ee.vmulas.s16.accx q3, q2\n"
129 "rur.accx_0 %[lo]\n"
130 "rur.accx_1 %[hi]\n"
131 : [lo] "=&r"(lo), [hi] "=&r"(hi), [a] "+r"(pa), [w] "+r"(pw)
132 :
133 : "memory");
134 uint64_t raw = (uint64_t)lo | ((uint64_t)(uint8_t)hi << 32);
135 return ((int64_t)(raw << 24)) >> 24;
136}
137
138// Shared tail: build the reversed-bs window array, run the ACCX convolution, fold + carry. as points at
139// a 16-byte-aligned int16[16]; bs is read scalar-only (no alignment needed).
140static void gf_conv_finish(pc_gf out, const int16_t *as, const int16_t *bs)
141{
142 // bp = [15 zeros][bs reversed: bs15..bs0][zeros]; output k's window starts at bp[30-k].
143 __attribute__((aligned(16))) int16_t bp[64];
144 for (int i = 0; i < 64; i++)
145 {
146 bp[i] = 0;
147 }
148 for (int m = 0; m < 16; m++)
149 {
150 bp[15 + m] = bs[15 - m];
151 }
152 int64_t t[31];
153 for (int k = 0; k < 31; k++)
154 {
155 t[k] = gf_accx_dot_win(as, bp + (30 - k));
156 }
157 for (int i = 0; i < 15; i++)
158 {
159 t[i] += 38 * t[i + 16];
160 }
161 for (int i = 0; i < 16; i++)
162 {
163 out[i] = t[i];
164 }
165 gf_carry(out);
166 gf_carry(out);
167}
168
169void pc_gf_mul(pc_gf out, const pc_gf a, const pc_gf b)
170{
171 __attribute__((aligned(16))) int16_t as[16];
172 int16_t bs[16];
173 gf_balance_s16(as, a);
174 gf_balance_s16(bs, b);
175 gf_conv_finish(out, as, bs);
176}
177
178// Squaring balances the operand ONCE (a == b, and gf_balance_s16 is deterministic, so the second balance
179// in mul(a,a) is pure waste). ~2/3 of the Montgomery-ladder field ops are squarings, so this matters.
180// Byte-exact with pc_gf_mul(out, a, a) by construction.
181void pc_gf_sq(pc_gf out, const pc_gf a)
182{
183 __attribute__((aligned(16))) int16_t as[16];
184 gf_balance_s16(as, a);
185 gf_conv_finish(out, as, as);
186}
187#else
188void pc_gf_mul(pc_gf out, const pc_gf a, const pc_gf b)
189{
190 int64_t t[31];
191 for (int i = 0; i < 31; i++)
192 {
193 t[i] = 0;
194 }
195 // Every limb fits in int32 (~16-18 bits after carry / add / sub), but a[i] and b[j] are int64, so a
196 // plain a[i]*b[j] compiles to a full emulated 64x64 multiply on the 32-bit xtensa core. Casting both
197 // operands to int32 makes gcc emit a single widening 32x32->64 multiply (mull+mulsh) instead - the
198 // products (~34 bits) and their sums (~38 bits) still accumulate in the int64 t[].
199 for (int i = 0; i < 16; i++)
200 {
201 int32_t ai = (int32_t)a[i];
202 for (int j = 0; j < 16; j++)
203 {
204 t[i + j] += (int64_t)ai * (int32_t)b[j];
205 }
206 }
207 for (int i = 0; i < 15; i++)
208 {
209 t[i] += 38 * t[i + 16]; // fold the upper half (weight >= 2^256) down
210 }
211 for (int i = 0; i < 16; i++)
212 {
213 out[i] = t[i];
214 }
215 gf_carry(out);
216 gf_carry(out);
217}
218#endif
219
220#if !(defined(CONFIG_IDF_TARGET_ESP32S3) && CONFIG_IDF_TARGET_ESP32S3)
221void pc_gf_sq(pc_gf out, const pc_gf a)
222{
223 pc_gf_mul(out, a, a);
224}
225#endif
226
227// Software field inversion out = a^-1 = a^(p-2). Fixed addition chain: square 255 times,
228// multiplying in a at every bit except positions 2 and 4 (which are 0 in p-2 = 2^255 - 21).
229// The reference path (native builds) and the fallback if the hardware modexp ever fails.
230static void gf_inv_sw(pc_gf out, const pc_gf a)
231{
232 pc_gf c;
233 pc_gf_copy(c, a);
234 for (int i = 253; i >= 0; i--)
235 {
236 pc_gf_sq(c, c);
237 if (i != 2 && i != 4)
238 {
239 pc_gf_mul(c, c, a);
240 }
241 }
242 pc_gf_copy(out, c);
243}
244
245#ifdef ARDUINO
246// p = 2^255 - 19 and the inversion exponent p-2 = 2^255 - 21, big-endian for mbedtls.
247static const uint8_t P25519_BE[32] = {0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
248 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
249 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xed};
250static const uint8_t P25519_MINUS2_BE[32] = {0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
251 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
252 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xeb};
253
254// out = a^(p-2) mod p on the ESP32 MPI/RSA hardware accelerator - the same modexp engine
255// that runs RSA sign and DH-group14. mbedtls_mpi_exp_mod takes an arbitrary modulus, so
256// 2^255-19 runs on the accelerator. Only the inversion is offloaded (one big modexp);
257// the 255-round Montgomery ladder multiply stays in the software radix-2^16 core, where
258// per-multiply marshalling to the peripheral would cost more than it saves. The exponent
259// is a public constant; the base is packed to its canonical residue first.
260void pc_gf_inv(pc_gf out, const pc_gf a)
261{
262 uint8_t le[32];
263 uint8_t be[32];
264 pc_gf_pack(le, a); // canonical little-endian residue in [0, p)
265 for (int i = 0; i < 32; i++)
266 {
267 be[i] = le[31 - i]; // to big-endian for mbedtls
268 }
269
270 mbedtls_mpi A;
271 mbedtls_mpi E;
272 mbedtls_mpi N;
273 mbedtls_mpi X;
274 mbedtls_mpi_init(&A);
275 mbedtls_mpi_init(&E);
276 mbedtls_mpi_init(&N);
277 mbedtls_mpi_init(&X);
278 bool ok = mbedtls_mpi_read_binary(&A, be, 32) == 0 && mbedtls_mpi_read_binary(&E, P25519_MINUS2_BE, 32) == 0 &&
279 mbedtls_mpi_read_binary(&N, P25519_BE, 32) == 0 && mbedtls_mpi_exp_mod(&X, &A, &E, &N, nullptr) == 0 &&
280 mbedtls_mpi_write_binary(&X, be, 32) == 0;
281 mbedtls_mpi_free(&A);
282 mbedtls_mpi_free(&E);
283 mbedtls_mpi_free(&N);
284 mbedtls_mpi_free(&X);
285 if (!ok)
286 {
287 gf_inv_sw(out, a); // never expected; keep correctness if the peripheral path fails
288 return;
289 }
290 for (int i = 0; i < 32; i++)
291 {
292 le[i] = be[31 - i];
293 }
294 pc_gf_unpack(out, le);
295}
296#else
297void pc_gf_inv(pc_gf out, const pc_gf a)
298{
299 gf_inv_sw(out, a);
300}
301#endif
302
303// Constant-time conditional swap of p and q when b == 1 (b must be 0 or 1).
304void pc_gf_cswap(pc_gf p, pc_gf q, int b)
305{
306 int64_t mask = ~((int64_t)b - 1); // all ones when b==1, zero when b==0
307 for (int i = 0; i < 16; i++)
308 {
309 int64_t t = mask & (p[i] ^ q[i]);
310 p[i] ^= t;
311 q[i] ^= t;
312 }
313}
314
315// Canonical little-endian encoding: fully reduce mod p (conditional subtract twice),
316// then emit 16-bit limbs low byte first.
317void pc_gf_pack(uint8_t out[32], const pc_gf a)
318{
319 pc_gf t;
320 pc_gf m;
321 pc_gf_copy(t, a);
322 gf_carry(t);
323 gf_carry(t);
324 gf_carry(t);
325 for (int j = 0; j < 2; j++)
326 {
327 m[0] = t[0] - 0xffed;
328 for (int i = 1; i < 15; i++)
329 {
330 m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1);
331 m[i - 1] &= 0xffff;
332 }
333 m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1);
334 int b = (int)((m[15] >> 16) & 1);
335 m[14] &= 0xffff;
336 pc_gf_cswap(t, m, 1 - b); // keep the subtracted value only if it did not borrow
337 }
338 for (int i = 0; i < 16; i++)
339 {
340 out[2 * i] = (uint8_t)(t[i] & 0xff);
341 out[2 * i + 1] = (uint8_t)(t[i] >> 8);
342 }
343}
344
345// Decode 32 little-endian bytes into a field element; the top bit is masked off (255-bit).
346void pc_gf_unpack(pc_gf out, const uint8_t in[32])
347{
348 for (int i = 0; i < 16; i++)
349 {
350 out[i] = (int64_t)in[2 * i] + ((int64_t)in[2 * i + 1] << 8);
351 }
352 out[15] &= 0x7fff;
353}
354
355#ifdef PC_FE25519_MPI_HW
356// ============================= ESP32-S3 X25519 on the RSA/MPI accelerator =================================
357// The canonical uint32[8] field layer (fe, fe_add/sub/mul/sq/..., the MODMULT, and the lock+power bring-up)
358// lives in pc_fe25519.h - shared with Ed25519. Here is only the X25519-specific a24 and the RFC 7748 ladder.
359static const uint32_t FE_A24[8] = {121665u, 0, 0, 0, 0, 0, 0, 0}; // X25519 a24 = (486662-2)/4 (RFC 7748 §5)
360
361void pc_x25519(uint8_t out[32], const uint8_t scalar[32], const uint8_t point[32])
362{
363 uint8_t e[32];
364 for (int i = 0; i < 32; i++)
365 {
366 e[i] = scalar[i];
367 }
368 e[0] &= 248; // clamp the scalar (RFC 7748 §5)
369 e[31] &= 127;
370 e[31] |= 64;
371
372 pc_fe_hw_enable(); // lock + power the accelerator for the whole ladder
373
374 fe x1;
375 fe x2;
376 fe z2;
377 fe x3;
378 fe z3;
379 fe A;
380 fe AA;
381 fe B;
382 fe BB;
383 fe E;
384 fe C;
385 fe D;
386 fe DA;
387 fe CB;
388 fe t0;
389 fe t1;
390 fe_frombytes(x1, point);
391 fe_1(x2);
392 fe_0(z2);
393 fe_copy(x3, x1);
394 fe_1(z3);
395 uint32_t swap = 0;
396
397 // Montgomery ladder over the 255 scalar bits, high to low (RFC 7748 §5).
398 for (int t = 254; t >= 0; t--)
399 {
400 uint32_t k_t = (e[t >> 3] >> (t & 7)) & 1;
401 swap ^= k_t;
402 fe_cswap(x2, x3, swap);
403 fe_cswap(z2, z3, swap);
404 swap = k_t;
405 fe_add(A, x2, z2);
406 fe_sq(AA, A);
407 fe_sub(B, x2, z2);
408 fe_sq(BB, B);
409 fe_sub(E, AA, BB);
410 fe_add(C, x3, z3);
411 fe_sub(D, x3, z3);
412 fe_mul(DA, D, A);
413 fe_mul(CB, C, B);
414 fe_add(t0, DA, CB);
415 fe_sq(x3, t0);
416 fe_sub(t1, DA, CB);
417 fe_sq(t1, t1);
418 fe_mul(z3, x1, t1);
419 fe_mul(x2, AA, BB);
420 fe_mul(t0, FE_A24, E);
421 fe_add(t0, AA, t0);
422 fe_mul(z2, E, t0);
423 }
424 fe_cswap(x2, x3, swap);
425 fe_cswap(z2, z3, swap);
426
427 // Result = X / Z = x2 * z2^-1.
428 fe_invert(z2, z2);
429 fe_mul(x2, x2, z2);
430 fe_tobytes(out, x2);
431 pc_fe_hw_disable(); // release the lock + power down
432}
433#else
434void pc_x25519(uint8_t out[32], const uint8_t scalar[32], const uint8_t point[32])
435{
436 uint8_t z[32];
437 for (int i = 0; i < 31; i++)
438 {
439 z[i] = scalar[i];
440 }
441 z[31] = (uint8_t)((scalar[31] & 127) | 64); // clamp the scalar (RFC 7748 §5)
442 z[0] &= 248;
443
444 pc_gf x;
445 pc_gf a;
446 pc_gf b;
447 pc_gf c;
448 pc_gf d;
449 pc_gf e;
450 pc_gf f;
451 pc_gf_unpack(x, point);
452 for (int i = 0; i < 16; i++)
453 {
454 b[i] = x[i];
455 a[i] = c[i] = d[i] = 0;
456 }
457 a[0] = d[0] = 1;
458
459 // Montgomery ladder over the 255 scalar bits, high to low.
460 for (int i = 254; i >= 0; i--)
461 {
462 int r = (z[i >> 3] >> (i & 7)) & 1;
463 pc_gf_cswap(a, b, r);
464 pc_gf_cswap(c, d, r);
465 pc_gf_add(e, a, c);
466 pc_gf_sub(a, a, c);
467 pc_gf_add(c, b, d);
468 pc_gf_sub(b, b, d);
469 pc_gf_sq(d, e);
470 pc_gf_sq(f, a);
471 pc_gf_mul(a, c, a);
472 pc_gf_mul(c, b, e);
473 pc_gf_add(e, a, c);
474 pc_gf_sub(a, a, c);
475 pc_gf_sq(b, a);
476 pc_gf_sub(c, d, f);
477 pc_gf_mul(a, c, GF_121665);
478 pc_gf_add(a, a, d);
479 pc_gf_mul(c, c, a);
480 pc_gf_mul(a, d, f);
481 pc_gf_mul(d, b, x);
482 pc_gf_sq(b, e);
483 pc_gf_cswap(a, b, r);
484 pc_gf_cswap(c, d, r);
485 }
486
487 // Result = X / Z = a * c^-1.
488 pc_gf_inv(c, c);
489 pc_gf_mul(a, a, c);
490 pc_gf_pack(out, a);
491}
492#endif // PC_FE25519_MPI_HW
493
494void pc_x25519_base(uint8_t out[32], const uint8_t scalar[32])
495{
496 uint8_t base[32] = {9};
497 pc_x25519(out, scalar, base);
498}
Per-translation-unit optimization override for hot, pure-integer crypto.
void pc_gf_cswap(pc_gf p, pc_gf q, int b)
constant-time conditional swap of p,q when b==1
void pc_gf_sub(pc_gf out, const pc_gf a, const pc_gf b)
out = a - b (unreduced)
void pc_gf_mul(pc_gf out, const pc_gf a, const pc_gf b)
out = a * b mod p
void pc_gf_unpack(pc_gf out, const uint8_t in[32])
decode 32 bytes (high bit ignored)
void pc_gf_copy(pc_gf out, const pc_gf in)
out = in
void pc_gf_inv(pc_gf out, const pc_gf a)
out = a^-1 mod p (= a^(p-2))
void pc_x25519(uint8_t out[32], const uint8_t scalar[32], const uint8_t point[32])
X25519 scalar multiplication: out = scalar * point (RFC 7748 §5).
void pc_gf_pack(uint8_t out[32], const pc_gf a)
canonical little-endian 32-byte encoding
void pc_gf_sq(pc_gf out, const pc_gf a)
out = a^2 mod p
void pc_gf_add(pc_gf out, const pc_gf a, const pc_gf b)
out = a + b (unreduced)
void pc_x25519_base(uint8_t out[32], const uint8_t scalar[32])
X25519 with the standard base point u=9: out = scalar * G.
Curve25519 field arithmetic + X25519 (RFC 7748) for the curve25519-sha256 KEX.
int64_t pc_gf[16]
A field element of GF(2^255 - 19): 16 limbs, radix 2^16 (limb i weighs 2^(16i)).
Definition curve25519.h:25
Per-variant GF(2^255-19) field layer on the RSA/MPI hardware accelerator (X25519 + Ed25519).