Skip to content

Commit 9617f81

Browse files
ralyodioclaude
andauthored
feat(dns): finish the job — install trust, not just routing (#275)
* feat(dns): finish the job — install trust, not just routing `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> * fix(trust): make the name-constraints gate actually constrain 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> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 0ab56aa commit 9617f81

4 files changed

Lines changed: 809 additions & 0 deletions

File tree

src/cli-schema.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ export const CORE_CLI_COMMANDS = [
189189
flags: [
190190
["--port <n>", "port for the bridge", "5354"],
191191
["--registry <url>", "registry to resolve against", "https://pit.moshcode.sh"],
192+
["--no-trust", "with enable: route names but skip the local CA", ""],
192193
],
193194
examples: [
194195
["sudo moshcode dns enable", "route Moshpit endings here"],

src/dns.mjs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1930,6 +1930,7 @@ import { createParkingServer, DEFAULT_PARKING_HTTP_PORT } from "./parking-http.m
19301930
// use it without importing this one back.
19311931
export { pitNameUrl } from "./pit-url.mjs";
19321932
import { pitNameUrl } from "./pit-url.mjs";
1933+
import { applyTrust, verifyStockTls } from "./trust.mjs";
19331934
import { readFile, writeFile } from "node:fs/promises";
19341935
import { existsSync } from "node:fs";
19351936
import { fileURLToPath } from "node:url";
@@ -1974,6 +1975,9 @@ const USAGE = `moshcode dns — resolve Moshpit names on this machine
19741975
removed, without this.
19751976
--backend linux only: systemd-resolved (default) or dnsmasq
19761977
--port N the bridge's port (Windows must use 53 — NRPT carries no port)
1978+
--no-trust with enable: route names but skip the local CA. They will
1979+
resolve and then fail TLS, which is the state this flag exists
1980+
to leave you in deliberately.
19771981
19781982
The registry speaks HTTP, not DNS, so nothing outside a browser can reach a
19791983
Moshpit name until this bridge is running and your resolver points at it.
@@ -2548,6 +2552,26 @@ export async function dnsCommand(args = [], out = console.log, deps = {}) {
25482552
}
25492553

25502554
if (!outcome.rolledBack) {
2555+
out("");
2556+
// Resolving was never the whole job. A name that resolves and then fails
2557+
// its TLS handshake reads as broken to the person who typed the URL, and
2558+
// no CA will ever sign for a Moshpit name — so the local root that
2559+
// moshpit-proxy generates is the only thing that closes it.
2560+
if (!rest.includes("--no-trust")) {
2561+
const trusted = await applyTrust(tlds, out, deps);
2562+
// The claim this whole feature makes is that an ordinary client now
2563+
// works, so check it as an ordinary client would — a plain HTTPS GET
2564+
// with nothing relaxed. Only worth asking when a root actually went in
2565+
// and there is a real name to ask about, and never fatal: a name that
2566+
// resolves but serves no HTTPS is not a failure of the trust store.
2567+
if (trusted?.ok && trusted.installed && moshpitProbe) {
2568+
const proof = await verifyStockTls(moshpitProbe);
2569+
out(proof.ok
2570+
? ` ok https://${moshpitProbe}/ verified by a stock client (${proof.status})`
2571+
: ` -- https://${moshpitProbe}/ not verified yet — ${proof.why}`);
2572+
}
2573+
}
2574+
25512575
out("");
25522576
out(`Moshpit names now resolve on this machine. Try: moshcode dns resolve ${moshpitProbe || "<name>"}`);
25532577
out(`Routing covers the ${tlds.length} TLDs claimed right now. New ones do not route`);

0 commit comments

Comments
 (0)