Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

Usage

Table of Contents


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):

FlagDescription
--path <dir>Use a vault at this path instead of ~/.keep (also KEEP_HOME)
--hiddenOperate on the hidden volume (see Hidden Volumes)
--no-mlockDisable memory locking, accepting degraded security

Vault & keys:

CommandDescription
keep initCreate encrypted vault
keep generate --name <n>Generate new Nostr key
keep import --name <n>Import existing nsec
keep listList all keys
keep export --name <n>Print a raw nsec (interactive TTY only, by design)
keep delete --name <n>Delete key
keep rotate-passwordChange the vault unlock password
keep rotate-data-keyRotate 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:

CommandDescription
keep serveStart 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:

CommandDescription
keep audit ...Inspect, verify, export, or prune the audit log (see Audit Log)
keep config show | path | initInspect or initialize the CLI config file
keep migrate statusInspect 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

AspectTrusted Dealer (frost generate)Distributed DKG (frost network dkg)
Key exposureFull key exists on one machineFull key never exists anywhere
Entropy sourceSingle machineAll participants contribute
Compromise riskSingle point of failureRequires threshold breach
Use caseTesting/developmentProduction

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

VariableDescription
KEEP_HOMECustom vault path (default: ~/.keep). Must be absolute. Equivalent to --path
KEEP_PASSWORDVault password (avoids interactive prompt)
KEEP_HIDDEN_PASSWORDHidden volume password (with --hidden)
KEEP_YESAuto-confirm non-destructive prompts in scripts
WARDEN_TOKENJWT for Warden API authentication (requires --features warden)

KEEP_PATH is not used by the CLI; it configures the vault path for the keep-web co-signer only. For the CLI, use KEEP_HOME or --path. See keep-web/README.md for 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

FeatureImplementation
Key DerivationArgon2id (256MB memory, 4 iterations)
EncryptionXChaCha20-Poly1305
ChecksumsBLAKE2b
RAM ProtectionKeys encrypted with Ascon-128a, zeroized on drop
Memory Lockingmlock(2) prevents secrets from swapping to disk
Code SafetyPure Rust, #![forbid(unsafe_code)]
Threshold SigsFROST with BIP-340 Schnorr
Hardware IsolationAWS 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 unlimited before running keep
  • Permanent: Add to /etc/security/limits.conf:
    * soft memlock unlimited
    * hard memlock unlimited
    
  • Containers: Use --no-mlock flag 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 i from a vault, they can sign as share i from 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 export produces 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 split retains the original key by default unless --keep-original is passed. After frost split, the original single-key Nostr identity is deleted. Use --keep-original only 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 refresh after 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

Deployment Options

MethodBest forComplexity
EnclaverQuick setup, simpler operationsLow
CDK (automated)Production with full AWS integrationMedium
ManualLearning, debugging, custom setupsHigher

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 (use echo $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>: From pcrs.json after 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

ErrorFix
Enclave boot failedIncrease --memory
No enclave supportUse Nitro-capable instance
Resource busynitro-cli terminate-enclave --all
AccessDeniedExceptionPCR mismatch: rebuild, update KMS policy
Vsock failedCheck 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

ComponentVersion
Rust1.85.0
Base Imagerust: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:

  1. Pinned Rust version via rust-toolchain.toml and Docker image tag
  2. Locked dependencies via Cargo.lock and --locked flag
  3. Stripped symbols removing non-deterministic debug info
  4. Single codegen unit ensuring consistent compilation order
  5. 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.