# Rotating a corridor secret

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](/custody.md)) | 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_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](/retention-schedule.md)). 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.
