Skip to content

Ask before moving a mint's signing-key pin - #66

Merged
TheCryptoDonkey merged 1 commit into
mainfrom
fix/approve-key-rotation
Sep 6, 2026
Merged

Ask before moving a mint's signing-key pin#66
TheCryptoDonkey merged 1 commit into
mainfrom
fix/approve-key-rotation

Conversation

@TheCryptoDonkey

Copy link
Copy Markdown
Member

LUD-25 gained this rule on 2026-09-04, in the same change that dropped previousPubkeys from the spec:

A newly advertised mintPubkey MUST NOT silently replace a pinned one, WALLET MUST require explicit holder approval.

This wallet moved the pin on its own whenever the mint published the old key as retired, and mentioned it afterwards in a warning nobody has to read.

Why the old behaviour was wrong

A published retirement is evidence, never consent. Whoever controls the host controls what it publishes, so a mint someone else now runs can list the key it displaced and look exactly like a mint rotating properly. The two are indistinguishable from here - which is precisely why the holder is the one who decides.

What changes

  • An announced rotation throws the new KeyRotationError and writes nothing: not the pin, not the key history, not the note being received. receive --accept-key-rotation (or approveKeyRotation: true on the API) takes it.
  • An unannounced one is unchanged: PinMismatchError, no approval on offer. The difference between the two errors is whether there is a decision to make at all.
  • The KeyRotationError message names both possibilities rather than picking one. A message that only said "rotated" would be asking for a rubber stamp.

A half-applied refusal would be worse than either outcome: the pin would still name the old key while the history and the rotated-on date said a rotation had happened, so the next receive would look like a second one. There is a test for exactly that.

Verification

  • 324 tests pass, npm run check clean
  • test/rotation.test.ts grew to 11 cases: the two acceptance tests now prove the refusal first, plus new cases for "writes nothing when unapproved" and for the wording of the question

LUD-25 gained the rule on 2026-09-04, in the same change that dropped
previousPubkeys from the spec: "a newly advertised mintPubkey MUST NOT
silently replace a pinned one, WALLET MUST require explicit holder approval".

This wallet moved the pin on its own whenever the mint published the old key
as retired, and mentioned it afterwards in a warning nobody has to read. That
treats a published retirement as consent, and it is only evidence. Whoever
controls the host controls what it publishes, so a mint someone else now runs
can list the key it displaced and look exactly like a mint rotating properly.
The two cases are indistinguishable from here, which is precisely why the
holder is the one who decides.

An announced rotation now throws KeyRotationError and writes nothing - not
the pin, not the key history, not the note being received. A refusal that
half-applied would be worse than either outcome: the pin would still name the
old key while the history and the rotated-on date said a rotation had
happened, so the next receive would look like a second one. receive
--accept-key-rotation takes it.

An unannounced rotation is unchanged: PinMismatchError, no approval on offer.
The difference between the two errors is whether there is a decision to make
at all, and the KeyRotationError message names both possibilities rather than
picking one - a message that only said "rotated" would be asking for a rubber
stamp.

324 tests pass.
@TheCryptoDonkey
TheCryptoDonkey merged commit c2e0158 into main Sep 6, 2026
2 checks passed
@TheCryptoDonkey
TheCryptoDonkey deleted the fix/approve-key-rotation branch September 6, 2026 09:35
TheCryptoDonkey added a commit that referenced this pull request Sep 6, 2026
#66 made an announced signing-key rotation a decision rather than something
the wallet takes on its own. It gave the CLI a flag and left the browser with
a refusal it could not show: main.ts catches BadSignatureError and rethrows
anything else, so a KeyRotationError left `take()` as an unhandled rejection.
The receive would fail with nothing on screen and no way past it.

So the PWA gets its own refusal card. It is deliberately not the signature
card with different words, because the two refusals are different in kind. A
signature that does not verify is a fact about the note in your hand. A mint
presenting a new signing key is a question about the mint, and one the wallet
genuinely cannot answer: a mint rotating properly and a mint somebody else now
runs publish the same two claims from the same server.

The card says exactly that, and says the one thing a holder can actually do
about it - check the new key by some route that is not this mint. It also says
that notes already held keep working, because the old key is kept, so the
decision is not being made under a false clock.

Two taps to accept, matching the signature override, and for the same reason:
the honest case for overriding is that you already know something the wallet
does not.

324 tests pass, web typecheck clean.

Co-authored-by: TheCryptoDonkey <TheCryptoDonkey@users.noreply.github.com>
@TheCryptoDonkey TheCryptoDonkey mentioned this pull request Sep 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant