diff --git a/docs/00-status.md b/docs/00-status.md index d5c1ef1..fcab355 100644 --- a/docs/00-status.md +++ b/docs/00-status.md @@ -118,6 +118,7 @@ that sequences its tasks. Read one, approve, then the next starts. | 9 | [Database engine](stories/language-runtime-database/09-database-engine.md) | ⬜ | | 9b | [`@table`, relations, query](stories/language-runtime-database/09b-table-relations-query.md) | ⬜ spec + plan ready (2026-08-15) | | 9c | [Cross-program tables](stories/language-runtime-database/09c-cross-program-tables.md) | ⬜ needs a spec first | +| 9d | [Keypair attach auth](stories/language-runtime-database/09d-keypair-attach-auth.md) | ⬜ needs a spec first | | 10 | [HTTP service layer](stories/language-runtime-database/10-http-service.md) | ⬜ | Hold | | 11 | [Fibers](stories/language-runtime-database/11-fibers.md) | ⬜ | Hold | | 12 | [Blue-green deploy](stories/language-runtime-database/12-blue-green-deploy.md) | ⬜ | Hold | @@ -325,6 +326,7 @@ Ecommerce sample (verified 2026-06-13): `api.rest` 17/17 expected statuses pass. | 9 | Database engine binding | [plan 5](superpowers/plans/2026-08-01-db-engine-binding.md) | | 9b | `@table` + relations + language-integrated query — comprehension queries, `ref`/`backlink` navigation, GroupBy aggregates; acceptance: new `docs/examples/employee` sample | [spec](superpowers/specs/2026-08-15-table-relations-query-design.md) · [plan](plan/compiler/2026-08-15-employee-relations-query.md) | | 9c | Cross-program tables — attach to a running program's database (IPC string in wo.toml, manifest-granted rights, owner stays the single writer) | **no spec yet** — four open forks recorded in the iteration; brainstorm before planning | +| 9d | Keypair attach auth — mutual challenge–response, grants name public keys, uid superseded | **no spec yet** — four forks recorded; plan folds into 9c's | | 10 | HTTP service layer | [plan 6](superpowers/plans/2026-08-01-http-service-layer.md) | | 11 | Fibers | vision §3, [blue-green exploration](plan/exploration/blue-green-vm/00-vision.md) | | 12 | Blue-green deploy | [spec](superpowers/specs/2026-08-03-blue-green-vm-design.md) — plan authored after iterations 9–10 | diff --git a/docs/stories/language-runtime-database/00-story.md b/docs/stories/language-runtime-database/00-story.md index 81fd6cc..4bec861 100644 --- a/docs/stories/language-runtime-database/00-story.md +++ b/docs/stories/language-runtime-database/00-story.md @@ -54,6 +54,7 @@ iterations); no commits by agents — drafts go to `.dev/commit.md`. | 9 | [Database engine](09-database-engine.md) | class-shaped tables, typed WAL + recovery, `insert`/`select` execute | | 9b | [`@table`, relations, query](09b-table-relations-query.md) | `@table` becomes real storage; typed `ref`/`backlink`/`multi` relations; compiler-checked LINQ-shaped queries lowered to engine ops | | 9c | [Cross-program tables](09c-cross-program-tables.md) | attach to a running program's database over a local IPC channel: manifest-granted read/write rights, typed statements checked against the owner's shapes, owner stays the single writer | +| 9d | [Keypair attach auth](09d-keypair-attach-auth.md) | program identity is a keypair: mutual challenge–response at attach, grants name public keys, replay-proof, rotation is a config change | | 10 | [HTTP service layer](10-http-service.md) | `service` blocks route to VM methods; REST parity with Stage 2 | | 11 | [Fibers](11-fibers.md) | green threads on the shard scheduler: reduction-budget preemption, park on I/O | | 12 | [Blue-green deploy](12-blue-green-deploy.md) | two VM slots, in-runtime compile, atomic switch, resident rollback | diff --git a/docs/stories/language-runtime-database/09c-cross-program-tables.md b/docs/stories/language-runtime-database/09c-cross-program-tables.md index 1df4d18..b18fea3 100644 --- a/docs/stories/language-runtime-database/09c-cross-program-tables.md +++ b/docs/stories/language-runtime-database/09c-cross-program-tables.md @@ -134,7 +134,10 @@ must be bound to something a peer cannot fake — unix peer credentials the milestone (same-machine, same-trust-domain), rights whole-database read or read+write (per-table refinement deferred until a workload needs it), and the registration is A's manifest so a grant is a config change + -restart, not an API. +restart, not an API. **Superseded as the end state (2026-08-15):** +identity is a keypair and grants name public keys — iteration +[9d](09d-keypair-attach-auth.md) owns that; the uid check is only this +iteration's bootstrap and must be flagged pre-9d wherever it ships. **4. What does B's statement actually block on?** B's insert crosses the channel, executes in A (RAM + WAL + fsync), and acknowledges back — a diff --git a/docs/stories/language-runtime-database/09d-keypair-attach-auth.md b/docs/stories/language-runtime-database/09d-keypair-attach-auth.md new file mode 100644 index 0000000..32dc470 --- /dev/null +++ b/docs/stories/language-runtime-database/09d-keypair-attach-auth.md @@ -0,0 +1,143 @@ +# Iteration 9d — keypair authentication for cross-program attach + +> Format: fiberloom `product/story-iteration-template`. Part of +> [Story — one language, one runtime, one database, one binary](00-story.md). +> +> **Inserted 2026-08-15.** Promotes iteration 9c's identity fork (Info, +> fork 3) to its own iteration: the name + unix-uid lean is the milestone +> bootstrap, and THIS is what replaces it — program identity is a keypair, +> and an attachment is granted to a public key, not to a process that +> happens to share a uid. It follows 9c (there is nothing to authenticate +> until attach exists) and stays same-machine; the same handshake is what +> a future remote channel would reuse, which is the point of doing it +> properly now. +> +> **No spec exists yet.** The forks in *Info* are genuine decisions. + +## Goals + +- **A program's identity is a keypair.** Each writeonce program owns a + private key (generated once, stored beside its data, never in the + manifest) and a public key it can print/export. Identity stops being + "whoever reached the socket first with the right uid". +- **Grants name public keys.** A's `[share]` registers a client by its + public key (fingerprint), with rights exactly as 9c defined them; B's + `[connect.a]` **pins A's public key** beside the IPC string. Both sides + authenticate: A proves it is A before B sends a byte of intent, B proves + it is B before A executes a statement. +- **The handshake is mutual challenge–response, replay-proof**: fresh + nonces each attach, signatures over the nonce + channel binding, no + secret ever crosses the channel. A failed handshake refuses the + attachment with a catchable trap on the connecting side and one log line + naming the offered fingerprint on the listening side. + +## Acceptance Criteria + +- What to achieve? + - **Given** A's `[share]` registering B's public-key fingerprint with + read+write, and B's `[connect.a]` pinning A's public key, + - **when** B attaches, + - **then** the mutual handshake completes, the attachment carries B's + granted rights, and every 9c acceptance behavior (statements, traps, + refusals) holds unchanged on top of it. +- What to achieve? + - **Given** a client presenting a keypair A never registered, + - **when** it attempts the handshake, + - **then** the attach is refused before any statement is read, the + client sees the catchable authentication trap, and A logs the offered + fingerprint (so granting it is a copy-paste, not an investigation). +- What to achieve? + - **Given** a same-uid process (the 9c bootstrap's whole trust basis) + presenting no key or the wrong key, + - **when** it attempts to attach, + - **then** it is refused — proving the uid check has been superseded, + not merely supplemented. +- What to achieve? + - **Given** an impostor listening on A's socket path (or a swapped + socket file), + - **when** B attaches and the impostor cannot sign A's challenge + response with A's private key, + - **then** B aborts before sending any statement or data, with a trap + that names the fingerprint mismatch — the pinned-key check working in + the B→A direction. +- What to achieve? + - **Given** a recorded handshake transcript from a legitimate attach, + - **when** it is replayed against A, + - **then** the attach is refused — the nonce is fresh per handshake and + a signature over an old nonce proves nothing. +- What to achieve? + - **Given** A rotates B's registered key (manifest update + restart), + - **when** B attaches with the old key and then with the new one, + - **then** the old key is refused and the new one works — rotation is a + config change, exactly like the grant itself. + +## Out Of Scope + +- **Transport encryption.** Same-machine unix sockets; the kernel is the + wire. Session encryption (and the key exchange it needs) arrives with a + remote channel, if one ever ships — this iteration's handshake is + designed not to preclude it, nothing more. +- **Certificate hierarchies, expiry, revocation lists.** A grant is a + public key in a manifest; revocation is deleting the line and + restarting. CA machinery has no workload here. +- **Key escrow / multi-key identities / agent forwarding.** One program, + one keypair. +- **Protecting the private key from a root attacker or from the program's + own uid.** File permissions (0600) are the boundary this iteration + claims; anything stronger (TPM, keyring) is explicitly not promised. + +## Info + +Forks the spec must settle: + +**1. Where does the crypto come from?** The runtime is libc-only by +doctrine, and hand-rolling signature crypto is the one wheel nobody gets to +reinvent. The realistic options: **vendor a compact, audited Ed25519** +implementation (TweetNaCl-lineage, a few files, no allocation, no OS +dependencies) into `database/src/` or a new `vendor/`; or take libsodium as +the first external dependency and break the doctrine openly. Leaning: +vendored compact Ed25519, recorded as the single sanctioned vendored +component with its provenance pinned in the tree — the doctrine's spirit is +"no dependency sprawl", not "write your own constant-time field +arithmetic". + +**2. Key generation and storage.** Options: a `woc keygen` subcommand +(keys are a toolchain concern), or first-boot generation by the runtime +into the data directory (keys are a runtime concern, zero setup). Leaning: +first-boot generation into `WO_DATA` (0600, alongside the WAL — a program +with a persistent database already has the directory), plus a way to print +the public fingerprint (`program --identity` or a stdlib call) so the +operator can paste it into A's `[share]`. A program without `WO_DATA` has +no identity and cannot attach anywhere — which is coherent: attach is a +database feature. + +**3. What exactly gets signed.** A bare nonce signature is vulnerable to +cross-protocol reuse; the lean is signing a transcript hash: protocol tag, +both fingerprints, both nonces, and the channel identity — so a signature +from this handshake means nothing in any other context. The spec should +write the exact byte layout down (the WAL encoding conventions apply: the +format is normative, little-endian, versioned by the protocol tag). + +**4. Does the uid check survive at all?** Options: keys only (one +mechanism, one story), or keys AND peer-cred as defense in depth. Leaning: +keys only — two mechanisms invite "it worked because of the other one" +confusion in exactly the code that must never be confusing; `SO_PEERCRED` +remains a log-line enrichment (who was that fingerprint), never an +authorization input. + +## Proposed Solution + +- **Brainstorm the spec** settling the four forks, then fold the plan into + 9c's implementation plan as its authentication tasks — one plan, because + 9c without 9d ships a placeholder identity and 9d without 9c has nothing + to authenticate. The 9c milestone may still land first with the uid + bootstrap, flagged loudly as pre-9d. +- **Acceptance extends the 9c workload**: the employee-A / report-B pair + gains the key exchange in both manifests; the acceptance script adds the + wrong-key, no-key, same-uid-wrong-key, replay, impostor-socket, and + rotation checks above, each asserting the exact trap/refusal. +- Expected shape: handshake module beside the channel code (both ends), + `[share]`/`[connect]` manifest keys for fingerprints, first-boot keygen + in the runtime's data-directory setup, vendored signature primitive with + its own unit suite (known-answer tests from the algorithm's reference + vectors).