Skip to content

feat(dns): --trust-all, so every name works without a command per name - #281

Merged
ralyodio merged 1 commit into
fix/first-run-trustfrom
feat/trust-all
Aug 4, 2026
Merged

feat(dns): --trust-all, so every name works without a command per name#281
ralyodio merged 1 commit into
fix/first-run-trustfrom
feat/trust-all

Conversation

@ralyodio

@ralyodio ralyodio commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Stacked on #279 — review that first; this branch targets it, not main.

dns trust <name> works and does not scale. Someone browsing Moshpit meets a certificate error on every site they have not personally thought about, which is indistinguishable from the namespace being broken.

moshcode dns start --trust-all

trusting names as they resolve — only where the registry publishes a matching pin
  seo.rank → 67.205.189.229
  trusted seo.rank
  evil.rank → 203.0.113.9
  ! evil.rank — REFUSED — the served key is not among the pins the registry publishes

Nothing is trusted on sight. A name is trusted only when the key it serves is one the registry already published for it, so this is registry-backed trust rather than trust-on-first-use. A name with no published pin gets nothing, silently and forever.

The three ways this could go wrong, none about cryptography

Blocking a DNS answer on certificate work. consider() queues and returns; the drain runs detached from a UDP handler that owes a client a reply.

Asking once per query rather than once per name. A browser sends A and AAAA together and retries, so "on resolve" is a firehose. Ten lookups of two names is two certificate fetches, and there is a test pinning that.

Retrying a name that will never succeed. A refusal is final for that name until restart. Otherwise every lookup of a mismatched name writes a log line and fails, forever.

Deliberate

  • Only a name that actually resolved to one of ours is considered — a forwarded clearnet name is not ours to trust, and NXDOMAIN has no origin to fetch a certificate from.
  • Without root it says so once, rather than failing per name forever in the query log.
  • A registry outage is not narrated per name. If the registry is down every name fails; saying so each time turns the query log into the outage.
  • A thrown attempt cannot take the resolver down — it runs detached, so an unhandled rejection there would be a process exit on a box whose whole job is to stay up.

Found while testing

idle() awaited a boolean rather than the in-flight drain, so it reported a queue as settled while it was still being worked. A flag can say someone else is draining; it cannot be awaited. Fixed, and the test that caught it is the last one in the file.

9 new tests. Full suite green.

`dns trust <name>` works and does not scale. Someone browsing Moshpit
meets a certificate error on every site they have not personally thought
about, which is indistinguishable from the namespace being broken.

`dns start --trust-all` trusts a name as it resolves: fetch the
certificate it serves, check the key against the pin the registry
published for that name, install it only on a match. Nothing is trusted
on sight — a name with no published pin gets nothing, silently and
forever — so this is registry-backed trust rather than trust-on-first-use.

Three ways the automation could go wrong, none about cryptography:

  - blocking a DNS answer on certificate work. consider() queues and
    returns; the drain runs detached from the query handler.
  - asking once per query rather than once per name. A browser sends A
    and AAAA together and retries, so "on resolve" is a firehose: ten
    lookups of two names is two certificate fetches.
  - retrying a name that will never succeed. A refusal is final for that
    name until restart, or every lookup writes a log line and fails.

Only a name that actually resolved to one of ours is considered: a
forwarded clearnet name is not ours to trust, and NXDOMAIN has no origin
to fetch from. Without root it says so once, rather than failing per name
forever in the query log.

A registry outage is not narrated per name — if the registry is down
every name fails, and saying so each time turns the query log into the
outage. Only refusals and successes are reported.

Fixed while testing: idle() awaited a boolean rather than the in-flight
drain, so it reported a queue as settled while it was still being worked.
A flag can say someone else is draining; it cannot be awaited.

Stacked on #279, which added the per-name command this automates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

88 finding(s)

HIGH/CRITICAL: 50 | MEDIUM: 38

