ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
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 SSH RSA host-key layer: NVS/fixture host key, host-key signing, "ssh-rsa" blob (see ssh_rsa.h).
7 *
8 * The RSASSA-PKCS1-v1.5 math lives in crypto/rsa; this file owns the SSH host key and calls into it.
9 */
10
13#include "crypto/hash/sha256.h"
14#include "crypto/hash/sha512.h"
16#include "server/mmgr/secure.h"
17#include <string.h>
18
19// Public host key (BSS - no secret material).
20#if defined(ARDUINO)
21#include <Preferences.h> // ESP-IDF NVS wrapper
22#include <esp_random.h> // esp_fill_random() for the RSA blinding RNG
23#include <freertos/FreeRTOS.h>
24#include <freertos/semphr.h> // serialize signs on the shared cached key context
25#include <mbedtls/md.h>
26#include <mbedtls/pk.h>
27#include <mbedtls/rsa.h>
28#endif
30
31#ifdef ARDUINO
32
33// ---------------------------------------------------------------------------
34// Arduino - cached mbedtls host-key signer (NVS-backed)
35// ---------------------------------------------------------------------------
36
37// RNG callback for mbedtls private-key operations (mbedtls v3 requires a real f_rng for RSA blinding).
38static int ssh_mbedtls_rng(void *ctx, unsigned char *buf, size_t len)
39{
40 (void)ctx;
41 esp_fill_random(buf, len);
42 return 0;
43}
44
45// Cached RSA host-key signer. Re-parsing the PKCS#8 key per handshake also re-ran mbedtls's first-use
46// blinding setup (~170 ms wasted per sign); the parsed context caches the blinding state, so keeping it
47// resident means each sign pays only the CRT modexp. The private key stays in RAM for the server
48// lifetime (as an SSH host key normally does); the mutex serializes signs because mbedtls mutates the
49// blinding values per operation. Loaded once at startup by pc_ssh_rsa_load_pubkey().
51{
52 mbedtls_pk_context pk; ///< parsed host key + cached blinding state
53 SemaphoreHandle_t lock; ///< serializes signs on the shared context
54 bool ready; ///< pk holds a valid parsed key
55};
56static SshRsaCtx s_rsa;
57
59{
60 if (!s_rsa.lock)
61 {
62 s_rsa.lock = xSemaphoreCreateMutex();
63 }
64
65 Preferences prefs;
66 if (!prefs.begin("ssh_host_key", true))
67 {
68 return -1;
69 }
70
71 uint8_t der[SSH_RSA_KEY_DER_MAX];
72 size_t der_len = prefs.getBytesLength("priv_der");
73 if (der_len == 0 || der_len > SSH_RSA_KEY_DER_MAX)
74 {
75 prefs.end();
76 return -1;
77 }
78 prefs.getBytes("priv_der", der, der_len);
79 prefs.end();
80
81 // (Re)parse into the persistent context. Free any prior key first.
82 if (s_rsa.ready)
83 {
84 mbedtls_pk_free(&s_rsa.pk);
85 s_rsa.ready = false;
86 }
87 mbedtls_pk_init(&s_rsa.pk);
88 int rc = mbedtls_pk_parse_key(&s_rsa.pk, der, der_len, nullptr, 0
89#if MBEDTLS_VERSION_MAJOR >= 3
90 ,
91 ssh_mbedtls_rng, nullptr
92#endif
93 );
94 pc_secure_wipe(der, der_len);
95
96 if (rc != 0)
97 {
98 mbedtls_pk_free(&s_rsa.pk);
99 return -1;
100 }
101
102 mbedtls_rsa_context *rsa = mbedtls_pk_rsa(s_rsa.pk);
103 if (mbedtls_rsa_get_len(rsa) != PC_RSA_KEY_BYTES)
104 {
105 mbedtls_pk_free(&s_rsa.pk);
106 return -1;
107 }
108
109 // Write n and e into the public-only BSS struct.
110 mbedtls_mpi n_mpi;
111 mbedtls_mpi e_mpi;
112 mbedtls_mpi_init(&n_mpi);
113 mbedtls_mpi_init(&e_mpi);
114 mbedtls_rsa_export(rsa, &n_mpi, nullptr, nullptr, nullptr, &e_mpi);
115 mbedtls_mpi_write_binary(&n_mpi, ssh_host_pubkey.n, PC_RSA_KEY_BYTES);
116 mbedtls_mpi_write_binary(&e_mpi, ssh_host_pubkey.e_bytes + 4 - sizeof(ssh_host_pubkey.e_bytes),
117 sizeof(ssh_host_pubkey.e_bytes));
118 mbedtls_mpi_free(&n_mpi);
119 mbedtls_mpi_free(&e_mpi);
120
121 s_rsa.ready = true;
122 ssh_host_pubkey.loaded = true;
123 return 0;
124}
125
126int ssh_rsa_sign(const uint8_t *msg, size_t msg_len, pc_rsa_hash hash, uint8_t sig[PC_RSA_SIG_BYTES])
127{
128 // Reuse the key parsed once at startup; lazy-load as a fallback if the sketch never did.
129 if (!s_rsa.ready && pc_ssh_rsa_load_pubkey() != 0)
130 {
131 return -1;
132 }
133
134 // mbedtls_pk_sign() PKCS#1-pads the supplied digest (it does NOT hash), so for rsa-sha2-256/512 we
135 // pass SHA-256(msg) / SHA-512(msg).
136 const bool sha512 = (hash == pc_rsa_hash::SHA512);
137 const mbedtls_md_type_t md = sha512 ? MBEDTLS_MD_SHA512 : MBEDTLS_MD_SHA256;
138 const size_t dlen = sha512 ? PC_SHA512_DIGEST_LEN : PC_SHA256_DIGEST_LEN;
139 uint8_t digest[PC_SHA512_DIGEST_LEN];
140 if (sha512)
141 {
142 pc_sha512(msg, msg_len, digest);
143 }
144 else
145 {
146 pc_sha256(msg, msg_len, digest);
147 }
148
149 // Serialize: mbedtls mutates the context's blinding state on each private op.
150 if (s_rsa.lock)
151 {
152 xSemaphoreTake(s_rsa.lock, portMAX_DELAY);
153 }
154 size_t sig_len = 0;
155#if MBEDTLS_VERSION_MAJOR >= 3
156 int rc = mbedtls_pk_sign(&s_rsa.pk, md, digest, dlen, sig, PC_RSA_SIG_BYTES, &sig_len, ssh_mbedtls_rng, nullptr);
157#else
158 int rc = mbedtls_pk_sign(&s_rsa.pk, md, digest, dlen, sig, &sig_len, ssh_mbedtls_rng, nullptr);
159#endif
160 if (s_rsa.lock)
161 {
162 xSemaphoreGive(s_rsa.lock);
163 }
164 pc_secure_wipe(digest, sizeof(digest));
165
166 return (rc == 0 && sig_len == PC_RSA_SIG_BYTES) ? 0 : -1;
167}
168
169#else
170
171// ---------------------------------------------------------------------------
172// Native - test fixture host key; signing delegates to the crypto/rsa software path.
173// ---------------------------------------------------------------------------
174
175// The native test fixture sets these before calling ssh_rsa_sign(); plain arrays, test-owned.
176uint8_t _test_rsa_n[PC_RSA_KEY_BYTES];
177uint8_t _test_rsa_d[PC_RSA_KEY_BYTES];
178uint8_t _test_rsa_e[4];
179
181{
182 memcpy(ssh_host_pubkey.n, _test_rsa_n, PC_RSA_KEY_BYTES);
183 memcpy(ssh_host_pubkey.e_bytes, _test_rsa_e, 4);
184 ssh_host_pubkey.loaded = true;
185 return 0;
186}
187
188int ssh_rsa_sign(const uint8_t *msg, size_t msg_len, pc_rsa_hash hash, uint8_t sig[PC_RSA_SIG_BYTES])
189{
190 return pc_rsa_sign_sw(_test_rsa_n, _test_rsa_d, msg, msg_len, hash, sig);
191}
192
193#endif // ARDUINO
194
195// ---------------------------------------------------------------------------
196// "ssh-rsa" public-key blob serialization (both platforms)
197// ---------------------------------------------------------------------------
198
199// Write a 4-byte big-endian uint32 to p and advance p by 4.
200static uint8_t *put_u32(uint8_t *p, uint32_t v)
201{
202 p[0] = (uint8_t)(v >> 24);
203 p[1] = (uint8_t)(v >> 16);
204 p[2] = (uint8_t)(v >> 8);
205 p[3] = (uint8_t)(v);
206 return p + 4;
207}
208
209// Write an SSH mpint (4-byte length + optional 0x00 prefix + data). data is big-endian, data_len bytes.
210static uint8_t *put_mpint(uint8_t *p, const uint8_t *data, size_t data_len)
211{
212 size_t off = 0;
213 while (off < data_len && data[off] == 0)
214 {
215 off++;
216 }
217 const uint8_t *src = data + off;
218 size_t src_len = data_len - off;
219 bool need_pad = (src_len > 0) && (src[0] & 0x80u);
220 uint32_t mpint_len = (uint32_t)src_len + (need_pad ? 1u : 0u);
221 p = put_u32(p, mpint_len);
222 if (need_pad)
223 {
224 *p++ = 0x00;
225 }
226 memcpy(p, src, src_len);
227 return p + src_len;
228}
229
230int ssh_rsa_encode_pubkey(uint8_t *out, size_t *out_len, size_t out_cap)
231{
233 {
234 return -1;
235 }
236 if (out_cap < SSH_RSA_PUBKEY_BLOB_MAX)
237 {
238 return -1;
239 }
240
241 const char *alg = SSH_RSA_PUBKEY_ALG; // "ssh-rsa" (RFC 8332 §3)
242 size_t alg_len = SSH_RSA_PUBKEY_ALG_LEN;
243
244 uint8_t *p = out;
245 p = put_u32(p, (uint32_t)alg_len);
246 memcpy(p, alg, alg_len);
247 p += alg_len;
248 p = put_mpint(p, ssh_host_pubkey.e_bytes, sizeof(ssh_host_pubkey.e_bytes));
249 p = put_mpint(p, ssh_host_pubkey.n, PC_RSA_KEY_BYTES);
250
251 *out_len = (size_t)(p - out);
252 return 0;
253}
RSA-2048 PKCS#1 v1.5 signature primitive (RFC 8017) - verify + software sign.
pc_rsa_hash
Hash algorithm selecting the RSA signature scheme (RFC 8017 §9.2).
Definition rsa.h:39
@ SHA512
RSASSA-PKCS1-v1.5 with SHA-512.
#define PC_RSA_SIG_BYTES
PKCS#1 v1.5 signature size for RSA-2048 in bytes.
Definition rsa.h:31
#define PC_RSA_KEY_BYTES
RSA modulus / signature size in bytes (RSA-2048).
Definition rsa.h:28
Secure pool accessor - borrows that hold key material.
void pc_sha256(const uint8_t *data, size_t len, uint8_t digest[PC_SHA256_DIGEST_LEN])
One-shot SHA-256: hash len bytes of data into digest (32 bytes).
Definition sha256.cpp:58
SHA-256 (FIPS 180-4) - streaming context and one-shot API.
#define PC_SHA256_DIGEST_LEN
SHA-256 digest length in bytes.
Definition sha256.h:25
void pc_sha512(const uint8_t *data, size_t len, uint8_t digest[PC_SHA512_DIGEST_LEN])
One-shot SHA-512: hash len bytes of data into digest (64 bytes).
Definition sha512.cpp:57
SHA-512 (FIPS 180-4) - streaming context and one-shot API.
#define PC_SHA512_DIGEST_LEN
SHA-512 digest length in bytes.
Definition sha512.h:24
SSH session key material - types, pools, and security model.
int pc_ssh_rsa_load_pubkey(void)
Load the public portion of the RSA host key into ssh_host_pubkey.
Definition ssh_rsa.cpp:58
SshRsaPubKey ssh_host_pubkey
Static host public key (BSS). Set by pc_ssh_rsa_load_pubkey().
Definition ssh_rsa.cpp:29
int ssh_rsa_sign(const uint8_t *msg, size_t msg_len, pc_rsa_hash hash, uint8_t sig[PC_RSA_SIG_BYTES])
Sign msg with the RSA host key (PKCS#1 v1.5, rsa-sha2-256/512).
Definition ssh_rsa.cpp:126
int ssh_rsa_encode_pubkey(uint8_t *out, size_t *out_len, size_t out_cap)
Encode ssh_host_pubkey as the RFC 4253 §6.6 "ssh-rsa" public-key blob.
Definition ssh_rsa.cpp:230
SSH RSA host-key layer: NVS-backed host key, host-key signing, and "ssh-rsa" blob encoding.
#define SSH_RSA_PUBKEY_ALG_LEN
Length of SSH_RSA_PUBKEY_ALG ("ssh-rsa" = 7 bytes).
Definition ssh_rsa.h:51
#define SSH_RSA_KEY_DER_MAX
Maximum DER size for a PKCS#1 RSAPrivateKey with 2048-bit fields.
Definition ssh_rsa.h:38
#define SSH_RSA_PUBKEY_BLOB_MAX
Upper bound on the encoded "ssh-rsa" public-key blob (len+alg + mpint e + mpint n).
Definition ssh_rsa.h:77
#define SSH_RSA_PUBKEY_ALG
Key-blob type string for an RSA host key.
Definition ssh_rsa.h:48
SemaphoreHandle_t lock
serializes signs on the shared context
Definition ssh_rsa.cpp:53
bool ready
pk holds a valid parsed key
Definition ssh_rsa.cpp:54
mbedtls_pk_context pk
parsed host key + cached blinding state
Definition ssh_rsa.cpp:52
RSA-2048 public host key parameters. Allocated in BSS; contains only n and e.
Definition ssh_rsa.h:67
uint8_t n[PC_RSA_KEY_BYTES]
Modulus n (256 bytes, big-endian).
Definition ssh_rsa.h:68
bool loaded
True after pc_ssh_rsa_load_pubkey() succeeds.
Definition ssh_rsa.h:70
uint8_t e_bytes[4]
Public exponent e (big-endian uint32).
Definition ssh_rsa.h:69