Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/ENCLAVE.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,6 @@ keep enclave verify --cid 16 # Verify attesta
keep enclave generate-key --name mykey --cid 16 # Generate key in enclave
keep enclave import-key --name enclavekey --from-vault vaultkey --cid 16
keep enclave sign --key mykey --message <hex> --cid 16
keep enclave sign-psbt --key mykey --psbt tx.psbt --network testnet --cid 16
```

Policy example (enforced inside enclave):
Expand All @@ -214,7 +213,9 @@ PolicyRule::MaxAmountSats(1_000_000) // 0.01 BTC limit
PolicyRule::MaxPerHour(10) // Rate limit
```

A FROST share is imported sealed with its policy, the same way as a wallet key: seal the serialized key package with `EnvelopeEncryption::encrypt_for_enclave` and import it with `EnclaveClient::import_encrypted_frost_key`, which sends the public key package alongside in the clear. The first sealed key imported, wallet or FROST, sets the enclave's policy, and a key sealed to any other policy is refused. Both FROST rounds are held to that policy, and round 2 judges it again before making the share, so sessions opened together cannot all be signed past a rate limit; a session the policy refuses is closed. A FROST message is opaque to the enclave and carries no amount, destination or event kind, so only the rate, `AllowedHours` and `BlockWeekends` rules can stop a FROST share; amount, fee, address and event kind rules do not limit it. Plaintext `ImportFrostKey`, like `ImportKey`, is refused unless the enclave is built with the test-only `allow-plaintext-import` feature.
The amount, fee and address rules judge what the enclave reads from a PSBT it analyzes, never what the host says about a request. A raw `Sign` message and a FROST message are opaque to the enclave, so they fail `MaxAmountSats`, `AddressAllowlist` and `AddressBlocklist`, and a failed rule acts as it would for a PSBT that broke it: under a `Deny` policy the request is denied, under `RequireApproval` it is refused with "Requires approval", and under `Allow` evaluation moves on to the next policy. A `MaxFeeSats` anywhere in the policy, whatever its action and however it is nested, refuses every raw message and FROST message, since they may pay any fee. The rate, `AllowedHours` and `BlockWeekends` rules judge them as they judge a PSBT. To spend under amount, fee or address rules, sign a PSBT with a `SignPsbt` request (`EnclaveClient::sign_psbt`). No request carries an event kind the enclave can read, so `AllowedEventKinds` fails every request, a PSBT included.

A FROST share is imported sealed with its policy, the same way as a wallet key: seal the serialized key package with `EnvelopeEncryption::encrypt_for_enclave` and import it with `EnclaveClient::import_encrypted_frost_key`, which sends the public key package alongside in the clear. The first sealed key imported, wallet or FROST, sets the enclave's policy, and a key sealed to any other policy is refused. Both FROST rounds are held to that policy, and round 2 judges it again before making the share, so sessions opened together cannot all be signed past a rate limit; a session the policy refuses is closed. A FROST message is judged like a raw `Sign` message, as described above. Plaintext `ImportFrostKey`, like `ImportKey`, is refused unless the enclave is built with the test-only `allow-plaintext-import` feature.

Rate limits count each Sign or SignPsbt request a key signs (a PSBT counts once, however many inputs it signs) and each FROST signature share it makes, against the key's public key (a FROST share's group key) rather than the name it was imported under. The enclave keeps no count across restarts, so it cannot know what a key signed before it last started; it treats every rate limit as used up when a policy is loaded, which every launch needs before it can sign, and refills from there. A restart therefore buys nothing beyond the normal refill, at the cost of a wait after every launch: `MaxPerHour(10)` allows its first signature six minutes after the policy is loaded and its full ten an hour after. Each running enclave keeps its own count, so the limits bound one enclave, not every enclave that can unseal the key.

Expand Down
6 changes: 0 additions & 6 deletions keep-cli/src/commands/enclave.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,9 +294,6 @@ pub fn cmd_enclave_sign(
keep_enclave_host::EnclaveRequest::Sign(keep_enclave_host::SigningRequest {
key_id: key.to_string(),
message: message_bytes,
event_kind: None,
amount_sats: None,
destination: None,
nonce: None,
timestamp: None,
});
Expand Down Expand Up @@ -324,9 +321,6 @@ pub fn cmd_enclave_sign(
let request = keep_enclave_host::SigningRequest {
key_id: key.to_string(),
message: message_bytes,
event_kind: None,
amount_sats: None,
destination: None,
nonce: None,
timestamp: None,
};
Expand Down
Loading
Loading