DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
ssh_rsa.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_rsa.cpp
6 * @brief RSA-SHA2-256/512 host-key signing (stack-only private key, PKCS#1 v1.5).
7 */
8
11#include <string.h>
12
13// ---------------------------------------------------------------------------
14// DigestInfo for SHA-256 / SHA-512 (PKCS#1 v1.5, RFC 8017 §9.2, RFC 5754)
15// ---------------------------------------------------------------------------
16
17const uint8_t ssh_pkcs1_sha256_digestinfo[SSH_PKCS1_DIGESTINFO_LEN] = {
18 0x30, 0x31, // SEQUENCE, length 49
19 0x30, 0x0d, // SEQUENCE, length 13 (AlgorithmIdentifier)
20 0x06, 0x09, // OID, length 9
21 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, // OID 2.16.840.1.101.3.4.2.1
22 0x05, 0x00, // NULL parameters
23 0x04, 0x20 // OCTET STRING, length 32 (digest follows)
24};
25
26const uint8_t ssh_pkcs1_sha512_digestinfo[SSH_PKCS1_SHA512_DIGESTINFO_LEN] = {
27 0x30, 0x51, // SEQUENCE, length 81
28 0x30, 0x0d, // SEQUENCE, length 13 (AlgorithmIdentifier)
29 0x06, 0x09, // OID, length 9
30 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, // OID 2.16.840.1.101.3.4.2.3
31 0x05, 0x00, // NULL parameters
32 0x04, 0x40 // OCTET STRING, length 64 (digest follows)
33};
34
35// ---------------------------------------------------------------------------
36// Public host key (BSS - no secret material)
37// ---------------------------------------------------------------------------
38
40
41// ---------------------------------------------------------------------------
42// Arduino - mbedtls path
43// ---------------------------------------------------------------------------
44
45#ifdef ARDUINO
46
47#include <Preferences.h> // ESP-IDF NVS wrapper
48#include <esp_random.h> // esp_fill_random() for the RSA blinding RNG
49#include <freertos/FreeRTOS.h>
50#include <freertos/semphr.h> // serialise signs on the shared cached key context
51#include <mbedtls/md.h>
52#include <mbedtls/pk.h>
53#include <mbedtls/rsa.h>
54
55// RNG callback for mbedtls private-key operations. mbedtls v3 requires a real
56// f_rng for RSA signing (blinding); v2 uses it harmlessly. Backed by the ESP32
57// hardware RNG.
58static int ssh_mbedtls_rng(void *ctx, unsigned char *buf, size_t len)
59{
60 (void)ctx;
61 esp_fill_random(buf, len);
62 return 0;
63}
64
65// Cached RSA host-key signer. Re-parsing the PKCS#8 key per handshake also re-ran mbedtls's first-use
66// blinding setup (a software r^-1 mod n), ~170 ms wasted on every sign; the parsed context caches the
67// blinding state (Vf/Vi), so keeping it resident means each sign pays only the CRT modexp (~440 -> ~270 ms
68// on an ESP32-S3). The private key therefore stays in RAM for the server lifetime (as an SSH host key
69// normally does); the mutex serialises signs because mbedtls mutates the blinding values per operation, so
70// two worker cores must not enter mbedtls_pk_sign on the shared context at once. Loaded once at startup by
71// ssh_rsa_load_pubkey() (called from the sketch's setup(), single-threaded).
73{
74 mbedtls_pk_context pk; ///< parsed host key + cached blinding state
75 SemaphoreHandle_t lock; ///< serialises signs on the shared context
76 bool ready; ///< pk holds a valid parsed key
77};
78static SshRsaCtx s_rsa;
79
80// Load the NVS DER blob, parse it once into the cached signer context, and extract n and e into
81// ssh_host_pubkey. Idempotent (re-parses / frees any previously loaded key), so calling it again reloads.
83{
84 if (!s_rsa.lock)
85 s_rsa.lock = xSemaphoreCreateMutex();
86
87 Preferences prefs;
88 if (!prefs.begin("ssh_host_key", true))
89 return -1;
90
91 uint8_t der[SSH_RSA_KEY_DER_MAX];
92 size_t der_len = prefs.getBytesLength("priv_der");
93 if (der_len == 0 || der_len > SSH_RSA_KEY_DER_MAX)
94 {
95 prefs.end();
96 return -1;
97 }
98 prefs.getBytes("priv_der", der, der_len);
99 prefs.end();
100
101 // (Re)parse into the persistent context. Free any prior key first.
102 if (s_rsa.ready)
103 {
104 mbedtls_pk_free(&s_rsa.pk);
105 s_rsa.ready = false;
106 }
107 mbedtls_pk_init(&s_rsa.pk);
108 int rc = mbedtls_pk_parse_key(&s_rsa.pk, der, der_len, nullptr, 0
109#if MBEDTLS_VERSION_MAJOR >= 3
110 ,
111 ssh_mbedtls_rng, nullptr
112#endif
113 );
114 // Wipe the DER stack buffer whether or not parse succeeded; the parsed key stays in s_rsa.pk.
115 ssh_wipe(der, der_len);
116
117 if (rc != 0)
118 {
119 mbedtls_pk_free(&s_rsa.pk);
120 return -1;
121 }
122
123 mbedtls_rsa_context *rsa = mbedtls_pk_rsa(s_rsa.pk);
124 if (mbedtls_rsa_get_len(rsa) != SSH_RSA_KEY_BYTES)
125 {
126 mbedtls_pk_free(&s_rsa.pk);
127 return -1;
128 }
129
130 // Write n and e into the public-only BSS struct.
131 mbedtls_mpi n_mpi;
132 mbedtls_mpi e_mpi;
133 mbedtls_mpi_init(&n_mpi);
134 mbedtls_mpi_init(&e_mpi);
135 mbedtls_rsa_export(rsa, &n_mpi, nullptr, nullptr, nullptr, &e_mpi);
136 mbedtls_mpi_write_binary(&n_mpi, ssh_host_pubkey.n, SSH_RSA_KEY_BYTES);
137 mbedtls_mpi_write_binary(&e_mpi, ssh_host_pubkey.e_bytes + 4 - sizeof(ssh_host_pubkey.e_bytes),
138 sizeof(ssh_host_pubkey.e_bytes));
139 mbedtls_mpi_free(&n_mpi);
140 mbedtls_mpi_free(&e_mpi);
141
142 s_rsa.ready = true;
143 ssh_host_pubkey.loaded = true;
144 return 0;
145}
146
147int ssh_rsa_sign(const uint8_t *msg, size_t msg_len, SshRsaHash hash, uint8_t sig[SSH_RSA_SIG_BYTES])
148{
149 // Reuse the key parsed once at startup. Lazy-load as a fallback if the sketch never did (e.g. a sign
150 // before ssh_rsa_load_pubkey()); load runs single-threaded at boot so first-use here is the edge case.
151 if (!s_rsa.ready && ssh_rsa_load_pubkey() != 0)
152 return -1;
153
154 // Hash the message, then sign the digest. mbedtls_pk_sign() does NOT hash its input - it PKCS#1-pads
155 // the supplied digest - so for rsa-sha2-256/512 (RFC 8332) we pass SHA-256(msg) / SHA-512(msg), not msg.
156 const bool sha512 = (hash == SshRsaHash::SHA512);
157 const mbedtls_md_type_t md = sha512 ? MBEDTLS_MD_SHA512 : MBEDTLS_MD_SHA256;
158 const size_t dlen = sha512 ? SSH_SHA512_DIGEST_LEN : SSH_SHA256_DIGEST_LEN;
159 uint8_t digest[SSH_SHA512_DIGEST_LEN];
160 if (sha512)
161 ssh_sha512(msg, msg_len, digest);
162 else
163 ssh_sha256(msg, msg_len, digest);
164
165 // Serialise: mbedtls mutates the context's blinding state (Vf/Vi) on each private op.
166 if (s_rsa.lock)
167 xSemaphoreTake(s_rsa.lock, portMAX_DELAY);
168 size_t sig_len = 0;
169#if MBEDTLS_VERSION_MAJOR >= 3
170 int rc = mbedtls_pk_sign(&s_rsa.pk, md, digest, dlen, sig, SSH_RSA_SIG_BYTES, &sig_len, ssh_mbedtls_rng, nullptr);
171#else
172 int rc = mbedtls_pk_sign(&s_rsa.pk, md, digest, dlen, sig, &sig_len, ssh_mbedtls_rng, nullptr);
173#endif
174 if (s_rsa.lock)
175 xSemaphoreGive(s_rsa.lock);
176 ssh_wipe(digest, sizeof(digest));
177
178 return (rc == 0 && sig_len == SSH_RSA_SIG_BYTES) ? 0 : -1;
179}
180
181int 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,
182 const uint8_t *sig, size_t sig_len, SshRsaHash hash)
183{
184 if (sig_len != SSH_RSA_KEY_BYTES)
185 return -1;
186
187 mbedtls_rsa_context rsa;
188#if MBEDTLS_VERSION_MAJOR >= 3
189 mbedtls_rsa_init(&rsa);
190#else
191 mbedtls_rsa_init(&rsa, MBEDTLS_RSA_PKCS_V15, 0);
192#endif
193
194 mbedtls_mpi N;
195 mbedtls_mpi E;
196 mbedtls_mpi_init(&N);
197 mbedtls_mpi_init(&E);
198 mbedtls_mpi_read_binary(&N, n_be, SSH_RSA_KEY_BYTES);
199 mbedtls_mpi_read_binary(&E, e_be4, 4);
200
201 int rc = mbedtls_rsa_import(&rsa, &N, nullptr, nullptr, nullptr, &E);
202 if (rc == 0)
203 rc = mbedtls_rsa_complete(&rsa);
204
205 const bool sha512 = (hash == SshRsaHash::SHA512);
206 const mbedtls_md_type_t md = sha512 ? MBEDTLS_MD_SHA512 : MBEDTLS_MD_SHA256;
207 const size_t dlen = sha512 ? SSH_SHA512_DIGEST_LEN : SSH_SHA256_DIGEST_LEN;
208 uint8_t digest[SSH_SHA512_DIGEST_LEN];
209 if (rc == 0)
210 {
211 if (sha512)
212 ssh_sha512(msg, msg_len, digest);
213 else
214 ssh_sha256(msg, msg_len, digest);
215#if MBEDTLS_VERSION_MAJOR >= 3
216 rc = mbedtls_rsa_pkcs1_verify(&rsa, md, dlen, digest, sig);
217#else
218 rc = mbedtls_rsa_pkcs1_verify(&rsa, nullptr, nullptr, MBEDTLS_RSA_PUBLIC, md, dlen, digest, sig);
219#endif
220 }
221
222 mbedtls_mpi_free(&N);
223 mbedtls_mpi_free(&E);
224 mbedtls_rsa_free(&rsa);
225 return rc == 0 ? 0 : -1;
226}
227
228// ---------------------------------------------------------------------------
229// Native - software RSA path (test-only; private key injected by fixture)
230// ---------------------------------------------------------------------------
231
232#else
233
234// The native test fixture sets these before calling ssh_rsa_sign().
235// They are plain arrays (no pointers to heap); the test is responsible for
236// wiping them after the test if desired.
237uint8_t _test_rsa_n[SSH_RSA_KEY_BYTES];
238uint8_t _test_rsa_d[SSH_RSA_KEY_BYTES];
239uint8_t _test_rsa_e[4];
240
241int ssh_rsa_load_pubkey(void)
242{
243 memcpy(ssh_host_pubkey.n, _test_rsa_n, SSH_RSA_KEY_BYTES);
244 memcpy(ssh_host_pubkey.e_bytes, _test_rsa_e, 4);
245 ssh_host_pubkey.loaded = true;
246 return 0;
247}
248
249// ---------------------------------------------------------------------------
250// PKCS#1 v1.5 pad-and-encode (software; Arduino delegates padding to mbedtls)
251// ---------------------------------------------------------------------------
252
253// Hash msg with the selected algorithm and return the matching DigestInfo.
254// digest must be >= SSH_SHA512_DIGEST_LEN bytes.
255static void rsa_digest(const uint8_t *msg, size_t msg_len, SshRsaHash hash, uint8_t digest[SSH_SHA512_DIGEST_LEN],
256 size_t *digest_len, const uint8_t **di, size_t *di_len)
257{
258 if (hash == SshRsaHash::SHA512)
259 {
260 ssh_sha512(msg, msg_len, digest);
261 *digest_len = SSH_SHA512_DIGEST_LEN;
263 *di_len = SSH_PKCS1_SHA512_DIGESTINFO_LEN;
264 }
265 else
266 {
267 ssh_sha256(msg, msg_len, digest);
268 *digest_len = SSH_SHA256_DIGEST_LEN;
270 *di_len = SSH_PKCS1_DIGESTINFO_LEN;
271 }
272}
273
274// Builds the 256-byte padded message:
275// 0x00 0x01 [pad × 0xFF] 0x00 [DigestInfo] [digest]
276// pad = 256 - 3 - di_len - digest_len (202 for SHA-256, 170 for SHA-512).
277static void pkcs1v15_encode(const uint8_t *digest, size_t digest_len, const uint8_t *di, size_t di_len,
278 uint8_t em[SSH_RSA_KEY_BYTES])
279{
280 const size_t total = di_len + digest_len;
281 const size_t pad_len = SSH_RSA_KEY_BYTES - 3 - total;
282 em[0] = 0x00;
283 em[1] = 0x01;
284 memset(em + 2, 0xFF, pad_len);
285 em[2 + pad_len] = 0x00;
286 memcpy(em + 3 + pad_len, di, di_len);
287 memcpy(em + 3 + pad_len + di_len, digest, digest_len);
288}
289
290// ---------------------------------------------------------------------------
291// Native full-width modular arithmetic.
292// These helpers back both the private signing operation (s = em^d mod n) and
293// the public verify operation (s^e mod n). Schoolbook multiply + bit-serial
294// reduction: correct and full-width (no truncation), but NOT constant-time -
295// the native path is test-only (ESP32/mbedTLS is the real one).
296// ---------------------------------------------------------------------------
297
298// Full 128-limb product of two 64-limb little-endian integers.
299static void bn_mul_full(const uint32_t a[SSH_BN_LIMBS], const uint32_t b[SSH_BN_LIMBS], uint32_t p[2 * SSH_BN_LIMBS])
300{
301 for (int k = 0; k < 2 * SSH_BN_LIMBS; k++)
302 p[k] = 0;
303 for (int i = 0; i < SSH_BN_LIMBS; i++)
304 {
305 uint64_t carry = 0;
306 for (int j = 0; j < SSH_BN_LIMBS; j++)
307 {
308 uint64_t cur = (uint64_t)p[i + j] + (uint64_t)a[i] * b[j] + carry;
309 p[i + j] = (uint32_t)cur;
310 carry = cur >> 32;
311 }
312 // Propagate the final carry into the upper half.
313 int k = i + SSH_BN_LIMBS;
314 while (carry && k < 2 * SSH_BN_LIMBS)
315 {
316 uint64_t cur = (uint64_t)p[k] + carry;
317 p[k] = (uint32_t)cur;
318 carry = cur >> 32;
319 k++;
320 }
321 }
322}
323
324// Reduce a 128-limb value mod a 64-limb modulus, bit-serial. out = p mod m.
325static void bn_reduce_full(const uint32_t p[2 * SSH_BN_LIMBS], const uint32_t m[SSH_BN_LIMBS],
326 uint32_t out[SSH_BN_LIMBS])
327{
328 uint32_t r[SSH_BN_LIMBS + 1];
329 for (int k = 0; k <= SSH_BN_LIMBS; k++)
330 r[k] = 0;
331
332 for (int bit = 2 * SSH_BN_LIMBS * 32 - 1; bit >= 0; bit--)
333 {
334 // r <<= 1
335 uint32_t carry = 0;
336 for (int k = 0; k <= SSH_BN_LIMBS; k++)
337 {
338 uint32_t nc = r[k] >> 31;
339 r[k] = (r[k] << 1) | carry;
340 carry = nc;
341 }
342 // bring in the next bit of p
343 r[0] |= (p[bit >> 5] >> (bit & 31)) & 1u;
344
345 // if r >= m, subtract m. r has one guard limb (r[SSH_BN_LIMBS]).
346 bool ge = r[SSH_BN_LIMBS] != 0;
347 if (!ge)
348 {
349 ge = true; // assume equal/greater until a limb says otherwise
350 for (int k = SSH_BN_LIMBS - 1; k >= 0; k--)
351 {
352 if (r[k] != m[k])
353 {
354 ge = (r[k] > m[k]);
355 break;
356 }
357 }
358 }
359 if (ge)
360 {
361 uint64_t borrow = 0;
362 for (int k = 0; k < SSH_BN_LIMBS; k++)
363 {
364 uint64_t v = (uint64_t)r[k] - m[k] - borrow;
365 r[k] = (uint32_t)v;
366 borrow = (v >> 32) & 1u;
367 }
368 r[SSH_BN_LIMBS] -= (uint32_t)borrow;
369 }
370 }
371 for (int k = 0; k < SSH_BN_LIMBS; k++)
372 out[k] = r[k];
373}
374
375// out = base^e mod n, e a small public exponent.
376static void bn_modexp_pub(const SshBigNum *base, uint32_t e, const SshBigNum *n, SshBigNum *out)
377{
378 uint32_t prod[2 * SSH_BN_LIMBS];
379
380 // Reduce the base mod n (signatures are < n, but be safe).
381 SshBigNum b;
382 for (int k = 0; k < SSH_BN_LIMBS; k++)
383 {
384 prod[k] = base->d[k];
385 prod[k + SSH_BN_LIMBS] = 0;
386 }
387 bn_reduce_full(prod, n->d, b.d);
388
389 SshBigNum r;
390 memset(r.d, 0, sizeof(r.d));
391 r.d[0] = 1; // r = 1
392
393 int top = 31;
394 while (top >= 0 && !((e >> top) & 1u))
395 top--;
396 for (int i = top; i >= 0; i--)
397 {
398 bn_mul_full(r.d, r.d, prod); // r = r^2 mod n
399 bn_reduce_full(prod, n->d, r.d);
400 if ((e >> i) & 1u)
401 {
402 bn_mul_full(r.d, b.d, prod); // r = r*base mod n
403 bn_reduce_full(prod, n->d, r.d);
404 }
405 }
406 *out = r;
407}
408
409// out = base^exp mod n, exp a full-width 2048-bit private exponent.
410// Left-to-right square-and-multiply over every bit of exp (MSB to LSB,
411// skipping leading zero limbs/bits). Same helpers as the public path, so the
412// reduction is full-width and correct for any d (not just d=1).
413static void bn_modexp_full(const SshBigNum *base, const SshBigNum *exp, const SshBigNum *n, SshBigNum *out)
414{
415 uint32_t prod[2 * SSH_BN_LIMBS];
416
417 // Reduce the base mod n up front.
418 SshBigNum b;
419 for (int k = 0; k < SSH_BN_LIMBS; k++)
420 {
421 prod[k] = base->d[k];
422 prod[k + SSH_BN_LIMBS] = 0;
423 }
424 bn_reduce_full(prod, n->d, b.d);
425
426 SshBigNum r;
427 memset(r.d, 0, sizeof(r.d));
428 r.d[0] = 1; // r = 1
429
430 // Locate the most-significant set bit of exp.
431 int top_limb = SSH_BN_LIMBS - 1;
432 while (top_limb >= 0 && exp->d[top_limb] == 0)
433 top_limb--;
434 if (top_limb < 0)
435 {
436 *out = r; // exp == 0 -> result is 1
437 return;
438 }
439 int top_bit = 31;
440 while (top_bit >= 0 && !((exp->d[top_limb] >> top_bit) & 1u))
441 top_bit--;
442
443 for (int limb = top_limb; limb >= 0; limb--)
444 {
445 int start = (limb == top_limb) ? top_bit : 31;
446 for (int bit = start; bit >= 0; bit--)
447 {
448 bn_mul_full(r.d, r.d, prod); // r = r^2 mod n
449 bn_reduce_full(prod, n->d, r.d);
450 if ((exp->d[limb] >> bit) & 1u)
451 {
452 bn_mul_full(r.d, b.d, prod); // r = r*base mod n
453 bn_reduce_full(prod, n->d, r.d);
454 }
455 }
456 }
457 *out = r;
458}
459
460int ssh_rsa_sign(const uint8_t *msg, size_t msg_len, SshRsaHash hash, uint8_t sig[SSH_RSA_SIG_BYTES])
461{
462 // SECURITY: private key lives only in this stack frame.
463 SshRsaPrivKey priv;
464 memcpy(priv.n, _test_rsa_n, SSH_RSA_KEY_BYTES);
465 memcpy(priv.d, _test_rsa_d, SSH_RSA_KEY_BYTES);
466 memcpy(priv.e_bytes, _test_rsa_e, 4);
467
468 // 1. SHA-256/512 digest of the message + matching DigestInfo.
469 uint8_t digest[SSH_SHA512_DIGEST_LEN];
470 size_t digest_len = 0;
471 const uint8_t *di = nullptr;
472 size_t di_len = 0;
473 rsa_digest(msg, msg_len, hash, digest, &digest_len, &di, &di_len);
474
475 // 2. PKCS#1 v1.5 encode: 0x00 0x01 0xFF... 0x00 DigestInfo digest
476 uint8_t em[SSH_RSA_KEY_BYTES];
477 pkcs1v15_encode(digest, digest_len, di, di_len, em);
478 ssh_wipe(digest, sizeof(digest));
479
480 // 3. RSA private-key operation: s = em^d mod n (full-width, correct for
481 // any private exponent - no longer a d=1 stub).
482 SshBigNum n_bn;
483 SshBigNum d_bn;
484 SshBigNum m_bn;
485 SshBigNum s_bn;
486 bn_from_bytes(&n_bn, priv.n, SSH_RSA_KEY_BYTES);
487 bn_from_bytes(&d_bn, priv.d, SSH_RSA_KEY_BYTES);
489 ssh_wipe(em, sizeof(em));
490
491 bn_modexp_full(&m_bn, &d_bn, &n_bn, &s_bn);
492
493 // Write result as big-endian signature.
494 bn_to_bytes(sig, &s_bn);
495
496 // Wipe all sensitive stack material.
497 ssh_wipe(&priv, sizeof(priv));
498 ssh_wipe(&n_bn, sizeof(n_bn));
499 ssh_wipe(&d_bn, sizeof(d_bn));
500 ssh_wipe(&m_bn, sizeof(m_bn));
501 ssh_wipe(&s_bn, sizeof(s_bn));
502
503 return 0;
504}
505
506int 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,
507 const uint8_t *sig, size_t sig_len, SshRsaHash hash)
508{
509 if (sig_len != SSH_RSA_KEY_BYTES)
510 return -1;
511
512 SshBigNum n;
513 SshBigNum s;
514 SshBigNum m;
517 if (bn_cmp(&s, &n) >= 0)
518 return -1; // signature must be reduced mod n
519
520 uint32_t e = ((uint32_t)e_be4[0] << 24) | ((uint32_t)e_be4[1] << 16) | ((uint32_t)e_be4[2] << 8) | e_be4[3];
521 bn_modexp_pub(&s, e, &n, &m);
522
523 uint8_t em[SSH_RSA_KEY_BYTES];
524 bn_to_bytes(em, &m);
525
526 // Recompute the expected PKCS#1 v1.5 block and compare in constant time.
527 uint8_t digest[SSH_SHA512_DIGEST_LEN];
528 size_t digest_len = 0;
529 const uint8_t *di = nullptr;
530 size_t di_len = 0;
531 rsa_digest(msg, msg_len, hash, digest, &digest_len, &di, &di_len);
532 uint8_t expected[SSH_RSA_KEY_BYTES];
533 pkcs1v15_encode(digest, digest_len, di, di_len, expected);
534
535 uint8_t diff = 0;
536 for (size_t k = 0; k < SSH_RSA_KEY_BYTES; k++)
537 diff |= (uint8_t)(em[k] ^ expected[k]);
538 return diff == 0 ? 0 : -1;
539}
540
541#endif // ARDUINO
542
543// ---------------------------------------------------------------------------
544// Public-key blob serialization (both platforms)
545// ---------------------------------------------------------------------------
546
547// Write a 4-byte big-endian uint32 to p and advance p by 4.
548static uint8_t *put_u32(uint8_t *p, uint32_t v)
549{
550 p[0] = (uint8_t)(v >> 24);
551 p[1] = (uint8_t)(v >> 16);
552 p[2] = (uint8_t)(v >> 8);
553 p[3] = (uint8_t)(v);
554 return p + 4;
555}
556
557// Write an SSH mpint (4-byte length + optional 0x00 prefix + data).
558// data is big-endian, data_len bytes. Returns pointer past the written data.
559static uint8_t *put_mpint(uint8_t *p, const uint8_t *data, size_t data_len)
560{
561 // Skip leading zeros to find first non-zero byte.
562 size_t off = 0;
563 while (off < data_len && data[off] == 0)
564 off++;
565 const uint8_t *src = data + off;
566 size_t src_len = data_len - off;
567 bool need_pad = (src_len > 0) && (src[0] & 0x80u);
568 uint32_t mpint_len = (uint32_t)src_len + (need_pad ? 1u : 0u);
569 p = put_u32(p, mpint_len);
570 if (need_pad)
571 *p++ = 0x00;
572 memcpy(p, src, src_len);
573 return p + src_len;
574}
575
576int ssh_rsa_encode_pubkey(uint8_t *out, size_t *out_len, size_t out_cap)
577{
579 return -1;
580
581 // Conservative upper bound already defined: SSH_RSA_PUBKEY_BLOB_MAX
582 if (out_cap < SSH_RSA_PUBKEY_BLOB_MAX)
583 return -1;
584
585 const char *alg = SSH_RSA_PUBKEY_ALG; // "ssh-rsa" (RFC 8332 §3)
586 size_t alg_len = SSH_RSA_PUBKEY_ALG_LEN;
587
588 uint8_t *p = out;
589
590 // uint32 len(alg) + alg bytes
591 p = put_u32(p, (uint32_t)alg_len);
592 memcpy(p, alg, alg_len);
593 p += alg_len;
594
595 // mpint e
596 p = put_mpint(p, ssh_host_pubkey.e_bytes, sizeof(ssh_host_pubkey.e_bytes));
597
598 // mpint n
599 p = put_mpint(p, ssh_host_pubkey.n, SSH_RSA_KEY_BYTES);
600
601 *out_len = (size_t)(p - out);
602 return 0;
603}
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.
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.
#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.
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
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
int ssh_rsa_load_pubkey(void)
Load the public portion of the RSA host key into ssh_host_pubkey.
Definition ssh_rsa.cpp:82
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
RSA-SHA2-256/512 host-key signing and public-key serialization.
#define SSH_RSA_PUBKEY_ALG_LEN
Length of SSH_RSA_PUBKEY_ALG ("ssh-rsa" = 7 bytes).
Definition ssh_rsa.h:137
#define SSH_RSA_KEY_DER_MAX
Maximum DER size for a PKCS#1 RSAPrivateKey with 2048-bit fields.
Definition ssh_rsa.h:116
SshRsaHash
Hash algorithm selecting the RSA signature scheme (RFC 8332).
Definition ssh_rsa.h:106
@ SHA512
rsa-sha2-512
#define SSH_RSA_SIG_BYTES
PKCS#1 v1.5 signature size for RSA-2048 in bytes.
Definition ssh_rsa.h:122
#define SSH_RSA_KEY_BYTES
RSA modulus and private exponent size in bytes (RSA-2048).
Definition ssh_rsa.h:119
#define SSH_RSA_PUBKEY_BLOB_MAX
Maximum byte length of the serialized RSA public key blob.
Definition ssh_rsa.h:301
#define SSH_RSA_PUBKEY_ALG
Key-blob type string for an RSA host key.
Definition ssh_rsa.h:134
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.
#define SSH_SHA256_DIGEST_LEN
SHA-256 digest length in bytes.
Definition ssh_sha256.h:29
void ssh_sha512(const uint8_t *data, size_t len, uint8_t digest[SSH_SHA512_DIGEST_LEN])
One-shot SHA-512: hash len bytes of data into digest (64 bytes).
#define SSH_SHA512_DIGEST_LEN
SHA-512 digest length in bytes.
Definition ssh_sha512.h:24
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
SemaphoreHandle_t lock
serialises signs on the shared context
Definition ssh_rsa.cpp:75
bool ready
pk holds a valid parsed key
Definition ssh_rsa.cpp:76
mbedtls_pk_context pk
parsed host key + cached blinding state
Definition ssh_rsa.cpp:74
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