Skip to content

moshpit: publish the keys a name may present - #137

Merged
ralyodio merged 1 commit into
mainfrom
feat/name-pins
Jul 31, 2026
Merged

moshpit: publish the keys a name may present#137
ralyodio merged 1 commit into
mainfrom
feat/name-pins

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

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.eggs can 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

releaseName now deletes the name's pins explicitly. SQLite only honours ON DELETE CASCADE with PRAGMA 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.

kind

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'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

GET    /api/moshpit/pins?name=&kind=       public;  400 not a name, 404 no key published
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 aliased to .agent connects to whatever serves foo.agent, so those are the keys that will actually be presented.

400 vs 404 is load-bearing — clients cache on it. A definite no means refuse the connection; a 5xx or timeout means retry. pinsForName returns null for an unclaimed TLD so example.com is "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

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>
@github-actions

Copy link
Copy Markdown

vu1nz Security Review

0 finding(s) in PR #?

No security issues found.

@ralyodio
ralyodio merged commit 586f55f into main Jul 31, 2026
3 checks passed
@ralyodio
ralyodio deleted the feat/name-pins branch July 31, 2026 08:56
@ralyodio ralyodio mentioned this pull request Jul 31, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant