Milestone 6 (ACL/PKI): Ed25519 verify + SHA-512, built from scratch

New freestanding, verify-only Ed25519 (RFC 8032) implementation:
include/starkernel/{sha512,fe25519,scalar25519,ed25519}.h +
src/starkernel/crypto/{sha512,fe25519,scalar25519,ed25519}.c, wired into
Makefile.starkernel. Kernel never signs or generates keys -- only
ed25519_verify() is needed; signing happens in the host-side mkcapsule
build tool via libsodium/OpenSSL.

Confirmed __int128 multiply/add/shift-by-constant compile with zero
undefined symbols on all three target toolchains (only division needs
libgcc's __udivti3, per timer.c's existing documented finding -- that
file's comment updated to narrow the claim, since it had been read as
"avoid __int128 entirely"). This enabled the standard 5-limb radix-2^51
field arithmetic representation.

An abandoned first attempt (10-limb radix-2^26, avoiding __int128 out of
premature caution) hit two real bugs, both invisible on inspection and
found only by property-based testing against Python's own bignum
arithmetic: a non-uniform-radix limb misalignment in multiplication, and
a double-counted carry. Verification chain: SHA-512 against known +
boundary vectors (7/7); field arithmetic property-tested 25,045 cases;
scalar-mod-L arithmetic 300 cases (L confirmed prime via Miller-Rabin
first); full verify() end-to-end against 110 real signatures from
Python's cryptography library, including tampered inputs and the RFC
8032 S>=L malleability attack -- all correctly accepted/rejected.

