# Custody: the keys stay in your HSM

Mirror never needs a key in a file. Two kinds of key are involved, and both fit operations every HSM and cloud KMS already performs:

| Key | What it does | HSM / KMS operation |
|---|---|---|
| **Corridor secret** (shared by the two banks on a corridor) | Derives each shipment's salt: `HMAC-SHA-256(secret, "mirror:" + shipment_ref)` | HMAC-SHA-256 on a non-exportable MAC key |
| **Signing keys** (member, witness, relay desk, origination) | Sign legs, events, measurements, origins, logins | ECDSA P-256 with SHA-256 on a non-exportable key |

The bander and the origination sidecar talk to your HSM through `mirror-custody`. The tool is configured as an executable path plus an argument list, and it is spawned **without a shell**. The input goes in on stdin and the answer comes back on stdout. The bander opens no socket, and `verify-diode` reports the one program it is allowed to run.

## 1. The corridor secret

### Generate it once

```bash
mirror-bander new-secret --out corridor-secret.txt
```

This writes 64 hex characters (256 bits), readable by its owner only, and never prints them. Do it on an offline machine under dual control.

**The key material is those 64 ASCII characters, not the 32 bytes they decode to.** The kernel computes `HMAC(secret-as-text, ...)`, so the HSM must hold the 64-byte ASCII string as the HMAC key. Import the decoded 32 bytes instead and every salt comes out different, silently. The KCV check in step 3 is what catches this.

### Import it into each bank's HSM

Both banks must hold **identical** key material. Use whichever transfer your security policy already approves:

- **Components under dual control.** Split the 64 bytes into two XOR components. Two custodians carry one each, and they are combined inside each bank's HSM.
- **Wrapped transfer.** Wrap the material under a key-encryption key of the receiving HSM and import it there.
- **Cloud KMS import.** For AWS KMS, create an HMAC key with external origin, import the wrapped material, then use it for MAC generation:

```bash
aws kms create-key --key-spec HMAC_256 --key-usage GENERATE_VERIFY_MAC --origin EXTERNAL
aws kms get-parameters-for-import --key-id <key> --wrapping-algorithm RSAES_OAEP_SHA_256 --wrapping-key-spec RSA_4096
# wrap the 64-byte material under the returned public key, then:
aws kms import-key-material --key-id <key> --encrypted-key-material fileb://wrapped.bin --import-token fileb://token.bin --expiration-model KEY_MATERIAL_DOES_NOT_EXPIRE
```

Check your KMS's accepted key-material lengths for an HMAC-SHA-256 key before you begin; this material is exactly 64 bytes.

Destroy `corridor-secret.txt` once both imports are confirmed by step 3.

### Point the bander at it

`custody.json`:

```json
{
  "kind": "command",
  "command": "/usr/local/bin/aws",
  "args": ["kms", "generate-mac", "--key-id", "alias/mirror-corridor-cu-v1",
           "--mac-algorithm", "HMAC_SHA_256", "--message", "fileb:///dev/stdin", "--output", "json"],
  "output": "aws-json",
  "timeout_ms": 15000
}
```

For a PKCS#11 HSM, the command is your vendor's tool, or OpenSC's `pkcs11-tool --sign --mechanism SHA256-HMAC`, reading the input from stdin. Set `"output"` to `"hex"`, `"base64"` or `"aws-json"` to match what it prints.

Never put a PIN or a password in `args`: argv is readable by other processes. Use your HSM's own authentication, such as a PIN source, an agent or an IAM role. `readSpec` refuses any command containing shell metacharacters.

On Windows there is no `/dev/stdin`; wrap the tool in a small script that reads stdin into the tool's input.

```bash
mirror-bander band --in legs.csv --out batch.json --edges 50,100,150,200 --custody custody.json --expect-kcv <kcv>
```

## 2. Signing keys

Create a P-256 key in the HSM for each role, then ratify its **public** half in the registry: a member through `member.propose`; a witness, relay or origination key through `witness.propose` with its class. The signer spec names that public key. Every signature the HSM returns is checked against it before use, so a wrong key, a wrong algorithm, or a tool that signed something else is refused.

```json
{
  "kind": "command",
  "command": "/usr/local/bin/aws",
  "args": ["kms", "sign", "--key-id", "alias/mirror-member", "--message-type", "DIGEST",
           "--signing-algorithm", "ECDSA_SHA_256", "--message", "fileb:///dev/stdin", "--output", "json"],
  "output": "aws-json",
  "input": "digest",
  "kid": "<your kid>",
  "public_jwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." }
}
```

`"input": "digest"` sends the SHA-256 of the canonical text; this is the KMS `DIGEST` message type and PKCS#11 `CKM_ECDSA`. Use `"message"` for tools that hash the input themselves, such as `CKM_ECDSA_SHA256`.

DER or raw 64-byte output is accepted. It is converted to JOSE and normalised to low-S, so every Mirror signature has one canonical form.

The same spec works everywhere a key signs:

- `mirror-bander band --sign --custody`
- `sign-event --custody`
- `sign-login --custody`
- `relay-measure --custody`
- the witness publisher's `--custody`
- the origination sidecar's `--custody`

## 3. The ceremony check: KCV

Each bank runs:

```bash
mirror-bander kcv --custody custody.json
```

Each reads the 16-hex key check value to the other over a channel you already trust, such as a call-back or a signed letter. **Equal KCVs mean the same corridor key.** A KCV is one HMAC output under a 256-bit key and reveals nothing usable.

Record both KCVs in the corridor charter. From then on, `band --expect-kcv <kcv>` refuses to band if the HSM answers with a different key.

## 4. What Mirror cannot see, and does not claim

- **That your key is non-exportable, your HSM is certified, and your import was dual-control.** These are your controls, attested by you. Mirror verifies only that the signatures verify and that the KCVs match.
- **What a cloud KMS sees.** The MAC input is `mirror:` plus the shipment reference the bander uses, so a cloud KMS sees that reference, in your own tenancy and under your own contract. An on-premises HSM keeps it inside the building.
- **The file providers.** `kind: "file"` exists for development and trials, and reports itself as `file (development only)` everywhere it appears. A production corridor uses `command`.

Rotation of the corridor secret is its own ceremony, with an overlap window and a rollback path; see [corridor key rotation](/corridor-key-rotation.md).
