// The TLDs this installation will mint TLS certificates for. // // The BNS protocol is deliberately TLD-agnostic: bns.js accepts any // /^[a-z0-9-]{1,16}$/ as a TLD, because the chain decides what exists, not this // file. TLS is the one place that cannot stay open-ended. The local root CA is // installed into the machine trust store, and an X.509 name-constrained CA needs // a *finite* list of permitted dNSName subtrees (RFC 5280 §4.2.1.10). Without // one the root can vouch for every domain on the internet. // // So: resolution stays unbounded, certificate issuance is enumerated. A name on // a TLD that is not listed here still resolves and still serves over plain HTTP // — it just gets no certificate from the local CA. // // Adding a TLD requires regenerating the root CA and re-importing it on every // machine (see setup/install.ps1). That cost is the point: it is what stops this // root from being a general-purpose signing oracle. import { readFileSync, existsSync } from "node:fs"; import { fileURLToPath } from "node:url"; // RFC 2606 reserves `.test` forever, and none of the others are ICANN-delegated. export const DEFAULT_TLDS = ["bch", "p2p", "bit", "deviant", "nav", "test", "bcnr"]; // A tripwire, not a complete list — ICANN has ~1500 delegated TLDs and adds more. // Its job is to make the obvious mistake loud, not to be authoritative. Anything // added to the permitted list must be checked against the real IANA root zone: // https://data.iana.org/TLD/tlds-alpha-by-domain.txt const ICANN_TRIPWIRE = new Set([ "com", "net", "org", "edu", "gov", "mil", "int", "info", "biz", "io", "co", "app", "dev", "xyz", "online", "site", "cloud", "ai", "me", "tv", "sh", "gg", "uk", "us", "de", "fr", "nl", "eu", "ru", "cn", "jp", "ca", "au", "ch", "it", ]); const TLD_RE = /^[a-z0-9-]{1,16}$/; const CONFIG_FILE = fileURLToPath(new URL("../../ariadne.config.json", import.meta.url)); function validate(tlds) { const out = []; for (const raw of tlds) { const tld = String(raw).trim().toLowerCase().replace(/^\.+/, ""); if (!TLD_RE.test(tld)) throw new Error(`invalid TLD "${raw}" in permitted list (allowed: [a-z0-9-]{1,16})`); if (ICANN_TRIPWIRE.has(tld)) { throw new Error( `refusing to permit ".${tld}" — it is an ICANN-delegated TLD. Adding it would let the ` + `local root CA issue trusted certificates for real internet domains on every machine ` + `that installed it. If this is genuinely intended, edit ICANN_TRIPWIRE in src/lib/tlds.js.`, ); } if (!out.includes(tld)) out.push(tld); } if (!out.length) throw new Error("permitted TLD list is empty — the CA could issue nothing"); return out; } // Effective list = ariadne.config.json "tlds", else the built-in default. // Read once at import: the root CA is baked against this list, so changing it at // runtime would silently desync the constraints from the certificates. export const PERMITTED_TLDS = (() => { try { if (existsSync(CONFIG_FILE)) { const cfg = JSON.parse(readFileSync(CONFIG_FILE, "utf8")); if (Array.isArray(cfg.tlds) && cfg.tlds.length) return validate(cfg.tlds); } } catch (e) { if (e.message.startsWith("refusing") || e.message.startsWith("invalid TLD")) throw e; console.error(`tlds: ${CONFIG_FILE} unreadable (${e.message}) — using defaults`); } return validate(DEFAULT_TLDS); })(); // Rightmost label of a hostname, minus any port or trailing dot. null if unusable. export function hostTld(host) { if (typeof host !== "string" || !host) return null; const bare = host.trim().toLowerCase().replace(/\.$/, "").replace(/:\d+$/, ""); if (!bare || bare.includes("/")) return null; const tld = bare.slice(bare.lastIndexOf(".") + 1); return TLD_RE.test(tld) ? tld : null; } // May the local CA issue a certificate for this hostname? export function isPermittedHost(host, tlds = PERMITTED_TLDS) { const tld = hostTld(host); return tld !== null && tlds.includes(tld); }