Milestone 6: BLOCK_MAP.md signature-status column

--manifest mode's file scan is a completely separate code path from
build mode (only ever walks .4th files, never the embedded PKI cert or
font capsule) -- extended it to accept the same optional --sign-key
<path> prefix build mode already has, factoring the key-loading code
into a shared load_sign_key(), so the manifest can report real
per-capsule signing status without touching or requiring a rebuild of
capsule_generated.c.

New "Signed" column on the capsule summary table: yes/no when
--sign-key was given, n/a (with an explanatory footnote) when it
wasn't -- never a bare blank that could be misread as "unsigned".
Makefile.starkernel's manifest-generation call site now passes the same
SIGN_KEY_ARGS the real build uses, so capsules/BLOCK_MAP.md reflects
this machine's actual signed state by default.

Verified: clean compile, BLOCK_MAP.md correctly shows "yes" for all 31
tracked capsules on a real signed build; a quick amd64 boot (no kernel
code touched, host tooling only) confirmed no regression.

This closes every open Milestone 6 item except magic-number
content-type detection (shared with Milestone 4, not started) and the
hard-refuse flip (deliberately deferred). 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:43:50 -04:00
co-authored by Claude Sonnet 5
parent 2fc55f47e1
commit c640f99211
5 changed files with 9170 additions and 75 deletions
+21
View File
@@ -802,6 +802,27 @@ decisions get added here, not to `FABRIC-2.md`. Follow the same discipline `FABR
clean for a while, not bundled into this pass) and the `BLOCK_MAP.md` signature-status
column.
**`BLOCK_MAP.md` signature-status column, done 2026-08-26.** `--manifest` mode's file scan
(`manifest_file()`) is a completely separate code path from build mode's `process_file()` —
confirmed it only ever walks `.4th` files (never the embedded PKI cert or font capsule), and
the Makefile invokes `mkcapsule` twice per build for two different purposes (once to emit
`capsule_generated.c`, once for the manifest). Extended `--manifest` to accept the same
optional `--sign-key <path>` prefix as build mode (factored the key-loading code, previously
inline in build mode's arg parsing, into a shared `load_sign_key()` used by both) so the
manifest can report real per-capsule signing status without touching or requiring a rebuild
of `capsule_generated.c`. New `Signed` column on the capsule summary table: `yes`/`no` when
`--sign-key` was given, `n/a` (with an explanatory footnote) when it wasn't — never a bare
blank that could be misread as "unsigned." `Makefile.starkernel`'s manifest-generation call
site updated to pass the same `$(SIGN_KEY_ARGS)` the real build already uses, so
`capsules/BLOCK_MAP.md` reflects this machine's actual signed state by default. Verified:
clean compile, `capsules/BLOCK_MAP.md` correctly shows `yes` for all 31 tracked `.4th`
capsules on a real signed build; a quick amd64 boot (no kernel code touched by this step,
only host tooling) confirmed no regression, zero signature warnings, conservation intact.
**This closes every open item from FABRIC-2.md's original Milestone 6 punch list except
magic-number content-type detection (Section U item 14, shared with Milestone 4, not
started) and the hard-refuse flip (deliberately deferred).**
### From FABRIC-2.md §X, Milestone 7 — Contributor capsules / trust tiers
- [ ] Create the `capsules/contrib/` directory (mechanically trivial, matches existing
+2 -2
View File
@@ -588,12 +588,12 @@ $(CAPSULE_GENERATED): $(MKCAPSULE_BIN) $(CAPSULE_SRCS)
@echo " MKCAP $(CAPSULES_DIR) -> $@"
@$(MKCAPSULE_BIN) $(SIGN_KEY_ARGS) $(CAPSULES_DIR) $@
@echo " MKCAP $(CAPSULES_DIR) -> $(CAPSULES_DIR)/BLOCK_MAP.md"
@$(MKCAPSULE_BIN) --manifest $(CAPSULES_DIR) $(CAPSULES_DIR)/BLOCK_MAP.md
@$(MKCAPSULE_BIN) --manifest $(SIGN_KEY_ARGS) $(CAPSULES_DIR) $(CAPSULES_DIR)/BLOCK_MAP.md
# manifest — regenerate capsules/BLOCK_MAP.md without a full kernel build
.PHONY: manifest
manifest: $(MKCAPSULE_BIN)
@$(MKCAPSULE_BIN) --manifest $(CAPSULES_DIR) $(CAPSULES_DIR)/BLOCK_MAP.md
@$(MKCAPSULE_BIN) --manifest $(SIGN_KEY_ARGS) $(CAPSULES_DIR) $(CAPSULES_DIR)/BLOCK_MAP.md
@echo " Wrote $(CAPSULES_DIR)/BLOCK_MAP.md"
$(CAPSULE_GENERATED_OBJ): $(CAPSULE_GENERATED)
+34 -34
View File
@@ -1,44 +1,44 @@
# Capsule Block Manifest — Auto-generated
<!-- Generated by mkcapsule --manifest 2026-08-27T01:28:54Z -->
<!-- Generated by mkcapsule --manifest 2026-08-27T01:40:17Z -->
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
<!-- Hand-written justifications and immutability notes live -->
<!-- in MANIFEST.md alongside this auto-generated index. -->
## Capsule Summary
| Capsule | Blocks claimed | xxHash64 |
|---------|----------------|----------|
| `ACL.4th` | 4000, 4001, 4002, 4003, 4004, 4005, 4006, 4007, 4015 | `0xd781d22148ff171d` |
| `artemis:init.4th` | 4110, 4111, 4112, 4113, 4122, 4123, 4124, 4125, 4126, 4127, 4128, 4129, 4130, 4131, 4132, 4133, 4134, 4135, 4136, 4137, 4138, 4139, 4140, 4141, 4160, 4161, 4162, 4163, 4164, 4165, 4166, 4167, 4168, 4169, 4170, 4171, 4172, 4173, 4174, 4177, 4178, 4179, 4180, 4181, 4182, 4851, 4852 | `0x1859f0148048a2c1` |
| `common:msg.4th` | 4055 | `0x850a0382344ea6c4` |
| `doe-campaign.4th` | 4060, 4061, 4062, 4063, 4064, 4065 | `0x3d4549142d91ec20` |
| `doe.4th` | 2100, 2101, 2102, 2103, 2104, 2105, 2106, 2107 | `0xf154616d248e861f` |
| `fabric.4th` | 4900, 4901, 4902, 4903, 4904, 4905, 4906, 4907, 4908, 4909, 4910, 4911, 4912, 4913, 4914, 4915, 4916, 4917, 4918, 4919, 4920, 4921, 4922, 4923, 4924, 5000, 5001, 5002 | `0x9b1d061339cea98d` |
| `font.4th` | 4925, 4926, 4927, 4928, 4929, 4930, 4931, 4932, 4933, 4934, 4935, 4936, 4937, 4938, 4939, 4940, 4941, 4942, 4943, 4944, 4945, 4946, 4947, 4948, 4949, 4950, 4951, 4952, 4953, 4954, 4955, 4956, 4957, 4958, 4959, 4960, 4961, 4962, 4963, 4964, 4965, 4966, 4967, 4968, 4969, 4970, 4971, 4972, 4973, 4974, 4975, 4976, 4977, 4978, 4979, 4980, 4981, 4982, 4983, 4984, 4985 | `0x720792b4fc758156` |
| `hermes:init.4th` | 4100, 4101, 4102, 4103, 4104, 4105, 4106, 4107, 4108, 4109, 4114, 4115, 4116, 4117, 4118, 4119, 4120, 4121, 4142, 4143, 4144, 4145, 4146, 4147, 4148, 4149, 4150, 4151, 4152, 4153, 4154, 4155, 4156, 4157, 4158, 4159, 4175, 4176 | `0xfbb1208133abbeef` |
| `init-0.4th` | 2200, 2201 | `0xd0a9550baf786bb3` |
| `init-1.4th` | 4406, 4415, 4425, 4435 | `0x63e251adb0a03613` |
| `init-2.4th` | 4506, 4515, 4525, 4535, 4545 | `0xf113b3d0bcccae47` |
| `init-3.4th` | 4606, 4615, 4625, 4635, 4645, 4655, 4665 | `0x62b7a71576ad1041` |
| `init-4.4th` | 2130, 2131, 2132 | `0x89e3ef1db5ac0627` |
| `init-5.4th` | *(none — raw code capsule)* | `0xda0c179af10a46a9` |
| `init-6.4th` | 2080, 2081, 2082, 2083, 2084, 2085, 2086, 2087, 2088, 2089, 2090, 2091, 2092, 2093, 2094, 2095 | `0x06fc0ce1e369ef5a` |
| `init-7.4th` | 2150 | `0xb88df2efeb753d64` |
| `init-8.4th` | 2160 | `0x714746312b72f83d` |
| `init-9.4th` | 4706, 4715, 4725, 4735, 4745 | `0x3f2bec73142aa424` |
| `init-l8-diverse.4th` | 4820, 4821, 4822 | `0xaa293201a6c91838` |
| `init-l8-omni.4th` | 2064, 2065, 2066, 2067, 2068, 2069, 2070, 2071, 2072, 2073, 2074, 2075, 2076, 2077, 2078, 2079 | `0x5979e314d6452045` |
| `init-l8-stable.4th` | 4806 | `0xdc3830f189063a9a` |
| `init-l8-temporal.4th` | 4830, 4831 | `0x51abd4c138246651` |
| `init-l8-transition.4th` | 4840, 4841, 4842 | `0xbcc1a81976f0a4c9` |
| `init-l8-volatile.4th` | 4810, 4811, 4812, 4813 | `0x98caabbbd92abac4` |
| `init.4th` | 2049, 2050, 2057 | `0x214d424abd382707` |
| `lib.4th` | 4050 | `0x1c77d7c9562a5b62` |
| `process.4th` | 4300, 4301 | `0x781afc1dbd0294f7` |
| `sdk.4th` | 5109, 5110, 5111, 5112, 5113, 5114, 5115 | `0x008fdbbb62c94a3a` |
| `turtle.4th` | 5100, 5101, 5102, 5103, 5104, 5105, 5106, 5107, 5108 | `0x4d470418ca543365` |
| `user-font-demo.4th` | 4200, 4201, 4202 | `0xce1fd7d1b581a56d` |
| `zuse.4th` | 4016, 4017, 4018 | `0x3b31872d02a43d83` |
| Capsule | Blocks claimed | xxHash64 | Signed |
|---------|----------------|----------|--------|
| `ACL.4th` | 4000, 4001, 4002, 4003, 4004, 4005, 4006, 4007, 4015 | `0xd781d22148ff171d` | yes |
| `artemis:init.4th` | 4110, 4111, 4112, 4113, 4122, 4123, 4124, 4125, 4126, 4127, 4128, 4129, 4130, 4131, 4132, 4133, 4134, 4135, 4136, 4137, 4138, 4139, 4140, 4141, 4160, 4161, 4162, 4163, 4164, 4165, 4166, 4167, 4168, 4169, 4170, 4171, 4172, 4173, 4174, 4177, 4178, 4179, 4180, 4181, 4182, 4851, 4852 | `0x1859f0148048a2c1` | yes |
| `common:msg.4th` | 4055 | `0x850a0382344ea6c4` | yes |
| `doe-campaign.4th` | 4060, 4061, 4062, 4063, 4064, 4065 | `0x3d4549142d91ec20` | yes |
| `doe.4th` | 2100, 2101, 2102, 2103, 2104, 2105, 2106, 2107 | `0xf154616d248e861f` | yes |
| `fabric.4th` | 4900, 4901, 4902, 4903, 4904, 4905, 4906, 4907, 4908, 4909, 4910, 4911, 4912, 4913, 4914, 4915, 4916, 4917, 4918, 4919, 4920, 4921, 4922, 4923, 4924, 5000, 5001, 5002 | `0x9b1d061339cea98d` | yes |
| `font.4th` | 4925, 4926, 4927, 4928, 4929, 4930, 4931, 4932, 4933, 4934, 4935, 4936, 4937, 4938, 4939, 4940, 4941, 4942, 4943, 4944, 4945, 4946, 4947, 4948, 4949, 4950, 4951, 4952, 4953, 4954, 4955, 4956, 4957, 4958, 4959, 4960, 4961, 4962, 4963, 4964, 4965, 4966, 4967, 4968, 4969, 4970, 4971, 4972, 4973, 4974, 4975, 4976, 4977, 4978, 4979, 4980, 4981, 4982, 4983, 4984, 4985 | `0x720792b4fc758156` | yes |
| `hermes:init.4th` | 4100, 4101, 4102, 4103, 4104, 4105, 4106, 4107, 4108, 4109, 4114, 4115, 4116, 4117, 4118, 4119, 4120, 4121, 4142, 4143, 4144, 4145, 4146, 4147, 4148, 4149, 4150, 4151, 4152, 4153, 4154, 4155, 4156, 4157, 4158, 4159, 4175, 4176 | `0xfbb1208133abbeef` | yes |
| `init-0.4th` | 2200, 2201 | `0xd0a9550baf786bb3` | yes |
| `init-1.4th` | 4406, 4415, 4425, 4435 | `0x63e251adb0a03613` | yes |
| `init-2.4th` | 4506, 4515, 4525, 4535, 4545 | `0xf113b3d0bcccae47` | yes |
| `init-3.4th` | 4606, 4615, 4625, 4635, 4645, 4655, 4665 | `0x62b7a71576ad1041` | yes |
| `init-4.4th` | 2130, 2131, 2132 | `0x89e3ef1db5ac0627` | yes |
| `init-5.4th` | *(none — raw code capsule)* | `0xda0c179af10a46a9` | yes |
| `init-6.4th` | 2080, 2081, 2082, 2083, 2084, 2085, 2086, 2087, 2088, 2089, 2090, 2091, 2092, 2093, 2094, 2095 | `0x06fc0ce1e369ef5a` | yes |
| `init-7.4th` | 2150 | `0xb88df2efeb753d64` | yes |
| `init-8.4th` | 2160 | `0x714746312b72f83d` | yes |
| `init-9.4th` | 4706, 4715, 4725, 4735, 4745 | `0x3f2bec73142aa424` | yes |
| `init-l8-diverse.4th` | 4820, 4821, 4822 | `0xaa293201a6c91838` | yes |
| `init-l8-omni.4th` | 2064, 2065, 2066, 2067, 2068, 2069, 2070, 2071, 2072, 2073, 2074, 2075, 2076, 2077, 2078, 2079 | `0x5979e314d6452045` | yes |
| `init-l8-stable.4th` | 4806 | `0xdc3830f189063a9a` | yes |
| `init-l8-temporal.4th` | 4830, 4831 | `0x51abd4c138246651` | yes |
| `init-l8-transition.4th` | 4840, 4841, 4842 | `0xbcc1a81976f0a4c9` | yes |
| `init-l8-volatile.4th` | 4810, 4811, 4812, 4813 | `0x98caabbbd92abac4` | yes |
| `init.4th` | 2049, 2050, 2057 | `0x214d424abd382707` | yes |
| `lib.4th` | 4050 | `0x1c77d7c9562a5b62` | yes |
| `process.4th` | 4300, 4301 | `0x781afc1dbd0294f7` | yes |
| `sdk.4th` | 5109, 5110, 5111, 5112, 5113, 5114, 5115 | `0x008fdbbb62c94a3a` | yes |
| `turtle.4th` | 5100, 5101, 5102, 5103, 5104, 5105, 5106, 5107, 5108 | `0x4d470418ca543365` | yes |
| `user-font-demo.4th` | 4200, 4201, 4202 | `0xce1fd7d1b581a56d` | yes |
| `zuse.4th` | 4016, 4017, 4018 | `0x3b31872d02a43d83` | yes |
## Block Map (sorted by LBN)
File diff suppressed because it is too large Load Diff
+76 -39
View File
@@ -169,6 +169,7 @@ typedef struct {
uint64_t hash;
int blocks[MAX_BLOCKS_PER_CAPSULE];
int block_count;
int has_sig; /* 1 if signed this run (--manifest --sign-key <path>) */
} ManifestEntry;
static ManifestEntry manifest_entries[MAX_CAPSULES];
@@ -607,6 +608,12 @@ static int manifest_file(const char *fpath, const struct stat *sb,
e->block_count = collect_block_numbers(data, (size_t)sz,
e->blocks, MAX_BLOCKS_PER_CAPSULE);
qsort(e->blocks, (size_t)e->block_count, sizeof(int), cmp_int);
e->has_sig = 0;
if (have_sign_seed) {
uint8_t sig[64];
sign_capsule_bytes(data, (size_t)sz, sig);
e->has_sig = 1;
}
free(data);
return 0;
@@ -654,8 +661,8 @@ static void generate_manifest(FILE *out) {
/* Capsule summary */
fprintf(out, "## Capsule Summary\n\n");
fprintf(out, "| Capsule | Blocks claimed | xxHash64 |\n");
fprintf(out, "|---------|----------------|----------|\n");
fprintf(out, "| Capsule | Blocks claimed | xxHash64 | Signed |\n");
fprintf(out, "|---------|----------------|----------|--------|\n");
for (int i = 0; i < manifest_count; i++) {
ManifestEntry *e = &manifest_entries[i];
fprintf(out, "| `%s` | ", e->name);
@@ -667,9 +674,16 @@ static void generate_manifest(FILE *out) {
fprintf(out, "%d", e->blocks[j]);
}
}
fprintf(out, " | `0x%016" PRIx64 "` |\n", e->hash);
fprintf(out, " | `0x%016" PRIx64 "` | %s |\n", e->hash,
have_sign_seed ? (e->has_sig ? "yes" : "no") : "n/a");
}
fprintf(out, "\n");
if (!have_sign_seed) {
fprintf(out,
"*Signed column is `n/a`: this manifest run had no `--sign-key`. "
"Re-run with `--sign-key <path>` to check signing status "
"(does not modify or require rebuilding capsule_generated.c).*\n\n");
}
/* Block map sorted by LBN */
fprintf(out, "## Block Map (sorted by LBN)\n\n");
@@ -972,10 +986,57 @@ static void generate_output(FILE *out) {
* Main
*===========================================================================*/
/* Loads key_path, extracts its Ed25519 seed into sign_seed, sets
* have_sign_seed=1 on success. Shared by build mode and --manifest mode
* (both accept an optional --sign-key <path>). Returns 0 on success,
* writes an ERROR to stderr and returns -1 on failure. */
static int load_sign_key(const char *key_path) {
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);
return 0;
}
int main(int argc, char **argv) {
/* --manifest mode: generate block ownership markdown */
/* --manifest mode: generate block ownership markdown. Optional
* --sign-key <keyfile> prefix: mkcapsule --manifest [--sign-key
* <keyfile>] <capsules_dir> [<out.md>] -- lets the manifest report
* real signing status without touching capsule_generated.c. */
if (argc >= 3 && strcmp(argv[1], "--manifest") == 0) {
base_dir = argv[2];
int a = 2;
if (argc >= 4 && strcmp(argv[2], "--sign-key") == 0) {
if (argc < 5) {
fprintf(stderr, "mkcapsule: ERROR: --manifest --sign-key "
"requires <keyfile> <capsules_dir>\n");
return 1;
}
if (load_sign_key(argv[3]) != 0) return 1;
a = 4;
}
base_dir = argv[a];
base_dir_len = strlen(base_dir);
while (base_dir_len > 0 && base_dir[base_dir_len - 1] == '/') base_dir_len--;
@@ -985,18 +1046,19 @@ int main(int argc, char **argv) {
}
FILE *mout = stdout;
if (argc == 4) {
mout = fopen(argv[3], "w");
int have_out = (argc == a + 2);
if (have_out) {
mout = fopen(argv[a + 1], "w");
if (!mout) {
fprintf(stderr, "mkcapsule: ERROR: cannot create '%s'\n", argv[3]);
fprintf(stderr, "mkcapsule: ERROR: cannot create '%s'\n", argv[a + 1]);
return 1;
}
}
generate_manifest(mout);
if (mout != stdout) {
if (have_out) {
fclose(mout);
fprintf(stderr, "mkcapsule: manifest written to %s (%d capsule(s))\n",
argv[3], manifest_count);
argv[a + 1], manifest_count);
}
return 0;
}
@@ -1026,32 +1088,7 @@ int main(int argc, char **argv) {
"<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);
if (load_sign_key(argv[2]) != 0) return 1;
arg0 = 3;
}
@@ -1061,8 +1098,8 @@ int main(int argc, char **argv) {
" %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"
" %s --manifest [--sign-key <keyfile>] <capsules_dir> [<out.md>]\n"
" print/write block manifest\n"
"\n"
"Lint checks (per .4th file):\n"
" - each block opens with 'Block N'\n"
@@ -1078,7 +1115,7 @@ int main(int argc, char **argv) {
"\n"
"Names are colon-separated relative paths (e.g. core:init.4th).\n"
"Files whose encoded name exceeds %d bytes are skipped with a warning.\n",
argv[0], argv[0], argv[0], argv[0], CAPSULE_NAME_MAX - 1);
argv[0], argv[0], argv[0], CAPSULE_NAME_MAX - 1);
return 1;
}