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:
Robert Allan James
2026-08-26 21:32:29 -04:00
co-authored by Claude Sonnet 5
parent 431bcb1f34
commit 2fc55f47e1
16 changed files with 45762 additions and 11 deletions
+113 -6
View File
@@ -41,6 +41,9 @@
#include <time.h>
#include <sys/stat.h>
#include "pkcs8_ed25519.h"
#include "starkernel/ed25519.h"
/*===========================================================================
* xxHash64 (embedded for build tool — no external dependency)
*===========================================================================*/
@@ -152,6 +155,8 @@ typedef struct {
size_t length; /* File size in bytes */
uint64_t hash; /* xxHash64 of contents */
uint32_t flags; /* Capsule flags */
uint8_t sig[64]; /* Ed25519 R||S, valid iff has_sig */
int has_sig; /* 1 if sig[] was produced by --sign-key */
} CapsuleEntry;
static CapsuleEntry capsules[MAX_CAPSULES];
@@ -169,6 +174,39 @@ typedef struct {
static ManifestEntry manifest_entries[MAX_CAPSULES];
static int manifest_count = 0;
/*===========================================================================
* Capsule signing (Milestone 6, Phase 8) -- host-side only
*===========================================================================
* Signs using this project's own ed25519_sign() (src/starkernel/crypto/
* ed25519.c), linked directly into this tool, and pkcs8_ed25519.c to
* extract the raw seed from the intermediate's PKCS#8 private key file --
* NOT by shelling out to the external `openssl` binary. Corrected
* 2026-08-26: an earlier version of this function forked+exec'd
* `openssl pkeyutl -sign`, reasoning that private-key handling was
* sensitive enough to prefer an already-audited external tool; the
* project's own standing preference is the opposite -- no new host
* binary dependency when the repo's own code (already independently
* verified against OpenSSL in Phase B) can do the job. Verified
* end-to-end (2026-08-26): the seed this extracts, run through this
* project's own ed25519_keygen(), reproduces the exact pubkey embedded
* in the cert, and a full self-contained sign+verify round-trip (no
* openssl involved at all) passes.
*
* The seed is read once at startup (see main()'s --sign-key handling),
* not per-capsule -- this function just signs.
*
* Signs the exact same bytes content_hash already covers (the raw
* capsule payload), not the hash value -- the natural reading of
* "a signature alongside each capsule's existing xxHash64."
*/
static uint8_t sign_seed[32]; /* set once by main() if --sign-key given */
static int have_sign_seed = 0;
static void sign_capsule_bytes(const uint8_t *data, size_t len,
uint8_t sig_out[64]) {
ed25519_sign(sign_seed, data, len, sig_out);
}
/* Flag constants — must match capsule.h */
#define FLAG_ACTIVE 0x00000001
#define FLAG_PRODUCTION 0x00000010
@@ -405,6 +443,12 @@ static int process_file(const char *fpath, const struct stat *sb,
e->length = (size_t)size;
e->hash = xxhash64(data, (size_t)size, 0);
e->flags = flags_from_name(name);
e->has_sig = 0;
if (have_sign_seed) {
sign_capsule_bytes(data, (size_t)size, e->sig);
e->has_sig = 1;
}
capsule_count++;
@@ -412,8 +456,9 @@ static int process_file(const char *fpath, const struct stat *sb,
if (e->flags & FLAG_MAMA_INIT) mode = 'm';
else if (e->flags & FLAG_PRODUCTION) mode = 'p';
fprintf(stderr, " [%c] %s (%" PRIu64 " bytes, hash=0x%016" PRIx64 ")\n",
mode, e->name, (uint64_t)e->length, e->hash);
fprintf(stderr, " [%c] %s (%" PRIu64 " bytes, hash=0x%016" PRIx64 "%s)\n",
mode, e->name, (uint64_t)e->length, e->hash,
e->has_sig ? ", signed" : "");
return 0;
}
@@ -856,6 +901,26 @@ static void generate_output(FILE *out) {
}
fprintf(out, "};\n\n");
/*------------------------------------------------------------------
* Signature array (parallel to descriptors, Milestone 6 / Phase 8)
*------------------------------------------------------------------*/
fprintf(out,
"/*===========================================================================\n"
" * Capsule Signatures (Ed25519 R||S, parallel to descriptors)\n"
" *===========================================================================*/\n\n"
"const CapsuleSigEntry capsule_signatures[%d] = {\n", capsule_count);
for (int i = 0; i < capsule_count; i++) {
CapsuleEntry *e = &capsules[i];
fprintf(out, " /* [%d] %s */\n { .sig = {", i, e->name);
for (int j = 0; j < 64; j++) {
fprintf(out, "0x%02X,", e->sig[j]);
if (j % 16 == 15) fprintf(out, "\n ");
}
fprintf(out, "}, .has_sig = %d },\n", e->has_sig);
}
fprintf(out, "};\n\n");
/*------------------------------------------------------------------
* Directory header
*------------------------------------------------------------------*/
@@ -895,6 +960,8 @@ static void generate_output(FILE *out) {
"__attribute__((visibility(\"hidden\")))\n"
"const CapsuleNameEntry *capsule_get_names(void) { return capsule_names; }\n\n"
"__attribute__((visibility(\"hidden\")))\n"
"const CapsuleSigEntry *capsule_get_signatures(void) { return capsule_signatures; }\n\n"
"__attribute__((visibility(\"hidden\")))\n"
"const uint8_t *capsule_get_arena(void) { return capsule_arena; }\n",
(uint64_t)0x44504143ULL,
capsule_count, MAX_CAPSULES, capsule_count,
@@ -949,10 +1016,50 @@ int main(int argc, char **argv) {
return lint_errors > 0 ? 1 : 0;
}
if (argc != 3) {
/* Optional --sign-key <keyfile> prefix on build mode: mkcapsule
* [--sign-key <keyfile>] <capsules_dir> <output.c>. Read once here,
* not per-capsule -- process_file() just checks have_sign_seed. */
int arg0 = 1;
if (argc >= 2 && strcmp(argv[1], "--sign-key") == 0) {
if (argc != 5) {
fprintf(stderr, "mkcapsule: ERROR: --sign-key requires "
"<keyfile> <capsules_dir> <output.c>\n");
return 1;
}
const char *key_path = argv[2];
FILE *kf = fopen(key_path, "rb");
if (!kf) {
fprintf(stderr, "mkcapsule: ERROR: cannot open key '%s'\n", key_path);
return 1;
}
fseek(kf, 0, SEEK_END);
long klen = ftell(kf);
fseek(kf, 0, SEEK_SET);
uint8_t *kbuf = malloc((size_t)klen);
if (!kbuf || fread(kbuf, 1, (size_t)klen, kf) != (size_t)klen) {
fprintf(stderr, "mkcapsule: ERROR: cannot read key '%s'\n", key_path);
fclose(kf);
free(kbuf);
return 1;
}
fclose(kf);
if (pkcs8_extract_ed25519_seed(kbuf, (size_t)klen, sign_seed) != 0) {
fprintf(stderr, "mkcapsule: ERROR: '%s' is not a valid PKCS#8 "
"Ed25519 private key\n", key_path);
free(kbuf);
return 1;
}
free(kbuf);
have_sign_seed = 1;
fprintf(stderr, "mkcapsule: signing capsules with key '%s'\n", key_path);
arg0 = 3;
}
if (argc != arg0 + 2) {
fprintf(stderr,
"Usage:\n"
" %s <capsules_dir> <output.c> build capsule C source\n"
" %s [--sign-key <keyfile>] <capsules_dir> <output.c>\n"
" build capsule C source\n"
" %s --lint <capsules_dir> lint all .4th files\n"
" %s --manifest <capsules_dir> print block manifest (stdout)\n"
" %s --manifest <capsules_dir> <out.md> write block manifest to file\n"
@@ -975,7 +1082,7 @@ int main(int argc, char **argv) {
return 1;
}
base_dir = argv[1];
base_dir = argv[arg0];
base_dir_len = strlen(base_dir);
/* Strip trailing slash from base_dir length so relpath strips cleanly */
@@ -983,7 +1090,7 @@ int main(int argc, char **argv) {
base_dir_len--;
}
const char *output_path = argv[2];
const char *output_path = argv[arg0 + 1];
fprintf(stderr, "mkcapsule: checking for block conflicts...\n");
if (check_block_conflicts() != 0) {