Files
LithosAnanke/kernel/src/crypto/fe25519.c
T
rajamesandJunie a8b70e88d3 Reorganize source tree: kernel/, v3/, v4/ split and board infrastructure
Source tree reorganization:
- Move StarForth v3 engine to v3/ (src/, include/, Makefile)
- Move kernel to kernel/ (src/, include/, linker/, Makefile)
- Create v4/ skeleton for F18-ISA golden model (DECOMPOSITION.md, JUSTIFICATION.md)
- Move FABRIC-0..4.md to docs/fabric/
- Move ONTOLOGY.md and ROADMAP.md to docs/

Board infrastructure:
- Add boards/ser5/, boards/raspi/, boards/milkv/, boards/zynq7020/
- Each board has board.mk (ISA, CPU flags, boot recipe) and README.md
- Root Makefile becomes thin dispatcher: boot_image, all, clean, docs take TARGET
- make boot_image TARGET=SER5|RASPI|MILKV builds one GPT/MBR image per board
- ZYNQ7020 target exists but stops with clear error (ARMv7 port not built yet)
- scripts/mkdiskimage.sh builds disk images for all boards

Docs pipeline:
- docs/book/ with LaTeX master (main.tex) and Makefile
- pandoc converts Markdown to LaTeX at build time
- Two Lua filters: table-widths.lua (wide tables wrap), code-breaks.lua (inline code breaks)
- make docs builds single PDF (754 pages, 0 missing characters)
- make docs TARGET=<board> adds board appendix
- build/docs/<book|board>/meta.tex stamps git commit into PDF

Bug fixes:
- 42 include paths that only worked by accident now use correct relative paths
- clang-18 hardcode replaced with configurable CC variable (fixed aarch64 build)
- Pi 5: kernel_2712.img linked at 0x80000, .bss zeroed, memory reserved
- Doxyfile, .clang-tidy, README.md, Kconfig paths updated

Verified:
- Hosted v3 build passes 1012 tests, 0 failures
- SER5 image boots in QEMU (OVMF), POST passes, K exact (65536 = Q48_ONE)
- Milk-V image boots in QEMU (OpenSBI + U-Boot + bootefi), POST passes
- make clean TARGET=<board> removes only that board and its ISA objects
- make all builds all boards, hosted v3, and docs in one run

Co-authored-by: Junie <junie@jetbrains.com>
2026-10-01 15:40:09 -04:00

250 lines
8.3 KiB
C

