ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
fe25519.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 fe25519.h
6 * @brief Per-variant GF(2^255-19) field layer on the RSA/MPI hardware accelerator (X25519 + Ed25519).
7 *
8 * Field elements are canonical `uint32[8]` (< p = 2^255-19) so every field multiply is a single
9 * 256-bit modular multiply on the RSA accelerator (S3: ~1,386 cycles vs 7,955 for the software SIMD
10 * `pc_gf_mul`; P4: ~2,118 cycles / 5.9 us). add/sub are native 32-bit (carry + one conditional subtract
11 * of p); bytes<->fe is a per-scalar-mult conversion, not per multiply. This is the shared engine behind
12 * both the X25519 KEX (`pc_curve25519.cpp`) and the Ed25519 host-key signature (`pc_ed25519.cpp`) on
13 * every die with a single-shot hardware MODMULT (S3 hw_ver1, P4 and newer hw_ver3 - see the gate below);
14 * the radix-2^16 `pc_gf` path is the native / classic-ESP32 fallback in both.
15 *
16 * The accelerator (and its lock) are shared with mbedTLS RSA/DH, so a scalar-mult brackets itself with
17 * `pc_fe_hw_enable()` / `pc_fe_hw_disable()` (mbedTLS's own `esp_mpi_{enable,disable}_hardware_hw_op`,
18 * i.e. acquire the MPI lock + clock/power the peripheral) and holds the lock for its whole run.
19 *
20 * Header-only `static inline` on purpose: the cheap ops (add/sub/cswap) inline into the ladder in each
21 * translation unit with no cross-TU call overhead, and the whole layer stays one source of truth.
22 */
23
24#ifndef PROTOCORE_FE25519_H
25#define PROTOCORE_FE25519_H
26
27#include <stdint.h>
28
29#include "board_drivers/hal/esp/esp_crypto_hal.h" // pc_rsa_modmul + pc_rsa_hw_acquire/release (the RSA-accelerator HAL)
30#include "crypto/crypto_scratch.h" // pc_ct_eq (the canonical constant-time compare)
31
32// 25519 has no dedicated ECC accelerator on any ESP32 die, so the RSA MODMULT is the field-layer win wherever
33// it exists - track the HAL's PC_RSA_MODMUL_HW (S3, P4, ...). Classic ESP32 / native keep the software ladder.
34#ifdef PC_RSA_MODMUL_HW
35#define PC_FE25519_MPI_HW 1
36#endif
37
38#ifdef PC_FE25519_MPI_HW
39
40/** @brief A field element of GF(2^255-19): canonical, eight little-endian 32-bit limbs (< p). */
41typedef uint32_t fe[8];
42
43// Constants for the 256-bit modular multiply mod p = 2^255-19 (scratchpad/montconst.py): Montgomery m'
44// and R^2 mod p (= 38^2 = 1444 = 0x5a4). Preloading R^2 into the result block makes the accelerator
45// return a plain residue X*Y mod p rather than a Montgomery form (the esp_mpi_mul_mpi_mod convention).
46static const uint32_t FE_MOD_MPRIME = 0x286bca1bu;
47static const uint32_t FE_MOD_P[8] = {0xffffffedu, 0xffffffffu, 0xffffffffu, 0xffffffffu,
48 0xffffffffu, 0xffffffffu, 0xffffffffu, 0x7fffffffu};
49static const uint32_t FE_MOD_R2[8] = {0x000005a4u, 0, 0, 0, 0, 0, 0, 0};
50
51// Acquire the accelerator (lock + power) for a scalar-mult, and drop it after. Bracket every run. Thin names
52// over the HAL so the X25519 ladder / Ed25519 point arithmetic read as before.
53static inline void pc_fe_hw_enable(void)
54{
55 pc_rsa_hw_acquire();
56}
57static inline void pc_fe_hw_disable(void)
58{
59 pc_rsa_hw_release();
60}
61
62// z = x*y mod p (8 words / 256-bit) on the RSA MODMULT. Requires pc_fe_hw_enable() first. Canonical (< p),
63// safe if z aliases x/y. Delegates to the HAL modmul with this domain's constants; the crypto TUs that pull
64// this in build at -O2 (PC_CRYPTO_HOT), where the always_inline HAL folds FE_MOD_P / the mostly-zero
65// FE_MOD_R2 into immediate stores - the hand-tuned ~1,380-cyc path.
66static inline void fe_mul(fe z, const fe x, const fe y)
67{
68 pc_rsa_modmul(z, x, y, FE_MOD_P, FE_MOD_MPRIME, FE_MOD_R2, 8);
69}
70static inline void fe_sq(fe o, const fe x)
71{
72 fe_mul(o, x, x);
73}
74
75static inline void fe_copy(fe o, const fe a)
76{
77 for (int i = 0; i < 8; i++)
78 {
79 o[i] = a[i];
80 }
81}
82static inline void fe_0(fe o)
83{
84 for (int i = 0; i < 8; i++)
85 {
86 o[i] = 0;
87 }
88}
89static inline void fe_1(fe o)
90{
91 o[0] = 1;
92 for (int i = 1; i < 8; i++)
93 {
94 o[i] = 0;
95 }
96}
97// If o >= p (o is in [p, 2p)), subtract p. Constant-time: the borrow out of o-p selects o or o-p.
98static inline void fe_reduce_once(fe o)
99{
100 uint32_t t[8];
101 int64_t b = 0;
102 for (int i = 0; i < 8; i++)
103 {
104 b += (int64_t)o[i] - (int64_t)FE_MOD_P[i];
105 t[i] = (uint32_t)b;
106 b >>= 32;
107 }
108 uint32_t keep = (uint32_t)b; // 0 if o>=p (take t=o-p), 0xffffffff if o<p (keep o)
109 for (int i = 0; i < 8; i++)
110 {
111 o[i] = (o[i] & keep) | (t[i] & ~keep);
112 }
113}
114static inline void fe_add(fe o, const fe x, const fe y) // x,y < p -> o = x+y mod p
115{
116 uint64_t c = 0;
117 for (int i = 0; i < 8; i++)
118 {
119 c += (uint64_t)x[i] + y[i];
120 o[i] = (uint32_t)c;
121 c >>= 32;
122 }
123 fe_reduce_once(o); // x+y < 2p, one conditional subtract
124}
125static inline void fe_sub(fe o, const fe x, const fe y) // x,y < p -> o = x-y mod p
126{
127 int64_t b = 0;
128 uint32_t t[8];
129 for (int i = 0; i < 8; i++)
130 {
131 b += (int64_t)x[i] - (int64_t)y[i];
132 t[i] = (uint32_t)b;
133 b >>= 32;
134 }
135 uint32_t borrow = (uint32_t)b; // 0xffffffff if x<y -> add p back
136 uint64_t c = 0;
137 for (int i = 0; i < 8; i++)
138 {
139 c += (uint64_t)t[i] + (FE_MOD_P[i] & borrow);
140 o[i] = (uint32_t)c;
141 c >>= 32;
142 }
143}
144static inline void fe_cswap(fe x, fe y, uint32_t swap) // constant-time swap of x,y when swap==1
145{
146 uint32_t mask = (uint32_t)(-(int32_t)swap);
147 for (int i = 0; i < 8; i++)
148 {
149 uint32_t t = mask & (x[i] ^ y[i]);
150 x[i] ^= t;
151 y[i] ^= t;
152 }
153}
154static inline void fe_frombytes(fe o, const uint8_t b[32])
155{
156 for (int i = 0; i < 8; i++)
157 {
158 o[i] = (uint32_t)b[4 * i] | ((uint32_t)b[4 * i + 1] << 8) | ((uint32_t)b[4 * i + 2] << 16) |
159 ((uint32_t)b[4 * i + 3] << 24);
160 }
161 o[7] &= 0x7fffffffu; // Ed25519/X25519 both ignore bit 255 of the y/u coordinate
162 fe_reduce_once(o); // the masked value can still be in [p, 2^255) -> canonicalize
163}
164static inline void fe_tobytes(uint8_t b[32], const fe a)
165{
166 fe t;
167 fe_copy(t, a);
168 fe_reduce_once(t); // freeze to the canonical residue
169 for (int i = 0; i < 8; i++)
170 {
171 b[4 * i] = (uint8_t)t[i];
172 b[4 * i + 1] = (uint8_t)(t[i] >> 8);
173 b[4 * i + 2] = (uint8_t)(t[i] >> 16);
174 b[4 * i + 3] = (uint8_t)(t[i] >> 24);
175 }
176}
177// o = a^(p-2) = a^-1 mod p (tweetnacl square-and-multiply chain for the exponent 2^255-21).
178static inline void fe_invert(fe o, const fe a)
179{
180 fe c;
181 fe_copy(c, a);
182 for (int i = 253; i >= 0; i--)
183 {
184 fe_sq(c, c);
185 if (i != 2 && i != 4)
186 {
187 fe_mul(c, c, a);
188 }
189 }
190 fe_copy(o, c);
191}
192// o = a^((p-5)/8) = a^(2^252-3) - the square-root exponent for Ed25519 point decompression.
193static inline void fe_pow2523(fe o, const fe a)
194{
195 fe c;
196 fe_copy(c, a);
197 for (int i = 250; i >= 0; i--)
198 {
199 fe_sq(c, c);
200 if (i != 1)
201 {
202 fe_mul(c, c, a);
203 }
204 }
205 fe_copy(o, c);
206}
207// Low bit of the canonical encoding (Ed25519 x-coordinate sign).
208static inline int fe_parity(const fe a)
209{
210 uint8_t d[32];
211 fe_tobytes(d, a);
212 return d[0] & 1;
213}
214// 0 if a and b encode the same field element, -1 otherwise (constant-time over the 32 bytes).
215static inline int fe_neq(const fe a, const fe b)
216{
217 uint8_t c[32];
218 uint8_t d[32];
219 fe_tobytes(c, a);
220 fe_tobytes(d, b);
221 return pc_ct_eq(c, d, 32) ? 0 : -1;
222}
223
224#endif // PC_FE25519_MPI_HW
225#endif // PROTOCORE_FE25519_H
Constant-time comparison for secret-dependent checks.
Single owner of the ESP32 RSA/MPI accelerator, by DIRECT register access - a self-contained HAL.
uint32_t mask(uint8_t width)
Mask of width low bits (width 32 handled without a 32-bit shift, which is UB).
Definition crc.h:61