Severity Rule Location
HIGH manifest-typosquat apps/pwa/package.json:19
HIGH js-ssrf-outbound-request apps/pwa/public/sw.js:45
HIGH secret-generic-credential apps/pwa/test/apikey-bearer-scheme.test.mjs:30
HIGH secret-generic-credential apps/pwa/test/apikey-mask.test.mjs:38
HIGH secret-generic-credential apps/pwa/test/apikey-reveal.test.mjs:35
HIGH secret-generic-credential apps/pwa/test/approvals-context.test.mjs:28
HIGH secret-generic-credential apps/pwa/test/approvals-credits.test.mjs:28
HIGH secret-generic-credential apps/pwa/test/approvals-notify.test.mjs:26
HIGH secret-generic-credential apps/pwa/test/approvals-resolve-race.test.mjs:20
HIGH secret-generic-credential apps/pwa/test/auth-form-email.test.mjs:29
HIGH secret-generic-credential apps/pwa/test/auth-form-email.test.mjs:33
HIGH secret-generic-credential apps/pwa/test/auth-page-error.test.mjs:36
HIGH secret-generic-credential apps/pwa/test/cli-device-token.test.mjs:28
HIGH secret-generic-credential apps/pwa/test/cli-pages-balance.test.mjs:32
HIGH secret-generic-credential apps/pwa/test/cli-token.test.mjs:28
HIGH secret-generic-credential apps/pwa/test/credits-pack.test.mjs:51
HIGH secret-generic-credential apps/pwa/test/credits-webhook-event-match.test.mjs:35
HIGH secret-generic-credential apps/pwa/test/credits-webhook.test.mjs:28
HIGH secret-generic-credential apps/pwa/test/csrf-input-escaping.test.mjs:31
HIGH secret-generic-credential apps/pwa/test/csrf-input-escaping.test.mjs:101
HIGH secret-generic-credential apps/pwa/test/logout-csrf.test.mjs:29
HIGH secret-generic-credential apps/pwa/test/moshpit-api-key.test.mjs:28
HIGH secret-generic-credential apps/pwa/test/moshpit-bulk-claim.test.mjs:23
HIGH secret-generic-credential apps/pwa/test/moshpit-claim-full-name.test.mjs:29
HIGH secret-generic-credential apps/pwa/test/moshpit-crawlable.test.mjs:28
HIGH secret-generic-credential apps/pwa/test/moshpit-ending-page.test.mjs:28
HIGH secret-generic-credential apps/pwa/test/moshpit-pins.test.mjs:22
HIGH secret-generic-credential apps/pwa/test/moshpit-pit-page.test.mjs:33
HIGH secret-generic-credential apps/pwa/test/moshpit-records-page.test.mjs:23
HIGH secret-generic-credential apps/pwa/test/moshpit-records.test.mjs:23
HIGH secret-generic-credential apps/pwa/test/moshpit-registry.test.mjs:20
HIGH secret-generic-credential apps/pwa/test/moshpit-related-endings.test.mjs:28
HIGH secret-generic-credential apps/pwa/test/moshpit-sales.test.mjs:16
HIGH secret-generic-credential apps/pwa/test/moshpit-search.test.mjs:74
HIGH secret-generic-credential apps/pwa/test/moshpit-terms.test.mjs:19
HIGH secret-generic-credential apps/pwa/test/moshpit-tlds-pagination.test.mjs:28
HIGH secret-generic-credential apps/pwa/test/passkey-register-duplicate.test.mjs:38
HIGH secret-generic-credential apps/pwa/test/require-auth-next.test.mjs:31
HIGH secret-generic-credential apps/pwa/test/require-auth-next.test.mjs:35
HIGH secret-generic-credential apps/pwa/test/sessions-output-seq.test.mjs:30
HIGH secret-generic-credential apps/pwa/test/sessions-paste.test.mjs:24
HIGH secret-generic-credential apps/pwa/test/sessions-stream-replay.test.mjs:34
HIGH secret-generic-credential apps/pwa/test/sessions.test.mjs:24
HIGH secret-generic-credential apps/pwa/test/signature.test.mjs:6
HIGH secret-generic-credential test/auth.test.mjs:13
HIGH secret-generic-credential test/auth.test.mjs:63
HIGH secret-generic-credential test/console-cookie-malformed.test.mjs:15
HIGH secret-generic-credential test/console.test.mjs:12
HIGH secret-generic-credential test/mirror.test.mjs:37
HIGH secret-generic-credential test/mirror.test.mjs:77