/* fe25519.c -- see fe25519.h for the representation and why. */
#include "starkernel/fe25519.h"
#include <string.h>
typedef unsigned __int128 u128;
#define WBITS 51
#define MASK51 (((int64_t)1 << WBITS) - 1)
void fe25519_0(fe25519 *r) { memset(r->v, 0, sizeof(r->v)); }
void fe25519_1(fe25519 *r) { memset(r->v, 0, sizeof(r->v)); r->v[0] = 1; }
void fe25519_copy(fe25519 *r, const fe25519 *a) { *r = *a; }
/* Carry-propagate v[] in place, base 2^51, 5 limbs; the carry out of
* limb 4 wraps back into limb 0 multiplied by 19 (2^255 = 19 mod p, and
* 51*5 = 255 exactly, so this is the plain reduction constant -- no
* extra scaling needed, unlike a limb count/width that doesn't divide
* 255 evenly). Two passes: the wraparound carry can itself need to
* ripple through limb 0 again on rare inputs. Works for negative limbs
* too (arithmetic shift preserves sign) -- needed after fe25519_sub. */
static void fe25519_carry(fe25519 *r)
{
for (int pass = 0; pass < 2; pass++) {
for (int i = 0; i < 4; i++) {
int64_t c = r->v[i] >> WBITS;
r->v[i] -= c << WBITS;
r->v[i+1] += c;
}
int64_t c = r->v[4] >> WBITS;
r->v[4] -= c << WBITS;
r->v[0] += c * 19;
}
}
void fe25519_add(fe25519 *r, const fe25519 *a, const fe25519 *b)
{
for (int i = 0; i < 5; i++) r->v[i] = a->v[i] + b->v[i];
fe25519_carry(r);
}
void fe25519_sub(fe25519 *r, const fe25519 *a, const fe25519 *b)
{
for (int i = 0; i < 5; i++) r->v[i] = a->v[i] - b->v[i];
fe25519_carry(r);
}
void fe25519_neg(fe25519 *r, const fe25519 *a)
{
fe25519 zero; fe25519_0(&zero);
fe25519_sub(r, &zero, a);
}
/* Schoolbook multiply into a 9-element __int128 wide accumulator (each
* product a[i]*b[j] is at most ~51+51=102 bits; a bucket sums at most 5
* such products, at most ~105 bits -- __int128 has ~127 bits of range,
* comfortable headroom, confirmed this kernel's toolchains compile
* __int128 multiply/add/shift-by-constant with no libgcc calls -- see
* fe25519.h's own doc comment). Fold positions 5-8 into 0-3 (2^255 = 19
* mod p, and since 51*5=255 exactly, position k for k=5..8 represents
* 2^(255+51*(k-5)) = 19 * 2^(51*(k-5)) mod p, i.e. folds directly into
* position k-5 with the plain multiplier 19 -- no extra scale factor,
* unlike a limb width that doesn't divide 255 evenly). */
void fe25519_mul(fe25519 *r, const fe25519 *a, const fe25519 *b)
{
u128 wide[9] = {0};
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
wide[i+j] += (u128)(uint64_t)a->v[i] * (uint64_t)b->v[j];
}
}
for (int k = 8; k >= 5; k--) {
wide[k-5] += 19 * wide[k];
}
/* wide[0..4] can each exceed 51 (even 63) bits after folding --
* carry-propagate using __int128 arithmetic (roomy enough that no
* intermediate step here risks overflow) before narrowing to the
* int64_t storage representation. */
u128 carry = 0;
for (int i = 0; i < 5; i++) {
u128 full = wide[i] + carry;
r->v[i] = (int64_t)(full & (u128)MASK51);
carry = full >> WBITS;
}
r->v[0] += (int64_t)(carry * 19);
fe25519_carry(r);
}
void fe25519_sq(fe25519 *r, const fe25519 *a)
{
fe25519_mul(r, a, a);
}
void fe25519_mul_small(fe25519 *r, const fe25519 *a, uint32_t c)
{
u128 wide[5];
for (int i = 0; i < 5; i++) wide[i] = (u128)(uint64_t)a->v[i] * c;
u128 carry = 0;
for (int i = 0; i < 5; i++) {
u128 full = wide[i] + carry;
r->v[i] = (int64_t)(full & (u128)MASK51);
carry = full >> WBITS;
}
r->v[0] += (int64_t)(carry * 19);
fe25519_carry(r);
}
/* Modular inverse via Fermat's little theorem: a^(p-2) mod p, using the
* standard Ed25519 addition chain (222 squarings + 24 extra multiplies)
* -- the same exponent decomposition every Ed25519 reference
* implementation uses, not something invented here; verified by the
* property-based host test (a * inverse(a) == 1) rather than trusted by
* inspection. */
void fe25519_invert(fe25519 *r, const fe25519 *a)
{
fe25519 z2, z9, z11, z2_5_0, z2_10_0, z2_20_0, z2_50_0, z2_100_0, t;
fe25519_sq(&z2, a); /* 2 */
fe25519_sq(&t, &z2); /* 4 */
fe25519_sq(&t, &t); /* 8 */
fe25519_mul(&z9, &t, a); /* 9 */
fe25519_mul(&z11, &z9, &z2); /* 11 */
fe25519_sq(&t, &z11); /* 22 */
fe25519_mul(&z2_5_0, &t, &z9); /* 2^5 - 2^0 = 31 */
fe25519_sq(&t, &z2_5_0);
for (int i = 1; i < 5; i++) fe25519_sq(&t, &t);
fe25519_mul(&z2_10_0, &t, &z2_5_0); /* 2^10 - 2^0 */
fe25519_sq(&t, &z2_10_0);
for (int i = 1; i < 10; i++) fe25519_sq(&t, &t);
fe25519_mul(&z2_20_0, &t, &z2_10_0); /* 2^20 - 2^0 */
fe25519_sq(&t, &z2_20_0);
for (int i = 1; i < 20; i++) fe25519_sq(&t, &t);
fe25519_mul(&t, &t, &z2_20_0); /* 2^40 - 2^0 */
for (int i = 0; i < 10; i++) fe25519_sq(&t, &t);
fe25519_mul(&z2_50_0, &t, &z2_10_0); /* 2^50 - 2^0 */
fe25519_sq(&t, &z2_50_0);
for (int i = 1; i < 50; i++) fe25519_sq(&t, &t);
fe25519_mul(&z2_100_0, &t, &z2_50_0); /* 2^100 - 2^0 */
fe25519_sq(&t, &z2_100_0);
for (int i = 1; i < 100; i++) fe25519_sq(&t, &t);
fe25519_mul(&t, &t, &z2_100_0); /* 2^200 - 2^0 */
for (int i = 0; i < 50; i++) fe25519_sq(&t, &t);
fe25519_mul(&t, &t, &z2_50_0); /* 2^250 - 2^0 */
fe25519_sq(&t, &t);
fe25519_sq(&t, &t);
fe25519_sq(&t, &t);
fe25519_sq(&t, &t);
fe25519_sq(&t, &t); /* 2^255 - 2^5 */
fe25519_mul(r, &t, &z11); /* 2^255 - 21 = p - 2 */
}
/* Fully reduce a's limbs to a unique representative in [0, p), then pack
* little-endian into 32 bytes. */
void fe25519_pack(uint8_t out[32], const fe25519 *a)
{
fe25519 t = *a;
fe25519_carry(&t);
/* Force full reduction: subtract p once if t >= p, by subtracting
* p's exact base-2^51 representation (limb0 = 2^51-19, limbs 1-4 =
* 2^51-1, computed directly, not memorized) and checking whether
* that needed a borrow out of the top -- if not, t was already >= p
* and the subtracted value is the reduced one; if so, t was already
* < p. Not constant-time; fine here since packing only ever happens
* on public data (a verified signature's derived point, never a
* secret) in this verify-only library. */
static const int64_t P_LIMBS[5] = {
MASK51 - 18, MASK51, MASK51, MASK51, MASK51
};
int64_t borrow = 0;
int64_t rawlimb[5];
for (int i = 0; i < 5; i++) {
int64_t v = t.v[i] - P_LIMBS[i] - borrow;
if (v < 0) {
v += (int64_t)1 << WBITS;
borrow = 1;
} else {
borrow = 0;
}
rawlimb[i] = v;
}
if (!borrow) {
for (int i = 0; i < 5; i++) t.v[i] = rawlimb[i];
}
uint64_t bits = 0;
int bitcnt = 0;
int outpos = 0;
for (int i = 0; i < 5; i++) {
bits |= ((uint64_t)(uint64_t)t.v[i]) << bitcnt;
bitcnt += WBITS;
while (bitcnt >= 8) {
out[outpos++] = (uint8_t)bits;
bits >>= 8;
bitcnt -= 8;
}
}
if (bitcnt > 0 && outpos < 32) {
out[outpos++] = (uint8_t)bits;
}
while (outpos < 32) out[outpos++] = 0;
}
void fe25519_unpack(fe25519 *r, const uint8_t in[32])
{
uint64_t bits = 0;
int bitcnt = 0;
int inpos = 0;
for (int i = 0; i < 5; i++) {
while (bitcnt < WBITS && inpos < 32) {
bits |= ((uint64_t)in[inpos++]) << bitcnt;
bitcnt += 8;
}
r->v[i] = (int64_t)(bits & (uint64_t)MASK51);
bits >>= WBITS;
bitcnt -= WBITS;
}
/* Bit 255 of the input (the top bit of byte 31) is Ed25519's point-
* encoding sign bit, not part of y's magnitude -- caller (point
* decompression) masks it out before calling this. Limb 4 here only
* ever receives up to 51 significant unpacked bits from a 255-bit
* input by construction (4*51=204, 255-204=51), so no explicit mask
* against a stray bit 255 is needed as long as the caller has
* already cleared it in the input bytes. */
}
int fe25519_eq(const fe25519 *a, const fe25519 *b)
{
uint8_t pa[32], pb[32];
fe25519_pack(pa, a);
fe25519_pack(pb, b);
return memcmp(pa, pb, 32) == 0;
}
int fe25519_parity(const fe25519 *a)
{
uint8_t p[32];
fe25519_pack(p, a);
return p[0] & 1;
}