DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
ssh_fe25519.h
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_fe25519.h
6 * @brief ESP32-S3 GF(2^255-19) field layer on the RSA/MPI hardware accelerator (X25519 + Ed25519).
7 *
8 * Field elements are canonical `uint32[8]` (< p = 2^255-19) so every field multiply is a single
9 * 256-bit modular multiply on the S3 RSA accelerator (~1,386 cycles vs 7,955 for the software SIMD
10 * `ssh_gf_mul`). add/sub are native 32-bit (carry + one conditional subtract of p); bytes<->fe is a
11 * per-scalar-mult conversion, not per multiply. This is the shared engine behind both the X25519 KEX
12 * (`ssh_curve25519.cpp`) and the Ed25519 host-key signature (`ssh_ed25519.cpp`) on the S3; the
13 * radix-2^16 `ssh_gf` path is the native / non-S3 fallback in both.
14 *
15 * The accelerator (and its lock) are shared with mbedTLS RSA/DH, so a scalar-mult brackets itself with
16 * `ssh_fe_hw_enable()` / `ssh_fe_hw_disable()` (mbedTLS's own `esp_mpi_{enable,disable}_hardware_hw_op`,
17 * i.e. acquire the MPI lock + clock/power the peripheral) and holds the lock for its whole run.
18 *
19 * Header-only `static inline` on purpose: the cheap ops (add/sub/cswap) inline into the ladder in each
20 * translation unit with no cross-TU call overhead, and the whole layer stays one source of truth.
21 */
22
23#ifndef DETERMINISTICESPASYNCWEBSERVER_SSH_FE25519_H
24#define DETERMINISTICESPASYNCWEBSERVER_SSH_FE25519_H
25
26#include <stdint.h>
27
28#ifdef ARDUINO
29#include "sdkconfig.h" // CONFIG_IDF_TARGET_ESP32S3
30#endif
31
32// Gated to Arduino on the S3: the field layer drives the RSA peripheral through mbedTLS's port
33// (esp_mpi_*), which only exists in the on-device toolchain.
34#if defined(ARDUINO) && defined(CONFIG_IDF_TARGET_ESP32S3) && CONFIG_IDF_TARGET_ESP32S3
35#define DETWS_FE25519_MPI_HW 1
36#endif
37
38#ifdef DETWS_FE25519_MPI_HW
39
40#include "soc/hwcrypto_reg.h" // RSA/MPI accelerator register map (MODMULT)
41#include "soc/soc.h" // DR_REG_RSA_BASE
42
43extern "C"
44{
45 void esp_mpi_enable_hardware_hw_op(void); // mbedTLS port: acquire the MPI lock + clock/power the peripheral
46 void esp_mpi_disable_hardware_hw_op(void); // release the lock + power down
47}
48
49#define SSH_RSA_REG(a) (*(volatile uint32_t *)(a))
50
51/** @brief A field element of GF(2^255-19): canonical, eight little-endian 32-bit limbs (< p). */
52typedef uint32_t fe[8];
53
54// Constants for the 256-bit modular multiply mod p = 2^255-19 (scratchpad/montconst.py): Montgomery m'
55// and R^2 mod p (= 38^2 = 1444 = 0x5a4). Preloading R^2 into the result block makes the accelerator
56// return a plain residue X*Y mod p rather than a Montgomery form (the esp_mpi_mul_mpi_mod convention).
57static const uint32_t FE_MOD_MPRIME = 0x286bca1bu;
58static const uint32_t FE_MOD_P[8] = {0xffffffedu, 0xffffffffu, 0xffffffffu, 0xffffffffu,
59 0xffffffffu, 0xffffffffu, 0xffffffffu, 0x7fffffffu};
60static const uint32_t FE_MOD_R2[8] = {0x000005a4u, 0, 0, 0, 0, 0, 0, 0};
61
62// Acquire the accelerator (lock + power) for a scalar-mult, and drop it after. Bracket every run.
63static inline void ssh_fe_hw_enable(void)
64{
65 esp_mpi_enable_hardware_hw_op(); // lock + clock/power the peripheral
66 SSH_RSA_REG(RSA_INTERRUPT_REG) = 0; // poll only, no completion IRQ
67}
68static inline void ssh_fe_hw_disable(void)
69{
70 esp_mpi_disable_hardware_hw_op(); // release the lock + power down
71}
72
73// z = x*y mod p (8 words / 256-bit). Requires ssh_fe_hw_enable() first. Output is always canonical (< p).
74static inline void fe_mul(fe z, const fe x, const fe y) // safe if z aliases x/y
75{
76 volatile uint32_t *M = (volatile uint32_t *)RSA_MEM_M_BLOCK_BASE;
77 volatile uint32_t *X = (volatile uint32_t *)RSA_MEM_X_BLOCK_BASE;
78 volatile uint32_t *Y = (volatile uint32_t *)RSA_MEM_Y_BLOCK_BASE;
79 volatile uint32_t *Z = (volatile uint32_t *)RSA_MEM_Z_BLOCK_BASE;
80 SSH_RSA_REG(RSA_LENGTH_REG) = 8 - 1; // mode = words - 1
81 SSH_RSA_REG(RSA_M_DASH_REG) = FE_MOD_MPRIME;
82 for (int i = 0; i < 8; i++)
83 {
84 M[i] = FE_MOD_P[i];
85 X[i] = x[i];
86 Y[i] = y[i];
87 Z[i] = FE_MOD_R2[i]; // r = R^2 mod p in the result block -> plain (non-Montgomery) output
88 }
89 SSH_RSA_REG(RSA_CLEAR_INTERRUPT_REG) = 1; // clear any stale done flag before starting
90 SSH_RSA_REG(RSA_MOD_MULT_START_REG) = 1;
91 while (SSH_RSA_REG(RSA_QUERY_INTERRUPT_REG) == 0)
92 ;
93 SSH_RSA_REG(RSA_CLEAR_INTERRUPT_REG) = 1;
94 for (int i = 0; i < 8; i++)
95 z[i] = Z[i];
96}
97static inline void fe_sq(fe o, const fe x)
98{
99 fe_mul(o, x, x);
100}
101
102static inline void fe_copy(fe o, const fe a)
103{
104 for (int i = 0; i < 8; i++)
105 o[i] = a[i];
106}
107static inline void fe_0(fe o)
108{
109 for (int i = 0; i < 8; i++)
110 o[i] = 0;
111}
112static inline void fe_1(fe o)
113{
114 o[0] = 1;
115 for (int i = 1; i < 8; i++)
116 o[i] = 0;
117}
118// If o >= p (o is in [p, 2p)), subtract p. Constant-time: the borrow out of o-p selects o or o-p.
119static inline void fe_reduce_once(fe o)
120{
121 uint32_t t[8];
122 int64_t b = 0;
123 for (int i = 0; i < 8; i++)
124 {
125 b += (int64_t)o[i] - (int64_t)FE_MOD_P[i];
126 t[i] = (uint32_t)b;
127 b >>= 32;
128 }
129 uint32_t keep = (uint32_t)b; // 0 if o>=p (take t=o-p), 0xffffffff if o<p (keep o)
130 for (int i = 0; i < 8; i++)
131 o[i] = (o[i] & keep) | (t[i] & ~keep);
132}
133static inline void fe_add(fe o, const fe x, const fe y) // x,y < p -> o = x+y mod p
134{
135 uint64_t c = 0;
136 for (int i = 0; i < 8; i++)
137 {
138 c += (uint64_t)x[i] + y[i];
139 o[i] = (uint32_t)c;
140 c >>= 32;
141 }
142 fe_reduce_once(o); // x+y < 2p, one conditional subtract
143}
144static inline void fe_sub(fe o, const fe x, const fe y) // x,y < p -> o = x-y mod p
145{
146 int64_t b = 0;
147 uint32_t t[8];
148 for (int i = 0; i < 8; i++)
149 {
150 b += (int64_t)x[i] - (int64_t)y[i];
151 t[i] = (uint32_t)b;
152 b >>= 32;
153 }
154 uint32_t borrow = (uint32_t)b; // 0xffffffff if x<y -> add p back
155 uint64_t c = 0;
156 for (int i = 0; i < 8; i++)
157 {
158 c += (uint64_t)t[i] + (FE_MOD_P[i] & borrow);
159 o[i] = (uint32_t)c;
160 c >>= 32;
161 }
162}
163static inline void fe_cswap(fe x, fe y, uint32_t swap) // constant-time swap of x,y when swap==1
164{
165 uint32_t mask = (uint32_t)(-(int32_t)swap);
166 for (int i = 0; i < 8; i++)
167 {
168 uint32_t t = mask & (x[i] ^ y[i]);
169 x[i] ^= t;
170 y[i] ^= t;
171 }
172}
173static inline void fe_frombytes(fe o, const uint8_t b[32])
174{
175 for (int i = 0; i < 8; i++)
176 o[i] = (uint32_t)b[4 * i] | ((uint32_t)b[4 * i + 1] << 8) | ((uint32_t)b[4 * i + 2] << 16) |
177 ((uint32_t)b[4 * i + 3] << 24);
178 o[7] &= 0x7fffffffu; // Ed25519/X25519 both ignore bit 255 of the y/u coordinate
179 fe_reduce_once(o); // the masked value can still be in [p, 2^255) -> canonicalize
180}
181static inline void fe_tobytes(uint8_t b[32], const fe a)
182{
183 fe t;
184 fe_copy(t, a);
185 fe_reduce_once(t); // freeze to the canonical residue
186 for (int i = 0; i < 8; i++)
187 {
188 b[4 * i] = (uint8_t)t[i];
189 b[4 * i + 1] = (uint8_t)(t[i] >> 8);
190 b[4 * i + 2] = (uint8_t)(t[i] >> 16);
191 b[4 * i + 3] = (uint8_t)(t[i] >> 24);
192 }
193}
194// o = a^(p-2) = a^-1 mod p (tweetnacl square-and-multiply chain for the exponent 2^255-21).
195static inline void fe_invert(fe o, const fe a)
196{
197 fe c;
198 fe_copy(c, a);
199 for (int i = 253; i >= 0; i--)
200 {
201 fe_sq(c, c);
202 if (i != 2 && i != 4)
203 fe_mul(c, c, a);
204 }
205 fe_copy(o, c);
206}
207// o = a^((p-5)/8) = a^(2^252-3) - the square-root exponent for Ed25519 point decompression.
208static inline void fe_pow2523(fe o, const fe a)
209{
210 fe c;
211 fe_copy(c, a);
212 for (int i = 250; i >= 0; i--)
213 {
214 fe_sq(c, c);
215 if (i != 1)
216 fe_mul(c, c, a);
217 }
218 fe_copy(o, c);
219}
220// Low bit of the canonical encoding (Ed25519 x-coordinate sign).
221static inline int fe_parity(const fe a)
222{
223 uint8_t d[32];
224 fe_tobytes(d, a);
225 return d[0] & 1;
226}
227// 0 if a and b encode the same field element, -1 otherwise (constant-time over the 32 bytes).
228static inline int fe_neq(const fe a, const fe b)
229{
230 uint8_t c[32];
231 uint8_t d[32];
232 fe_tobytes(c, a);
233 fe_tobytes(d, b);
234 unsigned diff = 0;
235 for (int i = 0; i < 32; i++)
236 diff |= (unsigned)(c[i] ^ d[i]);
237 return (int)((1 & ((diff - 1) >> 8)) - 1);
238}
239
240#endif // DETWS_FE25519_MPI_HW
241#endif // DETERMINISTICESPASYNCWEBSERVER_SSH_FE25519_H