moshpit: publish key pins per TLD - #82
Merged
Merged
Conversation
The registry has never had a per-name record — you claim `.eggs`, not `scrambled.eggs` — so there was nowhere for a key to hang. Pins therefore attach to the TLD: `.eggs` publishes the keys that names under it may present, and a client that resolved `scrambled.eggs` knows what to expect without the registry ever having to know that `scrambled` exists. The trade is real and is written down in the code: names under one TLD share a key, so there is no isolation between them. The TLD's operator could impersonate any name under it — which they can do regardless, since they own the namespace and decide where its names point. `kind` separates the transports. A `tls` pin covers a certificate's SubjectPublicKeyInfo; an `mtp` pin covers an ML-DSA-65 identity. Both are SHA-256 over an SPKI, so as strings they are indistinguishable, and nothing but that column stops a client being handed the wrong one and failing with no idea why. The same pin cannot be published under two kinds. Several rows per (tld, kind) on purpose: a key cannot rotate without a window where the old and new are both published. Withdrawing the last key of a kind is allowed — that is how a compromised key is revoked, and refusing it on the grounds that it breaks connections would be refusing the point. GET /api/moshpit/pins?name=&kind= public; 400 not a name, 404 no key GET /api/moshpit/tlds/:tld/pins public POST /api/moshpit/tlds/:tld/pins owner only DELETE /api/moshpit/tlds/:tld/pins?pin= owner only Aliases are followed before pins are read: `foo.agentic` under an alias to `.agent` connects to whatever serves `foo.agent`, so `.agent`'s keys are the ones that will be presented. 15 tests against a real SQLite database rather than a stub, since the behaviour worth checking is in the SQL and the ownership checks. One of them caught a genuine bug: `pinsForName` did not check `registered`, so `example.com` parsed as label `example` under TLD `com` and was answered "no key published" instead of "not a Moshpit name" — two things clients cache differently and act on differently. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
vu1nz Security Review0 finding(s) in PR #? No security issues found. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds the missing piece between the registry and the two Moshpit clients: a place to publish the keys a name is allowed to present.
Why per-TLD
The registry has never had a per-name record — you claim
.eggs, notscrambled.eggs— so there was nowhere for a key to hang. Pins attach to the TLD instead, and a client that resolvedscrambled.eggsgets an answer without the registry ever having to know thatscrambledexists.The trade is real and documented in the code: names under one TLD share a key, so there is no isolation between them. The TLD's operator could impersonate any name under it — which they can do regardless, since they own the namespace and decide where its names point.
kindA
tlspin covers a certificate's SubjectPublicKeyInfo (moshpit-proxy); anmtppin covers an ML-DSA-65 identity (moshpit-transport). Both are SHA-256 over an SPKI, so as strings they're indistinguishable — nothing but that column stops a client being handed the wrong one and failing with no idea why. The same pin can't be published under two kinds.Omitting
kindon lookup is safe rather than merely convenient: a pin of the wrong kind can never match, since an ML-DSA SPKI hash won't equal a presented TLS SPKI hash.Rotation
Several rows per
(tld, kind)on purpose — a key can't rotate without a window where old and new are both published. Withdrawing the last key of a kind is allowed; that's how a compromised key is revoked.API
Aliases are followed before pins are read:
foo.agenticunder an alias to.agentconnects to whatever servesfoo.agent, so.agent's keys are the ones presented.Tests
15 new tests against a real SQLite database (
libsqltakes afile:URL) rather than a stub, because the behaviour worth checking lives in the SQL and the ownership checks. 142/142 repo-wide,tsc --noEmitclean.One test caught a genuine bug:
pinsForNamedidn't checkregistered, soexample.comparsed as labelexampleunder TLDcomand got answered "no key published" instead of "not a Moshpit name" — two things clients cache differently and act on differently.Not in this PR
app/pit/has onlypage.tsx; publishing a pin is API-only for now.🤖 Generated with Claude Code