DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
ssh_bignum.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_bignum.h
6 * @brief 2048-bit big-integer arithmetic for DH-group14 and RSA-2048.
7 *
8 * ═══════════════════════════════════════════════════════════════════════════
9 * DESIGN RATIONALE
10 * ═══════════════════════════════════════════════════════════════════════════
11 *
12 * SshBigNum is a fixed-width 2048-bit integer stored as 64 little-endian
13 * 32-bit limbs (d[0] = least significant). Fixed width means:
14 * - Struct size is a compile-time constant: 256 bytes.
15 * - No dynamic allocation - both DH scalars and RSA key fragments fit in
16 * the same type and can live in BSS or on the stack.
17 * - Array indexing is bounds-safe; no VLA or pointer arithmetic hazards.
18 *
19 * On Arduino (ESP32), DH uses mbedtls_mpi from ESP-IDF (heap-allocated,
20 * variable-length bignum), which has hardware-accelerated multiplication.
21 * On native builds, the software Montgomery path is used - correct but
22 * slower (~200 ms for a 2048-bit exponentiation on x86 at test time).
23 * Since DH happens once per connection and ESP32 uses the HW path in
24 * production, the native speed is acceptable for testing.
25 *
26 * ═══════════════════════════════════════════════════════════════════════════
27 * MONTGOMERY MULTIPLICATION
28 * ═══════════════════════════════════════════════════════════════════════════
29 *
30 * For DH-group14 the modulus p ends in ...FFFFFFFF FFFFFFFF (little-endian
31 * d[0]=0xFFFFFFFF). This gives Montgomery parameter:
32 *
33 * p_inv = (-(p mod 2^32))^(-1) mod 2^32
34 * = (-(0xFFFFFFFF))^(-1) mod 2^32
35 * = (0x00000001)^(-1) mod 2^32
36 * = 1
37 *
38 * p_inv = 1 simplifies the inner reduction loop: m_i = t[i] * 1 = t[i].
39 *
40 * Montgomery product: MonPro(a,b) = a·b·R^-1 mod p
41 * where R = 2^2048.
42 *
43 * To compute a·b mod p normally:
44 * 1. Convert a, b to Montgomery form: aR = a·R mod p (= MonPro(a, R²mod p))
45 * 2. Compute MonPro(aR, bR) = a·b·R mod p
46 * 3. Convert back: MonPro(a·b·R, 1) = a·b mod p
47 *
48 * R² mod p is a precomputed 2048-bit constant (see ssh_bignum.cpp).
49 *
50 * ═══════════════════════════════════════════════════════════════════════════
51 * SCRATCH BUFFER
52 * ═══════════════════════════════════════════════════════════════════════════
53 *
54 * The Montgomery SOS multiplication uses a 129-word (516-byte) temporary
55 * array. The expmod function needs three SshBigNum temporaries (768 bytes).
56 * All of these live in crypto_work[] (SSH_CRYPTO_WORK_SIZE = 1536 bytes)
57 * defined in ServerConfig.h and allocated in ssh_bignum.cpp.
58 *
59 * Layout during bn_expmod_group14():
60 * [0..255] base_mont (SshBigNum)
61 * [256..511] result (SshBigNum)
62 * [512..767] tmp (SshBigNum)
63 * [768..1283] mont_t (uint32_t[129])
64 *
65 * crypto_work[] is zeroed via ssh_wipe() immediately after bn_expmod_group14()
66 * returns so intermediate DH/RSA products do not persist in memory.
67 *
68 * ═══════════════════════════════════════════════════════════════════════════
69 *
70 * @author Douglas Quigg (dstroy0)
71 * @date 2026
72 */
73
74#ifndef DETERMINISTICESPASYNCWEBSERVER_SSH_BIGNUM_H
75#define DETERMINISTICESPASYNCWEBSERVER_SSH_BIGNUM_H
76
77#include "ServerConfig.h"
78#include <stddef.h>
79#include <stdint.h>
80#include <string.h>
81
82// ---------------------------------------------------------------------------
83// Fixed-width 2048-bit integer
84// ---------------------------------------------------------------------------
85
86/** @brief Number of 32-bit limbs in a 2048-bit integer. */
87#define SSH_BN_LIMBS 64
88
89/**
90 * @brief A 2048-bit unsigned integer stored as 64 little-endian 32-bit limbs.
91 *
92 * d[0] = least significant 32 bits.
93 * d[63] = most significant 32 bits.
94 */
96{
97 uint32_t d[SSH_BN_LIMBS]; ///< 256 bytes of magnitude, little-endian limbs.
98};
99
100// ---------------------------------------------------------------------------
101// Scratch buffer (defined in ssh_bignum.cpp)
102// ---------------------------------------------------------------------------
103
104/**
105 * @brief Global scratch buffer for big-number temporaries.
106 *
107 * Only one SSH KEX or RSA-sign operation runs at a time (single Arduino
108 * loop task, synchronous handshake). Zeroed after every use via ssh_wipe().
109 *
110 * See the layout comment in the file header for the exact field map.
111 */
112extern uint8_t crypto_work[SSH_CRYPTO_WORK_SIZE];
113
114// ---------------------------------------------------------------------------
115// Conversion helpers
116// ---------------------------------------------------------------------------
117
118/**
119 * @brief Read a big-endian byte array of @p len bytes into a SshBigNum.
120 *
121 * If @p len < 256 the most-significant limbs are zeroed.
122 * If @p len > 256 only the least-significant 256 bytes are read.
123 *
124 * @param out Destination bignum.
125 * @param bytes Big-endian source bytes.
126 * @param len Number of source bytes (typically 256 for 2048-bit).
127 */
128void bn_from_bytes(SshBigNum *out, const uint8_t *bytes, size_t len);
129
130/**
131 * @brief Write a SshBigNum as a 256-byte big-endian array.
132 *
133 * @param bytes Destination buffer (exactly 256 bytes).
134 * @param in Source bignum.
135 */
136void bn_to_bytes(uint8_t bytes[256], const SshBigNum *in);
137
138// ---------------------------------------------------------------------------
139// Comparison
140// ---------------------------------------------------------------------------
141
142/**
143 * @brief Compare two SshBigNum values.
144 * @return -1 if a < b, 0 if a == b, 1 if a > b.
145 */
146int bn_cmp(const SshBigNum *a, const SshBigNum *b);
147
148/**
149 * @brief Return non-zero if @p a is zero (all limbs zero).
150 */
151int bn_is_zero(const SshBigNum *a);
152
153// ---------------------------------------------------------------------------
154// DH-group14 modular exponentiation
155// ---------------------------------------------------------------------------
156
157/**
158 * @brief Compute out = base^exp mod group14_p.
159 *
160 * Uses Montgomery modular exponentiation with left-to-right binary scan.
161 * Uses crypto_work[] for all temporaries; zeros crypto_work[] on exit.
162 *
163 * On Arduino the computation is delegated to mbedtls_mpi_exp_mod() which
164 * uses hardware multiplication and blinding.
165 *
166 * @param out Result (base^exp mod p, 2048-bit).
167 * @param base Base value; must satisfy 1 < base < p-1.
168 * @param exp Exponent (e.g. the 2048-bit private DH scalar y).
169 */
170void bn_expmod_group14(SshBigNum *out, const SshBigNum *base, const SshBigNum *exp);
171
172/**
173 * @brief Validate a received DH public value.
174 *
175 * RFC 4253 §8: the received value e (or f) must satisfy 1 < e < p-1.
176 * Returns 0 if the value is valid, -1 otherwise.
177 *
178 * @param v Received public DH value.
179 */
180int bn_dh_validate(const SshBigNum *v);
181
182// ---------------------------------------------------------------------------
183// Group-14 prime constant (exposed for key-derivation and validation)
184// ---------------------------------------------------------------------------
185
186/** @brief The RFC 3526 MODP group-14 prime (2048-bit). */
187extern const SshBigNum group14_p;
188
189/** @brief Generator for group-14: g = 2. */
190extern const SshBigNum group14_g;
191
192#endif // DETERMINISTICESPASYNCWEBSERVER_SSH_BIGNUM_H
User-facing configuration for DeterministicESPAsyncWebServer.
#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.
#define SSH_BN_LIMBS
Number of 32-bit limbs in a 2048-bit integer.
Definition ssh_bignum.h:87
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).
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