Two banks share one corridor secret. Every salt either of them derives comes from it, so a rotation is the one key operation they must do together. Mirror makes it a signed plan, a rehearsal, and a cutover by corridor version, never by the clock.
Why by version#
A clock cutover ("we both switch at midnight") has a window where one bank has switched and the other has not.
Mirror already fails that window safe. Legs banded under different keys carry different corridor commitments, so the operator answers undetermined ("different corridor tables or corridor keys"), never divergent. But every straddling shipment then needs a manual re-band.
Binding the new key to the corridor's next version removes the window. Every shipment names its corridor version in its policy, so both banks band it under the same key, by construction.
The procedure#
| Step | Who | Command / action | Pass condition |
|---|---|---|---|
| 1. Generate | One bank, dual control, offline | mirror-bander new-secret --out next.txt | 64 hex characters, owner-only file |
| 2. Import | Both banks | Import the 64 ASCII characters as HMAC key material (see custody) | Both HSMs hold it, non-exportable |
| 3. Plan | Proposing bank | mirror-bander rotate-plan --product cu --from 1 --to 2 --next-custody next.json --cutover T --rollback-until T+7d --retain-until T+7y --key member.jwk --out plan.json | The plan carries the next key's KCV only, signed |
| 4. Counter-sign | Other bank | Check the plan's next_kcv against its own mirror-bander kcv --custody next.json; sign the same plan | Both banks hold both signatures |
| 5. Rehearse | Both banks | mirror-bander rotate-rehearse --plan plan.json --current-custody cur.json --next-custody next.json | Exit 0, every check ok |
| 6. Ratify | Both banks | Propose and ratify cu@2 (same edges unless the table changes too) | cu@2 ratified on /registry |
| 7. Cut over | Both banks | From cutover_at, new shipments name version 2 and are banded with --custody next.json | The first version-2 verdicts come back reconciled or divergent on their merits, never "different corridor keys" |
| 8. Retain | Both banks | Keep the old key, MAC-only, until retain_old_until | Audit openings of version-1 legs keep working |
| 9. Shred | Both banks, after checking no hold applies | Destroy the old key in both HSMs | Version-1 legs can never be opened again |
The rehearsal (step 5) checks:
current_key_answers: the old key still answers, so openings of existing legs keep working. If it does not, do not rotate.next_key_answers: the new custody spec, key id and MAC algorithm are right.keys_distinct: the new key material is actually new.next_kcv_matches_plan: this bank imported what the other bank imported. The classic failure it catches is importing the 32 decoded bytes instead of the 64 ASCII characters.salts_differ: sample salts differ between the two keys.next_key_deterministic: repeated MACs under the new key are identical. A MAC that varies would never let two banks' commitments meet.
The report carries only KCVs and booleans: no key, no salt, no shipment reference.
Rollback#
- Clean until the first shipment is banded under the new key, and only while
rollback_untilhas not passed. Rolling back is simply not using version 2. Nothing was committed under the new key, so nothing is stranded. - Forward only after that. Legs exist that only the new key can open, so "rolling back" means rotating forward to a third key (version 3).
mirror-rotation.state()reports which of the two you are in. - Suspected compromise: a plan with
reason: suspected_compromisehas no rollback window. Rotate forward immediately.
A leaked old key exposes the band of every shipment committed under it, because the band space is small. Rotation limits the damage from the cutover on; it cannot undo it.
Retention and shredding#
Destroying the old key is crypto-shredding: after it, no leg committed under that key can be opened, by anyone. That is Mirror's erasure path (see the retention schedule). It is also why:
retain_old_untilmust cover your record-keeping period;- nobody schedules the destruction before checking for a litigation hold or a supervisory request.
What is proven, and where#
- The whole rotation is rehearsed end to end over the operator by
server/rotation-rehearsal.test.js:- plan, rehearsal against two custody providers, and cutover by version;
- the straddling shipment that fails safe;
- opening a version-1 leg with the retained old key after cutover;
- the rollback state closing on first use.
- The rehearsal catches a bank that imported the decoded bytes.
- Mirror cannot see inside either HSM. Non-exportability and dual control are the banks' controls, attested by the banks.