Compiles clean (zero warnings) and links on all three architectures,
confirmed via the mandatory three-arch QEMU boot. The code is linked but
not yet called from anywhere -- wiring into capsule_birth.c needs a
from-scratch X.509/DER parser first (Captain Bob chose real X.509 over a
raw-blob cert format this session), which is the next open item.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZ8kNoTuP63pbQtro4qvrm
This commit is contained in:
Robert Allan James
2026-08-22 12:41:26 -04:00
co-authored by Claude Sonnet 5
parent 2b7743027c
commit 2e7e957680
17 changed files with 28046 additions and 5 deletions
+78 -2
View File
@@ -3461,8 +3461,12 @@ sized around, not the size of every test image.
key needed at all.** Trust is established once, at build time, by whoever holds the real
root CA and produces the build — the embedded intermediate is already signed before it
ever reaches the kernel, so there's no runtime chicken-and-egg to solve.
- [ ] Implement per-capsule signature verification (Ed25519) at capsule-load time, checked
against the embedded (already-trusted) intermediate cert directly
- [x] Implement per-capsule signature verification (Ed25519) at capsule-load time, checked
against the embedded (already-trusted) intermediate cert directly — **the Ed25519
cryptographic primitive itself is done, 2026-08-22, see writeup below.** Wiring it into
`capsule_birth.c`'s actual load path, and the X.509/DER parsing needed to read the
embedded cert (Captain Bob chose real X.509 over a raw-blob format, 2026-08-22 — see
writeup), are both still open.
- [ ] Add a signing step to the `mkcapsule` build tool (or a separate signing tool) that
produces a signature alongside each capsule's existing xxHash64
- [ ] Extend `MANIFEST_AUTO.md`'s generation to add a signature-status column, matching the
@@ -3470,6 +3474,78 @@ sized around, not the size of every test image.
- [ ] Implement magic-number-based content-type detection (Section U item 14) — a shared
primitive, per Section V, also usable for Milestone 4's foreign-drive check
**Ed25519 verification primitive, done 2026-08-22.** Kernel needs only `ed25519_verify()` --
this kernel never signs or generates keys (no entropy source to do so safely anyway); signing
happens in the host-side build tool, which can link libsodium/OpenSSL since it's a normal
Linux binary. That halved the scope: no scalar clamping, no key derivation, no constant-time
discipline (verify only ever touches public data -- pubkey, message, signature -- no
secret-dependent branch to leak).
New files: `include/starkernel/{sha512,fe25519,scalar25519,ed25519}.h` +
`src/starkernel/crypto/{sha512,fe25519,scalar25519,ed25519}.c`, wired into
`Makefile.starkernel`'s source wildcards (both `LOADER_SRCS_BASE` and `KERNEL_SRCS_BASE`).
Confirmed present in this tree: none of SHA-512, Ed25519, or any big-integer field arithmetic
existed anywhere before this (`tools/mkcapsule.c`'s "anchor for future Ed25519 fingerprints"
comment was the only prior trace).
**`__int128` is usable here after all -- for multiply/add/shift, just not divide.**
`arch/amd64/timer.c`'s own history (see that file, and this milestone's memory note) documents
a prior `__int128` attempt failing because 128-bit *division* needs libgcc's `__udivti3`,
undefined in this `-ffreestanding -nostdlib` build -- which had been read as "avoid `__int128`
entirely." Checked directly this session, against the exact `Makefile.starkernel` flags, on all
three target toolchains (`gcc`, `aarch64-linux-gnu-gcc`, `riscv64-linux-gnu-gcc`): `__int128`
multiply-accumulate and shift-by-constant compile with zero undefined symbols. `timer.c`'s
comment updated to narrow the claim to division specifically. This let the field arithmetic use
the standard, widely-reviewed **5-limb, radix-2^51** Ed25519 representation (51*5=255 bits
exactly -- the reduction constant is the clean `19`, matching `2^255 mod p`) instead of a
from-scratch 10-limb/26-bit scheme designed to avoid `__int128` out of premature caution.
**That from-scratch scheme was tried first and abandoned after two real bugs, not from
`__int128` unavailability.** Documented here because the failure mode is instructive: (1) a
fundamentally flawed limb-alignment assumption -- for a non-uniform (26/25-bit alternating)
radix, `shift[i]+shift[j] != shift[i+j]` in general, so naive schoolbook multiplication silently
misaligned terms; found by property-based testing against Python's own bignum arithmetic, not
by inspection -- inspection had already declared the code correct. (2) After switching to a
uniform 26-bit radix to fix that, a double-counted carry (`wide[i+1] += c` both explicitly at
the end of one loop iteration and implicitly via `wide[i] += c` at the top of the next, using
the same stale `c`) silently doubled every multiplication result. Both bugs produced plausible-
looking, internally-consistent-seeming output; neither was visible from reading the code, only
from testing against an independent reference. This is the second time this session a
"the math looks right" belief was wrong until checked against ground truth (the first was a
hand-transcribed SHA-512 test vector, off by one hex digit) -- **treat any hand-derived
numeric/algorithmic claim in crypto or register-layout code as unverified until checked against
an independent source, not as verified because it was reasoned through carefully.**
**Verification method, in order:** (1) SHA-512 against known vectors (`empty`, `"abc"`) plus
five boundary-case vectors independently generated via `sha512sum` (111/112/113/128/130-byte
inputs, exercising the padding-wraparound path) -- 7/7 pass. (2) Field arithmetic
(`fe25519_{add,sub,mul,sq,invert}`) against Python's own `pow()`/modular arithmetic,
property-based, 25,045 random + edge cases -- ALL PASS (this is what caught both bugs above).
(3) Scalar arithmetic mod `L` (`scalar_reduce512`, `scalar_lt_L`) similarly, 300 cases -- ALL
PASS. `L` itself (`2^252 + 27742317777372353535851937790883648493`) confirmed prime via 20
rounds of Miller-Rabin in Python before use, rather than trusted from memory. (4) The full
`ed25519_verify()` pipeline end-to-end against 110 real signatures generated by Python's
`cryptography` library (itself backed by a well-audited OpenSSL) -- valid signatures across
message lengths 0..1000 bytes, tampered messages, tampered signatures, wrong public keys, and
the RFC 8032 `S >= L` malleability attack, all correctly accepted/rejected. Curve constants (`d`,
base point `Bx`/`By`, `sqrt(-1)`, the `(p+3)/8` exponent) were all computed directly via Python
arbitrary-precision arithmetic and pasted into the C source as byte arrays, not hand-derived or
recalled -- the base point's encoding (`0x58, 0x66, 0x66, ...`) matches the widely-published
Ed25519 base point as a corroborating check.
Compiles clean (`-Wall -Werror -Wextra`, zero warnings) and links on all three architectures;
confirmed via the mandatory three-arch QEMU boot (`logs/20260822-123758/amd64/`,
`logs/20260822-123842/aarch64/`, `logs/20260822-123701/riscv64/`) -- the code is not called from
anywhere yet (dead but linked), so this confirms compile/link correctness across toolchains,
not runtime behavior in the kernel specifically; the host-side test harnesses above are what
established runtime correctness.
Still open, per Captain Bob's X.509 decision this session (2026-08-22): a from-scratch DER/
ASN.1 parser and X.509 structure walker (scoped to exactly what's needed -- Ed25519
`SubjectPublicKeyInfo`/signature per RFC 8410, Issuer/Subject/Validity, chain matching -- not a
general RFC 5280 parser), before the embedded intermediate cert can actually be read and
`capsule_birth.c` can be wired to verify against it.
### Milestone 7 — Contributor capsules / trust tiers (Section U items 15-18, Section V area F)
Explicitly sequenced after Milestone 6 closes.