DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
ssh_rsa.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_rsa.h
6 * @brief RSA-SHA2-256/512 host-key signing and public-key serialization.
7 *
8 * ═══════════════════════════════════════════════════════════════════════════
9 * SECURITY MODEL - PRIVATE KEY LIFETIME
10 * ═══════════════════════════════════════════════════════════════════════════
11 *
12 * The RSA-2048 private key (d, p, q, dp, dq, qinv) MUST NEVER live in static
13 * or global memory. Reasons:
14 *
15 * 1. A linear heap/BSS overflow from any direction can reach a static
16 * variable; the stack is a separate region with its own growth direction
17 * and is far harder to reach from a single-direction overflow.
18 *
19 * 2. A use-after-free or dangling-pointer bug that reads static memory can
20 * silently expose a key that "should have been zeroed" but was not.
21 *
22 * 3. The private key is only needed for the KEX (once per connection, during
23 * the handshake). There is no reason to keep it resident between uses.
24 *
25 * MANDATED LIFETIME:
26 * load (from NVS) → stack → sign → volatile-wipe → return
27 *
28 * The struct SshRsaPrivKey is declared here for documentation purposes; it
29 * must only ever be declared as a local variable inside ssh_rsa_sign().
30 *
31 * ═══════════════════════════════════════════════════════════════════════════
32 * PKCS#1 v1.5 SIGNATURE SCHEME
33 * ═══════════════════════════════════════════════════════════════════════════
34 *
35 * The signature produced by ssh_rsa_sign() follows PKCS#1 v1.5 (RFC 8017
36 * §8.2), which is what SSH uses for "rsa-sha2-256" and "rsa-sha2-512"
37 * (RFC 8332) - the only difference is the hash and its DigestInfo OID:
38 *
39 * 1. Compute digest = SHA256(msg) or SHA512(msg), per @p hash.
40 * 2. Encode digest in a DER DigestInfo wrapper:
41 * SHA-256: 30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20 <32 bytes>
42 * SHA-512: 30 51 30 0d 06 09 60 86 48 01 65 03 04 02 03 05 00 04 40 <64 bytes>
43 * The OID 2.16.840.1.101.3.4.2.1 identifies SHA-256, .2.3 SHA-512 (RFC 5754 §3.2).
44 * 3. Pad to the RSA modulus length (256 bytes for RSA-2048):
45 * 0x00 0x01 &lt;0xFF padding&gt; 0x00 &lt;DigestInfo&gt;
46 * The 0xFF padding fills bytes [2 .. 256-1-len(DigestInfo)-1].
47 * SHA-256 DigestInfo = 51 bytes (padding 202); SHA-512 DigestInfo = 83 bytes (padding 170).
48 * 4. Interpret the 256-byte padded message M as a bignum m.
49 * 5. Compute s = m^d mod n (RSA private-key operation).
50 * 6. Output s as a 256-byte big-endian integer.
51 *
52 * ═══════════════════════════════════════════════════════════════════════════
53 * ARDUINO VS NATIVE
54 * ═══════════════════════════════════════════════════════════════════════════
55 *
56 * Arduino: uses mbedtls_pk_sign() with MBEDTLS_MD_SHA256 or MBEDTLS_MD_SHA512,
57 * which performs the full PKCS#1 v1.5 pad-and-sign in hardware-accelerated
58 * multiprecision arithmetic via ESP-IDF.
59 *
60 * Native: software path - SHA-256/512 via ssh_sha256()/ssh_sha512(), PKCS#1
61 * v1.5 padding built by hand, RSA exponentiation via bn_expmod_group14()
62 * (same Montgomery path used for DH). Both paths use the same key layout.
63 *
64 * ═══════════════════════════════════════════════════════════════════════════
65 * NVS KEY FORMAT
66 * ═══════════════════════════════════════════════════════════════════════════
67 *
68 * On Arduino the private key is stored in NVS namespace "ssh_host_key" under
69 * key "priv_der", as a DER-encoded PKCS#1 RSAPrivateKey (RFC 8017 App. C):
70 *
71 * RSAPrivateKey ::= SEQUENCE {
72 * version Version (INTEGER: 0),
73 * modulus INTEGER, -- n
74 * publicExponent INTEGER, -- e
75 * privateExponent INTEGER, -- d
76 * prime1 INTEGER, -- p
77 * prime2 INTEGER, -- q
78 * exponent1 INTEGER, -- dp = d mod (p-1)
79 * exponent2 INTEGER, -- dq = d mod (q-1)
80 * coefficient INTEGER, -- qinv = q^(-1) mod p
81 * }
82 *
83 * The DER blob is parsed by mbedtls_rsa_parse_key() on Arduino. On native
84 * builds, the test fixture injects n, e, d directly as raw 256-byte arrays.
85 *
86 * @author Douglas Quigg (dstroy0)
87 * @date 2026
88 */
89
90#ifndef DETERMINISTICESPASYNCWEBSERVER_SSH_RSA_H
91#define DETERMINISTICESPASYNCWEBSERVER_SSH_RSA_H
92
96#include <stddef.h>
97#include <stdint.h>
98
99/**
100 * @brief Hash algorithm selecting the RSA signature scheme (RFC 8332).
101 *
102 * The RSA public-key blob is "ssh-rsa" for both; only the message hash and its
103 * DigestInfo OID differ. SHA256 = "rsa-sha2-256", SHA512 = "rsa-sha2-512".
104 */
105enum class SshRsaHash : uint8_t
106{
107 SHA256 = 0, ///< rsa-sha2-256
108 SHA512 = 1 ///< rsa-sha2-512
109};
110
111// ---------------------------------------------------------------------------
112// Key-blob sizes
113// ---------------------------------------------------------------------------
114
115/** @brief Maximum DER size for a PKCS#1 RSAPrivateKey with 2048-bit fields. */
116#define SSH_RSA_KEY_DER_MAX 1700
117
118/** @brief RSA modulus and private exponent size in bytes (RSA-2048). */
119#define SSH_RSA_KEY_BYTES 256
120
121/** @brief PKCS#1 v1.5 signature size for RSA-2048 in bytes. */
122#define SSH_RSA_SIG_BYTES 256
123
124/**
125 * @brief Key-blob type string for an RSA host key.
126 *
127 * Per RFC 8332 §3, the RSA *public-key blob* always carries the type string
128 * "ssh-rsa" - even when the negotiated *signature* algorithm is
129 * "rsa-sha2-256" or "rsa-sha2-512". Only the signature and authentication
130 * algorithm-name fields use "rsa-sha2-256"; the key blob format is unchanged
131 * from RFC 4253 §6.6. Emitting "rsa-sha2-256" here would make compliant
132 * clients (e.g. OpenSSH) fail to parse the host key.
133 */
134#define SSH_RSA_PUBKEY_ALG "ssh-rsa"
135
136/** @brief Length of SSH_RSA_PUBKEY_ALG ("ssh-rsa" = 7 bytes). */
137#define SSH_RSA_PUBKEY_ALG_LEN 7
138
139/** @brief Signature algorithm name for SHA-256 (RFC 8332). Used in the signature blob. */
140static constexpr char SSH_RSA_SIG_ALG_SHA256[] = "rsa-sha2-256";
141
142/** @brief Signature algorithm name for SHA-512 (RFC 8332). Used in the signature blob. */
143static constexpr char SSH_RSA_SIG_ALG_SHA512[] = "rsa-sha2-512";
144
145// ---------------------------------------------------------------------------
146// PKCS#1 v1.5 DigestInfo headers (RFC 8017, RFC 5754)
147// SHA-256: 30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20
148// SHA-512: 30 51 30 0d 06 09 60 86 48 01 65 03 04 02 03 05 00 04 40
149// ---------------------------------------------------------------------------
150
151/** @brief Length of the DER DigestInfo wrapper for SHA-256. */
152static constexpr size_t SSH_PKCS1_DIGESTINFO_LEN = 19;
153
154/** @brief Length of the DER DigestInfo wrapper for SHA-512. */
155static constexpr size_t SSH_PKCS1_SHA512_DIGESTINFO_LEN = 19;
156
157/**
158 * @brief The DER-encoded DigestInfo wrapper for SHA-256.
159 *
160 * Prepend this to the 32-byte SHA-256 digest to form the 51-byte
161 * DigestInfo structure for PKCS#1 v1.5.
162 */
163extern const uint8_t ssh_pkcs1_sha256_digestinfo[SSH_PKCS1_DIGESTINFO_LEN];
164
165/**
166 * @brief The DER-encoded DigestInfo wrapper for SHA-512.
167 *
168 * Prepend this to the 64-byte SHA-512 digest to form the 83-byte
169 * DigestInfo structure for PKCS#1 v1.5.
170 */
171extern const uint8_t ssh_pkcs1_sha512_digestinfo[SSH_PKCS1_SHA512_DIGESTINFO_LEN];
172
173// ---------------------------------------------------------------------------
174// RSA private key - stack-local only, never static or global
175// ---------------------------------------------------------------------------
176
177/**
178 * @brief RSA-2048 private key parameters.
179 *
180 * WARNING: This struct is intentionally NOT to be declared as a global or
181 * static variable. The ONLY valid use is as an automatic (stack) variable
182 * inside ssh_rsa_sign(). After the signature is computed, every byte is
183 * overwritten via ssh_wipe() before the function returns.
184 *
185 * On Arduino, these fields are populated from mbedtls_rsa_context by reading
186 * the mpi limbs into 256-byte big-endian buffers.
187 *
188 * On native, the fields are set directly by the test fixture or loaded from
189 * a DER blob via a minimal DER parser included in ssh_rsa.cpp.
190 */
192{
193 uint8_t n[SSH_RSA_KEY_BYTES]; ///< Modulus n (256 bytes, big-endian).
194 uint8_t d[SSH_RSA_KEY_BYTES]; ///< Private exponent d (SENSITIVE).
195 uint8_t e_bytes[4]; ///< Public exponent e (typically 65537).
196};
197
198// ---------------------------------------------------------------------------
199// RSA public key (safe to keep in static/flash - no secret material)
200// ---------------------------------------------------------------------------
201
202/**
203 * @brief RSA-2048 public key parameters for the host key blob.
204 *
205 * Allocated in BSS at link time. Contains only n and e; no secret material.
206 */
208{
209 uint8_t n[SSH_RSA_KEY_BYTES]; ///< Modulus n (256 bytes, big-endian).
210 uint8_t e_bytes[4]; ///< Public exponent e (big-endian uint32).
211 bool loaded; ///< True after ssh_rsa_load_pubkey() succeeds.
212};
213
214/** @brief Static host public key (BSS). Set by ssh_rsa_load_pubkey(). */
216
217// ---------------------------------------------------------------------------
218// Public API
219// ---------------------------------------------------------------------------
220
221/**
222 * @brief Load the public portion of the RSA host key into ssh_host_pubkey.
223 *
224 * Arduino: reads the DER blob from NVS ("ssh_host_key"/"priv_der"), parses
225 * n and e via mbedtls, stores them in ssh_host_pubkey.
226 *
227 * Native: reads n and e from the test fixture arrays (see ssh_rsa.cpp).
228 *
229 * Call once at startup (or after begin()). Must succeed before any SSH
230 * connections are accepted.
231 *
232 * @return 0 on success, -1 if the key is absent or malformed.
233 */
234int ssh_rsa_load_pubkey(void);
235
236/**
237 * @brief Sign @p msg using the RSA host key (PKCS#1 v1.5, rsa-sha2-256/512).
238 *
239 * The private key is loaded from NVS directly into a stack-local
240 * SshRsaPrivKey struct, used, then zeroed before this function returns.
241 * The key NEVER touches static or global memory.
242 *
243 * On Arduino: delegates to mbedtls_pk_sign() (hardware-accelerated).
244 * On native: software SHA-256/512 + hand-built PKCS#1 pad + bn_expmod_group14().
245 *
246 * @param[in] msg Message to sign (typically the exchange hash H, 32 bytes).
247 * @param[in] msg_len Length of @p msg.
248 * @param[in] hash Signature hash: SHA256 (rsa-sha2-256) or SHA512 (rsa-sha2-512).
249 * @param[out] sig Output buffer, must be SSH_RSA_SIG_BYTES (256) bytes.
250 * @return 0 on success, -1 on failure (NVS read error, RSA error).
251 */
252int ssh_rsa_sign(const uint8_t *msg, size_t msg_len, SshRsaHash hash, uint8_t sig[SSH_RSA_SIG_BYTES]);
253
254/**
255 * @brief Serialize the RSA public host key into an SSH "ssh-rsa" key blob.
256 *
257 * Format (RFC 4253 §6.6, RFC 8332 §3):
258 * uint32 len("ssh-rsa") = 7
259 * byte[7] "ssh-rsa"
260 * mpint e
261 * mpint n
262 *
263 * Writes into @p out; sets *@p out_len to the number of bytes written.
264 * @p out must be at least SSH_RSA_PUBKEY_BLOB_MAX bytes.
265 *
266 * @param[out] out Destination buffer.
267 * @param[out] out_len Number of bytes written.
268 * @param[in] out_cap Capacity of @p out.
269 * @return 0 on success, -1 if pubkey not loaded or buffer too small.
270 */
271int ssh_rsa_encode_pubkey(uint8_t *out, size_t *out_len, size_t out_cap);
272
273/**
274 * @brief Verify an RSA PKCS#1 v1.5 signature (rsa-sha2-256/512) with a public key.
275 *
276 * Used for client publickey authentication (RFC 4252 §7): @p n_be / @p e_be4
277 * come from the client-supplied key blob, not the host key. The public exponent
278 * is small (typically 65537), so the modular exponentiation s^e mod n is cheap
279 * and is performed for real on both platforms (native: schoolbook bignum;
280 * Arduino: mbedTLS). The hash is selected by the client's signature algorithm
281 * name (rsa-sha2-256 -> SHA256, rsa-sha2-512 -> SHA512), not by the key blob.
282 *
283 * @param[in] n_be RSA modulus n, big-endian, 256 bytes.
284 * @param[in] e_be4 RSA public exponent e, big-endian, 4 bytes.
285 * @param[in] msg Signed message.
286 * @param[in] msg_len Length of @p msg.
287 * @param[in] sig Signature bytes (256 for RSA-2048).
288 * @param[in] sig_len Length of @p sig.
289 * @param[in] hash Signature hash: SHA256 (rsa-sha2-256) or SHA512 (rsa-sha2-512).
290 * @return 0 if the signature is valid, -1 otherwise.
291 */
292int ssh_rsa_verify(const uint8_t n_be[SSH_RSA_KEY_BYTES], const uint8_t e_be4[4], const uint8_t *msg, size_t msg_len,
293 const uint8_t *sig, size_t sig_len, SshRsaHash hash);
294
295/**
296 * @brief Maximum byte length of the serialized RSA public key blob.
297 *
298 * uint32 len + "ssh-rsa"(7) + mpint e (4 len + 1 pad + up to 4 bytes)
299 * + mpint n (4 len + 1 pad + 256 bytes)
300 */
301#define SSH_RSA_PUBKEY_BLOB_MAX (4 + 7 + 4 + 1 + 4 + 4 + 1 + 256)
302
303#endif // DETERMINISTICESPASYNCWEBSERVER_SSH_RSA_H
2048-bit big-integer arithmetic for DH-group14 and RSA-2048.
int ssh_rsa_verify(const uint8_t n_be[SSH_RSA_KEY_BYTES], const uint8_t e_be4[4], const uint8_t *msg, size_t msg_len, const uint8_t *sig, size_t sig_len, SshRsaHash hash)
Verify an RSA PKCS#1 v1.5 signature (rsa-sha2-256/512) with a public key.
Definition ssh_rsa.cpp:181
SshRsaHash
Hash algorithm selecting the RSA signature scheme (RFC 8332).
Definition ssh_rsa.h:106
@ SHA256
rsa-sha2-256
@ SHA512
rsa-sha2-512
int ssh_rsa_sign(const uint8_t *msg, size_t msg_len, SshRsaHash hash, uint8_t sig[SSH_RSA_SIG_BYTES])
Sign msg using the RSA host key (PKCS#1 v1.5, rsa-sha2-256/512).
Definition ssh_rsa.cpp:147
SshRsaPubKey ssh_host_pubkey
Static host public key (BSS). Set by ssh_rsa_load_pubkey().
Definition ssh_rsa.cpp:39
const uint8_t ssh_pkcs1_sha512_digestinfo[SSH_PKCS1_SHA512_DIGESTINFO_LEN]
The DER-encoded DigestInfo wrapper for SHA-512.
Definition ssh_rsa.cpp:26
#define SSH_RSA_SIG_BYTES
PKCS#1 v1.5 signature size for RSA-2048 in bytes.
Definition ssh_rsa.h:122
int ssh_rsa_load_pubkey(void)
Load the public portion of the RSA host key into ssh_host_pubkey.
Definition ssh_rsa.cpp:82
#define SSH_RSA_KEY_BYTES
RSA modulus and private exponent size in bytes (RSA-2048).
Definition ssh_rsa.h:119
int ssh_rsa_encode_pubkey(uint8_t *out, size_t *out_len, size_t out_cap)
Serialize the RSA public host key into an SSH "ssh-rsa" key blob.
Definition ssh_rsa.cpp:576
const uint8_t ssh_pkcs1_sha256_digestinfo[SSH_PKCS1_DIGESTINFO_LEN]
The DER-encoded DigestInfo wrapper for SHA-256.
Definition ssh_rsa.cpp:17
SHA-256 (FIPS 180-4) - streaming context and one-shot API.
SHA-512 (FIPS 180-4) - streaming context and one-shot API.
RSA-2048 private key parameters.
Definition ssh_rsa.h:192
uint8_t e_bytes[4]
Public exponent e (typically 65537).
Definition ssh_rsa.h:195
uint8_t d[SSH_RSA_KEY_BYTES]
Private exponent d (SENSITIVE).
Definition ssh_rsa.h:194
uint8_t n[SSH_RSA_KEY_BYTES]
Modulus n (256 bytes, big-endian).
Definition ssh_rsa.h:193
RSA-2048 public key parameters for the host key blob.
Definition ssh_rsa.h:208
bool loaded
True after ssh_rsa_load_pubkey() succeeds.
Definition ssh_rsa.h:211
uint8_t n[SSH_RSA_KEY_BYTES]
Modulus n (256 bytes, big-endian).
Definition ssh_rsa.h:209
uint8_t e_bytes[4]
Public exponent e (big-endian uint32).
Definition ssh_rsa.h:210