Documentation

Four-Eye Control

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 P256 keys)
  • 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/sign
  • POST /v1/keeper/ecies/decrypt
  • POST /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:

  • m must be at least 2
  • m must be less than or equal to n
  • 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:

  • P256 and SECP256K1: fingerprint is computed from the compressed public key bytes
  • ED25519: 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, and timestamp in 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).