DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
ssh_bignum.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_bignum.cpp
6 * @brief 2048-bit Montgomery modular exponentiation for DH-group14.
7 *
8 * ─── Montgomery parameter for group-14 ───────────────────────────────────
9 * The group-14 prime p ends in ...FFFFFFFF FFFFFFFF (little-endian d[0]=d[1]=
10 * 0xFFFFFFFF). The Montgomery parameter:
11 *
12 * p_inv = (-(p mod 2^32))^(-1) mod 2^32
13 *
14 * p mod 2^32 = 0xFFFFFFFF
15 * -(0xFFFFFFFF) mod 2^32 = 0x00000001
16 * 0x00000001^(-1) mod 2^32 = 1
17 *
18 * So p_inv = 1 for the group-14 prime.
19 * In the SOS reduction pass: m_i = t[i] * p_inv mod 2^32 = t[i].
20 *
21 * ─── R² mod p ────────────────────────────────────────────────────────────
22 * R = 2^2048. R² mod p = 2^4096 mod p.
23 * It is computed once at startup by bn_init() via 4096 doublings mod p,
24 * and stored in the static s_g14.r2 constant.
25 * ─────────────────────────────────────────────────────────────────────────
26 */
27
30#include <string.h>
31
32// ---------------------------------------------------------------------------
33// Scratch buffer (SSH_CRYPTO_WORK_SIZE bytes, zeroed after each crypto op)
34// ---------------------------------------------------------------------------
35
37
38// ---------------------------------------------------------------------------
39// Group-14 prime and generator (RFC 3526, §3)
40// Little-endian 32-bit limbs: d[0] = least significant.
41// ---------------------------------------------------------------------------
42
44 // 2048-bit MODP group-14 prime
45 0xFFFFFFFFu, 0xFFFFFFFFu, 0x8AACaa68u, 0x15728E5Au, 0x98FA0510u, 0x15D22618u, 0xEA956AE5u, 0x3995497Cu,
46 0x95581718u, 0xDE2BCBF6u, 0x6F4C52C9u, 0xB5C55DF0u, 0xEC07A28Fu, 0x9B2783A2u, 0x180E8603u, 0xE39E772Cu,
47 0x2E36CE3Bu, 0x32905E46u, 0xCA18217Cu, 0xF1746C08u, 0x4ABC9804u, 0x670C354Eu, 0x7096966Du, 0x9ED52907u,
48 0x208552BBu, 0x1C62F356u, 0xDCA3AD96u, 0x83655D23u, 0xFD24CF5Fu, 0x69163FA8u, 0x1C55D39Au, 0x98DA4836u,
49 0xA163BF05u, 0xC2007CB8u, 0xECE45B3Du, 0x49286651u, 0x7C4B1FE6u, 0xAE9F2411u, 0x5A899FA5u, 0xEE386BFBu,
50 0xF406B7EDu, 0x0BFF5CB6u, 0xA637ED6Bu, 0xF44C42E9u, 0x625E7EC6u, 0xE485B576u, 0x6D51C245u, 0x4FE1356Du,
51 0xF25F1437u, 0x302B0A6Du, 0xCD3A431Bu, 0xEF9519B3u, 0x8E3404DDu, 0x514A0879u, 0x3B139B22u, 0x020BBEa6u,
52 0x8A67CC74u, 0x29024E08u, 0x80DC1CD1u, 0xC4C6628Bu, 0x2168C234u, 0xC90FDAA2u, 0xFFFFFFFFu, 0xFFFFFFFFu,
53}};
54
56 2u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u,
57 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u,
58 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u,
59}};
60
61// Software Montgomery state and helpers below are used ONLY by the native
62// software modexp; on ARDUINO bn_expmod_group14() delegates to mbedtls (HW),
63// so this (data-dependent, non-constant-time) machinery must never be compiled
64// into firmware. Guarded out on ARDUINO accordingly (SECURITY.md §timing).
65#ifndef ARDUINO
66// Group14 Montgomery constants, owned by one instance (internal linkage): R mod p, R^2 mod p,
67// and the init flag (all filled by bn_init()). One named owner, unreachable cross-TU.
68struct Group14Ctx
69{
70 SshBigNum r1; // R mod p = 2^2048 - p (two's complement of p in 2048 bits)
71 SshBigNum r2; // R^2 mod p = 2^4096 mod p (bn_init() via repeated doubling)
72 bool initialized = false;
73};
74static Group14Ctx s_g14;
75#endif // !ARDUINO
76
77// ---------------------------------------------------------------------------
78// Internal helpers
79// ---------------------------------------------------------------------------
80
81static int bn_cmp_raw(const uint32_t *a, const uint32_t *b, int n)
82{
83 for (int i = n - 1; i >= 0; i--)
84 {
85 if (a[i] < b[i])
86 return -1;
87 if (a[i] > b[i])
88 return 1;
89 }
90 return 0;
91}
92
93#ifndef ARDUINO // native-only Montgomery helpers (see guard note above)
94// Subtract b from a in place (a -= b). Assumes a >= b. Both are n limbs.
95static void bn_sub_inplace(uint32_t *a, const uint32_t *b, int n)
96{
97 uint64_t borrow = 0;
98 for (int i = 0; i < n; i++)
99 {
100 uint64_t v = (uint64_t)a[i] - b[i] - borrow;
101 a[i] = (uint32_t)v;
102 borrow = (v >> 32) & 1u;
103 }
104}
105
106// Left-shift n-limb value by 1 bit. Returns the shifted-out MSB.
107static uint32_t bn_shl1(uint32_t *a, int n)
108{
109 uint32_t carry = 0;
110 for (int i = 0; i < n; i++)
111 {
112 uint32_t nc = a[i] >> 31;
113 a[i] = (a[i] << 1) | carry;
114 carry = nc;
115 }
116 return carry;
117}
118
119// ---------------------------------------------------------------------------
120// Montgomery initialization (compute R mod p and R^2 mod p for group-14)
121// ---------------------------------------------------------------------------
122
123static void bn_init(void)
124{
125 if (s_g14.initialized)
126 return;
127
128 // R mod p = 2^2048 mod p = 2^2048 - p
129 // (Since 2^2047 <= p < 2^2048, R mod p = 2^2048 - p which is positive and < p.)
130 // Compute via borrow subtraction: 0 - p with 2048-bit wrap.
131 {
132 uint64_t borrow = 0;
133 for (int i = 0; i < SSH_BN_LIMBS; i++)
134 {
135 uint64_t v = (uint64_t)0 - group14_p.d[i] - borrow;
136 s_g14.r1.d[i] = (uint32_t)v;
137 borrow = (v >> 32) & 1u;
138 }
139 // borrow == 1 here (expected - we wrapped around 2^2048), which is
140 // the carry that represents the implicit 2^2048 in R mod p. Correct.
141 }
142
143 // R^2 mod p = 2^4096 mod p.
144 // Compute by starting from R mod p and doubling it 2048 times mod p.
145 memcpy(s_g14.r2.d, s_g14.r1.d, sizeof(SshBigNum));
146 for (int i = 0; i < 2048; i++)
147 {
148 uint32_t overflow = bn_shl1(s_g14.r2.d, SSH_BN_LIMBS);
149 // If overflow bit set OR result >= p, subtract p.
150 if (overflow || bn_cmp_raw(s_g14.r2.d, group14_p.d, SSH_BN_LIMBS) >= 0)
151 bn_sub_inplace(s_g14.r2.d, group14_p.d, SSH_BN_LIMBS);
152 }
153
154 s_g14.initialized = true;
155}
156
157// ---------------------------------------------------------------------------
158// Montgomery SOS multiplication: out = a * b * R^-1 mod p
159// Requires: 0 <= a, b < p.
160// Uses crypto_work[768..1283] as the 129-limb temporary t[].
161// p_inv = 1 for group-14 (see file header).
162// ---------------------------------------------------------------------------
163
164static void bn_monpro(SshBigNum *out, const SshBigNum *a, const SshBigNum *b)
165{
166 // t lives at offset 768 in crypto_work (after base_mont, result, tmp).
167 uint32_t *t = (uint32_t *)(crypto_work + 768);
168 memset(t, 0, 129 * sizeof(uint32_t));
169
170 for (int i = 0; i < SSH_BN_LIMBS; i++)
171 {
172 // Multiply step: t[0..63] += a[i] * b[0..63]
173 uint64_t carry = 0;
174 for (int j = 0; j < SSH_BN_LIMBS; j++)
175 {
176 uint64_t uv = (uint64_t)t[i + j] + (uint64_t)a->d[i] * (uint64_t)b->d[j] + carry;
177 t[i + j] = (uint32_t)uv;
178 carry = uv >> 32;
179 }
180 t[i + SSH_BN_LIMBS] += (uint32_t)carry;
181
182 // Reduction step: m = t[i] * p_inv = t[i] * 1 = t[i]
183 uint32_t m = t[i];
184 carry = 0;
185 for (int j = 0; j < SSH_BN_LIMBS; j++)
186 {
187 uint64_t uv = (uint64_t)t[i + j] + (uint64_t)m * (uint64_t)group14_p.d[j] + carry;
188 t[i + j] = (uint32_t)uv;
189 carry = uv >> 32;
190 }
191 // Add carry into the high word (t[i+64]); t[128] absorbs final overflow.
192 uint64_t hi = (uint64_t)t[i + SSH_BN_LIMBS] + carry;
193 t[i + SSH_BN_LIMBS] = (uint32_t)hi;
194 t[i + SSH_BN_LIMBS + 1] += (uint32_t)(hi >> 32);
195 }
196
197 // Result is in t[64..127]. Conditionally subtract p if result >= p.
198 uint32_t *res = t + SSH_BN_LIMBS;
199 if (t[128] || bn_cmp_raw(res, group14_p.d, SSH_BN_LIMBS) >= 0)
200 bn_sub_inplace(res, group14_p.d, SSH_BN_LIMBS);
201
202 memcpy(out->d, res, SSH_BN_LIMBS * sizeof(uint32_t));
203}
204#endif // !ARDUINO (native-only Montgomery helpers)
205
206// ---------------------------------------------------------------------------
207// Public API (bn_from_bytes / bn_to_bytes / bn_cmp / ... shared both platforms)
208// ---------------------------------------------------------------------------
209
210void bn_from_bytes(SshBigNum *out, const uint8_t *bytes, size_t len)
211{
212 memset(out->d, 0, sizeof(SshBigNum));
213 // bytes are big-endian; map to little-endian limbs.
214 size_t blen = len < 256 ? len : 256;
215 for (size_t i = 0; i < blen; i++)
216 {
217 size_t byte_pos = i; // byte i from LSB (bytes[len-1-i] is the i-th byte from the end)
218 out->d[byte_pos / 4] |= (uint32_t)bytes[len - 1 - i] << ((byte_pos % 4) * 8);
219 }
220}
221
222void bn_to_bytes(uint8_t bytes[256], const SshBigNum *in)
223{
224 for (int i = 0; i < SSH_BN_LIMBS; i++)
225 {
226 uint32_t v = in->d[SSH_BN_LIMBS - 1 - i];
227 bytes[i * 4 + 0] = (uint8_t)(v >> 24);
228 bytes[i * 4 + 1] = (uint8_t)(v >> 16);
229 bytes[i * 4 + 2] = (uint8_t)(v >> 8);
230 bytes[i * 4 + 3] = (uint8_t)(v);
231 }
232}
233
234int bn_cmp(const SshBigNum *a, const SshBigNum *b)
235{
236 return bn_cmp_raw(a->d, b->d, SSH_BN_LIMBS);
237}
238
240{
241 for (int i = 0; i < SSH_BN_LIMBS; i++)
242 if (a->d[i])
243 return 0;
244 return 1;
245}
246
248{
249 // Must be > 1
250 int ok = 0;
251 for (int i = 1; i < SSH_BN_LIMBS; i++)
252 if (v->d[i])
253 {
254 ok = 1;
255 break;
256 }
257 if (!ok && v->d[0] <= 1u)
258 return -1;
259 // Must be < p-1
260 // p-1: subtract 1 from p
261 SshBigNum pm1 = group14_p;
262 pm1.d[0]--;
263 if (bn_cmp(v, &pm1) >= 0)
264 return -1;
265 return 0;
266}
267
268// ---------------------------------------------------------------------------
269// Modular exponentiation: out = base^exp mod group14_p
270// ---------------------------------------------------------------------------
271
272#ifdef ARDUINO
273// On ESP32 delegate to mbedtls which uses HW bignum acceleration.
274#include <mbedtls/bignum.h>
275
276void bn_expmod_group14(SshBigNum *out, const SshBigNum *base, const SshBigNum *exp)
277{
278 uint8_t base_be[256];
279 uint8_t exp_be[256];
280 uint8_t p_be[256];
281 uint8_t res_be[256];
282 bn_to_bytes(base_be, base);
283 bn_to_bytes(exp_be, exp);
284 bn_to_bytes(p_be, &group14_p);
285
286 mbedtls_mpi B;
287 mbedtls_mpi E;
288 mbedtls_mpi P;
289 mbedtls_mpi R;
290 mbedtls_mpi_init(&B);
291 mbedtls_mpi_init(&E);
292 mbedtls_mpi_init(&P);
293 mbedtls_mpi_init(&R);
294
295 mbedtls_mpi_read_binary(&B, base_be, 256);
296 mbedtls_mpi_read_binary(&E, exp_be, 256);
297 mbedtls_mpi_read_binary(&P, p_be, 256);
298
299 mbedtls_mpi_exp_mod(&R, &B, &E, &P, NULL);
300 mbedtls_mpi_write_binary(&R, res_be, 256);
301
302 mbedtls_mpi_free(&B);
303 mbedtls_mpi_free(&E);
304 mbedtls_mpi_free(&P);
305 mbedtls_mpi_free(&R);
306
307 bn_from_bytes(out, res_be, 256);
308 // Wipe the big-endian temporaries (they held the private exponent).
309 ssh_wipe(exp_be, 256);
310 ssh_wipe(res_be, 256);
311}
312
313#else // Native software path
314
315void bn_expmod_group14(SshBigNum *out, const SshBigNum *base, const SshBigNum *exp)
316{
317 bn_init(); // ensure s_g14.r1, s_g14.r2 are computed
318
319 // Map crypto_work regions (layout documented in ssh_bignum.h):
320 SshBigNum *base_mont = (SshBigNum *)(crypto_work + 0);
321 SshBigNum *result = (SshBigNum *)(crypto_work + 256);
322 SshBigNum *tmp = (SshBigNum *)(crypto_work + 512);
323 // t[] at offset 768 is used inside bn_monpro().
324
325 // Convert base to Montgomery form: base_mont = base * R mod p
326 // = MonPro(base, R^2 mod p)
327 bn_monpro(base_mont, base, &s_g14.r2);
328
329 // result = 1 in Montgomery form = R mod p = s_g14.r1
330 memcpy(result->d, s_g14.r1.d, sizeof(SshBigNum));
331
332 // Left-to-right binary square-and-multiply (MSB first: d[63]..d[0], bit 31..0)
333 for (int i = SSH_BN_LIMBS - 1; i >= 0; i--)
334 {
335 for (int b = 31; b >= 0; b--)
336 {
337 // Square: result = result^2 * R^-1 mod p
338 bn_monpro(tmp, result, result);
339 memcpy(result->d, tmp->d, sizeof(SshBigNum));
340
341 if ((exp->d[i] >> b) & 1u)
342 {
343 // Multiply: result = result * base_mont * R^-1 mod p
344 bn_monpro(tmp, result, base_mont);
345 memcpy(result->d, tmp->d, sizeof(SshBigNum));
346 }
347 }
348 }
349
350 // Convert back from Montgomery form: out = result * R^-1 mod p
351 // = MonPro(result, 1)
352 SshBigNum one;
353 memset(one.d, 0, sizeof(SshBigNum));
354 one.d[0] = 1u;
355 bn_monpro(out, result, &one);
356
357 // Wipe all temporaries in crypto_work (they contained DH private key fragments).
359}
360
361#endif // ARDUINO
#define SSH_CRYPTO_WORK_SIZE
Shared scratch buffer for SSH big-number operations.
void bn_expmod_group14(SshBigNum *out, const SshBigNum *base, const SshBigNum *exp)
Compute out = base^exp mod group14_p.
int bn_cmp(const SshBigNum *a, const SshBigNum *b)
Compare two SshBigNum values.
void bn_to_bytes(uint8_t bytes[256], const SshBigNum *in)
Write a SshBigNum as a 256-byte big-endian array.
const SshBigNum group14_g
Generator for group-14: g = 2.
uint8_t crypto_work[SSH_CRYPTO_WORK_SIZE]
Global scratch buffer for big-number temporaries.
int bn_dh_validate(const SshBigNum *v)
Validate a received DH public value.
const SshBigNum group14_p
The RFC 3526 MODP group-14 prime (2048-bit).
void bn_from_bytes(SshBigNum *out, const uint8_t *bytes, size_t len)
Read a big-endian byte array of len bytes into a SshBigNum.
int bn_is_zero(const SshBigNum *a)
Return non-zero if a is zero (all limbs zero).
2048-bit big-integer arithmetic for DH-group14 and RSA-2048.
#define SSH_BN_LIMBS
Number of 32-bit limbs in a 2048-bit integer.
Definition ssh_bignum.h:87
SSH session key material - types, pools, and security model.
A 2048-bit unsigned integer stored as 64 little-endian 32-bit limbs.
Definition ssh_bignum.h:96
uint32_t d[SSH_BN_LIMBS]
256 bytes of magnitude, little-endian limbs.
Definition ssh_bignum.h:97