- docs/stories/databasev2/, numbered from 1. Six PENDING database iterations
moved from the language track and renumbered, keeping the old id in
`was_language_iteration:` so a search for "iteration 32" still finds it:
32 -> 3 WAL checkpoint, 23 -> 4 io_uring commit, 33 -> 7 single-file store,
27 -> 8 query grammar, 20 -> 9 cross-program, 21 -> 10 keypair auth.
Done work (9, 9b, 22) stays as v1 history; language 18 left whole
- the problem, read off the engine not guessed: rows are malloc'd slabs with
addresses stable forever, NO eviction/spill/paging anywhere in database/src,
the WAL never checkpoints so boot replays all history, and durability is one
process-global WO_DATA so no table can say it matters more than another.
An allocation failure IS a clean catchable WO_T_OOM — but swap thrash
arrives first and carries no error signal at all, which is the real hazard
- four new iterations:
1 measure the ceiling FIRST (curve not cliff; the three exits; kill -9 at
exhaustion) — every later default should follow from a number
2 `@table(mode: ram | durable | cold)` — the grammar ask. Small surface
(Ast.table_cfg gains a key, the parser already rejects unknown args), big
semantics: `durable` defaults so nothing changes silently, and the
compiler refuses a durable row holding a `ref` into a ram table
5 bounded tables + refuse/evict/back-pressure, shedding BEFORE the OS acts
6 cold tiering — mostly forks, incl. whether the language surfaces the
fault cost and whether @unique on cold is refused outright. A paged
B-tree stays rejected: if tiering needs one, reject tiering
- 39 links repointed, link TEXT renumbered to track-local ids; arc gains one
pointer row replacing the six moved; board + board-views cover three tracks
- linkcheck 0 broken / 0 anchors; no code blocks in any story
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
157 lines
7.9 KiB
Markdown
157 lines
7.9 KiB
Markdown
---
|
||
track: databasev2
|
||
iteration: "10"
|
||
was_language_iteration: "21"
|
||
status: hold
|
||
---
|
||
|
||
# databasev2 10 — keypair authentication for cross-program attach
|
||
|
||
> **Moved 2026-08-26** from the language track, where this was iteration 21.
|
||
> Part of [Story — the database beyond RAM](../language-runtime-database/00-story.md). Content unchanged by
|
||
> the move; its dependencies are restated in that track index.
|
||
|
||
> Format: `product/story-iteration-template`. Part of
|
||
> [Story — one language, one runtime, one database, one binary](../language-runtime-database/00-story.md).
|
||
>
|
||
> **Inserted 2026-08-15.** Promotes iteration 20'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 20 (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 20 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 20 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 20 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
|
||
20's implementation plan as its authentication tasks — one plan, because
|
||
20 without 21 ships a placeholder identity and 21 without 20 has nothing
|
||
to authenticate. The 20 milestone may still land first with the uid
|
||
bootstrap, flagged loudly as pre-21.
|
||
- **Acceptance extends the 20 workload**: the employee-A /
|
||
employee-list-B pair (`docs/examples/employee-list`, pre-authored
|
||
2026-08-15) carries the key exchange in both manifests — A's
|
||
`[[share.clients]]` names B's fingerprint, B's `[connect.employee]` pins
|
||
A's; 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).
|