Four-eye control adds external multi-approver authorization for sensitive key operations.
When enabled on a key policy, requests must carry approver signatures from registered public keys (m-of-n threshold).
What it enables
Four-eye control helps enforce separation of duty at the API layer:
- no single operator can execute sensitive operations alone
- approvals can be delegated to external key holders (including HSM-backed
P256keys) - critical operations are bound to explicit, signed intent (
keeperId, payload, nonce, timestamp)
Where it applies
If policy.fourEye is configured for a key, approvals are required for:
POST /v1/keeper/dkg(ROTATE,REFRESH)POST /v1/keeper/signPOST /v1/keeper/ecies/decryptPOST /v1/keeper/destroy
Policy model (KeySetPolicy.fourEye)
fourEye is part of KeySetPolicy:
| Field | Type | Meaning |
|---|---|---|
m |
int | Minimum number of valid approver signatures required. |
n |
int | Total number of registered approver keys. |
keys |
set | Registered approver public keys. |
Validation rules:
mmust be at least2mmust be less than or equal ton- number of unique keys must be exactly
n - each public key must be valid for its declared curve
Example:
{
"apply": { "unit": "SECONDS", "notAfter": 1764417207 },
"process": { "unit": "SECONDS", "notAfter": 1767019207 },
"fourEye": {
"m": 2,
"n": 3,
"keys": [
{ "curve": "P256", "publicKey64": "base64(approver-public-key)" },
{ "curve": "SECP256K1", "publicKey64": "base64(approver-public-key)" },
{ "curve": "ED25519", "publicKey64": "base64(approver-public-key)" }
]
},
"allowHistoricalProcess": true
}
Approvals object
Each protected request includes:
{
"approvals": {
"keeperId": 1,
"nonce": "ops-2026-02-12-001",
"timestamp": 1764417000123,
"proofs": [
{
"fingerprint": "base64(sha256(public-key-bytes))",
"signature64": "base64(signature-over-request-hash)"
}
]
}
}
| Field | Meaning |
|---|---|
keeperId |
Index of the peer that will receive the request and act as coordinator for this operation. |
nonce |
Unique anti-replay value generated by the caller. |
timestamp |
Client timestamp used by coordinator TTL checks. |
proofs |
Approver signatures. At least m valid proofs from registered keys are required. |
Fingerprints
proofs[].fingerprint identifies which registered approver key produced each signature:
P256andSECP256K1: fingerprint is computed from the compressed public key bytesED25519: fingerprint is computed from Ed25519 public key bytes- format:
base64(sha256(public-key-bytes))
Using P256 approver keys is supported and works well with many HSM setups.
How signatures are generated
For each protected endpoint, clients build a canonical JSON payload, hash it with SHA-256, and sign that hash with approver private keys.
Canonicalization rules:
- include endpoint-specific request fields
- include
keeperId,nonce, andtimestampin the signed payload - sort keys alphabetically at every object level
- for nested maps, keys must also be sorted alphabetically
- serialize as canonical compact JSON (not pretty-printed), UTF-8 encode it, then compute SHA-256
Note JSON blocks in this document are formatted for readability only. Approval hashes are computed from canonical compact JSON bytes, not from prettified output.
/v1/keeper/dkg (ROTATE / REFRESH)
{
"keeperId": 1,
"keyId": "btc-hot-wallet",
"curve": "P256",
"mode": "ROTATE",
"policy": { "...": "..." },
"assetOwner": "treasury-team",
"nonce": "ops-2026-02-12-001",
"timestamp": 1764417000123
}
/v1/keeper/sign
{
"keeperId": 1,
"keyId": "btc-hot-wallet",
"algorithm": "FROST",
"hash": false,
"operations": {
"op1": "base64(message-bytes)"
},
"context": {
"kind": "BIP340"
},
"tweak": "my-user-id",
"nonce": "ops-2026-02-12-010",
"timestamp": 1764419000123
}
/v1/keeper/ecies/decrypt
{
"keeperId": 1,
"keyId": "custody-key",
"generation": 3,
"algorithm": "AES_GCM",
"ciphertext64": "base64(ciphertext-bytes)",
"tweak": "my-user-id",
"nonce": "ops-2026-02-12-020",
"timestamp": 1764420000123
}
/v1/keeper/destroy
{
"keeperId": 1,
"keyId": "btc-hot-wallet",
"version": 1,
"nonce": "ops-2026-02-12-002",
"timestamp": 1764418000123
}
Timestamp validity (keeper.approval.ttl)
Coordinator checks approvals.timestamp freshness using:
keeper {
approval {
ttl = 30s
}
}
- if request age exceeds
ttl, request is rejected - default is
30s
Security model and replay trade-off
All peers verify four-eye policy and proof validity.
If coordinator is compromised, it still cannot bypass the policy threshold (m-of-n) for protected operations.
However, replay risk remains:
- nonce/timestamp freshness is checked by coordinator only
- non-coordinator peers do not enforce nonce/timestamp by design
This design preserves availability and protocol retries in Byzantine-resilient flows.
See Byzantine Resilience and Detecting Imposters.
In practice, replay surface is limited to repeating an operation that approvers already authorized.
Rotation/refresh policy behavior
During ROTATE and REFRESH, KeySetPolicy is not inherited automatically.
If you need to keep or update policy settings, send policy explicitly in the request (including apply, process, fourEye, allowHistoricalProcess).