ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
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 rsa.cpp
6 * @brief RSA-2048 PKCS#1 v1.5 verify + software sign (see rsa.h).
7 *
8 * Verify: mbedtls on Arduino (hardware/mbedTLS), software full-width modexp on native. Software sign
9 * (native) is the test reference path. Protocol-agnostic - raw big-endian key material in, no SSH.
10 */
11
13#include "crypto/crypto_opt.h"
14#include "crypto/crypto_scratch.h" // pc_secure_wipe (the canonical secure wipe)
15#include "crypto/hash/sha256.h"
16#include "crypto/hash/sha512.h"
17#include "server/mmgr/secure.h"
18#include <string.h>
19#ifdef ARDUINO
20#include <mbedtls/md.h>
21#include <mbedtls/rsa.h>
22#else
23#include "crypto/asymmetric/bignum.h" // native software RSA (test reference)
24#endif
26
27// ---------------------------------------------------------------------------
28// DigestInfo for SHA-256 / SHA-512 (PKCS#1 v1.5, RFC 8017 §9.2, RFC 5754)
29// ---------------------------------------------------------------------------
30
32 0x30, 0x31, // SEQUENCE, length 49
33 0x30, 0x0d, // SEQUENCE, length 13 (AlgorithmIdentifier)
34 0x06, 0x09, // OID, length 9
35 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, // OID 2.16.840.1.101.3.4.2.1
36 0x05, 0x00, // NULL parameters
37 0x04, 0x20 // OCTET STRING, length 32 (digest follows)
38};
39
41 0x30, 0x51, // SEQUENCE, length 81
42 0x30, 0x0d, // SEQUENCE, length 13 (AlgorithmIdentifier)
43 0x06, 0x09, // OID, length 9
44 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, // OID 2.16.840.1.101.3.4.2.3
45 0x05, 0x00, // NULL parameters
46 0x04, 0x40 // OCTET STRING, length 64 (digest follows)
47};
48
49#ifdef ARDUINO
50
51// ---------------------------------------------------------------------------
52// Arduino - mbedtls verify path
53// ---------------------------------------------------------------------------
54
55int pc_rsa_verify(const uint8_t n_be[PC_RSA_KEY_BYTES], const uint8_t e_be4[4], const uint8_t *msg, size_t msg_len,
56 const uint8_t *sig, size_t sig_len, pc_rsa_hash hash)
57{
58 if (sig_len != PC_RSA_KEY_BYTES)
59 {
60 return -1;
61 }
62
63 mbedtls_rsa_context rsa;
64#if MBEDTLS_VERSION_MAJOR >= 3
65 mbedtls_rsa_init(&rsa);
66#else
67 mbedtls_rsa_init(&rsa, MBEDTLS_RSA_PKCS_V15, 0);
68#endif
69
70 mbedtls_mpi N;
71 mbedtls_mpi E;
72 mbedtls_mpi_init(&N);
73 mbedtls_mpi_init(&E);
74 mbedtls_mpi_read_binary(&N, n_be, PC_RSA_KEY_BYTES);
75 mbedtls_mpi_read_binary(&E, e_be4, 4);
76
77 int rc = mbedtls_rsa_import(&rsa, &N, nullptr, nullptr, nullptr, &E);
78 if (rc == 0)
79 {
80 rc = mbedtls_rsa_complete(&rsa);
81 }
82
83 const bool sha512 = (hash == pc_rsa_hash::SHA512);
84 const mbedtls_md_type_t md = sha512 ? MBEDTLS_MD_SHA512 : MBEDTLS_MD_SHA256;
85 const size_t dlen = sha512 ? PC_SHA512_DIGEST_LEN : PC_SHA256_DIGEST_LEN;
86 uint8_t digest[PC_SHA512_DIGEST_LEN];
87 if (rc == 0)
88 {
89 if (sha512)
90 {
91 pc_sha512(msg, msg_len, digest);
92 }
93 else
94 {
95 pc_sha256(msg, msg_len, digest);
96 }
97#if MBEDTLS_VERSION_MAJOR >= 3
98 rc = mbedtls_rsa_pkcs1_verify(&rsa, md, dlen, digest, sig);
99#else
100 rc = mbedtls_rsa_pkcs1_verify(&rsa, nullptr, nullptr, MBEDTLS_RSA_PUBLIC, md, dlen, digest, sig);
101#endif
102 }
103
104 mbedtls_mpi_free(&N);
105 mbedtls_mpi_free(&E);
106 mbedtls_rsa_free(&rsa);
107 return rc == 0 ? 0 : -1;
108}
109
110#else
111
112// ---------------------------------------------------------------------------
113// Native - software RSA path (test reference; NOT constant-time)
114// ---------------------------------------------------------------------------
115
116// Hash msg with the selected algorithm and return the matching DigestInfo.
117// digest must be >= PC_SHA512_DIGEST_LEN bytes.
118static void rsa_digest(const uint8_t *msg, size_t msg_len, pc_rsa_hash hash, uint8_t digest[PC_SHA512_DIGEST_LEN],
119 size_t *digest_len, const uint8_t **di, size_t *di_len)
120{
121 if (hash == pc_rsa_hash::SHA512)
122 {
123 pc_sha512(msg, msg_len, digest);
124 *digest_len = PC_SHA512_DIGEST_LEN;
127 }
128 else
129 {
130 pc_sha256(msg, msg_len, digest);
131 *digest_len = PC_SHA256_DIGEST_LEN;
133 *di_len = PC_PKCS1_DIGESTINFO_LEN;
134 }
135}
136
137// Builds the 256-byte padded message:
138// 0x00 0x01 [pad × 0xFF] 0x00 [DigestInfo] [digest]
139static void pkcs1v15_encode(const uint8_t *digest, size_t digest_len, const uint8_t *di, size_t di_len,
140 uint8_t em[PC_RSA_KEY_BYTES])
141{
142 const size_t total = di_len + digest_len;
143 const size_t pad_len = PC_RSA_KEY_BYTES - 3 - total;
144 em[0] = 0x00;
145 em[1] = 0x01;
146 memset(em + 2, 0xFF, pad_len);
147 em[2 + pad_len] = 0x00;
148 memcpy(em + 3 + pad_len, di, di_len);
149 memcpy(em + 3 + pad_len + di_len, digest, digest_len);
150}
151
152// ---------------------------------------------------------------------------
153// Native full-width modular arithmetic (schoolbook multiply + bit-serial
154// reduction: full-width and correct, but NOT constant-time - native is test-only).
155// ---------------------------------------------------------------------------
156
157// Full 128-limb product of two 64-limb little-endian integers.
158static void bn_mul_full(const uint32_t a[PC_BN_LIMBS], const uint32_t b[PC_BN_LIMBS], uint32_t p[2 * PC_BN_LIMBS])
159{
160 for (int k = 0; k < 2 * PC_BN_LIMBS; k++)
161 {
162 p[k] = 0;
163 }
164 for (int i = 0; i < PC_BN_LIMBS; i++)
165 {
166 uint64_t carry = 0;
167 for (int j = 0; j < PC_BN_LIMBS; j++)
168 {
169 uint64_t cur = (uint64_t)p[i + j] + (uint64_t)a[i] * b[j] + carry;
170 p[i + j] = (uint32_t)cur;
171 carry = cur >> 32;
172 }
173 int k = i + PC_BN_LIMBS;
174 // a and b are both PC_BN_LIMBS (64) limbs, so their full product is bounded by 2*PC_BN_LIMBS
175 // (128) limbs; carry propagation out of the top half can never still be pending when k reaches
176 // 2*PC_BN_LIMBS. The "k < 2*PC_BN_LIMBS" half of this guard is defensive and provably unreachable.
177 while (carry && k < 2 * PC_BN_LIMBS) // GCOVR_EXCL_BR_LINE
178 {
179 uint64_t cur = (uint64_t)p[k] + carry;
180 p[k] = (uint32_t)cur;
181 carry = cur >> 32;
182 k++;
183 }
184 }
185}
186
187// Reduce a 128-limb value mod a 64-limb modulus, bit-serial. out = p mod m.
188static void bn_reduce_full(const uint32_t p[2 * PC_BN_LIMBS], const uint32_t m[PC_BN_LIMBS], uint32_t out[PC_BN_LIMBS])
189{
190 uint32_t r[PC_BN_LIMBS + 1];
191 for (int k = 0; k <= PC_BN_LIMBS; k++)
192 {
193 r[k] = 0;
194 }
195
196 for (int bit = 2 * PC_BN_LIMBS * 32 - 1; bit >= 0; bit--)
197 {
198 uint32_t carry = 0;
199 for (int k = 0; k <= PC_BN_LIMBS; k++)
200 {
201 uint32_t nc = r[k] >> 31;
202 r[k] = (r[k] << 1) | carry;
203 carry = nc;
204 }
205 r[0] |= (p[bit >> 5] >> (bit & 31)) & 1u;
206
207 bool ge = r[PC_BN_LIMBS] != 0;
208 if (!ge)
209 {
210 ge = true;
211 for (int k = PC_BN_LIMBS - 1; k >= 0; k--)
212 {
213 if (r[k] != m[k])
214 {
215 ge = (r[k] > m[k]);
216 break;
217 }
218 }
219 }
220 if (ge)
221 {
222 uint64_t borrow = 0;
223 for (int k = 0; k < PC_BN_LIMBS; k++)
224 {
225 uint64_t v = (uint64_t)r[k] - m[k] - borrow;
226 r[k] = (uint32_t)v;
227 borrow = (v >> 32) & 1u;
228 }
229 r[PC_BN_LIMBS] -= (uint32_t)borrow;
230 }
231 }
232 for (int k = 0; k < PC_BN_LIMBS; k++)
233 {
234 out[k] = r[k];
235 }
236}
237
238// out = base^e mod n, e a small public exponent.
239static void bn_modexp_pub(const pc_bignum *base, uint32_t e, const pc_bignum *n, pc_bignum *out)
240{
241 uint32_t prod[2 * PC_BN_LIMBS];
242
243 pc_bignum b;
244 for (int k = 0; k < PC_BN_LIMBS; k++)
245 {
246 prod[k] = base->d[k];
247 prod[k + PC_BN_LIMBS] = 0;
248 }
249 bn_reduce_full(prod, n->d, b.d);
250
251 pc_bignum r;
252 memset(r.d, 0, sizeof(r.d));
253 r.d[0] = 1; // r = 1
254
255 int top = 31;
256 while (top >= 0 && !((e >> top) & 1u))
257 {
258 top--;
259 }
260 for (int i = top; i >= 0; i--)
261 {
262 bn_mul_full(r.d, r.d, prod); // r = r^2 mod n
263 bn_reduce_full(prod, n->d, r.d);
264 if ((e >> i) & 1u)
265 {
266 bn_mul_full(r.d, b.d, prod); // r = r*base mod n
267 bn_reduce_full(prod, n->d, r.d);
268 }
269 }
270 *out = r;
271}
272
273// out = base^exp mod n, exp a full-width 2048-bit private exponent.
274static void bn_modexp_full(const pc_bignum *base, const pc_bignum *exp, const pc_bignum *n, pc_bignum *out)
275{
276 uint32_t prod[2 * PC_BN_LIMBS];
277
278 pc_bignum b;
279 for (int k = 0; k < PC_BN_LIMBS; k++)
280 {
281 prod[k] = base->d[k];
282 prod[k + PC_BN_LIMBS] = 0;
283 }
284 bn_reduce_full(prod, n->d, b.d);
285
286 pc_bignum r;
287 memset(r.d, 0, sizeof(r.d));
288 r.d[0] = 1; // r = 1
289
290 int top_limb = PC_BN_LIMBS - 1;
291 while (top_limb >= 0 && exp->d[top_limb] == 0)
292 {
293 top_limb--;
294 }
295 if (top_limb < 0)
296 {
297 *out = r; // exp == 0 -> result is 1
298 return;
299 }
300 int top_bit = 31;
301 // exp->d[top_limb] != 0 by construction, so this scan finds a set bit before top_bit passes 0;
302 // the "top_bit >= 0" half of the guard is defensive, not reachable.
303 while (top_bit >= 0 && !((exp->d[top_limb] >> top_bit) & 1u)) // GCOVR_EXCL_BR_LINE
304 {
305 top_bit--;
306 }
307
308 for (int limb = top_limb; limb >= 0; limb--)
309 {
310 int start = (limb == top_limb) ? top_bit : 31;
311 for (int bit = start; bit >= 0; bit--)
312 {
313 bn_mul_full(r.d, r.d, prod); // r = r^2 mod n
314 bn_reduce_full(prod, n->d, r.d);
315 if ((exp->d[limb] >> bit) & 1u)
316 {
317 bn_mul_full(r.d, b.d, prod); // r = r*base mod n
318 bn_reduce_full(prod, n->d, r.d);
319 }
320 }
321 }
322 *out = r;
323}
324
325int pc_rsa_sign_sw(const uint8_t n_be[PC_RSA_KEY_BYTES], const uint8_t d_be[PC_RSA_KEY_BYTES], const uint8_t *msg,
326 size_t msg_len, pc_rsa_hash hash, uint8_t sig[PC_RSA_SIG_BYTES])
327{
328 // 1. SHA-256/512 digest of the message + matching DigestInfo.
329 uint8_t digest[PC_SHA512_DIGEST_LEN];
330 size_t digest_len = 0;
331 const uint8_t *di = nullptr;
332 size_t di_len = 0;
333 rsa_digest(msg, msg_len, hash, digest, &digest_len, &di, &di_len);
334
335 // 2. PKCS#1 v1.5 encode: 0x00 0x01 0xFF... 0x00 DigestInfo digest
336 uint8_t em[PC_RSA_KEY_BYTES];
337 pkcs1v15_encode(digest, digest_len, di, di_len, em);
338 pc_secure_wipe(digest, sizeof(digest));
339
340 // 3. RSA private-key operation: s = em^d mod n (full-width).
341 pc_bignum n_bn;
342 pc_bignum d_bn;
343 pc_bignum m_bn;
344 pc_bignum s_bn;
345 bn_from_bytes(&n_bn, n_be, PC_RSA_KEY_BYTES);
346 bn_from_bytes(&d_bn, d_be, PC_RSA_KEY_BYTES);
348 pc_secure_wipe(em, sizeof(em));
349
350 bn_modexp_full(&m_bn, &d_bn, &n_bn, &s_bn);
351
352 bn_to_bytes(sig, &s_bn);
353
354 pc_secure_wipe(&n_bn, sizeof(n_bn));
355 pc_secure_wipe(&d_bn, sizeof(d_bn));
356 pc_secure_wipe(&m_bn, sizeof(m_bn));
357 pc_secure_wipe(&s_bn, sizeof(s_bn));
358 return 0;
359}
360
361int pc_rsa_verify(const uint8_t n_be[PC_RSA_KEY_BYTES], const uint8_t e_be4[4], const uint8_t *msg, size_t msg_len,
362 const uint8_t *sig, size_t sig_len, pc_rsa_hash hash)
363{
364 if (sig_len != PC_RSA_KEY_BYTES)
365 {
366 return -1;
367 }
368
369 pc_bignum n;
370 pc_bignum s;
371 pc_bignum m;
374 if (bn_cmp(&s, &n) >= 0)
375 {
376 return -1; // signature must be reduced mod n
377 }
378
379 uint32_t e = ((uint32_t)e_be4[0] << 24) | ((uint32_t)e_be4[1] << 16) | ((uint32_t)e_be4[2] << 8) | e_be4[3];
380 bn_modexp_pub(&s, e, &n, &m);
381
382 uint8_t em[PC_RSA_KEY_BYTES];
383 bn_to_bytes(em, &m);
384
385 // Recompute the expected PKCS#1 v1.5 block and compare in constant time.
386 uint8_t digest[PC_SHA512_DIGEST_LEN];
387 size_t digest_len = 0;
388 const uint8_t *di = nullptr;
389 size_t di_len = 0;
390 rsa_digest(msg, msg_len, hash, digest, &digest_len, &di, &di_len);
391 uint8_t expected[PC_RSA_KEY_BYTES];
392 pkcs1v15_encode(digest, digest_len, di, di_len, expected);
393
394 return pc_ct_eq(em, expected, PC_RSA_KEY_BYTES) ? 0 : -1;
395}
396
397#endif // ARDUINO
void bn_to_bytes(uint8_t bytes[256], const pc_bignum *in)
Write a pc_bignum as a 256-byte big-endian array.
Definition bignum.cpp:96
int bn_cmp(const pc_bignum *a, const pc_bignum *b)
Compare two pc_bignum values.
Definition bignum.cpp:108
void bn_from_bytes(pc_bignum *out, const uint8_t *bytes, size_t len)
Read a big-endian byte array of len bytes into a pc_bignum.
Definition bignum.cpp:84
2048-bit big-integer arithmetic for DH-group14 and RSA-2048.
#define PC_BN_LIMBS
Number of 32-bit limbs in a 2048-bit integer.
Definition bignum.h:83
Per-translation-unit optimization override for hot, pure-integer crypto.
Constant-time comparison for secret-dependent checks.
PC_CRYPTO_HOT const uint8_t pc_pkcs1_sha256_digestinfo[PC_PKCS1_DIGESTINFO_LEN]
The DER-encoded DigestInfo wrapper for SHA-256 (prepend to the 32-byte digest).
Definition rsa.cpp:31
const uint8_t pc_pkcs1_sha512_digestinfo[PC_PKCS1_SHA512_DIGESTINFO_LEN]
The DER-encoded DigestInfo wrapper for SHA-512 (prepend to the 64-byte digest).
Definition rsa.cpp:40
int pc_rsa_verify(const uint8_t n_be[PC_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, pc_rsa_hash hash)
Verify an RSA-2048 PKCS#1 v1.5 signature over msg.
Definition rsa.cpp:55
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_PKCS1_DIGESTINFO_LEN
Length of the DER DigestInfo wrapper for SHA-256 (RFC 8017 / RFC 5754).
Definition rsa.h:45
#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
#define PC_PKCS1_SHA512_DIGESTINFO_LEN
Length of the DER DigestInfo wrapper for SHA-512.
Definition rsa.h:48
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
A 2048-bit unsigned integer stored as 64 little-endian 32-bit limbs.
Definition bignum.h:92
uint32_t d[PC_BN_LIMBS]
256 bytes of magnitude, little-endian limbs.
Definition bignum.h:93