ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
portable_bignum.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 portable_bignum.cpp
6 * @brief Software Montgomery modexp - the bignum backend for a vendor with no accelerator.
7 *
8 * Selected EXPLICITLY by a vendor profile that sets PC_HAS_HW_BIGNUM 0. It is never a fallback: the
9 * core declares bn_expmod_group14() and links whatever backend the vendor provides, so linking none
10 * is an undefined reference and linking two is a duplicate definition. Both fail the build.
11 *
12 * Software crypto is a legitimate choice and on some parts the only one - what must not happen is
13 * arriving here by default. This implementation is data-dependent and NOT constant time
14 * (SECURITY.md, timing), so a vendor whose silicon can do the modexp in hardware must never end up
15 * on it silently; with no weak symbol anywhere in the chain, it cannot.
16 */
17
18#include "board_drivers/board_profiles/pc_platform.h" // PC_HAS_HW_BIGNUM
20#include "crypto/crypto_opt.h"
21#include "server/mmgr/secure.h"
22#include <string.h>
23
24#if !PC_HAS_HW_BIGNUM
25
27
28namespace
29{
30// Group14 Montgomery constants, owned by one instance (internal linkage): R mod p, R^2 mod p,
31// and the init flag (all filled by bn_init()). One named owner, unreachable cross-TU.
33{
34 pc_bignum r1; // R mod p = 2^2048 - p (two's complement of p in 2048 bits)
35 pc_bignum r2; // R^2 mod p = 2^4096 mod p (bn_init() via repeated doubling)
36 bool initialized = false;
37};
38static Group14Ctx s_g14;
39} // namespace
40
41// Subtract b from a in place (a -= b). Assumes a >= b. Both are n limbs.
42static void bn_sub_inplace(uint32_t *a, const uint32_t *b, int n)
43{
44 uint64_t borrow = 0;
45 for (int i = 0; i < n; i++)
46 {
47 uint64_t v = (uint64_t)a[i] - b[i] - borrow;
48 a[i] = (uint32_t)v;
49 borrow = (v >> 32) & 1u;
50 }
51}
52
53// Left-shift n-limb value by 1 bit. Returns the shifted-out MSB.
54static uint32_t bn_shl1(uint32_t *a, int n)
55{
56 uint32_t carry = 0;
57 for (int i = 0; i < n; i++)
58 {
59 uint32_t nc = a[i] >> 31;
60 a[i] = (a[i] << 1) | carry;
61 carry = nc;
62 }
63 return carry;
64}
65
66// ---------------------------------------------------------------------------
67// Montgomery initialization (compute R mod p and R^2 mod p for group-14)
68// ---------------------------------------------------------------------------
69
70static void bn_init(void)
71{
72 if (s_g14.initialized)
73 {
74 return;
75 }
76
77 // R mod p = 2^2048 mod p = 2^2048 - p
78 // (Since 2^2047 <= p < 2^2048, R mod p = 2^2048 - p which is positive and < p.)
79 // Compute via borrow subtraction: 0 - p with 2048-bit wrap.
80 {
81 uint64_t borrow = 0;
82 for (int i = 0; i < PC_BN_LIMBS; i++)
83 {
84 uint64_t v = (uint64_t)0 - group14_p.d[i] - borrow;
85 s_g14.r1.d[i] = (uint32_t)v;
86 borrow = (v >> 32) & 1u;
87 }
88 // borrow == 1 here (expected - we wrapped around 2^2048), which is
89 // the carry that represents the implicit 2^2048 in R mod p. Correct.
90 }
91
92 // R^2 mod p = 2^4096 mod p.
93 // Compute by starting from R mod p and doubling it 2048 times mod p.
94 memcpy(s_g14.r2.d, s_g14.r1.d, sizeof(pc_bignum));
95 for (int i = 0; i < 2048; i++)
96 {
97 uint32_t overflow = bn_shl1(s_g14.r2.d, PC_BN_LIMBS);
98 // If overflow bit set OR result >= p, subtract p.
99 // bn_init() takes no parameters and reads no state besides the compile-time constant
100 // group14_p; combined with the s_g14.initialized memo guard above, this doubling loop
101 // runs as exactly one fixed 2048-step trace for the entire process lifetime - no test
102 // input, call order, or code path can make any step see different data. Verified
103 // independently by a bit-exact simulation of all 2048 steps (not the gcov run itself):
104 // for this specific prime/starting-value pair, every time bn_shl1() does not overflow,
105 // the doubled 2048-bit value is already < p, so the cmp_raw() half of this OR is never
106 // the deciding vote - it only ever agrees with an overflow that is already false.
107 // Structurally unreachable (a mathematical constant of this trace), not merely untested.
108 if (overflow || bn_cmp_raw(s_g14.r2.d, group14_p.d, PC_BN_LIMBS) >= 0) // GCOVR_EXCL_BR_LINE
109 {
110 bn_sub_inplace(s_g14.r2.d, group14_p.d, PC_BN_LIMBS);
111 }
112 }
113
114 s_g14.initialized = true;
115}
116
117namespace
118{
119// bn_expmod_group14's working set, borrowed whole. Both paths hold the DH private exponent and the
120// shared secret, so the secure pool wipes them when the borrow is released - on every exit path.
121
122// Native path: the Montgomery temporaries plus bn_monpro's 129-limb accumulator.
123struct BnExpmodWork
124{
125 pc_bignum base_mont;
126 pc_bignum result;
127 pc_bignum tmp;
128 uint32_t t[129];
129};
130
131// Worst-case bytes this backend borrows in one modexp. PC_SECURE_ARENA_SIZE is derived
132// from declarations like this one; the static_assert below is what proves it.
133static_assert(sizeof(BnExpmodWork) <= PC_WORK_BIGNUM_SW,
134 "BnExpmodWork outgrew PC_WORK_BIGNUM_SW - raise it; PC_SECURE_ARENA_SIZE derives from it");
135} // namespace
136
137// ---------------------------------------------------------------------------
138// Montgomery SOS multiplication: out = a * b * R^-1 mod p
139// Requires: 0 <= a, b < p.
140// The 129-limb temporary t[] is supplied by the caller (a member of its working set).
141// p_inv = 1 for group-14 (see file header).
142// ---------------------------------------------------------------------------
143
144static void bn_monpro(pc_bignum *out, const pc_bignum *a, const pc_bignum *b, uint32_t *t)
145{
146 memset(t, 0, 129 * sizeof(uint32_t));
147
148 for (int i = 0; i < PC_BN_LIMBS; i++)
149 {
150 // Multiply step: t[0..63] += a[i] * b[0..63]
151 uint64_t carry = 0;
152 for (int j = 0; j < PC_BN_LIMBS; j++)
153 {
154 uint64_t uv = (uint64_t)t[i + j] + (uint64_t)a->d[i] * (uint64_t)b->d[j] + carry;
155 t[i + j] = (uint32_t)uv;
156 carry = uv >> 32;
157 }
158 t[i + PC_BN_LIMBS] += (uint32_t)carry;
159
160 // Reduction step: m = t[i] * p_inv = t[i] * 1 = t[i]
161 uint32_t m = t[i];
162 carry = 0;
163 for (int j = 0; j < PC_BN_LIMBS; j++)
164 {
165 uint64_t uv = (uint64_t)t[i + j] + (uint64_t)m * (uint64_t)group14_p.d[j] + carry;
166 t[i + j] = (uint32_t)uv;
167 carry = uv >> 32;
168 }
169 // Add carry into the high word (t[i+64]); t[128] absorbs final overflow.
170 uint64_t hi = (uint64_t)t[i + PC_BN_LIMBS] + carry;
171 t[i + PC_BN_LIMBS] = (uint32_t)hi;
172 t[i + PC_BN_LIMBS + 1] += (uint32_t)(hi >> 32);
173 }
174
175 // Result is in t[64..127]. Conditionally subtract p if result >= p.
176 uint32_t *res = t + PC_BN_LIMBS;
177 // For 0 <= a,b < p the raw (pre-correction) SOS value is < 2p, so it needs the guard
178 // limb t[128] for the case where it reaches/exceeds 2^2048. group14_p's top two limbs are
179 // both 0xFFFFFFFF, i.e. p sits within about 2^-64 of 2^2048, so "the raw value is >= p but
180 // still < 2^2048" (t[128] == 0 yet the cmp_raw() half alone is true) is a razor-thin sliver
181 // of the [0, 2p) output range for a "generic" (a, b) - but it is directly constructible:
182 // base = R^-1 mod p (R = 2^2048) makes the very first MonPro call inside
183 // bn_expmod_group14() - MonPro(base, R^2 mod p), i.e. converting base to Montgomery form -
184 // compute base*R mod p = 1 exactly, landing the pre-correction raw value at exactly p+1
185 // (t[128]==0, cmp_raw()>=0 true). Exercised by
186 // test_bn_expmod_group14_hits_correction_sliver_without_overflow_limb (test_ssh_conn.cpp).
187 if (t[128] || bn_cmp_raw(res, group14_p.d, PC_BN_LIMBS) >= 0)
188 {
189 bn_sub_inplace(res, group14_p.d, PC_BN_LIMBS);
190 }
191
192 memcpy(out->d, res, PC_BN_LIMBS * sizeof(uint32_t));
193}
194
195void bn_expmod_group14(pc_bignum *out, const pc_bignum *base, const pc_bignum *exp)
196{
197 bn_init(); // ensure s_g14.r1, s_g14.r2 are computed
198
199 // The whole working set in one borrow. These are the same bytes the fixed layout carved by hand;
200 // as struct members they cannot drift from a layout documented somewhere else.
201 SecureBorrow ws_b(sizeof(BnExpmodWork), alignof(BnExpmodWork));
202 const pc_span &ws = ws_b.span();
203 if (!pc_span_ok(ws))
204 {
205 memset(out, 0, sizeof(*out)); // pool exhausted: a zero result fails every downstream check
206 return;
207 }
208 BnExpmodWork *w = reinterpret_cast<BnExpmodWork *>(ws.buf);
209 pc_bignum *base_mont = &w->base_mont;
210 pc_bignum *result = &w->result;
211 pc_bignum *tmp = &w->tmp;
212
213 // Convert base to Montgomery form: base_mont = base * R mod p
214 // = MonPro(base, R^2 mod p)
215 bn_monpro(base_mont, base, &s_g14.r2, w->t);
216
217 // result = 1 in Montgomery form = R mod p = s_g14.r1
218 memcpy(result->d, s_g14.r1.d, sizeof(pc_bignum));
219
220 // Left-to-right binary square-and-multiply (MSB first: d[63]..d[0], bit 31..0)
221 for (int i = PC_BN_LIMBS - 1; i >= 0; i--)
222 {
223 for (int b = 31; b >= 0; b--)
224 {
225 // Square: result = result^2 * R^-1 mod p
226 bn_monpro(tmp, result, result, w->t);
227 memcpy(result->d, tmp->d, sizeof(pc_bignum));
228
229 if ((exp->d[i] >> b) & 1u)
230 {
231 // Multiply: result = result * base_mont * R^-1 mod p
232 bn_monpro(tmp, result, base_mont, w->t);
233 memcpy(result->d, tmp->d, sizeof(pc_bignum));
234 }
235 }
236 }
237
238 // Convert back from Montgomery form: out = result * R^-1 mod p
239 // = MonPro(result, 1)
240 pc_bignum one;
241 memset(one.d, 0, sizeof(pc_bignum));
242 one.d[0] = 1u;
243 bn_monpro(out, result, &one, w->t);
244}
245
246#endif // !PC_HAS_HW_BIGNUM
PC_CRYPTO_HOT const pc_bignum group14_p
The RFC 3526 MODP group-14 prime (2048-bit).
Definition bignum.cpp:42
int bn_cmp_raw(const uint32_t *a, const uint32_t *b, int n)
Compute out = base^exp mod group14_p.
Definition bignum.cpp:64
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
A secure borrow whose acquire and release are one call each.
Definition secure.h:153
const pc_span & span() const
The borrowed region; empty if the pool could not satisfy it.
Definition secure.h:161
Per-translation-unit optimization override for hot, pure-integer crypto.
The one vendor/die selector for the whole library.
void bn_expmod_group14(pc_bignum *out, const pc_bignum *base, const pc_bignum *exp)
#define PC_WORK_BIGNUM_SW
Secure pool accessor - borrows that hold key material.
bool pc_span_ok(const pc_span &s)
True when the span refers to real storage and every write so far has fit.
Definition span.h:114
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
A writable byte region: the storage, the capacity that belongs to it, and what has been produced into...
Definition span.h:65
uint8_t * buf
first byte, or nullptr when the region could not be obtained
Definition span.h:66