DocumentsRunning it

Rotating a corridor secret

3 min readSource corridor-key-rotation.mdsha256 c6af53dba12eef37…

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#

StepWhoCommand / actionPass condition
1. GenerateOne bank, dual control, offlinemirror-bander new-secret --out next.txt64 hex characters, owner-only file
2. ImportBoth banksImport the 64 ASCII characters as HMAC key material (see custody)Both HSMs hold it, non-exportable
3. PlanProposing bankmirror-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.jsonThe plan carries the next key's KCV only, signed
4. Counter-signOther bankCheck the plan's next_kcv against its own mirror-bander kcv --custody next.json; sign the same planBoth banks hold both signatures
5. RehearseBoth banksmirror-bander rotate-rehearse --plan plan.json --current-custody cur.json --next-custody next.jsonExit 0, every check ok
6. RatifyBoth banksPropose and ratify cu@2 (same edges unless the table changes too)cu@2 ratified on /registry
7. Cut overBoth banksFrom cutover_at, new shipments name version 2 and are banded with --custody next.jsonThe first version-2 verdicts come back reconciled or divergent on their merits, never "different corridor keys"
8. RetainBoth banksKeep the old key, MAC-only, until retain_old_untilAudit openings of version-1 legs keep working
9. ShredBoth banks, after checking no hold appliesDestroy the old key in both HSMsVersion-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_until has 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_compromise has 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_until must 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.