DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
ssh_ecdsa.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_ecdsa.cpp
6 * @brief ECDSA over NIST P-256 for ecdsa-sha2-nistp256 (RFC 5656 / FIPS 186-4).
7 *
8 * ═══════════════════════════════════════════════════════════════════════════
9 * THREE BUILD PATHS
10 * ═══════════════════════════════════════════════════════════════════════════
11 *
12 * ESP32-S3 (Arduino): a self-contained P-256 whose every 256-bit field / scalar multiply is one
13 * modular multiply on the RSA/MPI hardware accelerator (the same engine ssh_fe25519.h drives for
14 * X25519 / Ed25519) - the MODMULT is modulus-generic, so it serves both the field domain (mod p)
15 * and the scalar domain (mod n) by swapping the {M, m', R^2} constants. Point arithmetic uses the
16 * exception-free complete formulas (Renes-Costello-Batina 2016, EFD add/dbl-2015-rcb, a = -3) driven by
17 * a constant-time 4-bit fixed-window scalar multiply (uniform op sequence, full-table masked select, no
18 * input-dependent branches). Signing is RFC 6979 deterministic, so the on-device output is byte-exact to
19 * the published vectors (same KATs as native). This is the production path (DETWS_ECDSA_MPI_HW); sign /
20 * verify / ecdh run ~2.7-2.9x faster than the mbedTLS ECP path it replaces on non-S3 targets.
21 *
22 * Native: the identical complete-formula / RFC 6979 code, but each field multiply is a software
23 * schoolbook product reduced bit-serially. Only fp_mul differs from the S3 path, so the native KATs
24 * validate the exact point / scalar arithmetic the accelerator runs. Test-only, not in firmware.
25 *
26 * Other Arduino (classic ESP32 etc.): mbedTLS (mbedtls_ecdsa_*, mbedtls_ecp_*) - hardware big-integer
27 * math and side-channel hardening, signing with the ESP32 hardware RNG. The MODMULT register layout is
28 * an S3 specialization, so non-S3 targets keep the portable mbedTLS path (no perf regression).
29 *
30 * ═══════════════════════════════════════════════════════════════════════════
31 * WIRE FORMATS (assembled by the SSH transport/auth layers, not here)
32 * ═══════════════════════════════════════════════════════════════════════════
33 *
34 * Public-key blob: string("ecdsa-sha2-nistp256") || string("nistp256") || string(Q), Q = 0x04||X||Y.
35 * Signature blob: string("ecdsa-sha2-nistp256") || string( mpint(r) || mpint(s) ); this module
36 * exposes raw r||s (32+32 big-endian) and the layers mpint-wrap them.
37 * ECDH shared secret (RFC 5656 sec 4): K = X coordinate of d*Q_peer, raw 32-byte big-endian.
38 *
39 * @author Douglas Quigg (dstroy0)
40 * @date 2026
41 */
42
45#include <string.h>
46
47#ifdef ARDUINO
48#include "sdkconfig.h" // CONFIG_IDF_TARGET_ESP32S3 - selects the MODMULT field layer
49#endif
50
51// The S3 field/scalar layer drives the RSA peripheral through mbedTLS's port (esp_mpi_*), which only
52// exists in the on-device toolchain and whose MODMULT register map is an S3 specialization.
53#if defined(ARDUINO) && defined(CONFIG_IDF_TARGET_ESP32S3) && CONFIG_IDF_TARGET_ESP32S3
54#define DETWS_ECDSA_MPI_HW 1
55#endif
56
57// ---------------------------------------------------------------------------
58// Other Arduino (non-S3) - mbedTLS path (portable, hardware-accelerated)
59// ---------------------------------------------------------------------------
60
61#if defined(ARDUINO) && !defined(DETWS_ECDSA_MPI_HW)
62
63#include <esp_random.h> // esp_fill_random() for the ECDSA nonce / blinding RNG
64#include <mbedtls/ecdh.h>
65#include <mbedtls/ecdsa.h>
66#include <mbedtls/ecp.h>
67
68namespace
69{
70// RNG callback backed by the ESP32 hardware RNG.
71int ecdsa_rng(void *ctx, unsigned char *buf, size_t len)
72{
73 (void)ctx;
74 esp_fill_random(buf, len);
75 return 0;
76}
77} // namespace
78
79bool ssh_ecdsa_p256_pubkey(uint8_t pub[SSH_ECDSA_P256_PUB_LEN], const uint8_t priv[SSH_ECDSA_P256_PRIV_LEN])
80{
81 mbedtls_ecp_group grp;
82 mbedtls_ecp_point Q;
83 mbedtls_mpi d;
84 mbedtls_ecp_group_init(&grp);
85 mbedtls_ecp_point_init(&Q);
86 mbedtls_mpi_init(&d);
87
88 bool ok = false;
89 if (mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1) == 0 &&
90 mbedtls_mpi_read_binary(&d, priv, SSH_ECDSA_P256_PRIV_LEN) == 0 &&
91 mbedtls_ecp_mul(&grp, &Q, &d, &grp.G, ecdsa_rng, nullptr) == 0)
92 {
93 size_t olen = 0;
94 if (mbedtls_ecp_point_write_binary(&grp, &Q, MBEDTLS_ECP_PF_UNCOMPRESSED, &olen, pub, SSH_ECDSA_P256_PUB_LEN) ==
95 0 &&
96 olen == SSH_ECDSA_P256_PUB_LEN)
97 ok = true;
98 }
99
100 mbedtls_mpi_free(&d);
101 mbedtls_ecp_point_free(&Q);
102 mbedtls_ecp_group_free(&grp);
103 return ok;
104}
105
106bool ssh_ecdsa_p256_sign(uint8_t sig[SSH_ECDSA_P256_SIG_LEN], const uint8_t *msg, size_t mlen,
107 const uint8_t priv[SSH_ECDSA_P256_PRIV_LEN])
108{
109 uint8_t h[SSH_SHA256_DIGEST_LEN];
110 ssh_sha256(msg, mlen, h);
111
112 mbedtls_ecp_group grp;
113 mbedtls_mpi d;
114 mbedtls_mpi r;
115 mbedtls_mpi s;
116 mbedtls_ecp_group_init(&grp);
117 mbedtls_mpi_init(&d);
118 mbedtls_mpi_init(&r);
119 mbedtls_mpi_init(&s);
120
121 bool ok = false;
122 if (mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1) == 0 &&
123 mbedtls_mpi_read_binary(&d, priv, SSH_ECDSA_P256_PRIV_LEN) == 0 &&
124 mbedtls_ecdsa_sign(&grp, &r, &s, &d, h, SSH_SHA256_DIGEST_LEN, ecdsa_rng, nullptr) == 0 &&
125 mbedtls_mpi_write_binary(&r, sig, SSH_ECDSA_P256_COORD_LEN) == 0 &&
126 mbedtls_mpi_write_binary(&s, sig + SSH_ECDSA_P256_COORD_LEN, SSH_ECDSA_P256_COORD_LEN) == 0)
127 ok = true;
128
129 mbedtls_mpi_free(&s);
130 mbedtls_mpi_free(&r);
131 mbedtls_mpi_free(&d);
132 mbedtls_ecp_group_free(&grp);
133 return ok;
134}
135
136bool ssh_ecdsa_p256_verify(const uint8_t pub[SSH_ECDSA_P256_PUB_LEN], const uint8_t *msg, size_t mlen,
137 const uint8_t sig[SSH_ECDSA_P256_SIG_LEN])
138{
139 uint8_t h[SSH_SHA256_DIGEST_LEN];
140 ssh_sha256(msg, mlen, h);
141
142 mbedtls_ecp_group grp;
143 mbedtls_ecp_point Q;
144 mbedtls_mpi r;
145 mbedtls_mpi s;
146 mbedtls_ecp_group_init(&grp);
147 mbedtls_ecp_point_init(&Q);
148 mbedtls_mpi_init(&r);
149 mbedtls_mpi_init(&s);
150
151 bool ok = false;
152 if (mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1) == 0 &&
153 mbedtls_ecp_point_read_binary(&grp, &Q, pub, SSH_ECDSA_P256_PUB_LEN) == 0 &&
154 mbedtls_ecp_check_pubkey(&grp, &Q) == 0 && mbedtls_mpi_read_binary(&r, sig, SSH_ECDSA_P256_COORD_LEN) == 0 &&
155 mbedtls_mpi_read_binary(&s, sig + SSH_ECDSA_P256_COORD_LEN, SSH_ECDSA_P256_COORD_LEN) == 0 &&
156 mbedtls_ecdsa_verify(&grp, h, SSH_SHA256_DIGEST_LEN, &Q, &r, &s) == 0)
157 ok = true;
158
159 mbedtls_mpi_free(&s);
160 mbedtls_mpi_free(&r);
161 mbedtls_ecp_point_free(&Q);
162 mbedtls_ecp_group_free(&grp);
163 return ok;
164}
165
166bool ssh_ecdsa_p256_ecdh(uint8_t shared_x[SSH_ECDSA_P256_COORD_LEN], const uint8_t peer_pub[SSH_ECDSA_P256_PUB_LEN],
167 const uint8_t priv[SSH_ECDSA_P256_PRIV_LEN])
168{
169 mbedtls_ecp_group grp;
170 mbedtls_ecp_point Q;
171 mbedtls_mpi d;
172 mbedtls_mpi z; // shared secret = the X coordinate of d*Q
173 mbedtls_ecp_group_init(&grp);
174 mbedtls_ecp_point_init(&Q);
175 mbedtls_mpi_init(&d);
176 mbedtls_mpi_init(&z);
177
178 bool ok = false;
179 if (mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1) == 0 &&
180 mbedtls_ecp_point_read_binary(&grp, &Q, peer_pub, SSH_ECDSA_P256_PUB_LEN) == 0 &&
181 mbedtls_ecp_check_pubkey(&grp, &Q) == 0 && mbedtls_mpi_read_binary(&d, priv, SSH_ECDSA_P256_PRIV_LEN) == 0 &&
182 mbedtls_ecdh_compute_shared(&grp, &z, &Q, &d, ecdsa_rng, nullptr) == 0 &&
183 mbedtls_mpi_write_binary(&z, shared_x, SSH_ECDSA_P256_COORD_LEN) == 0)
184 ok = true;
185
186 mbedtls_mpi_free(&z);
187 mbedtls_mpi_free(&d);
188 mbedtls_ecp_point_free(&Q);
189 mbedtls_ecp_group_free(&grp);
190 return ok;
191}
192
193#else // ---- S3 HW-MODMULT path, or native software path (shared complete-formula P-256) ----
194
196
197#ifdef DETWS_ECDSA_MPI_HW
198#include "soc/hwcrypto_reg.h" // RSA/MPI accelerator register map (MODMULT)
199#include "soc/soc.h" // DR_REG_RSA_BASE
200extern "C"
201{
202 void esp_mpi_enable_hardware_hw_op(void); // mbedTLS port: acquire the MPI lock + clock/power the peripheral
203 void esp_mpi_disable_hardware_hw_op(void); // release the lock + power down
204}
205#define SSH_RSA_REG(a) (*(volatile uint32_t *)(a))
206#endif
207
208namespace
209{
210// ---- 256-bit little-endian field / scalar arithmetic ----
211// Values are eight uint32 limbs (limb 0 least significant), held canonical (< the domain modulus).
212
213// P-256 domain parameters (little-endian words).
214const uint32_t P256_P[8] = {0xffffffffu, 0xffffffffu, 0xffffffffu, 0x00000000u,
215 0x00000000u, 0x00000000u, 0x00000001u, 0xffffffffu};
216const uint32_t P256_N[8] = {0xfc632551u, 0xf3b9cac2u, 0xa7179e84u, 0xbce6faadu,
217 0xffffffffu, 0xffffffffu, 0x00000000u, 0xffffffffu};
218const uint32_t P256_B[8] = {0x27d2604bu, 0x3bce3c3eu, 0xcc53b0f6u, 0x651d06b0u,
219 0x769886bcu, 0xb3ebbd55u, 0xaa3a93e7u, 0x5ac635d8u};
220const uint32_t P256_B3[8] = {0x777720e2u, 0xb36ab4bau, 0x64fb12e2u, 0x2f571411u,
221 0x63c99435u, 0x1bc33800u, 0xfeafbbb6u, 0x1052a18au}; // 3b mod p
222
223// R = 2^256. Montgomery constants for the MODMULT (m' = -M^-1 mod 2^32, R^2 mod M); scratchpad/p256_verify.py.
224const uint32_t P256_P_R2[8] = {0x00000003u, 0x00000000u, 0xffffffffu, 0xfffffffbu,
225 0xfffffffeu, 0xffffffffu, 0xfffffffdu, 0x00000004u};
226const uint32_t P256_N_R2[8] = {0xbe79eea2u, 0x83244c95u, 0x49bd6fa6u, 0x4699799cu,
227 0x2b6bec59u, 0x2845b239u, 0xf3d95620u, 0x66e12d94u};
228
229// A field/scalar domain: its modulus and (S3) the two MODMULT constants.
230struct Fp
231{
232 const uint32_t *m;
233 uint32_t mprime;
234 const uint32_t *r2;
235};
236const Fp FP = {P256_P, 0x00000001u, P256_P_R2}; // field domain (mod p): m' = 1 since p ends in 0xffffffff
237const Fp FN = {P256_N, 0xee00bc4fu, P256_N_R2}; // scalar domain (mod n)
238
239void fp_set(uint32_t r[8], const uint32_t a[8])
240{
241 for (int i = 0; i < 8; i++)
242 r[i] = a[i];
243}
244void fp_zero(uint32_t r[8])
245{
246 for (int i = 0; i < 8; i++)
247 r[i] = 0;
248}
249bool fp_is_zero(const uint32_t a[8])
250{
251 uint32_t x = 0;
252 for (int i = 0; i < 8; i++)
253 x |= a[i];
254 return x == 0;
255}
256// -1 if a<b, 0 if a==b, 1 if a>b.
257int fp_cmp(const uint32_t a[8], const uint32_t b[8])
258{
259 for (int i = 7; i >= 0; i--)
260 if (a[i] != b[i])
261 return a[i] > b[i] ? 1 : -1;
262 return 0;
263}
264// r = a - b (mod 2^256); returns borrow.
265uint32_t sub_raw(uint32_t r[8], const uint32_t a[8], const uint32_t b[8])
266{
267 uint64_t brw = 0;
268 for (int i = 0; i < 8; i++)
269 {
270 uint64_t t = (uint64_t)a[i] - b[i] - brw;
271 r[i] = (uint32_t)t;
272 brw = (t >> 32) & 1u;
273 }
274 return (uint32_t)brw;
275}
276// r = a + b (mod m); a,b < m -> one conditional subtract of m. Constant-time.
277void fp_add(uint32_t r[8], const uint32_t a[8], const uint32_t b[8], const Fp *F)
278{
279 uint32_t s[8];
280 uint64_t c = 0;
281 for (int i = 0; i < 8; i++)
282 {
283 c += (uint64_t)a[i] + b[i];
284 s[i] = (uint32_t)c;
285 c >>= 32;
286 }
287 uint32_t carry = (uint32_t)c; // a+b may be a 257-bit value
288 uint32_t t[8];
289 uint64_t b2 = 0;
290 for (int i = 0; i < 8; i++)
291 {
292 uint64_t v = (uint64_t)s[i] - F->m[i] - b2;
293 t[i] = (uint32_t)v;
294 b2 = (v >> 32) & 1u;
295 }
296 // keep s if (s - m) borrowed and there was no carry out of the add; else take s - m.
297 uint32_t take_t = carry | (uint32_t)(1u - b2); // 1 -> s>=m, use t
298 uint32_t mask = (uint32_t)(-(int32_t)take_t);
299 for (int i = 0; i < 8; i++)
300 r[i] = (t[i] & mask) | (s[i] & ~mask);
301}
302// r = a - b (mod m); a,b < m -> conditional add of m on borrow. Constant-time.
303void fp_sub(uint32_t r[8], const uint32_t a[8], const uint32_t b[8], const Fp *F)
304{
305 uint32_t t[8];
306 uint32_t borrow = sub_raw(t, a, b); // 1 if a < b
307 uint32_t mask = (uint32_t)(-(int32_t)borrow);
308 uint64_t c = 0;
309 for (int i = 0; i < 8; i++)
310 {
311 c += (uint64_t)t[i] + (F->m[i] & mask);
312 r[i] = (uint32_t)c;
313 c >>= 32;
314 }
315}
316// Reduce a single value a in [0, 2m) into [0, m): subtract m once if a >= m. Constant-time.
317void fp_reduce_once(uint32_t r[8], const uint32_t a[8], const uint32_t m[8])
318{
319 uint32_t t[8];
320 uint32_t borrow = sub_raw(t, a, m); // 1 -> a < m, keep a
321 uint32_t mask = (uint32_t)(-(int32_t)borrow);
322 for (int i = 0; i < 8; i++)
323 r[i] = (a[i] & mask) | (t[i] & ~mask);
324}
325
326#ifdef DETWS_ECDSA_MPI_HW
327// z = x*y mod F->m on the S3 RSA accelerator. Requires ecdsa_hw_on() first. Preloading R^2 into the result
328// block makes MODMULT return the plain residue (the esp_mpi_mul_mpi_mod convention). Output canonical (< m).
329void fp_mul(uint32_t z[8], const uint32_t x[8], const uint32_t y[8], const Fp *F) // safe if z aliases x/y
330{
331 volatile uint32_t *M = (volatile uint32_t *)RSA_MEM_M_BLOCK_BASE;
332 volatile uint32_t *X = (volatile uint32_t *)RSA_MEM_X_BLOCK_BASE;
333 volatile uint32_t *Y = (volatile uint32_t *)RSA_MEM_Y_BLOCK_BASE;
334 volatile uint32_t *Z = (volatile uint32_t *)RSA_MEM_Z_BLOCK_BASE;
335 SSH_RSA_REG(RSA_LENGTH_REG) = 8 - 1; // mode = words - 1
336 SSH_RSA_REG(RSA_M_DASH_REG) = F->mprime;
337 for (int i = 0; i < 8; i++)
338 {
339 M[i] = F->m[i];
340 X[i] = x[i];
341 Y[i] = y[i];
342 Z[i] = F->r2[i]; // r = R^2 mod m -> plain (non-Montgomery) output
343 }
344 SSH_RSA_REG(RSA_CLEAR_INTERRUPT_REG) = 1;
345 SSH_RSA_REG(RSA_MOD_MULT_START_REG) = 1;
346 while (SSH_RSA_REG(RSA_QUERY_INTERRUPT_REG) == 0)
347 ;
348 SSH_RSA_REG(RSA_CLEAR_INTERRUPT_REG) = 1;
349 for (int i = 0; i < 8; i++)
350 z[i] = Z[i];
351}
352void ecdsa_hw_on()
353{
354 esp_mpi_enable_hardware_hw_op(); // lock + clock/power the peripheral
355 SSH_RSA_REG(RSA_INTERRUPT_REG) = 0; // poll only, no completion IRQ
356}
357void ecdsa_hw_off()
358{
359 esp_mpi_disable_hardware_hw_op(); // release the lock + power down
360}
361#else
362// acc[0..7] >= m[0..7]? Compares the low 8 limbs from the most significant down.
363bool reduce_low8_ge(const uint32_t acc[8], const uint32_t m[8])
364{
365 for (int k = 7; k >= 0; k--)
366 if (acc[k] != m[k])
367 return acc[k] > m[k];
368 return true; // all limbs equal
369}
370// Reduce a 512-bit product mod m (bit-serial, MSB to LSB). Correct but slow; the native path is test-only.
371void reduce_mod(uint32_t r[8], const uint32_t prod[16], const uint32_t m[8])
372{
373 uint32_t acc[9];
374 for (int k = 0; k < 9; k++)
375 acc[k] = 0;
376 for (int bit = 511; bit >= 0; bit--)
377 {
378 uint32_t carry = 0;
379 for (int k = 0; k < 9; k++)
380 {
381 uint32_t nc = acc[k] >> 31;
382 acc[k] = (acc[k] << 1) | carry;
383 carry = nc;
384 }
385 acc[0] |= (prod[bit >> 5] >> (bit & 31)) & 1u;
386 bool ge = acc[8] != 0;
387 if (!ge)
388 ge = reduce_low8_ge(acc, m);
389 if (ge)
390 {
391 uint64_t brw = 0;
392 for (int k = 0; k < 8; k++)
393 {
394 uint64_t t = (uint64_t)acc[k] - m[k] - brw;
395 acc[k] = (uint32_t)t;
396 brw = (t >> 32) & 1u;
397 }
398 acc[8] -= (uint32_t)brw;
399 }
400 }
401 for (int k = 0; k < 8; k++)
402 r[k] = acc[k];
403}
404// z = (x * y) mod F->m (software schoolbook + reduction).
405void fp_mul(uint32_t z[8], const uint32_t x[8], const uint32_t y[8], const Fp *F)
406{
407 uint32_t prod[16];
408 for (int k = 0; k < 16; k++)
409 prod[k] = 0;
410 for (int i = 0; i < 8; i++)
411 {
412 uint64_t carry = 0;
413 for (int j = 0; j < 8; j++)
414 {
415 uint64_t t = (uint64_t)prod[i + j] + (uint64_t)x[i] * y[j] + carry;
416 prod[i + j] = (uint32_t)t;
417 carry = t >> 32;
418 }
419 int k = i + 8;
420 while (carry)
421 {
422 uint64_t t = (uint64_t)prod[k] + carry;
423 prod[k] = (uint32_t)t;
424 carry = t >> 32;
425 k++;
426 }
427 }
428 reduce_mod(z, prod, F->m);
429}
430void ecdsa_hw_on()
431{
432}
433void ecdsa_hw_off()
434{
435}
436#endif
437
438void fp_sqr(uint32_t r[8], const uint32_t a[8], const Fp *F)
439{
440 fp_mul(r, a, a, F);
441}
442// r = a*x mod p where the curve a = p - 3, i.e. r = -3x. Two adds + a negate instead of a MODMULT.
443// Alias-safe (r may be x). Only the field domain has this a, so it is hard-wired to FP.
444void fp_mul_by_a(uint32_t r[8], const uint32_t x[8])
445{
446 uint32_t tx[8];
447 fp_add(tx, x, x, &FP);
448 fp_add(tx, tx, x, &FP); // 3x
449 const uint32_t zero[8] = {0, 0, 0, 0, 0, 0, 0, 0};
450 fp_sub(r, zero, tx, &FP); // -3x
451}
452// r = a^(m-2) mod m (Fermat inverse; m prime). Fixed public exponent -> constant-time in a.
453void fp_inv(uint32_t r[8], const uint32_t a[8], const Fp *F)
454{
455 const uint32_t two[8] = {2, 0, 0, 0, 0, 0, 0, 0};
456 uint32_t e[8];
457 sub_raw(e, F->m, two); // e = m - 2
458 uint32_t res[8] = {1, 0, 0, 0, 0, 0, 0, 0};
459 uint32_t base[8];
460 fp_set(base, a);
461 for (int i = 0; i < 256; i++)
462 {
463 if ((e[i >> 5] >> (i & 31)) & 1u)
464 fp_mul(res, res, base, F);
465 fp_mul(base, base, base, F);
466 }
467 fp_set(r, res);
468}
469
470void load_be(uint32_t r[8], const uint8_t b[32])
471{
472 for (int i = 0; i < 8; i++)
473 {
474 const uint8_t *p = b + (28 - 4 * i);
475 r[i] = ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3];
476 }
477}
478void store_be(uint8_t b[32], const uint32_t r[8])
479{
480 for (int i = 0; i < 8; i++)
481 {
482 uint8_t *p = b + (28 - 4 * i);
483 p[0] = (uint8_t)(r[i] >> 24);
484 p[1] = (uint8_t)(r[i] >> 16);
485 p[2] = (uint8_t)(r[i] >> 8);
486 p[3] = (uint8_t)r[i];
487 }
488}
489
490// ---- Point arithmetic: complete formulas on y^2 = x^3 - 3x + b, projective (X:Y:Z), x=X/Z, y=Y/Z ----
491// Exception-free for all inputs on the prime-order curve (RCB 2016), so the constant-time ladder needs no
492// special cases. Identity is (0:1:0). Every field op is mod p (FP).
493
494struct Pt
495{
496 uint32_t X[8];
497 uint32_t Y[8];
498 uint32_t Z[8];
499};
500
501// Base point G (affine, Z = 1).
502const Pt P256_G = {
503 {0xd898c296u, 0xf4a13945u, 0x2deb33a0u, 0x77037d81u, 0x63a440f2u, 0xf8bce6e5u, 0xe12c4247u, 0x6b17d1f2u},
504 {0x37bf51f5u, 0xcbb64068u, 0x6b315eceu, 0x2bce3357u, 0x7c0f9e16u, 0x8ee7eb4au, 0xfe1a7f9bu, 0x4fe342e2u},
505 {1u, 0, 0, 0, 0, 0, 0, 0}};
506
507bool pt_is_infinity(const Pt *p)
508{
509 return fp_is_zero(p->Z);
510}
511void pt_set_infinity(Pt *p)
512{
513 fp_zero(p->X);
514 fp_zero(p->Y);
515 p->Y[0] = 1;
516 fp_zero(p->Z);
517}
518void pt_from_affine(Pt *p, const uint32_t x[8], const uint32_t y[8])
519{
520 fp_set(p->X, x);
521 fp_set(p->Y, y);
522 fp_zero(p->Z);
523 p->Z[0] = 1;
524}
525// (x, y) = (X/Z, Y/Z). Caller ensures p is not the identity.
526void pt_to_affine(uint32_t x[8], uint32_t y[8], const Pt *p)
527{
528 uint32_t zi[8];
529 fp_inv(zi, p->Z, &FP);
530 fp_mul(x, p->X, zi, &FP);
531 fp_mul(y, p->Y, zi, &FP);
532}
533
534// r = a + b (EFD add-2015-rcb, a = -3). Alias-safe: all reads land in locals before *r is written.
535void pt_add(Pt *r, const Pt *a, const Pt *b)
536{
537 uint32_t t0[8];
538 uint32_t t1[8];
539 uint32_t t2[8];
540 uint32_t t3[8];
541 uint32_t t4[8];
542 uint32_t t5[8];
543 uint32_t x3[8];
544 uint32_t y3[8];
545 uint32_t z3[8];
546 fp_mul(t0, a->X, b->X, &FP);
547 fp_mul(t1, a->Y, b->Y, &FP);
548 fp_mul(t2, a->Z, b->Z, &FP);
549 fp_add(t3, a->X, a->Y, &FP);
550 fp_add(t4, b->X, b->Y, &FP);
551 fp_mul(t3, t3, t4, &FP);
552 fp_add(t4, t0, t1, &FP);
553 fp_sub(t3, t3, t4, &FP);
554 fp_add(t4, a->X, a->Z, &FP);
555 fp_add(t5, b->X, b->Z, &FP);
556 fp_mul(t4, t4, t5, &FP);
557 fp_add(t5, t0, t2, &FP);
558 fp_sub(t4, t4, t5, &FP);
559 fp_add(t5, a->Y, a->Z, &FP);
560 fp_add(x3, b->Y, b->Z, &FP);
561 fp_mul(t5, t5, x3, &FP);
562 fp_add(x3, t1, t2, &FP);
563 fp_sub(t5, t5, x3, &FP);
564 fp_mul_by_a(z3, t4);
565 fp_mul(x3, P256_B3, t2, &FP);
566 fp_add(z3, x3, z3, &FP);
567 fp_sub(x3, t1, z3, &FP);
568 fp_add(z3, t1, z3, &FP);
569 fp_mul(y3, x3, z3, &FP);
570 fp_add(t1, t0, t0, &FP);
571 fp_add(t1, t1, t0, &FP);
572 fp_mul_by_a(t2, t2);
573 fp_mul(t4, P256_B3, t4, &FP);
574 fp_add(t1, t1, t2, &FP);
575 fp_sub(t2, t0, t2, &FP);
576 fp_mul_by_a(t2, t2);
577 fp_add(t4, t4, t2, &FP);
578 fp_mul(t0, t1, t4, &FP);
579 fp_add(y3, y3, t0, &FP);
580 fp_mul(t0, t5, t4, &FP);
581 fp_mul(x3, t3, x3, &FP);
582 fp_sub(x3, x3, t0, &FP);
583 fp_mul(t0, t3, t1, &FP);
584 fp_mul(z3, t5, z3, &FP);
585 fp_add(z3, z3, t0, &FP);
586 fp_set(r->X, x3);
587 fp_set(r->Y, y3);
588 fp_set(r->Z, z3);
589}
590
591// r = 2*a (EFD dbl-2015-rcb, a = -3). Alias-safe.
592void pt_dbl(Pt *r, const Pt *a)
593{
594 uint32_t t0[8];
595 uint32_t t1[8];
596 uint32_t t2[8];
597 uint32_t t3[8];
598 uint32_t x3[8];
599 uint32_t y3[8];
600 uint32_t z3[8];
601 fp_sqr(t0, a->X, &FP);
602 fp_sqr(t1, a->Y, &FP);
603 fp_sqr(t2, a->Z, &FP);
604 fp_mul(t3, a->X, a->Y, &FP);
605 fp_add(t3, t3, t3, &FP);
606 fp_mul(z3, a->X, a->Z, &FP);
607 fp_add(z3, z3, z3, &FP);
608 fp_mul_by_a(x3, z3);
609 fp_mul(y3, P256_B3, t2, &FP);
610 fp_add(y3, x3, y3, &FP);
611 fp_sub(x3, t1, y3, &FP);
612 fp_add(y3, t1, y3, &FP);
613 fp_mul(y3, x3, y3, &FP);
614 fp_mul(x3, t3, x3, &FP);
615 fp_mul(z3, P256_B3, z3, &FP);
616 fp_mul_by_a(t2, t2);
617 fp_sub(t3, t0, t2, &FP);
618 fp_mul_by_a(t3, t3);
619 fp_add(t3, t3, z3, &FP);
620 fp_add(z3, t0, t0, &FP);
621 fp_add(t0, z3, t0, &FP);
622 fp_add(t0, t0, t2, &FP);
623 fp_mul(t0, t0, t3, &FP);
624 fp_add(y3, y3, t0, &FP);
625 fp_mul(t2, a->Y, a->Z, &FP);
626 fp_add(t2, t2, t2, &FP);
627 fp_mul(t0, t2, t3, &FP);
628 fp_sub(x3, x3, t0, &FP);
629 fp_mul(z3, t2, t1, &FP);
630 fp_add(z3, z3, z3, &FP);
631 fp_add(z3, z3, z3, &FP);
632 fp_set(r->X, x3);
633 fp_set(r->Y, y3);
634 fp_set(r->Z, z3);
635}
636
637// dst = table[idx], scanning all 16 entries so the access pattern is independent of the (secret) idx.
638void pt_table_select(Pt *dst, const Pt table[16], uint32_t idx)
639{
640 fp_zero(dst->X);
641 fp_zero(dst->Y);
642 fp_zero(dst->Z);
643 for (uint32_t e = 0; e < 16; e++)
644 {
645 uint32_t x = e ^ idx;
646 uint32_t nz = (x | (0u - x)) >> 31; // 1 if e != idx, else 0
647 uint32_t mask = (uint32_t)(nz - 1u); // 0xffffffff if e == idx, else 0
648 for (int i = 0; i < 8; i++)
649 {
650 dst->X[i] |= table[e].X[i] & mask;
651 dst->Y[i] |= table[e].Y[i] & mask;
652 dst->Z[i] |= table[e].Z[i] & mask;
653 }
654 }
655}
656
657// r = k * p, k a 256-bit little-endian scalar. Constant-time 4-bit fixed window: a uniform op sequence
658// (256 doublings + 64 additions + a data-independent table build) with the only secret-dependent step a
659// full-table masked select - no input-dependent branches, and the complete formulas are exception-free.
660// The 16-entry table (~1.5 KB) is on the stack, live only in this shallow phase (well under the SSH KEX
661// peak), so it is reentrant across worker tasks. Used for secret scalars.
662void pt_scalarmul(Pt *r, const uint32_t k[8], const Pt *p)
663{
664 Pt table[16]; // table[i] = i * p; table[0] = identity
665 pt_set_infinity(&table[0]);
666 table[1] = *p;
667 for (int i = 2; i < 16; i++)
668 {
669 if (i & 1)
670 pt_add(&table[i], &table[i - 1], p);
671 else
672 pt_dbl(&table[i], &table[i / 2]);
673 }
674 Pt acc;
675 pt_set_infinity(&acc);
676 for (int w = 63; w >= 0; w--) // 64 nibbles, most significant first (Horner)
677 {
678 pt_dbl(&acc, &acc);
679 pt_dbl(&acc, &acc);
680 pt_dbl(&acc, &acc);
681 pt_dbl(&acc, &acc); // acc *= 16
682 uint32_t idx = (k[w >> 3] >> ((w & 7) * 4)) & 0xfu;
683 Pt sel;
684 pt_table_select(&sel, table, idx);
685 pt_add(&acc, &acc, &sel);
686 }
687 *r = acc;
688}
689
690// Check (x, y) is on y^2 = x^3 - 3x + b (mod p) and both coordinates are in range. Requires ecdsa_hw_on().
691bool on_curve(const uint32_t x[8], const uint32_t y[8])
692{
693 if (fp_cmp(x, P256_P) >= 0 || fp_cmp(y, P256_P) >= 0)
694 return false;
695 uint32_t lhs[8];
696 uint32_t rhs[8];
697 uint32_t t[8];
698 fp_sqr(lhs, y, &FP); // y^2
699 fp_sqr(rhs, x, &FP);
700 fp_mul(rhs, rhs, x, &FP); // x^3
701 fp_add(t, x, x, &FP);
702 fp_add(t, t, x, &FP); // 3x
703 fp_sub(rhs, rhs, t, &FP); // x^3 - 3x
704 fp_add(rhs, rhs, P256_B, &FP);
705 return fp_cmp(lhs, rhs) == 0;
706}
707
708// ---- RFC 6979 deterministic nonce (HMAC-SHA256 DRBG, hlen = qlen = 256) ----
709
710// out = HMAC-SHA256(key, V || (tag>=0 ? tag||x||e : nothing)).
711void hmac_cat(uint8_t out[32], const uint8_t key[32], const uint8_t *v, size_t vlen, const int tag, const uint8_t *x,
712 const uint8_t *e)
713{
714 uint8_t buf[97]; // 32 (V) + 1 (tag) + 32 (x) + 32 (e)
715 size_t n = 0;
716 memcpy(buf + n, v, vlen);
717 n += vlen;
718 if (tag >= 0)
719 {
720 buf[n++] = (uint8_t)tag;
721 memcpy(buf + n, x, 32);
722 n += 32;
723 memcpy(buf + n, e, 32);
724 n += 32;
725 }
726 ssh_hmac_sha256(key, 32, buf, n, out);
727}
728
729// One RFC 6979 candidate k: if it yields a valid r and s, write the 64-byte signature and return true.
730bool ecdsa_try_sign(const uint32_t k[8], const uint32_t d[8], const uint32_t e[8], uint8_t sig[64])
731{
732 if (fp_is_zero(k) || fp_cmp(k, P256_N) >= 0)
733 return false;
734 Pt R;
735 pt_scalarmul(&R, k, &P256_G);
736 if (pt_is_infinity(&R))
737 return false;
738 uint32_t rx[8];
739 uint32_t ry[8];
740 pt_to_affine(rx, ry, &R);
741 uint32_t r[8];
742 fp_reduce_once(r, rx, P256_N); // r = Rx mod n (Rx < p < 2n -> one subtract)
743 if (fp_is_zero(r))
744 return false;
745 uint32_t kinv[8];
746 uint32_t s[8];
747 fp_inv(kinv, k, &FN);
748 fp_mul(s, r, d, &FN); // r*d
749 fp_add(s, s, e, &FN); // e + r*d
750 fp_mul(s, kinv, s, &FN); // k^-1 (e + r*d)
751 if (fp_is_zero(s))
752 return false;
753 store_be(sig, r);
754 store_be(sig + 32, s);
755 return true;
756}
757
758// ECDSA core: sign hash h1 (32) with scalar d, deterministic k per RFC 6979. Requires ecdsa_hw_on().
759bool ecdsa_sign_core(uint8_t sig[64], const uint8_t h1[32], const uint32_t d[8])
760{
761 uint32_t e[8];
762 uint32_t etmp[8];
763 load_be(etmp, h1);
764 fp_reduce_once(e, etmp, P256_N); // bits2int(h1) mod n
765
766 uint8_t x_oct[32];
767 uint8_t h_oct[32];
768 store_be(x_oct, d);
769 store_be(h_oct, e); // bits2octets(h1)
770
771 uint8_t V[32];
772 uint8_t K[32];
773 memset(V, 0x01, 32);
774 memset(K, 0x00, 32);
775 hmac_cat(K, K, V, 32, 0x00, x_oct, h_oct);
776 hmac_cat(V, K, V, 32, -1, nullptr, nullptr);
777 hmac_cat(K, K, V, 32, 0x01, x_oct, h_oct);
778 hmac_cat(V, K, V, 32, -1, nullptr, nullptr);
779
780 for (int guard = 0; guard < 64; guard++)
781 {
782 hmac_cat(V, K, V, 32, -1, nullptr, nullptr); // T = HMAC_K(V), one block
783 uint32_t k[8];
784 load_be(k, V); // bits2int(T)
785 if (ecdsa_try_sign(k, d, e, sig))
786 return true;
787 uint8_t buf[33]; // retry: K = HMAC_K(V || 0x00); V = HMAC_K(V)
788 memcpy(buf, V, 32);
789 buf[32] = 0x00;
790 ssh_hmac_sha256(K, 32, buf, 33, K);
791 hmac_cat(V, K, V, 32, -1, nullptr, nullptr);
792 }
793 return false;
794}
795
796} // namespace
797
798bool ssh_ecdsa_p256_pubkey(uint8_t pub[SSH_ECDSA_P256_PUB_LEN], const uint8_t priv[SSH_ECDSA_P256_PRIV_LEN])
799{
800 uint32_t d[8];
801 load_be(d, priv);
802 if (fp_is_zero(d) || fp_cmp(d, P256_N) >= 0)
803 return false;
804
805 ecdsa_hw_on();
806 Pt Q;
807 pt_scalarmul(&Q, d, &P256_G);
808 bool ok = !pt_is_infinity(&Q);
809 if (ok)
810 {
811 uint32_t qx[8];
812 uint32_t qy[8];
813 pt_to_affine(qx, qy, &Q);
814 pub[0] = 0x04;
815 store_be(pub + 1, qx);
816 store_be(pub + 33, qy);
817 }
818 ecdsa_hw_off();
819 return ok;
820}
821
822bool ssh_ecdsa_p256_sign(uint8_t sig[SSH_ECDSA_P256_SIG_LEN], const uint8_t *msg, size_t mlen,
823 const uint8_t priv[SSH_ECDSA_P256_PRIV_LEN])
824{
825 uint32_t d[8];
826 load_be(d, priv);
827 if (fp_is_zero(d) || fp_cmp(d, P256_N) >= 0)
828 return false;
829 uint8_t h1[SSH_SHA256_DIGEST_LEN];
830 ssh_sha256(msg, mlen, h1);
831
832 ecdsa_hw_on();
833 bool ok = ecdsa_sign_core(sig, h1, d);
834 ecdsa_hw_off();
835 return ok;
836}
837
838bool ssh_ecdsa_p256_verify(const uint8_t pub[SSH_ECDSA_P256_PUB_LEN], const uint8_t *msg, size_t mlen,
839 const uint8_t sig[SSH_ECDSA_P256_SIG_LEN])
840{
841 if (pub[0] != 0x04)
842 return false;
843 uint32_t qx[8];
844 uint32_t qy[8];
845 load_be(qx, pub + 1);
846 load_be(qy, pub + 33);
847
848 uint32_t r[8];
849 uint32_t s[8];
850 load_be(r, sig);
851 load_be(s, sig + 32);
852 if (fp_is_zero(r) || fp_cmp(r, P256_N) >= 0 || fp_is_zero(s) || fp_cmp(s, P256_N) >= 0)
853 return false;
854
855 uint8_t h1[SSH_SHA256_DIGEST_LEN];
856 ssh_sha256(msg, mlen, h1);
857 uint32_t e[8];
858 uint32_t etmp[8];
859 load_be(etmp, h1);
860 fp_reduce_once(e, etmp, P256_N);
861
862 ecdsa_hw_on();
863 bool ok = on_curve(qx, qy);
864 if (ok)
865 {
866 uint32_t w[8];
867 uint32_t u1[8];
868 uint32_t u2[8];
869 fp_inv(w, s, &FN);
870 fp_mul(u1, e, w, &FN);
871 fp_mul(u2, r, w, &FN);
872
873 Pt Q;
874 Pt Rg;
875 Pt Rq;
876 Pt R;
877 pt_from_affine(&Q, qx, qy);
878 pt_scalarmul(&Rg, u1, &P256_G);
879 pt_scalarmul(&Rq, u2, &Q);
880 pt_add(&R, &Rg, &Rq);
881 if (pt_is_infinity(&R))
882 ok = false;
883 else
884 {
885 uint32_t rx[8];
886 uint32_t ry[8];
887 uint32_t rxn[8];
888 pt_to_affine(rx, ry, &R);
889 fp_reduce_once(rxn, rx, P256_N);
890 ok = fp_cmp(rxn, r) == 0;
891 }
892 }
893 ecdsa_hw_off();
894 return ok;
895}
896
897bool ssh_ecdsa_p256_ecdh(uint8_t shared_x[SSH_ECDSA_P256_COORD_LEN], const uint8_t peer_pub[SSH_ECDSA_P256_PUB_LEN],
898 const uint8_t priv[SSH_ECDSA_P256_PRIV_LEN])
899{
900 if (peer_pub[0] != 0x04)
901 return false;
902 uint32_t qx[8];
903 uint32_t qy[8];
904 load_be(qx, peer_pub + 1);
905 load_be(qy, peer_pub + 33);
906 uint32_t d[8];
907 load_be(d, priv);
908 if (fp_is_zero(d) || fp_cmp(d, P256_N) >= 0)
909 return false;
910
911 ecdsa_hw_on();
912 bool ok = on_curve(qx, qy); // rejects off-curve / out-of-range peer points
913 if (ok)
914 {
915 Pt Q;
916 Pt R;
917 pt_from_affine(&Q, qx, qy);
918 pt_scalarmul(&R, d, &Q);
919 if (pt_is_infinity(&R)) // d*Q is the identity -> invalid shared secret
920 ok = false;
921 else
922 {
923 uint32_t rx[8];
924 uint32_t ry[8];
925 pt_to_affine(rx, ry, &R);
926 store_be(shared_x, rx); // K = X coordinate (big-endian)
927 }
928 }
929 ecdsa_hw_off();
930 return ok;
931}
932
933#endif // ARDUINO path selection
bool ssh_ecdsa_p256_verify(const uint8_t pub[SSH_ECDSA_P256_PUB_LEN], const uint8_t *msg, size_t mlen, const uint8_t sig[SSH_ECDSA_P256_SIG_LEN])
Verify a P-256 ECDSA signature (SHA-256) against an uncompressed public point.
bool ssh_ecdsa_p256_ecdh(uint8_t shared_x[SSH_ECDSA_P256_COORD_LEN], const uint8_t peer_pub[SSH_ECDSA_P256_PUB_LEN], const uint8_t priv[SSH_ECDSA_P256_PRIV_LEN])
P-256 ECDH: the shared-secret X coordinate of d * Q_peer (RFC 5656 §4 / RFC 5903).
bool ssh_ecdsa_p256_sign(uint8_t sig[SSH_ECDSA_P256_SIG_LEN], const uint8_t *msg, size_t mlen, const uint8_t priv[SSH_ECDSA_P256_PRIV_LEN])
Sign mlen bytes of msg with a P-256 private key (ECDSA, SHA-256).
bool ssh_ecdsa_p256_pubkey(uint8_t pub[SSH_ECDSA_P256_PUB_LEN], const uint8_t priv[SSH_ECDSA_P256_PRIV_LEN])
Derive the uncompressed public point Q = d*G from a P-256 private scalar.
Definition ssh_ecdsa.cpp:79
NIST P-256 primitives for SSH: ECDSA signatures and ECDH (RFC 5656 / FIPS 186-4).
void ssh_hmac_sha256(const uint8_t *key, size_t key_len, const uint8_t *data, size_t len, uint8_t mac[SSH_HMAC_SHA256_LEN])
Compute HMAC-SHA2-256 over a single contiguous buffer.
HMAC-SHA2-256 (RFC 2104 + FIPS 198-1).
void ssh_sha256(const uint8_t *data, size_t len, uint8_t digest[SSH_SHA256_DIGEST_LEN])
One-shot SHA-256: hash len bytes and write the digest.
SHA-256 (FIPS 180-4) - streaming context and one-shot API.
#define SSH_SHA256_DIGEST_LEN
SHA-256 digest length in bytes.
Definition ssh_sha256.h:29