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#
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:
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:
{
"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.
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.
{
"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 --custodysign-event --custodysign-login --custodyrelay-measure --custody- the witness publisher's
--custody - the origination sidecar's
--custody
3. The ceremony check: KCV#
Each bank runs:
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 asfile (development only)everywhere it appears. A production corridor usescommand.
Rotation of the corridor secret is its own ceremony, with an overlap window and a rollback path; see corridor key rotation.