capsule_mint_identity()'s MINT_DEFAULT_PERSONALITY only ever defined WELCOME, unlike capsule_console.c's own CONSOLE_IDENTITY_SRC. The moment WIREBIND made a freshly-minted identity's VM live, Hera's per-idle-tick MSG-TICK pump (repl.c Phase C) began erroring on it every tick forever -- UNKNOWN WORD: 'MSG-TICK'. Every identity minted before this fix would hit the same infinite error-spam on going live, not just this one. Fixed by loading common:messaging.4th + calling MSG-CD-INIT before WELCOME, mirroring capsule_console.c's own pattern exactly. Found and verified minting the first real second identity end to end: disk/captain-bob.img, hot-attached via QMP alongside an already- authenticated Zuse session, MINTed, WIREBIND-attached, switched into live with USE, WELCOME confirmed. Zero MSG-TICK errors over 18+ seconds of idle-loop cycling after the fix (was immediate and continuous before). Full three-architecture regression clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KBjfeLPo71sUQ8zC7V7P5m
193 lines
8.5 KiB
C
193 lines
8.5 KiB
C
/*
|
||
StarForth — Steady-State Virtual Machine Runtime
|
||
|
||
Copyright (c) 2023–2025 Robert A. James
|
||
All rights reserved.
|
||
|
||
Licensed under the StarForth License, Version 1.0
|
||
*/
|
||
|
||
#ifndef __STARKERNEL__
|
||
#error "capsule_mint.c is kernel-only"
|
||
#endif
|
||
|
||
#include "starkernel/capsule_mint.h"
|
||
#include "starkernel/homeblocks_sig.h"
|
||
#include "starkernel/user_identity_seed.h"
|
||
#include "starkernel/x509_ed25519.h"
|
||
#include "starkernel/ed25519.h"
|
||
#include "starkernel/virtio_rng.h"
|
||
#include "block_subsystem.h" /* compute_crc64() */
|
||
#include "blkio.h"
|
||
#include <string.h>
|
||
#include <stddef.h>
|
||
|
||
/* Fixed layout, devblock offsets from the drive's own start -- see
|
||
* capsule_mint.h's own doc comment. */
|
||
#define MINT_CERT_DEVBLOCK 2u
|
||
#define MINT_CERT_DEVBLOCKS 1u
|
||
#define MINT_IDENTITY_SRC_OFFSET 3u
|
||
#define MINT_IDENTITY_SRC_DEVBLOCKS 2u /* 1 seed record + 1 personality source */
|
||
#define MINT_METADATA_DEVBLOCKS 5u /* devblocks 0..4 reserved */
|
||
|
||
/* Real, working, minimal default personality -- FABRIC-3.md §F.6/§F.8's
|
||
* "default personality content" question stays open (mint-time-fixed vs.
|
||
* user-editable), but the identity this mints must actually do something
|
||
* when RUNCAP births it, not just exist. Needs the "Block NNNN" header
|
||
* capsule_exec_payload() requires (§F.18) -- discovered live, not
|
||
* optional. 4999 is an unused block number (see capsules/ block-map).
|
||
*
|
||
* Loads common:messaging.4th + MSG-CD-INIT exactly like capsule_console.c's
|
||
* own CONSOLE_IDENTITY_SRC -- every live VM except Hera owns a MSG-TICK
|
||
* word for her per-idle-tick pump to VM-EXEC into (repl.c's Phase C loop).
|
||
* A minted identity's RUNCAP-born VM is not exempt from that pump, so
|
||
* omitting this left it spamming "UNKNOWN WORD: 'MSG-TICK'" every idle
|
||
* tick forever from the moment WIREBIND made the VM live -- found live
|
||
* minting a real second identity (FABRIC-3.md §F.26 follow-up). */
|
||
static const char MINT_DEFAULT_PERSONALITY[] =
|
||
"Block 4999\n"
|
||
"S\" common:messaging.4th\" EXEC\n"
|
||
"MSG-CD-INIT\n"
|
||
": WELCOME ( -- ) .\" Minted identity -- default personality\" CR ;\n"
|
||
"WELCOME\n";
|
||
|
||
/* Write exactly one devblock (4096 bytes) at devblock offset `devblock`,
|
||
* as 4 consecutive 1KiB forth-block writes -- same convention
|
||
* homeblocks_sig_check()'s own read loop already uses. */
|
||
static int write_devblock(struct blkio_dev *dev, uint32_t devblock,
|
||
const uint8_t *buf4096) {
|
||
uint32_t base = devblock * 4u;
|
||
for (uint32_t i = 0; i < 4u; i++) {
|
||
if (blkio_write((blkio_dev_t *)dev, base + i,
|
||
buf4096 + (size_t)i * BLKIO_FORTH_BLOCK_SIZE) != BLKIO_OK) {
|
||
return -1;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
MintResult capsule_mint_identity(struct blkio_dev *dev, VM *issuer_vm,
|
||
const char *full_name, const char *username,
|
||
const char *email, const char *phone,
|
||
uint8_t out_pubkey[32], uint8_t out_seed[32],
|
||
int drive_known_blank) {
|
||
if (!dev || !full_name || !username) return MINT_ERR_WRITE_FAIL;
|
||
|
||
/* full_name/username required and must fit; email/phone may be NULL
|
||
* (treated as empty/null) but must fit if given. */
|
||
if (full_name[0] == '\0' || strlen(full_name) >= USER_IDENTITY_FULL_NAME_MAX)
|
||
return MINT_ERR_INVALID_PROFILE;
|
||
if (username[0] == '\0' || strlen(username) >= USER_IDENTITY_USERNAME_MAX)
|
||
return MINT_ERR_INVALID_PROFILE;
|
||
if (email && strlen(email) >= USER_IDENTITY_EMAIL_MAX)
|
||
return MINT_ERR_INVALID_PROFILE;
|
||
if (phone && strlen(phone) >= USER_IDENTITY_PHONE_MAX)
|
||
return MINT_ERR_INVALID_PROFILE;
|
||
|
||
/* Refuse to overwrite an already-recognized home-blocks drive --
|
||
* mirrors WRITE(10)'s own refuse-on-non-blank-media posture (§F.8,
|
||
* decided 2026-08-28: "reasonable by analogy" there, now decided).
|
||
* Skipped when the caller already just confirmed HOMEBLOCKS_SIG_BLANK
|
||
* itself (drive_known_blank) -- otherwise this repeats the exact same
|
||
* full BOT read sequence a second time immediately after the first. */
|
||
if (!drive_known_blank) {
|
||
homeblocks_sig_t existing;
|
||
if (homeblocks_sig_check(dev, HOMEBLOCKS_SIG_START_FBLOCK, &existing)
|
||
== HOMEBLOCKS_SIG_OK) {
|
||
return MINT_ERR_ALREADY_MINTED;
|
||
}
|
||
}
|
||
|
||
/* issuer_vm==NULL is genesis mode (§F.21) -- no existing Zuse to
|
||
* require a cert from. */
|
||
if (issuer_vm && !issuer_vm->zuse_cert_installed) return MINT_ERR_NO_ZUSE_CERT;
|
||
if (!virtio_rng_ready()) return MINT_ERR_NO_ENTROPY;
|
||
|
||
/* Fresh identity keypair. */
|
||
uint8_t seed[32], pubkey[32];
|
||
if (virtio_rng_get_bytes(seed, sizeof(seed)) != 0) return MINT_ERR_NO_ENTROPY;
|
||
ed25519_keygen(seed, pubkey);
|
||
if (out_pubkey) memcpy(out_pubkey, pubkey, 32);
|
||
if (out_seed) memcpy(out_seed, seed, 32);
|
||
|
||
/* Fresh drive_uuid -- an independent random draw, not derived from
|
||
* the identity seed (§F.8 decision 3: "which physical drive," not
|
||
* "whose identity"). */
|
||
uint8_t drive_uuid[16];
|
||
if (virtio_rng_get_bytes(drive_uuid, sizeof(drive_uuid)) != 0)
|
||
return MINT_ERR_NO_ENTROPY;
|
||
|
||
uint32_t cert_devblock = 0;
|
||
uint32_t cert_devblocks = 0;
|
||
if (issuer_vm) {
|
||
/* Cert: SubjectPublicKeyInfo = new pubkey, serialNumber =
|
||
* drive_uuid, signed by Zuse's own on-device seed (§F.8
|
||
* decision 4). Genesis mode (issuer_vm==NULL) skips this
|
||
* entirely -- Zuse's own identity isn't verified against a
|
||
* separate signer, she's recognized directly by pubkey match
|
||
* against zuse_genesis_marker_t (§F.21). */
|
||
uint8_t cert[512];
|
||
size_t cert_len = x509_build_user_cert(cert, sizeof(cert), pubkey,
|
||
drive_uuid, issuer_vm->zuse_cert_seed);
|
||
if (cert_len == 0 || cert_len > 4096) return MINT_ERR_CERT_BUILD;
|
||
|
||
uint8_t cert_block[4096];
|
||
memset(cert_block, 0, sizeof(cert_block));
|
||
memcpy(cert_block, cert, cert_len);
|
||
if (write_devblock(dev, MINT_CERT_DEVBLOCK, cert_block) != 0)
|
||
return MINT_ERR_WRITE_FAIL;
|
||
|
||
cert_devblock = MINT_CERT_DEVBLOCK;
|
||
cert_devblocks = MINT_CERT_DEVBLOCKS;
|
||
}
|
||
|
||
/* user_identity_seed_t record. */
|
||
user_identity_seed_t idrec;
|
||
memset(&idrec, 0, sizeof(idrec));
|
||
idrec.magic = USER_IDENTITY_SEED_MAGIC;
|
||
idrec.version = USER_IDENTITY_SEED_VERSION;
|
||
memcpy(idrec.seed, seed, 32);
|
||
memcpy(idrec.pubkey, pubkey, 32);
|
||
memcpy(idrec.full_name, full_name, strlen(full_name));
|
||
memcpy(idrec.username, username, strlen(username));
|
||
if (email) memcpy(idrec.email, email, strlen(email));
|
||
if (phone) memcpy(idrec.phone, phone, strlen(phone));
|
||
idrec.crc = compute_crc64((const uint8_t *)&idrec, offsetof(user_identity_seed_t, crc));
|
||
if (write_devblock(dev, MINT_IDENTITY_SRC_OFFSET, (const uint8_t *)&idrec) != 0)
|
||
return MINT_ERR_WRITE_FAIL;
|
||
|
||
/* Default personality source, in the devblock right after the seed
|
||
* record -- RUNCAP reads exactly this layout. */
|
||
uint8_t personality_block[4096];
|
||
memset(personality_block, 0, sizeof(personality_block));
|
||
memcpy(personality_block, MINT_DEFAULT_PERSONALITY, sizeof(MINT_DEFAULT_PERSONALITY) - 1);
|
||
if (write_devblock(dev, MINT_IDENTITY_SRC_OFFSET + 1u, personality_block) != 0)
|
||
return MINT_ERR_WRITE_FAIL;
|
||
|
||
/* homeblocks_sig_t -- written last, so a crash/power-loss partway
|
||
* through minting never leaves a drive that reads as a valid,
|
||
* complete home-blocks drive with missing content behind it. */
|
||
homeblocks_sig_t sig;
|
||
memset(&sig, 0, sizeof(sig));
|
||
sig.magic = HOMEBLOCKS_SIG_PACK(HOMEBLOCKS_SIG_VERSION_0);
|
||
memcpy(sig.drive_uuid, drive_uuid, 16);
|
||
sig.minted_time_ns = 0; /* no monotonic-ns source exists
|
||
* anywhere in this codebase yet,
|
||
* §F.8's own open item */
|
||
sig.metadata_devblocks = MINT_METADATA_DEVBLOCKS;
|
||
sig.cert_offset = cert_devblock;
|
||
sig.cert_devblocks = cert_devblocks;
|
||
sig.identity_src_offset = MINT_IDENTITY_SRC_OFFSET;
|
||
sig.identity_src_devblocks = MINT_IDENTITY_SRC_DEVBLOCKS;
|
||
sig.hdr_crc = homeblocks_sig_compute_crc(&sig);
|
||
|
||
uint8_t sig_block[4096];
|
||
memcpy(sig_block, &sig, sizeof(sig));
|
||
if (write_devblock(dev, HOMEBLOCKS_SIG_START_FBLOCK / 4u, sig_block) != 0)
|
||
return MINT_ERR_WRITE_FAIL;
|
||
|
||
(void)blkio_flush((blkio_dev_t *)dev);
|
||
|
||
return MINT_OK;
|
||
}
|