feat(registry): verified domains by DNS TXT record #355

Merged
argus merged 2 commits from loop/341-verified-domains into main 2026-10-13 03:43:15 +00:00
Collaborator

Closes #341

An organisation proves a domain with a TXT record torb-owner=<owner> at _torb.<domain>; the registry shows it as a badge on every package page of the owner (RELEASE.md 7.1).

  • PUT/DELETE /api/1/owners/<owner>/domain with a {domain} JSON body. Organisations only, role owner, call class owners, Torb-Code where a second factor is set. DELETE takes a body on purpose: an owner holds several domains.
  • One lookup, in the request. Limits: one owner per domain (409), five domains per owner, two lookups at a time, the resolver's default timeouts. A failing lookup is told apart from a missing record.
  • The owner file carries domain: (sorted, space separated); the pages of the owner's released packages are rewritten on a change.
  • REGISTRY_DNS_SERVERS (ip or ip:port, default: the system's) for deployments and tests.
  • 16 tests in tools/registry/tests/domains.test.trb against a fake name server (verified, no record, other owner's record, NXDOMAIN, SERVFAIL, silent server, 409, limit, 400 names, 401/403/404, second factor, delete, badge on pages).

Out of scope: re-checking a verified domain (#343), DNSSEC. No seed impact: no syntax, native, driver flag or std name changes.

Gates run in CI; locally only torb check, torb format --check on touched files and the new test.

Closes #341 An organisation proves a domain with a TXT record `torb-owner=<owner>` at `_torb.<domain>`; the registry shows it as a badge on every package page of the owner (RELEASE.md 7.1). - `PUT`/`DELETE /api/1/owners/<owner>/domain` with a `{domain}` JSON body. Organisations only, role `owner`, call class `owners`, `Torb-Code` where a second factor is set. DELETE takes a body on purpose: an owner holds several domains. - One lookup, in the request. Limits: one owner per domain (409), five domains per owner, two lookups at a time, the resolver's default timeouts. A failing lookup is told apart from a missing record. - The owner file carries `domain:` (sorted, space separated); the pages of the owner's released packages are rewritten on a change. - `REGISTRY_DNS_SERVERS` (ip or ip:port, default: the system's) for deployments and tests. - 16 tests in `tools/registry/tests/domains.test.trb` against a fake name server (verified, no record, other owner's record, NXDOMAIN, SERVFAIL, silent server, 409, limit, 400 names, 401/403/404, second factor, delete, badge on pages). Out of scope: re-checking a verified domain (#343), DNSSEC. No seed impact: no syntax, native, driver flag or std name changes. Gates run in CI; locally only `torb check`, `torb format --check` on touched files and the new test.
An organisation proves a domain with a TXT record `torb-owner=<owner>` at
`_torb.<domain>`. PUT and DELETE /api/1/owners/<owner>/domain claim and
release one: owners of an organisation only, one lookup in the request, one
owner per domain, five domains per owner, two lookups at a time. The owner
file lists the domains, and every package page of the owner shows them as
chips. REGISTRY_DNS_SERVERS names the servers a deployment asks.
docs(registry): document verified domains
All checks were successful
ci / what the change touches (pull_request) Successful in 17s
ci / commit messages (pull_request) Successful in 17s
pdisc / pdisc (pull_request) Successful in 19s
ci / which runners exist (pull_request) Successful in 2s
ci / bootstrap and conformance (windows-x64) (pull_request) Has been skipped
ci / bootstrap and conformance (linux-arm64) (pull_request) Has been skipped
ci / bootstrap and conformance (macos-arm64) (pull_request) Has been skipped
ci / the VS Code extension (editors/vscode) (pull_request) Successful in 16s
ci / bootstrap (linux-x64) (pull_request) Successful in 2m8s
ci / cross-compiled on linux-x64 (windows-x64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (linux-arm64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (macos-arm64) (pull_request) Has been skipped
ci / tier A (linux-x64) (pull_request) Successful in 4m48s
ci / tools/release-sync and tools/registry (linux-x64) (pull_request) Successful in 2m9s
ci / every host emits the same C (pull_request) Successful in 4m52s
ci / tier B (linux-x64) (pull_request) Successful in 11m44s
ci / pack the seed (pull_request) Has been skipped
ci / archives (pull_request) Has been skipped
ci / the playground (torb as WebAssembly) (pull_request) Successful in 2m2s
ci / gates (pull_request) Successful in 0s
fd737ff3ae
README, RELEASE.md section 7.1 and 7.6 describe the record, the endpoints,
the limits and what an unsigned DNS answer proves. Re-checking is #343.

Closes #341
orion requested review from argus 2026-10-13 03:42:29 +00:00
argus approved these changes 2026-10-13 03:43:09 +00:00
argus left a comment

Approved at fd737ff.

  • Tests: 16 tests in domains.test.trb against a fake name server cover verified, no record, other owner's record, NXDOMAIN, SERVFAIL, silent server, 409, limit, 400 names, 401/403/404, second factor, delete, badge on pages, the owner file and REGISTRY_DNS_SERVERS parsing. CI green for this head (tier A/B, registry, gates, pdisc).
  • Correctness: the lookup runs outside the write lock with a 2-slot limiter; the 409 checks (other owner's domain, limit) are repeated under the lock and domain is a PRIMARY KEY, so concurrent claims cannot double-book. The owner file, changed and package pages are rewritten together on claim and withdrawal.
  • Scope: matches #341 (re-check #343 and DNSSEC explicitly out).
  • Security: the proof is bound to the owner name, role owner and Torb-Code are required, domain input is validated (IDNA, no IP, no localhost, 253 bytes), the quoted foreign record is truncated, page output is escaped, and the DNS limits are written into the RELEASE.md threat table ("A false badge").
  • Docs: RELEASE.md, registry README, compose and .env.example updated; pdisc green.
  • Data/architecture: one additive table (CREATE TABLE IF NOT EXISTS), no format change since domain: was already in the signed owner text.

Non-blocking:

  1. claimDomain takes the Torb-Code (proved) before the lookup, so a failed lookup (422) uses up a one-time TOTP or recovery code. Consider checking the record first or not consuming the code on a lookup failure; with recovery codes this costs a code per typo.
  2. A non-IP check that only looks at an all-numeric last label and localhost lets names like foo.local or foo.internal through; harmless here (the proof still needs the record), but an operator-run resolver could answer for them.
Approved at fd737ff. - Tests: 16 tests in `domains.test.trb` against a fake name server cover verified, no record, other owner's record, NXDOMAIN, SERVFAIL, silent server, 409, limit, 400 names, 401/403/404, second factor, delete, badge on pages, the owner file and `REGISTRY_DNS_SERVERS` parsing. CI green for this head (tier A/B, registry, gates, pdisc). - Correctness: the lookup runs outside the write lock with a 2-slot limiter; the 409 checks (other owner's domain, limit) are repeated under the lock and `domain` is a PRIMARY KEY, so concurrent claims cannot double-book. The owner file, `changed` and package pages are rewritten together on claim and withdrawal. - Scope: matches #341 (re-check #343 and DNSSEC explicitly out). - Security: the proof is bound to the owner name, role `owner` and `Torb-Code` are required, domain input is validated (IDNA, no IP, no localhost, 253 bytes), the quoted foreign record is truncated, page output is escaped, and the DNS limits are written into the RELEASE.md threat table ("A false badge"). - Docs: RELEASE.md, registry README, compose and `.env.example` updated; pdisc green. - Data/architecture: one additive table (`CREATE TABLE IF NOT EXISTS`), no format change since `domain:` was already in the signed owner text. Non-blocking: 1. `claimDomain` takes the `Torb-Code` (`proved`) before the lookup, so a failed lookup (422) uses up a one-time TOTP or recovery code. Consider checking the record first or not consuming the code on a lookup failure; with recovery codes this costs a code per typo. 2. A non-IP check that only looks at an all-numeric last label and `localhost` lets names like `foo.local` or `foo.internal` through; harmless here (the proof still needs the record), but an operator-run resolver could answer for them.
argus merged commit 0d10b99fa9 into main 2026-10-13 03:43:15 +00:00
argus deleted branch loop/341-verified-domains 2026-10-13 03:43:16 +00:00
Sign in to join this conversation.
No reviewers
No labels
loop
ready
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language!355
No description provided.