Keep
Self-custodial key management for Nostr and Bitcoin.
Keep is an encrypted vault for Nostr and Bitcoin keys. It stores keys locally with strong encryption, signs remotely via NIP-46 without exposing private keys, and supports FROST threshold signatures so no single device ever holds enough to spend.
Keep runs as a CLI, a desktop app (Linux/macOS/Windows), a mobile library (Android/iOS via UniFFI), a StartOS service for always-on FROST co-signing, or inside AWS Nitro Enclaves for hardware-isolated signing.
Where to start
- Usage — install the CLI, create a vault, generate keys, remote signing, Bitcoin, FROST, agents, and troubleshooting.
- Security — cryptography and threat model.
- AWS Nitro Enclaves — hardware-isolated signing deployment.
- Release Signing — how Keep threshold-signs its own releases.
- Reproducible Builds — verifying builds bit-for-bit.
Project links
- Source: https://github.com/privkeyio/keep
- License: MIT
- Self-hosting the headless co-signer: keep-web/README.md
Usage
Table of Contents
- Installation
- Quick Start
- CLI Reference
- Backup & Restore
- Remote Signing (NIP-46)
- Bitcoin
- Threshold Signatures (FROST)
- Wallet Descriptor Coordination
- Audit Log
- Mobile (NIP-55)
- Agent SDK
- AWS Nitro Enclaves
- Hidden Volumes
- Configuration
- Troubleshooting
Installation
Requires Rust 1.89+ (MSRV). System dependencies (for the hardware-signer serial support)
vary by platform: Linux needs pkg-config and libudev (build-essential/libudev-dev),
macOS needs the Xcode Command Line Tools, and Windows needs nothing extra. See
BUILD.md for the full list.
Install the CLI directly:
cargo install --git https://github.com/privkeyio/keep keep-cli
From a clone:
git clone https://github.com/privkeyio/keep
cd keep
cargo install --path keep-cli # installs `keep` to ~/.cargo/bin
# or, to run without installing:
cargo build --release
./target/release/keep --help
Quick Start
# Create your encrypted vault
keep init
# Generate a new Nostr key
keep generate --name main
# List your keys
keep list
Keys are stored encrypted at ~/.keep.
CLI Reference
Run keep <command> --help for the full flag list of any command.
Global flags (valid on every command):
| Flag | Description |
|---|---|
--path <dir> | Use a vault at this path instead of ~/.keep (also KEEP_HOME) |
--hidden | Operate on the hidden volume (see Hidden Volumes) |
--no-mlock | Disable memory locking, accepting degraded security |
Vault & keys:
| Command | Description |
|---|---|
keep init | Create encrypted vault |
keep generate --name <n> | Generate new Nostr key |
keep import --name <n> | Import existing nsec |
keep list | List all keys |
keep export --name <n> | Print a raw nsec (interactive TTY only, by design) |
keep delete --name <n> | Delete key |
keep rotate-password | Change the vault unlock password |
keep rotate-data-key | Rotate the data-encryption key (re-encrypts every secret) |
keep backup [--output <file>] | Write a passphrase-encrypted vault backup |
keep restore <file> --target <dir> | Restore a backup into a new vault |
Signing & coordination:
| Command | Description |
|---|---|
keep serve | Start the NIP-46 bunker (and optional FROST co-signer) |
keep frost ... | FROST threshold operations (see FROST) |
keep wallet ... | Wallet descriptors, proposals, PSBT spend coordination (see Wallet) |
keep bitcoin ... | Addresses, descriptors, PSBT signing (see Bitcoin) |
keep nip46 ... | NIP-46 client app grant management (see Remote Signing) |
keep enclave ... | AWS Nitro Enclave operations (see Enclaves) |
keep agent mcp --key <n> | Run the MCP signing server (see Agent SDK) |
keep sign <file> --group <npub> | Threshold-sign a file (minisign-compatible) |
keep verify <file> <sig> --group <npub> | Verify a minisign detached signature |
Maintenance:
| Command | Description |
|---|---|
keep audit ... | Inspect, verify, export, or prune the audit log (see Audit Log) |
keep config show | path | init | Inspect or initialize the CLI config file |
keep migrate status | Inspect on-disk schema migration state |
Backup & Restore
Your vault lives at ~/.keep (or KEEP_HOME / --path). Back it up regularly: if you
lose it and have no backup, the keys are gone.
# Write a passphrase-encrypted backup file
keep backup --output keep-backup.enc
# Restore into a NEW vault (never overwrites the active ~/.keep)
keep restore keep-backup.enc --target ~/keep-restored
restore requires an explicit --target and refuses to write over an existing vault, so
a restore can never clobber your live keys.
For FROST shares specifically, use keep frost export / keep frost import (per-share,
passphrase-encrypted) so shares can be moved or backed up independently. See
Threshold Signatures.
Remote Signing (NIP-46)
Sign from any NIP-46 compatible client without exposing your private key:
keep serve --relay wss://bucket.coracle.social
This displays a bunker URL to paste into your client.
Controls: Y approve, N reject, Q quit
Pre-granting client apps (headless)
Instead of approving each connection interactively, pre-authorize a client app’s pubkey so
keep serve accepts it automatically. This is the headless alternative to the interactive
prompt.
# Grant a client app a set of permissions
keep nip46 grant <client-npub> \
--name "my-client" \
--permissions get_public_key,sign_event \
--auto-approve-kinds 1,7 \
--duration forever
# List existing grants
keep nip46 apps
# Revoke a grant
keep nip46 revoke <client-npub>
# Globally auto-approve specific event kinds for every client (interactive serving)
keep nip46 auto-approve --kinds 1,7
Permission names: get_public_key, sign_event, nip04_encrypt, nip04_decrypt,
nip44_encrypt, nip44_decrypt (or all). --duration accepts session, forever, or
a number of seconds.
Bitcoin
BIP-86 Taproot addresses and PSBT signing:
# Get receive address
keep bitcoin address --key main
# Get change address
keep bitcoin address --key main --change
# Export watch-only descriptor
keep bitcoin descriptor --key main
# Analyze PSBT
keep bitcoin analyze --psbt unsigned.psbt
# Sign PSBT
keep bitcoin sign --key main --psbt unsigned.psbt
Threshold Signatures (FROST)
Split keys into t-of-n shares for distributed signing.
Local Operations
# Create new 2-of-3 threshold key
keep frost generate --threshold 2 --shares 3
# Split existing key into shares
keep frost split --key main -t 2 -s 3
# List shares
keep frost list
# Export share (encrypted with passphrase)
keep frost export --share 1 --group npub1...
# Import share
keep frost import
# Sign with local shares
keep frost sign --group npub1... --message <hex>
Network Signing
Coordinate signing across devices over nostr relays:
# Device 2: Start signer node
keep frost network serve --group npub1... --relay wss://bucket.coracle.social
# Device 1: Check online peers
keep frost network peers --group npub1...
# Device 1: Request signature
keep frost network sign --group npub1... --message "hello"
# Device 1: Sign nostr event
keep frost network sign-event --group npub1... --kind 1 --content "Posted via FROST"
Policy Enforcement (Warden)
Integrate with Warden for policy-based signing controls:
# Build with warden support
cargo build --release --features warden
# Sign with policy check
export WARDEN_TOKEN="<jwt>"
keep frost sign --warden-url http://localhost:3000 --group npub1... --message <hex>
# Network sign with policy check
keep frost network sign --warden-url http://localhost:3000 --group npub1... --message <hex>
Policy decisions:
- ALLOW: Signing proceeds
- DENY: Signing blocked with reason
- REQUIRE_APPROVAL: CLI waits for approval workflow (polls up to 5 minutes)
Hardware Signing
Store FROST shares on an air-gapped hardware signer. See keep-esp32 for firmware.
# Test connection
keep frost hardware ping --device /dev/ttyACM0
# List shares on device
keep frost hardware list --device /dev/ttyACM0
# Import share to hardware
keep frost hardware import --device /dev/ttyACM0 --group npub1... --share 1
# Export share from hardware (encrypted backup)
keep frost hardware export --device /dev/ttyACM0 --group npub1... --output backup.json
# Network sign using hardware
keep frost network sign --group npub1... --message <hex> --relay wss://bucket.coracle.social --hardware /dev/ttyACM0
Distributed Key Generation (DKG)
Generate threshold keys without any single party knowing the full private key. Each participant runs independently and coordinates via Nostr relay.
Security: DKG vs Trusted Dealer
| Aspect | Trusted Dealer (frost generate) | Distributed DKG (frost network dkg) |
|---|---|---|
| Key exposure | Full key exists on one machine | Full key never exists anywhere |
| Entropy source | Single machine | All participants contribute |
| Compromise risk | Single point of failure | Requires threshold breach |
| Use case | Testing/development | Production |
The trusted dealer approach (keep frost generate) generates the full private key on a single machine. If that machine is compromised during generation, all funds are at risk. Distributed DKG ensures the complete key is never computed: each participant generates their share from independent entropy, so no single device ever holds enough information to reconstruct the key.
# Participant 1 (on first device)
keep frost network dkg \
--group mygroup \
--threshold 2 \
--participants 3 \
--index 1 \
--relay wss://bucket.coracle.social \
--hardware /dev/ttyACM0
# Participant 2 (on second device, run simultaneously)
keep frost network dkg \
--group mygroup \
--threshold 2 \
--participants 3 \
--index 2 \
--relay wss://bucket.coracle.social \
--hardware /dev/ttyACM0
# Participant 3 (on third device, run simultaneously)
keep frost network dkg \
--group mygroup \
--threshold 2 \
--participants 3 \
--index 3 \
--relay wss://bucket.coracle.social \
--hardware /dev/ttyACM0
All participants must run the command within 5 minutes. On completion, each device stores its share and outputs the group public key.
Wallet Descriptor Coordination
Turn a FROST group into a Bitcoin wallet with proper external/internal address chains and optional time-locked recovery tiers, coordinated across signers over Nostr.
# Simple descriptor from a FROST group (no recovery tiers).
# Single-key FROST descriptors have no BIP-32 derivation, so receive and change
# collapse to one address; --allow-address-reuse is required to acknowledge that.
keep wallet descriptor --group npub1... --network mainnet --allow-address-reuse
# Propose a coordinated descriptor with a recovery tier (preferred: distinct chains)
keep wallet propose --group npub1... --network mainnet \
--recovery '2of3@6mo' --relay wss://bucket.coracle.social
# Inspect / export a stored descriptor
keep wallet show --group npub1...
keep wallet export --group npub1... --format sparrow
# Announce recovery xpubs to peers, register on a NIP-46 hardware signer
keep wallet announce-keys --group npub1... --xpub 'xpub.../fingerprint/label'
keep wallet register --group npub1... --device 'bunker://...'
# Coordinate a recovery-tier (scriptpath) spend via PSBT
keep wallet spend --group npub1... --recovery-tier 0 --psbt-file unsigned.psbt
keep wallet approve-psbt --group npub1... --session <id> --signer-bunker 'fp:bunker://...'
# List stored descriptors
keep wallet list
Recovery tier syntax is threshold-of-keys@timelock, e.g. 2of3@6mo or 3of5@1y.
Audit Log
Signing and key-lifecycle operations are recorded in a tamper-evident, hash-chained audit log inside the vault.
keep audit list --limit 50 # Recent entries
keep audit verify # Verify hash-chain integrity
keep audit stats # Summary statistics
keep audit export --output audit.json
keep audit retention --max-days 90 --apply # Prune old entries
Mobile (NIP-55)
UniFFI library for Android/iOS apps to hold FROST shares and sign via NIP-55 protocol. See keep-android for the Android app implementation.
// Android: Initialize with secure storage
val mobile = KeepMobile(AndroidSecureStorage(context))
mobile.importShare(kshare, passphrase, "phone")
mobile.initialize(listOf("wss://relay.example.com"))
// Handle NIP-55 intents
val handler = Nip55Handler(mobile)
val request = handler.parseIntentUri(intentUri)
val response = handler.handleRequest(request, callerPackage)
Supports get_public_key, sign_event, nip44_encrypt, nip44_decrypt.
Agent SDK
Secure signing for AI agents with constrained sessions.
Python
Not yet published to PyPI. Install from a repo clone (the build backend is maturin):
pip install ./keep-agent-py
from keep_agent import AgentSession, SessionScope, RateLimit
session = AgentSession(
scope=SessionScope.nostr_only(),
rate_limit=RateLimit.conservative(),
duration_hours=24,
)
if session.check_operation("sign_nostr_event"):
session.record_request()
info = session.get_session_info()
print(f"Requests remaining: {info.requests_remaining}")
LangChain:
from keep_agent import AgentSession
from keep_agent.langchain import KeepSignerTool
session = AgentSession()
tool = KeepSignerTool(session=session)
CrewAI:
from keep_agent import AgentSession
from keep_agent.crewai import create_keep_tools
session = AgentSession()
tools = create_keep_tools(session)
TypeScript
Not yet published to npm. Build from a repo clone:
cd keep-agent-ts && npm install && npm run build
import { KeepAgentSession, createNostrScope } from '@keep/agent';
const session = new KeepAgentSession(createNostrScope());
const info = await session.getSessionInfo();
MCP Server (Claude/Cursor)
keep agent mcp runs a stdio MCP signing server bound to one vault key. The server signs
under a constrained policy, so the model gets signing capability without ever seeing the
private key.
keep agent mcp --key main
Add it to your MCP client configuration:
{
"servers": {
"keep-signer": {
"command": "keep",
"args": ["agent", "mcp", "--key", "main"]
}
}
}
Set KEEP_PASSWORD in the server’s environment so it can unlock the vault non-interactively.
AWS Nitro Enclaves
Hardware-isolated signing, keys never leave enclave memory.
# Check enclave status
keep enclave status
# Verify attestation
keep enclave verify
# Generate key in enclave
keep enclave generate-key --name agent
# Import from vault to enclave
keep enclave import-key --name agent --from-vault mykey
# Sign message
keep enclave sign --key agent --message <hex>
# Sign PSBT
keep enclave sign-psbt --key agent --psbt tx.psbt --network testnet
Local testing (insecure, dev only):
keep enclave generate-key --name test --local
keep enclave sign --key test --message <hex> --local
See ENCLAVE.md for deployment.
Hidden Volumes
Plausibly deniable storage, hidden volume is cryptographically undetectable:
# Create vault with hidden volume
KEEP_PASSWORD="outer" KEEP_HIDDEN_PASSWORD="hidden" keep --hidden init
# Access outer (decoy) volume
KEEP_PASSWORD="outer" keep list
# Access hidden volume
KEEP_PASSWORD="hidden" keep --hidden list
Configuration
Environment Variables
| Variable | Description |
|---|---|
KEEP_HOME | Custom vault path (default: ~/.keep). Must be absolute. Equivalent to --path |
KEEP_PASSWORD | Vault password (avoids interactive prompt) |
KEEP_HIDDEN_PASSWORD | Hidden volume password (with --hidden) |
KEEP_YES | Auto-confirm non-destructive prompts in scripts |
WARDEN_TOKEN | JWT for Warden API authentication (requires --features warden) |
KEEP_PATHis not used by the CLI; it configures the vault path for thekeep-webco-signer only. For the CLI, useKEEP_HOMEor--path. Seekeep-web/README.mdfor the co-signer’s variables.
Troubleshooting
mlock warning at startup. Keep locks secret memory with mlock(2). On systems with a
low RLIMIT_MEMLOCK, locking can fail and Keep logs a warning but continues with degraded
protection. Raise the limit (ulimit -l) or pass --no-mlock to silence it deliberately.
keep export refuses to run. Raw nsec export is interactive-only by design: it requires
a real TTY on stdin and stderr and refuses when automation variables (KEEP_YES /
KEEP_PASSWORD) are set. Run it directly in a terminal.
Forgot which volume / hidden volume not appearing. Hidden volumes are unlocked by
password: pass --hidden with the hidden password. There is no way to detect or recover a
hidden volume without its password. See Hidden Volumes.
KEEP_HOME must be an absolute path. KEEP_HOME rejects relative paths. Use a full
path, e.g. KEEP_HOME=/home/you/.keep-work.
Network FROST signers cannot find each other. All participants must use the same
--group and at least one shared --relay, and (for DKG) run within the coordination
window. Check liveness with keep frost network peers --group npub1....
Security
Cryptography
| Feature | Implementation |
|---|---|
| Key Derivation | Argon2id (256MB memory, 4 iterations) |
| Encryption | XChaCha20-Poly1305 |
| Checksums | BLAKE2b |
| RAM Protection | Keys encrypted with Ascon-128a, zeroized on drop |
| Memory Locking | mlock(2) prevents secrets from swapping to disk |
| Code Safety | Pure Rust, #![forbid(unsafe_code)] |
| Threshold Sigs | FROST with BIP-340 Schnorr |
| Hardware Isolation | AWS Nitro Enclaves with attestation-based KMS |
Memory Locking
Keep uses mlock(2) to prevent secret key material from being paged to disk. If mlock fails (common on systems with low RLIMIT_MEMLOCK), Keep warns but continues with degraded security.
If you see the warning:
Warning: Failed to lock memory. Secrets may be paged to disk.
To fix: ulimit -l unlimited (or increase RLIMIT_MEMLOCK)
Solutions:
- Temporary:
ulimit -l unlimitedbefore running keep - Permanent: Add to
/etc/security/limits.conf:* soft memlock unlimited * hard memlock unlimited - Containers: Use
--no-mlockflag to disable (accepts degraded security)
FROST Threat Model
A FROST share is more than a cryptographic blinding factor: any device holding share i of a t-of-n group can act as that share for the lifetime of the share. There is no per-signing-round binding to a specific physical device.
This means:
- A copied share is a permanent compromise. If an attacker exfiltrates share
ifrom a vault, they can sign as shareifrom anywhere until the group rotates (keep frost refresh). Treat each share file with the same care you would treat an unsplit private key. keep frost exportproduces a transferable identity. Anyone with the exported share (and the export passphrase) can sign as that share’s identity. Do not export shares to long-lived files unless those files are themselves protected to the same standard as the vault.keep frost splitretains the original key by default unless--keep-originalis passed. Afterfrost split, the original single-key Nostr identity is deleted. Use--keep-originalonly when you explicitly want to retain it as a separate identity.- Share number leaks operational structure. A peer-discovered announce reveals which share index a device claims. Pair this with relay metadata and an attacker can map your topology even without breaking any crypto. Run announces on a dedicated FROST relay (
--frost-relay) when that matters. - Rotate shares with
keep frost refreshafter suspected compromise of any device, even if the group pubkey doesn’t need to change. Refresh invalidates the old share bundle without producing a new group identity.
Reporting Vulnerabilities
If you discover a security vulnerability, please report it privately via GitHub Security Advisories rather than opening a public issue.
Keep Enclave Deployment
Deploy Keep on AWS Nitro Enclaves for hardware-isolated signing.
Architecture
┌─────────────────────────────────────────────────────────┐
│ AWS Nitro Enclave │
│ ┌───────────────────────────────────────────────────┐ │
│ │ keep-enclave binary │ │
│ │ • Private keys (memory only) │ │
│ │ • Policy engine (amount limits, rate limits) │ │
│ │ • FROST threshold signing │ │
│ │ • PSBT sighash computation │ │
│ └───────────────────────────────────────────────────┘ │
│ ▲ │
│ │ vsock │
│ ▼ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Host (EC2) │ │
│ │ • Routes requests to enclave │ │
│ │ • Verifies attestation │ │
│ │ • Never sees private keys │ │
│ └───────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ AWS KMS │ │
│ │ Key Policy: Decrypt only if PCR0/1/2 match │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Keys live only in enclave memory. KMS decrypts only if PCRs match.
Prerequisites
- AWS account with EC2, KMS, IAM permissions
- AWS CLI configured
- Docker installed
- Nitro SDK binaries, see keep-enclave/build/README.md
Deployment Options
| Method | Best for | Complexity |
|---|---|---|
| Enclaver | Quick setup, simpler operations | Low |
| CDK (automated) | Production with full AWS integration | Medium |
| Manual | Learning, debugging, custom setups | Higher |
Using Enclaver
Enclaver simplifies enclave builds:
curl -sL https://github.com/edgebitio/enclaver/releases/latest/download/enclaver-linux-x86_64.tar.gz | tar xz
sudo mv enclaver /usr/local/bin/
docker build -f keep-enclave/build/Dockerfile.local -t keep-enclave:local .
enclaver build -f keep-enclave/enclaver.yaml
Deploy to EC2 (use CDK or Manual to provision an instance first):
docker save keep-enclave:enclave | gzip > keep-enclave.tar.gz
scp -i ~/.ssh/keep-enclave.pem keep-enclave.tar.gz ec2-user@<IP>:~/
# On EC2
docker load < keep-enclave.tar.gz
enclaver run keep-enclave:enclave
Still need KMS setup for key persistence.
Automated Deployment (CDK)
Create ECR repository and push enclave image first:
aws ecr create-repository --repository-name keep-enclave
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin <account>.dkr.ecr.us-east-1.amazonaws.com
docker build -f keep-enclave/build/Dockerfile.enclave -t keep-enclave:v1.0.0 .
docker tag keep-enclave:v1.0.0 <account>.dkr.ecr.us-east-1.amazonaws.com/keep-enclave:v1.0.0
docker push <account>.dkr.ecr.us-east-1.amazonaws.com/keep-enclave:v1.0.0
Deploy infrastructure:
cd deploy/cdk
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
CDK_DEPLOY_ACCOUNT=<account> CDK_DEPLOY_REGION=us-east-1 cdk deploy -c image_tag=v1.0.0
Provisions VPC, ASG, NLB, KMS. Update KMS policy with PCRs after build.
Manual Deployment
IAM Role
aws iam create-role --role-name keep-enclave-role \
--assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}]}'
aws iam create-instance-profile --instance-profile-name keep-enclave-profile
aws iam add-role-to-instance-profile --instance-profile-name keep-enclave-profile --role-name keep-enclave-role
export EC2_ROLE_ARN=$(aws iam get-role --role-name keep-enclave-role --query 'Role.Arn' --output text)
SSH Key
aws ec2 create-key-pair --key-name keep-enclave --query 'KeyMaterial' --output text > ~/.ssh/keep-enclave.pem
chmod 400 ~/.ssh/keep-enclave.pem
Launch EC2
aws ec2 run-instances \
--image-id ami-0c02fb55956c7d316 \
--count 1 \
--instance-type m5.xlarge \
--enclave-options 'Enabled=true' \
--key-name keep-enclave \
--iam-instance-profile Name=keep-enclave-profile \
--block-device-mappings '[{"DeviceName":"/dev/xvda","Ebs":{"VolumeSize":20}}]' \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=keep-enclave}]'
Note: AMI ami-0c02fb55956c7d316 is for us-east-1. Find your region’s Amazon Linux 2 AMI via aws ssm get-parameter --name /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2 --query 'Parameter.Value' --output text.
Supported: m5.xlarge, m6i.xlarge, c5.xlarge, r5.xlarge (and 2xlarge variants).
Instance Setup
ssh -i ~/.ssh/keep-enclave.pem ec2-user@<INSTANCE_IP>
sudo amazon-linux-extras install aws-nitro-enclaves-cli -y
sudo yum install aws-nitro-enclaves-cli-devel docker -y
sudo usermod -aG ne,docker ec2-user
sudo tee /etc/nitro_enclaves/allocator.yaml <<< $'memory_mib: 512\ncpu_count: 2'
sudo systemctl enable --now nitro-enclaves-allocator docker
exit # Re-login to apply groups
Build Enclave
See keep-enclave/build/README.md. Produces keep-enclave.eif and pcrs.json.
PCRs (Platform Configuration Registers) are hashes of the enclave code. They change on rebuild. KMS uses them to restrict decryption to your exact code.
scp -i ~/.ssh/keep-enclave.pem keep-enclave/build/keep-enclave.eif ec2-user@<IP>:~/
KMS Setup
aws kms create-key --description "Keep Enclave" --tags TagKey=Project,TagValue=keep
export KMS_KEY_ID=<key-id-from-output>
aws kms create-alias --alias-name alias/keep-enclave --target-key-id $KMS_KEY_ID
Edit keep-enclave/build/kms-policy.json, replace all placeholders:
<EC2_INSTANCE_ROLE_ARN>: Role ARN from IAM setup (useecho $EC2_ROLE_ARN)<KMS_ADMIN_ROLE_ARN>: Your IAM user/role ARN for key administration<REGION>: AWS region (e.g.,us-east-1)<ACCOUNT_ID>: Your AWS account ID<KMS_KEY_ID>: Key ID from above<PCR0_VALUE>,<PCR1_VALUE>,<PCR2_VALUE>: Frompcrs.jsonafter build
aws kms put-key-policy --key-id $KMS_KEY_ID --policy-name default \
--policy file://keep-enclave/build/kms-policy.json
Run Enclave
nitro-cli run-enclave --eif-path ~/keep-enclave.eif --memory 512 --cpu-count 2
nitro-cli describe-enclaves # Note EnclaveCID (usually 16)
Debug mode (PCR0 = zeros, insecure):
nitro-cli run-enclave --eif-path ~/keep-enclave.eif --memory 512 --cpu-count 2 --debug-mode
nitro-cli console --enclave-id <id> # View logs
Stop: nitro-cli terminate-enclave --all
CLI Commands
keep enclave verify --cid 16 # Verify attestation
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):
#![allow(unused)]
fn main() {
PolicyRule::MaxAmountSats(1_000_000) // 0.01 BTC limit
PolicyRule::MaxPerHour(10) // Rate limit
}
Local Development
Mock mode (no security, dev only):
keep enclave generate-key --name test --local
keep enclave sign --key test --message <hex> --local
QEMU emulation (vsock testing, no real attestation):
qemu-system-x86_64 -M nitro-enclave,vsock=parent -kernel keep-enclave.eif -m 512 -nographic --enable-kvm
Troubleshooting
| Error | Fix |
|---|---|
Enclave boot failed | Increase --memory |
No enclave support | Use Nitro-capable instance |
Resource busy | nitro-cli terminate-enclave --all |
AccessDeniedException | PCR mismatch: rebuild, update KMS policy |
| Vsock failed | Check nitro-cli describe-enclaves, verify CID |
Debug mode KMS: set PCR0 to 96 zeros in policy.
Security Notes
- PCRs change on rebuild; update KMS policy each time
- Debug mode sets PCR0=0; bypasses attestation; never in prod
- Keys lost on restart unless persisted via KMS envelope encryption
- Host can’t decrypt; KMS policy restricts to enclave with matching PCRs
- No network in enclave; host proxies KMS
Checklist
- Runs without
--debug-mode - Attestation passes
- KMS works with real PCRs
- Signing works
- Policy enforced
- Keys survive restart
- Wrong PCRs fail decrypt
Threshold Release Signing with Keep
Keep can act as a general-purpose threshold signer for software releases. A
project generates a FROST-Ed25519 group, distributes the n shares to its
maintainers, and signs release artifacts with keep sign. Producing a signature
requires a threshold t of those maintainers to cooperate, so no single person
holds the signing key. The output is minisign-compatible, so downstream users
verify with the stock minisign tool or
with keep verify.
This is a Keep capability you can adopt for your own project. Keep’s own GitHub
releases are not signed this way; they ship binaries and a SHA256SUMS manifest.
Establishing a signing group
Done once. Generate the group, distribute the shares, and publish the public key:
keep frost generate --ed25519 --threshold <t> --shares <n> \
--name release-signing --pubkey-out release-signing.pub
Export each share to its holder with keep frost export (bech32 / QR), then
publish release-signing.pub somewhere users can find it (in your repository,
on your site, and/or attached to each release).
Signing a release
Sign the checksum manifest rather than every artifact; the manifest covers them all.
# Generate a checksum manifest for the release artifacts.
sha256sum keep-* > SHA256SUMS
# Threshold-sign the manifest (writes SHA256SUMS.minisig).
keep sign SHA256SUMS --group <group-npub-or-hex> -t "release v1.2.3"
--group accepts an npub1... string or a 64-char hex group pubkey. The
current minisign signing path is local-threshold: the t shares must be present
on the signing machine. See “Distributed signing” below.
Attach SHA256SUMS, SHA256SUMS.minisig, and release-signing.pub to the
release.
Verifying a release
Users need the artifact, SHA256SUMS, SHA256SUMS.minisig, and the project
public key.
# 1. Confirm the downloaded files match the manifest.
sha256sum --check SHA256SUMS
# 2. Verify the manifest signature against the project public key.
minisign -V -p release-signing.pub -m SHA256SUMS
minisign -V prints Signature and comment signature verified on success. The
SHA256SUMS.minisig file must sit next to SHA256SUMS.
With Keep installed, verify without minisign:
keep verify SHA256SUMS SHA256SUMS.minisig --group release-signing.pub
--group accepts the public-key file, a hex group pubkey, or an npub1...
string.
GitHub Actions example
Signing should not run in CI: putting the threshold of shares into CI secrets recreates the single point of failure threshold signing exists to remove. Build and publish in CI, then sign offline and upload the signature.
# In your release job, after generating SHA256SUMS:
- name: Attach signing public key
run: cp release-signing.pub artifacts/
- uses: softprops/action-gh-release@v3
with:
files: artifacts/*
After the release publishes, a maintainer signs and uploads the signature:
gh release download <tag> --pattern SHA256SUMS
keep sign SHA256SUMS --group <group-npub-or-hex> -t "release <tag>"
minisign -V -p release-signing.pub -m SHA256SUMS
gh release upload <tag> SHA256SUMS.minisig
Optionally, a non-blocking workflow triggered on release: [published, edited]
can run minisign -V to surface the signature status as a check once the
.minisig is uploaded.
Distributed signing
The minisign signing path currently reconstructs the threshold from shares held
on one machine. Fully distributed signing, where no machine ever holds t
shares and signers cooperate over Nostr, reuses Keep’s existing FROST network
coordination but is not yet wired to the minisign output format. Tracked in
#500.
Reproducible Builds
Reproducible builds allow anyone to verify that a binary was built from a specific source commit.
Requirements
- Docker
- just (optional, for convenience commands)
Build Environment
| Component | Version |
|---|---|
| Rust | 1.85.0 |
| Base Image | rust:1.85.0-slim-bookworm |
| Build Flags | -C strip=symbols -C codegen-units=1 |
Building
# Using just
just build-reproducible
# Or directly with Docker
docker build -f Dockerfile.reproducible -o type=local,dest=./dist .
The binary will be output to dist/keep.
Verification
Verify two builds match
just verify-reproducible
Verify against expected hash
just verify-sha <expected_sha256_hash>
Manual verification
docker build -f Dockerfile.reproducible -o type=local,dest=./dist .
sha256sum dist/keep
Release Hashes
Expected hashes for official releases will be published in release notes.
Technical Details
Reproducibility is achieved through:
- Pinned Rust version via
rust-toolchain.tomland Docker image tag - Locked dependencies via
Cargo.lockand--lockedflag - Stripped symbols removing non-deterministic debug info
- Single codegen unit ensuring consistent compilation order
- Fixed SOURCE_DATE_EPOCH for deterministic embedded timestamps
CI Verification
Every PR and push to main runs the reproducibility check, building twice and comparing hashes.