moshpit: publish the keys a name may present - #137
Merged
Conversation
The registry knew who held a name and where it pointed, but not what key it was allowed to present — so a client had nothing to check a peer against, and "is this really scrambled.eggs" had no answer. Per name, not per TLD, and 008 is what forces it. Names under a TLD are sold, so `blue.eggs` can belong to someone who does not own `.eggs`. Hanging keys off the TLD would let its operator publish a key for a name they had already sold — impersonating a buyer inside the namespace they bought into. The pin therefore lives beside the name and is authorised by the name's holder. `kind` keeps the transports apart. 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 diagnosable reason. The same pin cannot be published under two kinds. Several rows per (tld, label, kind) on purpose: a key cannot rotate without a window where the old and the new are both published. Withdrawing the last key of a kind is allowed — that is how a compromised key is taken out of service. releaseName now deletes the name's pins explicitly. SQLite only honours ON DELETE CASCADE with `PRAGMA foreign_keys = ON`, which this app never sets, so a declared cascade would not have fired and whoever registered the name next would have inherited the previous holder's published keys. GET /api/moshpit/pins?name=&kind= public; 400 not a name, 404 no key GET /api/moshpit/tlds/:tld/pins?label= public POST /api/moshpit/tlds/:tld/pins the name's holder DELETE /api/moshpit/tlds/:tld/pins the name's holder Aliases are followed before pins are read: `foo.agentic` under an alias to `.agent` connects to whatever serves `foo.agent`, so those are the keys that will actually be presented. 16 tests against a real throwaway libSQL database, since the behaviour worth checking is in the SQL and the ownership rules — including that a TLD operator cannot pin a name they sold, and that a released name carries no keys forward. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
vu1nz Security Review0 finding(s) in PR #? No security issues found. |
Merged
ralyodio
added a commit
that referenced
this pull request
Jul 31, 2026
* chore(release): v0.13.3 install.sh resolves releases/latest, so everything merged since v0.13.2 has been sitting on main unreachable — `moshcode dns enable` exists in the source and not in anyone's binary. The headline is the DNS bridge (#141). Moshpit names now resolve for every program on the machine, not just inside TronBrowser: each OS gets the mechanism that routes ONE SUFFIX rather than the one that replaces the resolver — /etc/resolver on macOS, systemd-resolved routing-only domains or dnsmasq on Linux, an NRPT rule per namespace on Windows. moshcode dns enable / disable / status moshcode uninstall <engine|tool> (#150, completion in #151) The pit gained most of a namespace registry in between: key pins per name (#137), pasted bulk claiming with per-line price and target (#138, #142, #143, #146), all-numeric endings (#147), /n/<name> serving a name or a directory (#145, #149), and Buy Now on an unclaimed name (#148). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: restore the em-dash the version bump escaped The bump rewrote package.json through a JSON serialiser that defaults to ASCII, turning the em-dash in `description` into —. Valid JSON and the same string once parsed, but a gratuitous diff in a commit that should touch one line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
The registry knew who held a name and where it pointed, but not what key it may present — so a client had nothing to check a peer against, and "is this really
scrambled.eggs" had no answer.Per name, not per TLD
008 forces this. Names under a TLD are sold, so
blue.eggscan belong to someone who doesn't own.eggs. Hanging keys off the TLD would let its operator publish a key for a name they'd already sold — impersonating a buyer inside the namespace they bought into.The pin lives beside the name and is authorised by the name's holder. There's a test for exactly this: a TLD operator is refused when trying to pin a name owned by someone else.
(This started as a port of
moshcoder/moshcoding#82, which pins per TLD. That granularity is safe there because that registry has no name-level ownership and no sales. Here it would be a privilege escalation.)A cascade that would never have fired
releaseNamenow deletes the name's pins explicitly. SQLite only honoursON DELETE CASCADEwithPRAGMA foreign_keys = ON, and this app never sets it — so a declared cascade would have silently done nothing, and whoever registered the name next would have inherited the previous holder's published keys. Covered by a test that releases a name, re-registers it to another user, and asserts no keys came along.kindA
tlspin covers a certificate's SubjectPublicKeyInfo; anmtppin covers an ML-DSA-65 identity. 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 diagnosable reason. The same pin can't be published under two kinds.Rotation
Several rows per
(tld, label, 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 revocation.API
Aliases are followed before pins are read:
foo.agenticaliased to.agentconnects to whatever servesfoo.agent, so those are the keys that will actually be presented.400vs404is load-bearing — clients cache on it. A definite no means refuse the connection; a 5xx or timeout means retry.pinsForNamereturns null for an unclaimed TLD soexample.comis "not a Moshpit name" rather than "unpinned".Tests
16 new tests against a real throwaway libSQL database, following
moshpit-registry.test.mjs. 215/215 across the pwa suite.🤖 Generated with Claude Code