← All reference docs

Registry auth: signed puts, keys as principals

Status: BUILT, with named limits. Signed publication is live end to end (#83): oath publish and oath put --remote --key sign, oath serve verifies the request, and the accepted journal entry carries the author's canonical publication envelope, key and signature, so authorship is checkable offline by anyone holding the journal (oath audit). Bearer tokens remain, deliberately — they are the transport shim for MCP clients that cannot sign, never the source of provenance.

This header previously read "NOT yet built", which was accurate when the document was a target and stopped being so once #83 landed. A status line that lags its implementation is the same defect as a coverage claim that lags its vectors: it is a statement about the system that nothing re-derives.

What is NOT established, and is stated in full in "What this does NOT establish" below: atomic application of a signed transition (the envelope expresses a compare-and-swap the filesystem store cannot enforce), and custody separation (distinct keys are not evidence of independent control).

The decision

A principal is a keypair. Authorship is a signature over a canonical publication ENVELOPE, persisted verbatim. Bearer tokens are a compatibility shim for the connection, never the source of provenance.

Amended 2026-07-30, when this was implemented (#83). This section originally said authorship was "a signature over the object hash". That is not enough, and the correction is load-bearing: a signature over the artifact hash alone is valid forever, for any name, in any operation — a bearer credential wearing a signature's clothing. It cannot express "I publish THIS content under THIS name, replacing THIS version". The envelope (SPEC §8.6) binds operation, name, artifact, parent and per-name revision together, so a signature authorizes one state transition and no other.

It also said "every journal entry is signed". That was a target, not a fact, and the gap is now measured rather than asserted — oath audit reports signed versus unsigned coverage.

Measured, 2026-08-01. The live registry carries 1,014 journal entries, of which 186 are cryptographically authored across 184 distinct names, every one asserting Apache-2.0. Before the signed corpus adoption it was 1 of 825.

The remaining gap is stated rather than rounded off:

Why (and why now)

The substrate's whole promise is trust you don't have to grant anyone: objects are content-addressed and re-verified on import, so the host is never a root of trust. Bearer tokens quietly break that for the one dimension they cover — authorship. With a token, the server writes author: alice because a shared secret said so, so a compromised or dishonest server can forge provenance. The journal claims "authenticated principals, tamper-evident"; the hash-chain makes it tamper-evident, but token-based authorship is only tamper-evident if you trust the writer.

A signature makes authorship unforgeable and offline-verifiable: the author signs the canonical object hash (and the journal entry) with their private key; anyone verifies against the public key without trusting the hub. That upgrades a core feature from "the server says alice made this" to "alice mathematically made this" — which is what a provenance system should have said from entry one.

Starting here is also the cheap moment: #14 isn't built and the team store has no install base, so there's no token ecosystem to migrate. Retrofitting signatures onto a token-based journal later is the expensive path, and it leaves all historical provenance permanently weaker.

The model

Deliberately out of scope of the core

What this changes in the code (the build)

  1. [built] Kernel: a pubkey + Ed25519 signature on the journal entry (oath/store.go); the signature covers the authored fields; VerifyLog rejects a bad or forged signature; unattributed = unsigned. Specified in SPEC §8.4.
  2. [built] oath CLI: oath keygen generates a keypair; oath put --key <file> (or OATH_KEY) signs the entry and defaults the author label to the signer's pubkey.
  3. [built] oath serve: authenticates a principal by an Ed25519 signature over the request body (X-Oath-Pubkey + X-Oath-Signature) — the principal IS the key, no shared secret. Bearer tokens still work but are now the optional transport shim (serve runs without a tokens file). The repoint policy gains owner_pubkey: a name only its owning key may repoint. A present-but-invalid signature is a hard rejection, never a fall-through.
  4. export/import: carry and verify the signature end-to-end.

Related substrate (built alongside this): the verification worker pooloath prove-worker re-proves require_proven names out of band and signs the resulting proven verdict, so the badge is an authenticated "the verifier re-proved this" rather than a publisher's claim. See docs/registry-verification.md and SPEC §8.5.

The verification model (re-prove locally) is unchanged — signatures add the authorship dimension on top of the hash and the proof. The hosted infrastructure (GCS objects, Cloud Run API, Cloud SQL name index + journal) is in terraform/ and is agnostic to this: it stores signed bytes and an index; the trust lives in the signatures and the client, not the server.

As implemented (#83): two signatures, two different claims

The working system has two independent signatures. Conflating them was the mistake that took longest to see, because both are "a signature on a journal entry":

| | question it answers | signer | what it cannot do | |---|---|---|---| | entry signature (SPEC §8.4) | has this record been altered since it was written? | whoever holds the store's key | establish authorship — a store can sign an entry naming anyone | | publication envelope (SPEC §8.6) | who requested this exact transition? | the author, before submitting | prove the record was not later tampered with |

An author signs only what an author controls: name, artifact, parent, revision. It MUST NOT cover status, guarantee or termination — those are the registry's findings, re-derived from content bytes, and an author cannot predict them. Requiring a signature over them would make a policy decision indistinguishable from a forged statement.

The envelope BYTES are persisted, never reconstructed from the entry's fields. Reconstruction would let a future encoder change silently invalidate historical signatures: the signature stays correct and stops verifying, which is the worst failure an audit trail has.

The rule for tokens, unchanged and now enforced

The token says the caller may reach the registry. The signature says who authored the publication. An author statement is only accepted on a signed request — with a bearer token the principal is server-vouched, so a statement naming a key could not be tied to the caller.

What this does NOT establish


Rendered verbatim from docs/registry-auth.md in the repository. The markdown is the single source; this page is a copy checked for drift in CI, so what you read here is what an implementer reads.