Milestone 6: mkcapsule signing + capsule_birth.c wiring, WARN-only
First attempt shelled out to `openssl pkeyutl -sign` (fork/execlp, not system() -- avoided shell string interpolation of the key path). Corrected on request: no new external host binary dependency when the repo's own code can do the job -- same standing preference as the earlier anti-file correction. Rewritten to link ed25519_sign() (already verified against OpenSSL in Phase B) directly into mkcapsule. New tools/pkcs8_ed25519.c: a narrow DER walker (same shape as x509_ed25519.c, deliberately not shared -- small enough that duplicating a few TLV-walking lines beat threading a header between the kernel crypto tree and host tooling) extracting the raw seed from the intermediate's PKCS#8 private key, plus a minimal self-written base64 decoder (PEM is openssl genpkey's default output; no decoder existed anywhere in the repo). Verified end-to-end before wiring anything in: the extracted seed's derived pubkey matches the cert's exactly, and a full self-contained sign+verify round-trip (zero openssl) passes. CapsuleDesc had no spare bytes, so signatures live in a new parallel CapsuleSigEntry array, emitted by a new `mkcapsule --sign-key <path>` flag (omitted/missing key -> has_sig=0 everywhere, graceful, not a build failure -- CI has no access to the offline key). New capsule_sig.c/.h: capsule_verify_signature(), a separate function, not folded into the already-tested capsule_validate(). Finds and caches the embedded intermediate cert's pubkey once per boot, then verifies against it. Wired into all three capsule_validate() call sites in capsule_birth.c via log_message(LOG_WARN, ...) -- never refuses yet, per the earlier staged-rollout decision. Verified independently, both directions, live in the real kernel: a full clean build (38 signed capsules) boots clean on all three architectures with zero warnings. Separately, hand-corrupted one byte of Mama's own init.4th capsule's stored signature (not its payload/hash, which capsule_validate() already catches and would have masked the test) and rebuilt just the changed object: produced exactly "capsule sig: init.4th: INVALID -- signature does not verify" on boot, and the kernel still reached ok> -- proving warn-only doesn't refuse anything yet. Reverted before the final, untampered 3-arch acceptance pass. Still open: flipping WARN to hard-refuse (separate, deliberate step) and the BLOCK_MAP.md signature-status column. Documented in FABRIC-3.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U14ET9CWAtbQMbYqomKgXd
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
431bcb1f34
commit
2fc55f47e1
+58
@@ -744,6 +744,64 @@ decisions get added here, not to `FABRIC-2.md`. Follow the same discipline `FABR
|
||||
— `capsules/MANIFEST_AUTO.md` is stale/dead, not regenerated since 2026-07-05, flag as docs
|
||||
drift rather than a real target).
|
||||
|
||||
**mkcapsule signing + capsule_birth.c wiring, done 2026-08-26 -- landed WARN-only exactly as
|
||||
decided.** First attempt shelled out to `openssl pkeyutl -sign` (`fork`/`execlp`, not
|
||||
`system()` -- interpolating the key path into a shell string would have been a real, if
|
||||
narrow, injection vector). **Corrected on request: no new external host binary dependency
|
||||
when the repo's own code can do the job** — this project's standing preference, same theme
|
||||
as the earlier anti-file correction. Rewritten to link `ed25519_sign()`
|
||||
(`src/starkernel/crypto/ed25519.c`, already independently verified against OpenSSL in Phase
|
||||
B) directly into `mkcapsule`. This needed one more small piece: `tools/pkcs8_ed25519.c`, a
|
||||
narrow DER walker (same shape as `x509_ed25519.c`, deliberately not shared code — small
|
||||
enough that duplicating a few TLV-walking lines was simpler than threading a header between
|
||||
the kernel crypto tree and host tooling) extracting the raw 32-byte seed from the
|
||||
intermediate's PKCS#8 private key, plus a minimal self-written base64 decoder (PEM is
|
||||
`openssl genpkey`'s default output, not raw DER — checked the repo for an existing decoder
|
||||
first, found none). **Verified end-to-end before wiring anything in:** the extracted seed,
|
||||
run through this project's own `ed25519_keygen()`, reproduces the exact pubkey embedded in
|
||||
the cert; a full self-contained sign+verify round-trip (zero openssl involvement) passes.
|
||||
|
||||
`CapsuleDesc` had no spare bytes (confirmed exactly 64, every field used), so signatures live
|
||||
in a new parallel array, `CapsuleSigEntry capsule_signatures[]` (`{ sig[64], has_sig }`,
|
||||
mirroring `capsule_names[]`'s existing shape), emitted by `mkcapsule --sign-key <path>` (new
|
||||
optional CLI flag; omitted or the key file missing → every capsule gets `has_sig=0`,
|
||||
graceful, not a build failure -- CI has no access to the offline key). New
|
||||
`Makefile.starkernel` variable `SIGN_KEY` (defaults to this machine's actual key path,
|
||||
`?=`-overridable, gated by `$(wildcard ...)` so a fresh checkout or CI never fails for
|
||||
lacking it).
|
||||
|
||||
New `capsule_sig.c`/`.h`: `capsule_verify_signature()`, deliberately a separate, additive
|
||||
function, not folded into the already-tested `capsule_validate()` (whose signature and
|
||||
behavior stay untouched). Finds and parses the embedded `pki:snakeoil-intermediate.der`
|
||||
capsule's pubkey once per boot (cached — every later capsule birth reuses it, the cert can't
|
||||
change mid-boot), then `ed25519_verify()`s the requested capsule's payload against its stored
|
||||
signature. Wired into all three `capsule_validate()` call sites in `capsule_birth.c`
|
||||
(`capsule_birth_mama`, `capsule_birth_baby`, `capsule_run_experiment`) via
|
||||
`log_message(LOG_WARN, ...)` on anything other than `CAPSULE_SIG_OK` — never refuses.
|
||||
(Hit one unrelated build snag wiring this in: `vm.h` and `log.h` both define `LOG_LINE_MAX`
|
||||
for two completely unrelated concepts — vm.h's own is the persistent block-log line size,
|
||||
unconditional `#define`; log.h's is `#ifndef`-guarded. `log.h` has to be included *after*
|
||||
`vm.h` in any TU that needs both, or the build fails under `-Werror`; fixed and commented in
|
||||
`capsule_birth.c` so the next person touching includes there doesn't rediscover it the hard
|
||||
way.)
|
||||
|
||||
**Verified independently, both directions, live in the real kernel:** a full clean build (38
|
||||
real capsules, all signed) boots clean on all three architectures with zero signature
|
||||
warnings — the expected outcome when every signature is genuinely valid, not merely an
|
||||
absence of testing. Separately, hand-corrupted one byte of Mama's own `init.4th` capsule's
|
||||
*stored signature* (not its payload/hash, which `capsule_validate()` already checks
|
||||
independently and would have masked the test) directly in the generated
|
||||
`capsule_generated.c`, without regenerating from source, and rebuilt only the changed object:
|
||||
produced exactly `capsule sig: init.4th: INVALID -- signature does not verify` on boot, and
|
||||
**the kernel still reached `ok>`** — proving warn-only genuinely doesn't refuse anything yet,
|
||||
precisely as decided. Reverted the tampered build before the final acceptance pass. Final,
|
||||
untampered state: clean 3-arch acceptance boot, conservation intact, zero warnings, real disk
|
||||
untouched.
|
||||
|
||||
**Still open:** flipping WARN to hard-refuse (a separate, deliberate step once this has run
|
||||
clean for a while, not bundled into this pass) and the `BLOCK_MAP.md` signature-status
|
||||
column.
|
||||
|
||||
### From FABRIC-2.md §X, Milestone 7 — Contributor capsules / trust tiers
|
||||
|
||||
- [ ] Create the `capsules/contrib/` directory (mechanically trivial, matches existing
|
||||
|
||||
Reference in New Issue
Block a user