/* 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/rng.h" #include "block_subsystem.h" /* compute_crc64() */ #include "blkio.h" #include "log.h" #include #include /* 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-2.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-2.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; } /* Read exactly one devblock (4096 bytes) at devblock offset `devblock`, * mirroring write_devblock() above -- used only by the post-write * verification pass (2026-09-06), to read back what was just written * rather than trusting blkio_write()'s BLK_OK return alone. */ static int read_devblock(struct blkio_dev *dev, uint32_t devblock, uint8_t *buf4096) { uint32_t base = devblock * 4u; for (uint32_t i = 0; i < 4u; i++) { if (blkio_read((blkio_dev_t *)dev, base + i, buf4096 + (size_t)i * BLKIO_FORTH_BLOCK_SIZE) != BLKIO_OK) { return -1; } } return 0; } /* Post-write verification (2026-09-06, direct instruction after a live * xHCI enumeration failure raised the question of whether MINT itself * could silently succeed without the data actually being readable back): * re-reads the identity record and re-runs homeblocks_sig_check() -- * the exact same check a real attach later performs -- instead of trusting * every write_devblock() call's own BLK_OK return as proof the drive will * actually read back correctly. blkio_write() succeeding only means the * BOT command chain completed; it says nothing about whether those bytes * survive to be read back under real hardware/emulation conditions. * Logged entirely through log_message() -- LOG_ERROR with the specific * mismatch on failure, LOG_INFO confirming success -- not console_println, * so this doesn't add unconditional console/serial noise to every mint; * the MINT word's own caller (mama_forth_words.c) still reports the final * pass/fail result to the console either way via its existing console_ * println() switch. Returns 0 if everything reads back correctly, -1 * otherwise. */ static int verify_mint(struct blkio_dev *dev, uint32_t identity_src_offset, const user_identity_seed_t *written_idrec) { homeblocks_sig_t verify_sig; homeblocks_sig_result_t sig_rc = homeblocks_sig_check(dev, HOMEBLOCKS_SIG_START_FBLOCK, &verify_sig); if (sig_rc != HOMEBLOCKS_SIG_OK) { log_message(LOG_ERROR, "MINT verify: homeblocks_sig_check() did not read back OK (rc=%d)", (int) sig_rc); return -1; } uint8_t readback_block[4096]; if (read_devblock(dev, identity_src_offset, readback_block) != 0) { log_message(LOG_ERROR, "MINT verify: identity record read-back failed"); return -1; } const user_identity_seed_t *readback_idrec = (const user_identity_seed_t *) readback_block; if (readback_idrec->magic != written_idrec->magic || readback_idrec->version != written_idrec->version) { log_message(LOG_ERROR, "MINT verify: identity record magic/version mismatch on read-back"); return -1; } uint64_t want_crc = compute_crc64((const uint8_t *) readback_idrec, offsetof(user_identity_seed_t, crc)); if (want_crc != readback_idrec->crc) { log_message(LOG_ERROR, "MINT verify: identity record CRC mismatch on read-back"); return -1; } if (memcmp(readback_idrec, written_idrec, sizeof(*written_idrec)) != 0) { log_message(LOG_ERROR, "MINT verify: identity record content differs from what was written"); return -1; } log_message(LOG_INFO, "MINT verify: identity reads back correctly"); 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 (!rng_ready()) return MINT_ERR_NO_ENTROPY; /* Fresh identity keypair. */ uint8_t seed[32], pubkey[32]; if (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 (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); if (verify_mint(dev, MINT_IDENTITY_SRC_OFFSET, &idrec) != 0) { return MINT_ERR_VERIFY_FAILED; } return MINT_OK; }