- `readiness: ready | refine` is a SECOND axis, orthogonal to status.
`ready` = the brainstorm is complete and the decisions are LOCKED (a spec
approved, or the forks explicitly confirmed). `refine` = open forks remain
and it cannot be planned yet
- `status: refine` RETIRED because it carried both meanings at once, so a held
iteration with an approved spec (language 18, 26) was indistinguishable from
one nobody had thought about. status is now purely where the WORK is:
done | in-progress | pending | hold — `pending` was already the board's own
rendering word, so nothing new was invented
- all 47 iterations classified from EVIDENCE in their own text, not by guess:
"the four forks are SETTLED" / "spec + plan approved" / "Approved spec:" for
ready; "Forks the spec must settle" / "no spec exists yet" for refine. Every
shipped iteration is ready by definition. 19 done, 5 in-progress, 15
pending, 8 hold; 27 ready, 20 refine
- two iterations moved refine -> in-progress rather than -> pending: language
31 and 34 are absorbed into 24 and work on them is literally happening, which
the board already showed as 🔄 while their frontmatter said otherwise. That
disagreement is now gone
- board legend, board-views' frontmatter contract, and two new Dataview
queries updated — the useful one being `readiness: ready AND status:
pending`, the startable set
WHAT THE NEW AXIS IMMEDIATELY SURFACED: of 15 pending iterations, exactly ONE
is startable — databasev2 4, io_uring group-commit, whose forks were confirmed
settled 2026-08-20. Everything else pending needs a brainstorm first. That was
invisible while one key carried both meanings, and it is now on the board.
Also caught by the sweep, unrelated to readiness but found by cross-checking
frontmatter against the board: SIX duplicate rows. Every iteration moved into
databasev2 was still listed in the LANGUAGE pending table under its retired id
(23, 32, 33, 20, 21, 27) as well as its new one. Stale copies removed. And two
databasev2 rows made claims the sweep contradicts — iteration 1 was billed
"startable today" while its forks are open, and 6 still called itself the
ceiling-raiser after 2 took that role.
Docs only. linkcheck 0 broken / 0 anchors.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
159 lines
8 KiB
Markdown
159 lines
8 KiB
Markdown
---
|
||
track: databasev2
|
||
iteration: "10"
|
||
was_language_iteration: "21"
|
||
status: hold
|
||
readiness: refine
|
||
---
|
||
|
||
# databasev2 10 — keypair authentication for cross-program attach
|
||
|
||
> **Moved 2026-08-26** from the language track, where this was iteration 21.
|
||
> Part of [Story — databasev2: the database beyond RAM](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)
|
||
> — the track this iteration was authored in before the 2026-08-26 move.
|
||
>
|
||
> **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).
|