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
+113
-6
@@ -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) {
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
/* pkcs8_ed25519.c -- see pkcs8_ed25519.h. */
|
||||
#define _GNU_SOURCE /* memmem() */
|
||||
#include "pkcs8_ed25519.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/*
|
||||
* `openssl genpkey` writes PEM (base64 text between BEGIN/END markers)
|
||||
* by default, not raw DER -- accept either transparently rather than
|
||||
* requiring callers to pre-convert with an external tool (consistent
|
||||
* with this file's whole point: no new host binary dependency). A small,
|
||||
* self-contained base64 decoder, since none existed anywhere in this
|
||||
* repo to reuse (checked 2026-08-26).
|
||||
*
|
||||
* Returns a malloc'd DER buffer (caller frees) and sets *der_len, or
|
||||
* NULL if this doesn't look like a well-formed PEM block. Decodes only
|
||||
* the FIRST "-----BEGIN ... -----" / "-----END ... -----" pair found;
|
||||
* good enough for a single private key, not a general PEM bundle parser.
|
||||
*/
|
||||
static int b64_val(uint8_t c) {
|
||||
if (c >= 'A' && c <= 'Z') return c - 'A';
|
||||
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
|
||||
if (c >= '0' && c <= '9') return c - '0' + 52;
|
||||
if (c == '+') return 62;
|
||||
if (c == '/') return 63;
|
||||
return -1; /* not a base64 char (newline, '=', etc.) */
|
||||
}
|
||||
|
||||
static uint8_t *pem_to_der(const uint8_t *pem, size_t pem_len, size_t *der_len_out) {
|
||||
const char *begin_marker = "-----BEGIN";
|
||||
const char *end_marker = "-----END";
|
||||
|
||||
const uint8_t *begin = memmem(pem, pem_len, begin_marker, strlen(begin_marker));
|
||||
if (!begin) return NULL;
|
||||
const uint8_t *line_end = memchr(begin, '\n', (size_t)(pem + pem_len - begin));
|
||||
if (!line_end) return NULL;
|
||||
const uint8_t *body_start = line_end + 1;
|
||||
|
||||
const uint8_t *end = memmem(body_start, (size_t)(pem + pem_len - body_start),
|
||||
end_marker, strlen(end_marker));
|
||||
if (!end) return NULL;
|
||||
|
||||
/* Decode base64 chars between body_start and end, skipping anything
|
||||
* that isn't a valid base64 character (newlines, stray whitespace). */
|
||||
size_t max_out = (size_t)(end - body_start); /* over-allocate, safe upper bound */
|
||||
uint8_t *out = malloc(max_out ? max_out : 1);
|
||||
if (!out) return NULL;
|
||||
|
||||
size_t out_len = 0;
|
||||
int acc = 0, nbits = 0;
|
||||
for (const uint8_t *p = body_start; p < end; p++) {
|
||||
int v = b64_val(*p);
|
||||
if (v < 0) continue; /* skip newlines and '=' padding */
|
||||
acc = (acc << 6) | v;
|
||||
nbits += 6;
|
||||
if (nbits >= 8) {
|
||||
nbits -= 8;
|
||||
out[out_len++] = (uint8_t)((acc >> nbits) & 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
*der_len_out = out_len;
|
||||
return out;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
const uint8_t *p;
|
||||
size_t len;
|
||||
} der_span_t;
|
||||
|
||||
/* Same short-form/long-form DER TLV walker as x509_ed25519.c's der_next()
|
||||
* -- see that file's comment for the full rationale. Bounds-checked
|
||||
* against limit at every step; refuses malformed input, never faults. */
|
||||
static int der_next(const uint8_t **cursor, const uint8_t *limit,
|
||||
uint8_t *tag_out, der_span_t *content_out) {
|
||||
const uint8_t *p = *cursor;
|
||||
if (p >= limit) return -1;
|
||||
uint8_t tag = *p++;
|
||||
|
||||
if (p >= limit) return -1;
|
||||
uint8_t len_byte = *p++;
|
||||
size_t len;
|
||||
if (len_byte & 0x80u) {
|
||||
uint8_t nbytes = (uint8_t)(len_byte & 0x7Fu);
|
||||
if (nbytes == 0 || nbytes > 4) return -1;
|
||||
if ((size_t)(limit - p) < nbytes) return -1;
|
||||
len = 0;
|
||||
for (uint8_t i = 0; i < nbytes; i++) len = (len << 8) | *p++;
|
||||
} else {
|
||||
len = len_byte;
|
||||
}
|
||||
if ((size_t)(limit - p) < len) return -1;
|
||||
|
||||
*tag_out = tag;
|
||||
content_out->p = p;
|
||||
content_out->len = len;
|
||||
*cursor = p + len;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int pkcs8_extract_ed25519_seed_der(const uint8_t *der, size_t der_len,
|
||||
uint8_t seed_out[32]) {
|
||||
if (!der || !seed_out) return -1;
|
||||
|
||||
const uint8_t *cur = der;
|
||||
const uint8_t *end = der + der_len;
|
||||
uint8_t tag;
|
||||
der_span_t outer;
|
||||
|
||||
/* OneAsymmetricKey ::= SEQUENCE { version INTEGER, privateKeyAlgorithm
|
||||
* AlgorithmIdentifier, privateKey OCTET STRING, ... } */
|
||||
if (der_next(&cur, end, &tag, &outer) != 0 || tag != 0x30) return -1;
|
||||
|
||||
const uint8_t *cur2 = outer.p;
|
||||
const uint8_t *limit2 = outer.p + outer.len;
|
||||
|
||||
/* version INTEGER -- skip, don't care about the value */
|
||||
der_span_t version;
|
||||
if (der_next(&cur2, limit2, &tag, &version) != 0 || tag != 0x02) return -1;
|
||||
|
||||
/* privateKeyAlgorithm ::= AlgorithmIdentifier ::= SEQUENCE { OID, params OPTIONAL } */
|
||||
der_span_t algid;
|
||||
if (der_next(&cur2, limit2, &tag, &algid) != 0 || tag != 0x30) return -1;
|
||||
|
||||
const uint8_t *cur3 = algid.p;
|
||||
const uint8_t *limit3 = algid.p + algid.len;
|
||||
der_span_t oid;
|
||||
if (der_next(&cur3, limit3, &tag, &oid) != 0 || tag != 0x06) return -1;
|
||||
|
||||
static const uint8_t ED25519_OID[3] = { 0x2B, 0x65, 0x70 }; /* 1.3.101.112 */
|
||||
if (oid.len != sizeof(ED25519_OID) ||
|
||||
memcmp(oid.p, ED25519_OID, sizeof(ED25519_OID)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* privateKey ::= OCTET STRING, whose content is itself a DER-encoded
|
||||
* OCTET STRING (RFC 8410's CurvePrivateKey) wrapping the raw 32-byte
|
||||
* seed -- double-wrapped, confirmed empirically against a real
|
||||
* openssl-generated key (2026-08-26), not assumed from the RFC text
|
||||
* alone: `04 22 04 20 <32 bytes>` (outer OCTET STRING len=34,
|
||||
* containing inner OCTET STRING len=32). */
|
||||
der_span_t outer_octet;
|
||||
if (der_next(&cur2, limit2, &tag, &outer_octet) != 0 || tag != 0x04) return -1;
|
||||
|
||||
const uint8_t *cur4 = outer_octet.p;
|
||||
const uint8_t *limit4 = outer_octet.p + outer_octet.len;
|
||||
der_span_t inner_octet;
|
||||
if (der_next(&cur4, limit4, &tag, &inner_octet) != 0 || tag != 0x04) return -1;
|
||||
if (inner_octet.len != 32) return -1;
|
||||
|
||||
memcpy(seed_out, inner_octet.p, 32);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int pkcs8_extract_ed25519_seed(const uint8_t *data, size_t data_len,
|
||||
uint8_t seed_out[32]) {
|
||||
if (!data || !seed_out) return -1;
|
||||
|
||||
/* `openssl genpkey`'s default output is PEM, not raw DER -- accept
|
||||
* either. A PEM block always starts with "-----BEGIN" somewhere near
|
||||
* the top; anything else is assumed to already be raw DER. */
|
||||
if (memmem(data, data_len, "-----BEGIN", 10) != NULL) {
|
||||
size_t der_len = 0;
|
||||
uint8_t *der = pem_to_der(data, data_len, &der_len);
|
||||
if (!der) return -1;
|
||||
int rc = pkcs8_extract_ed25519_seed_der(der, der_len, seed_out);
|
||||
free(der);
|
||||
return rc;
|
||||
}
|
||||
|
||||
return pkcs8_extract_ed25519_seed_der(data, data_len, seed_out);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* pkcs8_ed25519.h -- extract the raw 32-byte Ed25519 seed from a
|
||||
* PKCS#8 DER-encoded private key (RFC 8410 OneAsymmetricKey), for
|
||||
* mkcapsule's host-side capsule signing step (Milestone 6, Phase 8).
|
||||
*
|
||||
* Host-only: this never runs in the kernel (the kernel never signs, see
|
||||
* ed25519.h). Deliberately its own narrow, from-scratch DER walker, not
|
||||
* shared with src/starkernel/crypto/x509_ed25519.c -- that file walks a
|
||||
* full Certificate structure to a *public* key; this one walks the much
|
||||
* smaller PKCS#8 OneAsymmetricKey structure to a *private* key seed.
|
||||
* Small enough that duplicating the handful of TLV-walking lines is
|
||||
* simpler and easier to audit independently than threading a shared
|
||||
* header between the kernel crypto tree and host build tooling.
|
||||
*/
|
||||
#ifndef TOOLS_PKCS8_ED25519_H
|
||||
#define TOOLS_PKCS8_ED25519_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/* Returns 0 on success (seed_out[32] filled), -1 on any malformed
|
||||
* encoding or non-Ed25519 algorithm. Never faults on malformed input. */
|
||||
int pkcs8_extract_ed25519_seed(const uint8_t *der, size_t der_len,
|
||||
uint8_t seed_out[32]);
|
||||
|
||||
#endif /* TOOLS_PKCS8_ED25519_H */
|
||||
Reference in New Issue
Block a user