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