feat(dns): finish the job — install trust, not just routing - #275
Conversation
`dns enable` pointed names at the resolver and stopped, so a Moshpit
name resolved and then failed its TLS handshake. To the person who typed
the URL that is not "DNS works", it is "still broken" — which is exactly
how it was reported.
No certificate authority will ever close that gap. The Baseline
Requirements banned issuance for non-IANA names: it stopped in November
2015 and the survivors were revoked by October 2016, and a CA in a root
store that issued for `.rank` would be distrusted for doing it. The rule
is the penalty, so this is not a matter of trying harder.
moshpit-proxy already solved it — verify the origin key against the pin
the registry published, then re-sign with a root generated on this
machine, because restating the result is the only language a stock client
accepts. Nobody had wired it up. This is the wiring, not new crypto.
The one thing it must never do is install a root that could vouch for the
clearnet, so that is checked rather than assumed. A root without a
critical nameConstraints extension permitting our endings is REFUSED, not
warned about — there is no wording of a warning that makes an
unconstrained root in a system store acceptable. moshpit-proxy sets the
constraint today and tests it; this side cannot assume it always will,
because that regression would otherwise land automatically on every
machine that ran `dns enable`.
Reported honestly rather than summarised:
- NSS and the system store are separate outcomes. NSS is user-level and
always lands; curl's store needs root. Saying "installed" while curl
still refuses is what sends someone back to conclude it is broken, so
without root it does the half it can and names the half it cannot.
- It never fails `enable`. DNS is switched and verified by the time
this runs; failing the command over a trust store would roll back
working resolution to fix a certificate.
`--no-trust` opts out.
This only helps machines it is run on. Nothing shippable makes a
stranger's curl trust a Moshpit name; TronBrowser is the answer for
people who have run nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ThreatCrush Security Scan88 finding(s) HIGH/CRITICAL: 50 | MEDIUM: 38
…and 38 more. Full results in the Security tab. Snippets are redacted; ThreatCrush never prints matched credential material. |
The gate accepted a root that can vouch for anything, which is the one
thing it exists to refuse.
`requireNameConstraints` searched the whole `openssl x509 -text` output for
`DNS:.<tld>`. That string reads identically under `Permitted:` and under
`Excluded:`, so the check could not tell the two apart -- and RFC 5280
4.2.1.10 says constraints bind only the name *types* they mention. A root
carrying `nameConstraints=critical,excluded;DNS:.hacker` therefore permits
every DNS name on the internet, names the ending we asked about, is marked
critical, and passed:
requireNameConstraints -> {"ok":true,"why":"constrained to Moshpit endings"}
trustPlan.ok -> true | steps -> ["nss","system"]
It then went into the system store. So did a root permitting `.evil` while
excluding `.hacker`, and one permitting `.hacker` *and* `.com`.
Parse the extension instead: read Permitted and Excluded as the separate
lists they are, require a permitted DNS subtree to exist at all, require
every claimed ending inside it, and refuse anything else in it.
Also: the paths were root's, not the operator's.
Everything here keys off a home directory -- moshpit-proxy's root under
~/.moshpit, the NSS database the browser reads at ~/.pki/nssdb. But `dns
enable` needs root, and since #274 it escalates itself, so os.homedir()
is /root by the time this runs. It looked for the root in a directory
moshpit-proxy never wrote to and reported "no local root" on a machine
that had one. operatorHome() resolves SUDO_USER/DOAS_USER through the
shell (passwd, so macOS and unusual layouts work), and the NSS store is
chowned back afterwards -- left root-owned, the browser silently loses
the ability to update its own store. Same $HOME-under-sudo trap #272 and
#274 closed.
Also in this commit:
- tests skip loudly instead of silently. Four used `if (!text) return`
when openssl was unavailable, passing without asserting anything --
a green guard that had tested nothing.
- `--no-trust` is documented, in `dns` usage and in the CLI schema, so
help renders it. It existed only as a string literal.
- `verifyStockTls` is wired in rather than exported, tested and never
called. It runs only when a root was installed and there is a probe
name, and never fails the command.
- merges main, so this sits on #274's self-escalation rather than
predating it.
1348 tests, 1345 pass, 0 fail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Pushed fixes for the review findings — The gate was not constraining anything
It carried a critical Name Constraints extension naming the exact ending asked about, so it passed. Run against the code as it stood: Straight into the system store. Two more shapes did the same: permitting Now parsed properly — Permitted and Excluded read as the separate lists they are, a permitted DNS subtree must exist at all, every claimed ending must be inside it, and nothing else may be. Same root against the fix: The paths were root's, not yours
Also
Note on one fixtureThe shared
|
|
|
||
| test("the root is looked for where moshpit-proxy writes it", () => { | ||
| assert.equal(caPath({ home: "/home/x" }), "/home/x/.moshpit/ca/ca.crt"); | ||
| assert.match(describeCertificateCommand("/tmp/ca.crt").args.join(" "), /-noout -text/); |
`dns enable` now installs the local CA as well as the routing, so a Moshpit name that resolves also survives a TLS handshake (#275). Minor, not patch: it does something new on a command people already run. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The name-constraints gate refused correctly and then left you there. Both
refusals printed the same line -- "moshcode will not put an unconstrained
root in your trust store" -- with no next step, and for one of them that
sentence was not even true.
They are nothing alike. A root permitting endings other than the ones
claimed today is *stale*, not dangerous: it was generated when a different
set was claimed, and regenerating it costs one command. A root with no
permitted DNS subtree can vouch for any name on the internet.
Collapsing those into one dead end is how a safety gate ends up being
worked around with --no-trust rather than satisfied -- the operator has
no way to tell "this is a real hole" from "this is out of date", so the
opt-out becomes the obvious move.
So each refusal now carries its own remedy, and only one of them has a
way forward:
STOP the root does not permit rank
the root is older than the endings claimed now — regenerating it is enough:
rm -rf /home/anthony/.moshpit/ca && moshpit-proxy # writes a fresh root
then re-run `moshcode dns enable`.
STOP the root permits no DNS subtree, so every name it does not exclude is allowed
moshcode will not put a root that can vouch for names outside Moshpit
into your trust store. ... This is not overridable.
The dangerous case is deliberately offered nothing, and a test asserts it
never advertises --no-trust as the fix.
This is the strictness note from #275 answered: the rule stays, because it
is the actual guarantee, but it is no longer a dead end when a root is
simply out of step.
1351 tests, 1348 pass, 0 fail.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…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>
curl https://seo.rankreturned this, on a machine where DNS was working perfectly:dns enablepointed names at the resolver and stopped. So a Moshpit name resolved and then failed its TLS handshake — which to the person who typed the URL is not "DNS works", it is "still broken".No CA is ever going to close this
The CA/Browser Forum Baseline Requirements banned certificates for non-IANA names. Issuance stopped in November 2015 and the survivors were revoked by October 2016, and a CA in a browser root store that issued for
.rankwould be distrusted for doing it. The rule is the penalty. This is not a matter of persuasion, budget, or trying a different CA.It was already solved, just not connected
moshpit-proxychecks the origin's key against the pin the registry published for that name, then re-signs with a root it generated on this machine — because restating the result is the only language a stock client accepts. All of that exists and is tested.What was missing is that nothing wired it up. moshcode's entire knowledge of
moshpit-proxybefore this PR was one string naming it as the installer of a drop-in file.So this is wiring, not new crypto.
dns enablenow finds the local root, checks it, installs it into the trust stores, and reports each one.The refusal is the feature
The only reason installing a local root is acceptable is one extension:
nameConstraints, marked critical, permitting Moshpit endings and nothing else. With it, the worst a stolen key reaches is a name its holder already controls. Without it, it can vouch for your bank.So moshcode refuses a root that lacks it, rather than warning:
moshpit-proxysets the constraint today and has its own test for it. This side still checks, because "upstream does it correctly" is not a property that can be assumed across a repo boundary — a regression there would otherwise become an unconstrained root installed automatically on every machine that randns enable. The tests generate real roots with openssl, constrained and not, and prove the guard rejects the bad one.Non-critical constraints are treated as absent, because a verifier may ignore an extension it does not recognise — which makes the guarantee a comment.
Reported, not summarised
NSS and the system store are separate outcomes. NSS (Chrome, Firefox) is user-level and always lands; curl's store needs root. Collapsing them into one "installed" line while curl still refuses is precisely the failure that produced this report, so without root it does the half it can and names the half it cannot, with the command to finish.
It never fails
enable. DNS is switched and verified by the time this runs. Failing the whole command over a trust store would roll back working resolution in order to fix a certificate — trading the larger thing for the smaller one.--no-trustopts out.Scope, stated plainly
This fixes machines you run it on. Nothing shippable can make a stranger's
curltrust a Moshpit name — that is the Baseline Requirements, not a gap in our tooling. TronBrowser remains the answer for someone who has run nothing, since it verifies registry pins natively and needs no DNS config, no proxy and no root.Tests
20 in
test/trust.test.mjs; full suite 1323 pass / 0 fail.