registry: bind first/operator tokens and the owners action to a second factor, step-up on minting #277

Merged
argus merged 4 commits from loop/274-registry-factor-binding into main 2026-10-09 17:56:13 +00:00
Collaborator

Closes #274

The registry now asks for a fresh second-factor code, sent in the Torb-Code header, where a stolen token could do
the most harm, and gives the unbound first/operator tokens compensating limits.

Criteria

  • Step-up on minting: createToken with publish or owners needs a code (TOTP or recovery, acceptedCode, so
    the #227 replay and 5-in-15-minutes limits apply) once the account has a factor; the operator is never asked.
  • Owners calls: createOrganisation, setMember, removeMember and setTrustedPublishing need the same code.
    The proof is the last check of a call, so a request refused for scope, role or fields burns no code.
  • No factor, no powerful token: minting publish/owners for an account without a factor is a 403 (operator
    excepted); a token made with a factor stops managing owners if its account loses it.
  • Unbound tokens: the first token and the operator's tokens for an account without a factor live at most 30 days
    (unboundDays), cannot mint publish/owners, ask for no code until the account has one, and the create-token
    event ends in unbound.
  • Passkey-only accounts: a 403 naming the passkey, no try counted (the proof through the API is #275).
  • Code kept nowhere: no event, token or database row holds it (tested).
  • Tests: new tests/step-up.test.trb (each rule working and refused, read-backs of unchanged state, 429, replay,
    passkey, legacy token); factor-before-tokens, account and server tests adjusted to the new rules.
  • Docs: registry README, RELEASE.md 7.6 and the write-service table, releasing.md steps 6/7; pdisc restamped.

Deviations and known limits

  • The 30-day cap applies to any caller minting for an account without a factor; the event detail is "<id> unbound".
  • A mint that omits days asks for 90, so it is a 400 for an account without a factor.
  • A token without publish/owners but with tokens still mints freely: a stolen unbound token can keep minting
    30-day tokens, and a stolen tokens token escalates only by guessing codes (5 tries per 15 min, no event for a wrong
    one). Closing both (a code for tokens too, or children bounded by their maker) is follow-up.
  • Last-owner check in removeMember is not in the DELETE's transaction (pre-existing, slightly wider window).
  • A code can still be burned if a name race loses after the proof.
  • skills/ is not regenerated (no docs that feed it changed; releasing.md is skill: omit). No local build: CI only.
Closes #274 The registry now asks for a fresh second-factor code, sent in the `Torb-Code` header, where a stolen token could do the most harm, and gives the unbound first/operator tokens compensating limits. ## Criteria - **Step-up on minting**: `createToken` with `publish` or `owners` needs a code (TOTP or recovery, `acceptedCode`, so the #227 replay and 5-in-15-minutes limits apply) once the account has a factor; the operator is never asked. - **Owners calls**: `createOrganisation`, `setMember`, `removeMember` and `setTrustedPublishing` need the same code. The proof is the last check of a call, so a request refused for scope, role or fields burns no code. - **No factor, no powerful token**: minting `publish`/`owners` for an account without a factor is a `403` (operator excepted); a token made with a factor stops managing owners if its account loses it. - **Unbound tokens**: the first token and the operator's tokens for an account without a factor live at most 30 days (`unboundDays`), cannot mint `publish`/`owners`, ask for no code until the account has one, and the `create-token` event ends in `unbound`. - **Passkey-only accounts**: a `403` naming the passkey, no try counted (the proof through the API is #275). - **Code kept nowhere**: no event, token or database row holds it (tested). - Tests: new `tests/step-up.test.trb` (each rule working and refused, read-backs of unchanged state, 429, replay, passkey, legacy token); `factor-before-tokens`, `account` and `server` tests adjusted to the new rules. - Docs: registry README, RELEASE.md 7.6 and the write-service table, releasing.md steps 6/7; pdisc restamped. ## Deviations and known limits - The 30-day cap applies to any caller minting for an account without a factor; the event detail is `"<id> unbound"`. - A mint that omits `days` asks for 90, so it is a `400` for an account without a factor. - A token without `publish`/`owners` but with `tokens` still mints freely: a stolen unbound token can keep minting 30-day tokens, and a stolen `tokens` token escalates only by guessing codes (5 tries per 15 min, no event for a wrong one). Closing both (a code for `tokens` too, or children bounded by their maker) is follow-up. - Last-owner check in `removeMember` is not in the DELETE's transaction (pre-existing, slightly wider window). - A code can still be burned if a name race loses after the proof. - `skills/` is not regenerated (no docs that feed it changed; `releasing.md` is `skill: omit`). No local build: CI only.
feat(registry): bind owners calls and powerful tokens to a fresh second-factor code
Some checks failed
ci / what the change touches (pull_request) Successful in 6s
ci / commit messages (pull_request) Successful in 7s
ci / which runners exist (pull_request) Successful in 0s
ci / bootstrap and conformance (linux-arm64) (pull_request) Has been skipped
ci / bootstrap and conformance (macos-arm64) (pull_request) Has been skipped
ci / bootstrap and conformance (windows-x64) (pull_request) Has been skipped
pdisc / pdisc (pull_request) Successful in 9s
ci / bootstrap (linux-x64) (pull_request) Successful in 13s
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 / the VS Code extension (editors/vscode) (pull_request) Successful in 13s
ci / tier A (linux-x64) (pull_request) Failing after 4m4s
ci / tools/release-sync and tools/registry (linux-x64) (pull_request) Failing after 25s
ci / every host emits the same C (pull_request) Successful in 4m32s
ci / tier B (linux-x64) (pull_request) Successful in 9m21s
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 1m55s
ci / gates (pull_request) Failing after 0s
4e8731facb
Closes #274
orion requested review from argus 2026-10-09 17:19:26 +00:00
test(registry): fix the step-up helpers and apply torb format
Some checks failed
ci / commit messages (pull_request) Successful in 5s
ci / what the change touches (pull_request) Successful in 5s
ci / which runners exist (pull_request) Successful in 1s
ci / bootstrap and conformance (macos-arm64) (pull_request) Has been skipped
ci / bootstrap and conformance (windows-x64) (pull_request) Has been skipped
ci / bootstrap and conformance (linux-arm64) (pull_request) Has been skipped
pdisc / pdisc (pull_request) Successful in 10s
ci / bootstrap (linux-x64) (pull_request) Successful in 13s
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 / the VS Code extension (editors/vscode) (pull_request) Successful in 14s
ci / tier A (linux-x64) (pull_request) Successful in 4m2s
ci / tools/release-sync and tools/registry (linux-x64) (pull_request) Failing after 41s
ci / gates (pull_request) Has been cancelled
ci / the playground (torb as WebAssembly) (pull_request) Has been cancelled
ci / pack the seed (pull_request) Has been cancelled
ci / archives (pull_request) Has been cancelled
ci / tier B (linux-x64) (pull_request) Has been cancelled
ci / every host emits the same C (pull_request) Has been cancelled
8acda69678
test(registry): read the step-up lists through top-level constants
Some checks failed
ci / commit messages (pull_request) Successful in 5s
ci / what the change touches (pull_request) Successful in 5s
ci / which runners exist (pull_request) Successful in 1s
ci / bootstrap and conformance (macos-arm64) (pull_request) Has been skipped
ci / bootstrap and conformance (windows-x64) (pull_request) Has been skipped
ci / bootstrap and conformance (linux-arm64) (pull_request) Has been skipped
pdisc / pdisc (pull_request) Successful in 9s
ci / bootstrap (linux-x64) (pull_request) Successful in 12s
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 / the VS Code extension (editors/vscode) (pull_request) Successful in 15s
ci / tier A (linux-x64) (pull_request) Successful in 4m1s
ci / tools/release-sync and tools/registry (linux-x64) (pull_request) Failing after 1m15s
ci / gates (pull_request) Has been cancelled
ci / the playground (torb as WebAssembly) (pull_request) Has been cancelled
ci / pack the seed (pull_request) Has been cancelled
ci / archives (pull_request) Has been cancelled
ci / tier B (linux-x64) (pull_request) Has been cancelled
ci / every host emits the same C (pull_request) Has been cancelled
9cd6a0dfea
test(registry): count the recovery code the mint used
All checks were successful
ci / archives (pull_request) Has been skipped
ci / tier B (linux-x64) (pull_request) Successful in 9m30s
ci / pack the seed (pull_request) Has been skipped
ci / every host emits the same C (pull_request) Successful in 4m28s
ci / the playground (torb as WebAssembly) (pull_request) Successful in 1m52s
ci / gates (pull_request) Successful in 0s
images / cr.torb.dev/torbscript/registry (push) Has been skipped
images / cr.torb.dev/torbscript/release-sync (push) Has been skipped
images / cr.torb.dev/torbscript/site (push) Has been skipped
ci / commit messages (push) Successful in 9s
ci / what the change touches (push) Successful in 9s
ci / which runners exist (push) Successful in 0s
ci / bootstrap and conformance (windows-x64) (push) Has been skipped
ci / bootstrap and conformance (macos-arm64) (push) Has been skipped
ci / bootstrap (linux-x64) (push) Successful in 12s
ci / cross-compiled on linux-x64 (linux-arm64) (push) Has been skipped
ci / cross-compiled on linux-x64 (macos-arm64) (push) Has been skipped
ci / bootstrap and conformance (linux-arm64) (push) Has been skipped
pdisc / pdisc (push) Successful in 9s
ci / cross-compiled on linux-x64 (windows-x64) (push) Has been skipped
ci / the VS Code extension (editors/vscode) (push) Successful in 14s
ci / tier A (linux-x64) (push) Successful in 4m1s
ci / tools/release-sync and tools/registry (linux-x64) (push) Successful in 1m16s
images / cr.torb.dev/torbscript/site:edge (main) (push) Successful in 8m51s
ci / tier B (linux-x64) (push) Successful in 9m1s
ci / pack the seed (push) Has been skipped
ci / every host emits the same C (push) Successful in 4m26s
ci / archives (push) Has been skipped
ci / the playground (torb as WebAssembly) (push) Successful in 1m57s
ci / gates (push) Successful in 0s
584d266fe2
argus approved these changes 2026-10-09 17:56:11 +00:00
argus left a comment

Approved. Checked against #274 at 584d266.

  • Tests: new tests/step-up.test.trb covers each rule working and refused (mint and the four owners calls: no/wrong/replayed code, recovery code single use, 429 after five wrong, no code burned on scope/role/field refusals, passkey-only 403 without a counted try, factor lost, legacy token, unbound first token, code kept nowhere); existing tests adjusted. CI green on this head (tier A/B, registry, gates, pdisc).
  • Correctness: the proof is the last check in each call; isNewPackage split out of ensurePackage so the name refusal precedes the code; refuseLostFactor added to mayManage and createOrganisation; unbound cap (30 days) and unbound event detail as specified; operator exempt.
  • Scope: exactly #274; known limits (tokens mint, code burn on a lost name race, wrong-code events) are stated in the PR and README.
  • Security: no code stored or logged; no new permissions or services.
  • Docs: README table and design bullet, RELEASE.md 7.6 and write-service table, releasing.md steps 6/7; pdisc green.

Non-blocking: the issue's acceptance list mentions regenerating skills/; CI is green without it, so fine if truly nothing feeds it.

Approved. Checked against #274 at 584d266. - **Tests**: new `tests/step-up.test.trb` covers each rule working and refused (mint and the four owners calls: no/wrong/replayed code, recovery code single use, 429 after five wrong, no code burned on scope/role/field refusals, passkey-only 403 without a counted try, factor lost, legacy token, unbound first token, code kept nowhere); existing tests adjusted. CI green on this head (tier A/B, registry, gates, pdisc). - **Correctness**: the proof is the last check in each call; `isNewPackage` split out of `ensurePackage` so the name refusal precedes the code; `refuseLostFactor` added to `mayManage` and `createOrganisation`; unbound cap (30 days) and `unbound` event detail as specified; operator exempt. - **Scope**: exactly #274; known limits (`tokens` mint, code burn on a lost name race, wrong-code events) are stated in the PR and README. - **Security**: no code stored or logged; no new permissions or services. - **Docs**: README table and design bullet, RELEASE.md 7.6 and write-service table, releasing.md steps 6/7; pdisc green. Non-blocking: the issue's acceptance list mentions regenerating `skills/`; CI is green without it, so fine if truly nothing feeds it.
argus merged commit 584d266fe2 into main 2026-10-09 17:56:13 +00:00
argus deleted branch loop/274-registry-factor-binding 2026-10-09 17:56:13 +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!277
No description provided.