…and 38 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio marked this pull request as ready for review August 4, 2026 00:59
@ralyodio
ralyodio merged commit b7694bc into fix/first-run-trust Aug 4, 2026
4 checks passed
@ralyodio
ralyodio deleted the feat/trust-all branch August 4, 2026 00:59
ralyodio added a commit that referenced this pull request Aug 4, 2026
…the registry (#279)

* feat(dns): trust a name on the strength of its pin, and stop blaming the registry

Two failures from the same first run, neither of which the machine could
explain.

1. `dns enable` printed "no TLDs claimed yet — nothing to route" on a
machine where the registry holds 5674 endings. The fetch had failed and
the failure was swallowed to an empty array, so a network problem was
reported as a fact about the registry — sending someone to claim an
ending they already own. Same bug as the one fixed in `dns start`; this
is the other half of it. The two cases now read differently, and the
failure prints the curl that checks it.

2. The certificate error survived everything. moshpit-proxy verifies the
registry pin and re-signs with a local root, but nothing routes to it:
the resolver answers the origin, the proxy sits on loopback, and the two
were never connected. So the root installed by #275 signs nothing, and
`curl https://seo.rank` still meets the origin's self-signed certificate.

`dns trust <name>` closes that without the proxy at all. A Moshpit name
serves a self-signed leaf whose SAN names only itself, so trusting it
vouches for that one name — a far smaller grant than a root, and why this
path needs no name-constraints argument to be defensible.

The pin check is what separates it from recklessness. Installing whatever
answered the socket is trusting whoever reached the port first;
installing it only when the registry already vouches for that exact key
is registry-backed trust, which is a stronger claim than domain
validation ever made. Every published pin is accepted, not just the
first, because the registry lists the old one beside the new during a
rotation precisely so a key can change without a flag day.

A registry outage is reported as an outage, never as a rejected
certificate: the answer to "the registry is down" is to wait, not to
distrust a name.

Verified against the live registry — the key seo.rank serves matches the
pin the Pit publishes, and trusting that leaf takes curl from error 60 to
200.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(dns): --trust-all, so every name works without a command per name (#281)

`dns trust <name>` works and does not scale. Someone browsing Moshpit
meets a certificate error on every site they have not personally thought
about, which is indistinguishable from the namespace being broken.

`dns start --trust-all` trusts a name as it resolves: fetch the
certificate it serves, check the key against the pin the registry
published for that name, install it only on a match. Nothing is trusted
on sight — a name with no published pin gets nothing, silently and
forever — so this is registry-backed trust rather than trust-on-first-use.

Three ways the automation could go wrong, none about cryptography:

  - blocking a DNS answer on certificate work. consider() queues and
    returns; the drain runs detached from the query handler.
  - asking once per query rather than once per name. A browser sends A
    and AAAA together and retries, so "on resolve" is a firehose: ten
    lookups of two names is two certificate fetches.
  - retrying a name that will never succeed. A refusal is final for that
    name until restart, or every lookup writes a log line and fails.

Only a name that actually resolved to one of ours is considered: a
forwarded clearnet name is not ours to trust, and NXDOMAIN has no origin
to fetch from. Without root it says so once, rather than failing per name
forever in the query log.

A registry outage is not narrated per name — if the registry is down
every name fails, and saying so each time turns the query log into the
outage. Only refusals and successes are reported.

Fixed while testing: idle() awaited a boolean rather than the in-flight
drain, so it reported a queue as settled while it was still being worked.
A flag can say someone else is draining; it cannot be awaited.

Stacked on #279, which added the per-name command this automates.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <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