Skip to content

id: design: issuer key rotation and compromise recovery for currency issuers (from #2041) #2804

Open

Description

@dezren39

From the #2041 investigation (token issuance in the ID graph)

The landed investigation doc for #2041 (doc/2026-10-30T19-18-00Z_investigation_token_issuance_id_graph/, §10 case 7) identifies issuer key compromise as the catastrophic case for any issuer system: an attacker mints/converts as the issuer until the compromise is noticed. The model names the mitigations — supply caps bound mint damage, registries/views can pin issuer key generations — but the rotation mechanics were explicitly not designed. This issue designs them, so the #2043 synthesis adopts a designed answer instead of an afterthought.

Design to produce

  • Signed rotation object: format and chain semantics for an issuer key rotation (old key names successor), as an ID-graph object consistent with the signed-event envelope in pkgs/id/src/chain.rs; what happens to currency definitions issued pre-rotation (investigation position: they keep their identity — verify or amend).
  • Counting-view pinning: how counting views (id: investigate: trust/visibility model — servers accept all signed objects; views count only allowed writers #2030) and wallets pin which issuer key generation(s) they count for a currency, and how a view learns of a rotation/compromise.
  • Anchored path handling: registry/gateway treatment of rotation and of a declared compromise — minter-set updates via signed registry writes (id: design: currency control plane — registry, account factory, gateway #2062 §2.5), in-flight issuance/conversion treatment, and the compromise-declaration flow (who can declare, what freezes, what stays valid).
  • Graph-native path handling: the same questions where there is no gateway to enforce anything.

Acceptance criteria


Program: ID platform expansion, epic #2056. Parent investigation: #2041. Related: #2251 (object-format spec), #2062 (control plane), #2030 (counting views), #2043 (currencies v1 synthesis — consumes the outcome).

Activity

  1. added
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    on Oct 30, 2026
  2. added
    kind/designA design proposal (design-architect): multi-step, phased, decision-bearing
    priority/normalOrdinary priority (the default)
    effort/lLarge: multi-day or open-ended; expect staged check-ins
    meta/enrichedIntake review done: acceptance criteria, links, and normalized labels added
    on Oct 31, 2026
  3. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    Intake review

  4. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed on Nov 1, 2026
  5. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — task 5743 design triage record (lane A shift, 2026-11-01). Decision-ready record for issuer key rotation + compromise recovery, from the #2041 investigation (doc doc/2026-10-30T19-18-00Z_investigation_token_issuance_id_graph/, §10 case 7, §11 hand-off). Consumer: the #2043 currencies v1 synthesis (OPEN, gated on this outcome among its inputs).

    Grounding (verified today, main 51919e9)

    • No issuer or currency implementation exists in pkgs/id/src yet — this is a design-first topic; nothing shipped constrains the mechanics.
    • The signed-event envelope the rotation object must use exists: pkgs/id/src/chain.rs — Event (:262) and sign() (:282).
    • The investigation position (§10 case 7), adopted unless amended below: supply caps bound mint damage even with a stolen key; rotation is a signed object (old key names successor); definitions issued pre-rotation keep their identity; registries/views pin which issuer key generation they count.

    The decision

    The exact format/chain semantics of the rotation object, the pinning rules counting views apply, and the compromise-declaration flow — for both the anchored path (registry/gateway, #2062) and the graph-native path (no enforcer).

    Recommendation — the v1 design in one pass

    1. Rotation object = one chain Event, dual-signed. Signed by the OLD key, naming: issuer identity, currency scope (one currency or all-of-issuer — per-currency is the default), successor public key, generation = previous + 1, and the effective frontier (the event position from which the successor is authoritative). Countersigned by the SUCCESSOR key (proof the successor is live and controlled). A rotation missing the countersignature is invalid, not advisory.
    2. Generations are linear; a fork is a compromise signal. Two valid-looking rotations from the same generation cannot both be counted: a view counts the first in its frontier order and flags the conflict as a suspected compromise of that generation. No silent tie-breaking by timestamp (self-authored times prove nothing — the same honesty limit as the id: design: match attestation + audit v1 #2039 frontier rule).
    3. Definitions keep their identity (investigation position: VERIFIED, adopted). A currency definition references the issuer identity, never a key generation. Historical issuance is judged against the generation that was authoritative at its own frontier — rotation alone never invalidates history.
    4. Counting-view pinning. A view pins, per currency: the set of generations it counts (default: all non-compromised generations) plus the compromise frontier per compromised generation. Issuance under a generation BEFORE its compromise frontier counts; issuance AFTER it does not. A compromise declaration may assert an earlier compromise start; that earlier window becomes DISPUTED in the view (excluded from settled counts, visible as disputed) — never silently counted, never silently erased.
    5. Compromise declaration needs a key the thief does not have. This is the crux, stated plainly: a declaration signed by the compromised key itself proves nothing (the thief holds it too). The design therefore has issuers pre-commit a recovery key hash in the currency definition (cold key, never used for minting). A compromise declaration is authoritative only when signed by the recovery key (which simultaneously names the successor generation, i.e. compromise recovery IS a recovery-key-signed rotation). A declaration signed by the current minting key alone is advisory: views and gateways MAY freeze on it, and must label the freeze as unproven.
    6. Anchored path. The registry records the rotation via a signed registry write updating the minter set (id: design: currency control plane — registry, account factory, gateway #2062 §2.5 pattern), referencing the rotation event id. From the rotation frontier the gateway rejects mint/convert signatures from the old generation; conversions signed before the frontier settle; conversions signed after it are rejected, not queued. A compromise declaration (recovery-key-signed) freezes mint/convert for the compromised generation at the gateway immediately on receipt; pre-frontier history stays valid per rule 4.
    7. Graph-native path. No enforcer exists, so nothing "stops" — the same objects are published to the graph and every view/wallet applies rules 2–5 locally. Wallet UX must surface generation status (current / rotated / compromised-disputed) wherever a balance is shown; a view that has not seen a rotation yet is simply behind, and its counts must name the frontier they were computed at.

    Compromise walkthrough (acceptance scenario)

    Attacker mints with a stolen issuer key. Bounded immediately by: the currency supply cap (mint damage ceiling even with the key — investigation §10 case 7). The issuer publishes a recovery-key-signed compromise rotation (rule 5): anchored gateways freeze the stolen generation on receipt (rule 6); counting views stop counting post-frontier issuance from it and mark the asserted-compromise window disputed (rule 4). Remains exposed, honestly: issuance between the actual theft and the declaration counts unless the declaration asserts the earlier start (then it is disputed, and counterparties decide) — no design without a trusted clock does better; conversion attempts with the stolen key after the frontier fail at gateways and do not count in pinned views.

    Invariants

    Rotation never rewrites history; generation forks are signals, not ties; no authoritative compromise state from the compromised key alone; every count names its frontier; caps are load-bearing and stay mandatory for issuers.

    Phasing

    Question for Drew (explicit, non-blocking)

    Recovery-key pre-commitment (rule 5): mandate it for registry-anchored currencies (recommended — without it, compromise recovery has no authoritative signer, and anchored issuance is exactly where real value concentrates), or make it optional everywhere (more graph-native in spirit; anchored issuers who skip it get advisory-only recovery)? Default until ruled: the recommendation — mandatory on the anchored path, optional graph-native. Ruling goes on this issue; the doc packet proceeds either way and records the ruling.

  6. 80 remaining items

  7. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  8. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

    • Agent: queue worker (repo-worker-code), lane code-h
    • Session / ID: task 7983
    • Found via: queue claim
    • Task: Triage issue id: design: issuer key rotation and compromise recovery for currency issuers (from #2041) #2804 at current main — handed off to the opencode execution lane; the laptop claimant launches the bound opencode session, which drives task 7983 to its terminal state. Claim labels were already in sync on pickup (queue/claimed, work/assigned, status/in-progress).
    • Expectation: effort l; done = triaged at current main with evidence, fix/design path scoped or closed with evidence. Note: task 7983 is at attempt 5 of 6 — the claimant should complete it on this pass.
    • Links: queue task 7983 (repo-health-code-pilot)
  9. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  10. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  11. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  12. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  13. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  14. added
    queue/queuedHas a queued task in the Supabase agent queue
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    on Nov 5, 2026
  15. dezren39 commented on Nov 5, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

    • Agent: fleet monitor
    • Session / ID: task 7983 (repo-fleet-monitor run 2026-11-05T15:45:48Z)
    • Found via: monitor reconciliation (Duty 1 claim/label divergence repair)
    • Task: Claim evaporated: task 7983 is queued and unclaimed in repo-health-code-pilot, but this issue still carried claimed labels. Labels reset to the truth (queue/queued + status/needs-triage). Checkpoint stage at evaporation: handed_off.
    • Expectation: The next lane claim picks task 7983 up from the queued state and posts a fresh pickup note.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)effort/lLarge: multi-day or open-ended; expect staged check-inskind/designA design proposal (design-architect): multi-step, phased, decision-bearingmeta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedpriority/normalOrdinary priority (the default)queue/queuedHas a queued task in the Supabase agent queuesource/creatorFiled by the new-issue-creator jobstatus/needs-triageNot yet triaged; needs a first looktask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions