registry: bind first/operator tokens and the owners action to a second factor, step-up on minting #274
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Goal
Close the three limits PR #273 (#228) documented: (1) tokens that are not bound to a factor, (2) the
ownersaction, which a stolen token can use to re-point trusted publishing or add a publisher, (3) minting a publishing token, which checks that a factor is enrolled but not that the caller just proved it. After this a stolen token alone can neither mint a token that publishes or manages owners, nor change an owner; the thief also needs a current code of the account's second factor. Parent: #16.Findings that shape the decisions (checked at
d87348e)Registry.acceptedCode(account, code)(#227) already checks a TOTP or recovery code with replay protection and the 5-in-15-minutes limit, so no new factor machinery is needed.POST /api/1/tokens, and the only owners calls arePOST /api/1/owners,PUT/DELETE .../owners/<owner>/members/<account>andPUT .../packages/<owner>/<name>/trusted-publishers(Registry.createOrganisation,setMember,removeMember,setTrustedPublishing).POST /api/1/accounts), and such an account has no linked identity, so it cannot sign in and cannot enrol a factor. That is why the first token has to stay unbound (point 1); the way out is #276.Decisions (loop decision, recorded; none changes a published contract with real users: the registry's only callers today are the operator and the account's own scripts)
Mechanism (points 2 and 3): a code in the
Torb-Codeheader. The value is a current TOTP code or an unused recovery code of the token's account, checked byacceptedCode(so single use per 30-second step, constant-time, rate limit with429). A header, not a body field, becauseDELETEand somePUTcalls have no usable body, and one place inapi.trbreads it for every call. The registry never logs or stores it.Guarded actions are
publishandowners.yankandtokensstay unguarded: a token withtokenscan mint only what its maker has, and minting a guarded action needs the code anyway;yankis reversible (unyank) and was never in the factor requirement.Point 3, step-up on minting.
POST /api/1/tokenswithpublishorownersamong the actions needs, for an account token: a factor on the account (as today,403otherwise) and a validTorb-Code(403"needs a current code", or the refusal ofacceptedCode). The code is checked last, after scope, validation and the factor check, so a refused request burns no code. The operator is exempt, as today (its own act).Point 2, the owners action. Each owners call by an account token needs a valid
Torb-Codewhen the account has a factor, checked after the token's scope, the membership and the request validation and before anything is written. The operator is exempt; trusted tokens already cannot call these. Minting a token withownersis guarded as above, and the use-time rule of #228 applies to it as topublish: a token made with a factor (factor= 1) is refused for owners calls once its account has none.Point 1, unbound tokens.
docs/contributing/releasing.mdsteps 6 and 7 (create an organisation, set trusted publishing, publish the first release) and cannot be removed without making the registry unusable before a person can sign in.POST /api/1/tokensfor an account without a factor refuses more than 30 with400; an account with a factor is not affected); (b) an owners call by any token needs the code as soon as the account has a factor, so an unbound token stops being enough the moment the account enrols one; (c) such a token cannot mint a guarded action (the account has no factor, so the mint rule refuses it); (d) the event log marks it (create-tokendetailunbound). The first token keeps every action, so the bootstrap steps keep working.factor= 0 on an account without a factor is allowed as today.Out of scope.
publishtokens); no such account can hold a token today, since a provider sign-up gets none, so nobody loses a working path.yankand the read calls; a step-up on the account page (it has no mint form); a second proof at sign-in (sign-in second step, still "Not built yet" in RELEASE.md).torb owner(#22) will have to sendTorb-Code; noted there when that command is refined.Existing behaviour that changes
POST /api/1/tokenswithpublishorowners: newTorb-Coderequirement for account tokens (breaking for scripts of accounts that mint such tokens; none known).POST /api/1/owners, memberPUT/DELETE, trusted-publishersPUT):Torb-Coderequired once the account has a factor.POST /api/1/accountslives 30 days, not 90.README.mdtable rows, RELEASE.md 7.6 and the write-service table change accordingly.Pinned records
acceptedCode)Acceptance criteria
api.trbreads theTorb-Codeheader once and passes it tocreateToken,createOrganisation,setMember,removeMemberandsetTrustedPublishing; the value is in no response, event, log line or database row (a test checks the event detail and the database for it).POST /api/1/tokenswithpublishorownersby an account token with a factor succeeds with a valid TOTP code and with an unused recovery code (which is consumed); is a403without the header; a403with a wrong code; a403with a replayed TOTP code (same step); a429after 5 wrong codes in 15 minutes without looking at the code; unchanged for a token with onlyyank/tokens; unchanged for the operator (no code).403(no code can help), and the code is not consumed when the request is refused for scope, validation, the factor check or the missingtokensaction (test: the same code works on the next valid request).requiredare read back unchanged); a call refused for role or scope or a400validation burns no code.POST /api/1/accountshas all actions, 30 days, and the event detailunbound; with it, creating an organisation and setting trusted publishing work with no code while the account has no factor; once a factor is enrolled the same token's owners calls need the code;POST /api/1/tokensby the operator for an account without a factor with more than 30 days is a400, with 30 or fewer it succeeds, and for an account with a factor 365 days still works.factor= 1 whose account lost its factor is refused for owners calls (the use-time rule of #228 extended toowners); a token made before this (factor= 0, any actions, account without a factor) works as before.torb test tools/registry/testsgreen natively and in the VM.tests/factor-before-tokens.test.trb(or a newtests/step-up.test.trb), each rule once as it works and once as it is refused, with a fixed clock for the TOTP codes as intotp-account.test.trb; existing tests that mintpublishor call owners with factor accounts are adjusted.tools/registry/README.md(the call table rows for tokens and owners, the header, the design bullet that lists the known limits of #228, the unbound-token rule), RELEASE.md 7.6 "A stolen token" row and the write-service Tokens row,docs/contributing/releasing.mdsteps 6 and 7 (30-day first token,Torb-Codeonce a factor exists), and the "Not built yet" paragraph;skills/regenerated, pdisc restamped.Seed impact
None, checked: TorbScript in
tools/registry(registry.trb,api.trb) onacceptedCodethat exists; no syntax change, no native, no driver flag, no std name the compiler looks up, no new std surface; no schema change (no new column or table). No seed refresh, so no two-commit rule.Size and dependencies
One PR, one or two Conventional Commits (
feat(registry): ..., anddocs(registry): ...if separate). Depends on #272 and #273 (both landed). Follow-ups filed with labelloop: #275 (passkey step-up), #276 (claim operator-made accounts). Parent: #16. Security-relevant: architect review of the diff (implementer opus/high).[loop:torbscript] intake: in scope (author orion, label loop; follow-up of the PR #273 review). Ready buffer is below 4 (#191 closed), so refine runs. claim refine by session:loop-torbscript-20261009 until 2026-10-09T19:00Z
[loop:torbscript] refine: decided by the loop: (1) first token and operator tokens for an account without a factor stay unbound (operator-made accounts cannot sign in or enrol), compensated by 30-day cap, code required on owners calls once the account has a factor, no guarded mint; (2) owners calls need a current TOTP/recovery code in
Torb-Codewhen the account has a factor; (3) mintingpublish/ownersneeds the same code (step-up); passkey-only proof split to #275, claiming operator-made accounts to #276; no seed impact, no schema change, one PR. next: implement[loop:torbscript] claim implement by session:loop-torbscript-20261009 until 2026-10-09T23:03Z
[loop:torbscript] implement: PR #277 opened, CI green, owners calls and publish/owners token minting now need a fresh Torb-Code once the account has a second factor; unbound first/operator tokens capped at 30 days. next: review (waiting on the merge gate argus). Claim released.
[loop:torbscript] close: PR #277 approved and merged by argus as
584d266fe2; main CI green on that commit. Claim released.login,logout,ownerandyankcommands #22