No description
  • Rust 54.4%
  • Swift 40.1%
  • Shell 5.2%
  • Dockerfile 0.1%
  • Makefile 0.1%
Find a file
2026-08-27 19:22:13 +02:00
apps Initial public source snapshot 2026-08-27 19:22:13 +02:00
crates Initial public source snapshot 2026-08-27 19:22:13 +02:00
scripts Initial public source snapshot 2026-08-27 19:22:13 +02:00
.dockerignore Initial public source snapshot 2026-08-27 19:22:13 +02:00
.gitignore Initial public source snapshot 2026-08-27 19:22:13 +02:00
AGENTS.md Initial public source snapshot 2026-08-27 19:22:13 +02:00
Cargo.lock Initial public source snapshot 2026-08-27 19:22:13 +02:00
Cargo.toml Initial public source snapshot 2026-08-27 19:22:13 +02:00
caution.hcl Initial public source snapshot 2026-08-27 19:22:13 +02:00
Containerfile Initial public source snapshot 2026-08-27 19:22:13 +02:00
LICENSE Initial public source snapshot 2026-08-27 19:22:13 +02:00
PROTOCOL.md Initial public source snapshot 2026-08-27 19:22:13 +02:00
README.md Initial public source snapshot 2026-08-27 19:22:13 +02:00

Caution MPC Cosigner Demo

This repository demonstrates a two-party MPC cosigner deployed with Caution. A client and a cosigner running inside an AWS Nitro Enclave each retain one share of a secp256k1 key. They generate the public key and produce signatures together; neither party can sign alone, and the private key is never reconstructed.

Remote clients use STEVE to verify Nitro evidence against expected PCR0/1/2 and bind that workload to the protected session carrying the cosigner protocol. The repository includes a Rust CLI and a reference iOS app with device-held identity and encrypted client shares.

This is an architectural demo, not production cryptography or a production custody product. The pinned CGGMP24 dependency is an alpha release and is intentionally non-constant-time.

How it works

CLI or iOS client
  client P-256 identity + encrypted MPC share
            |
            | signed CBOR over a STEVE session
            | client verifies Nitro evidence + expected PCR0/1/2
            v
Caution Nitro Enclave
  STEVE -> cosigner-service
           cosigner P-256 identity + encrypted MPC share
            |
            +---- two-party CGGMP24 key generation and signing ----+

The MPC protocol determines which two parties must participate. Attestation identifies the accepted enclave workload, STEVE binds that workload to the protected session, and application authentication determines which client is making the request. The implemented profile is additive two-party CGGMP24, not threshold MPC.

The service requires explicitly authorized clients by default. The included Caution demo profile deliberately runs with measured --allow-any-client, which removes the stored allowlist but still requires a canonical P-256 client identity and valid request signatures.

Components

Rust crates

Crate Responsibility
mpc-core Fixed two-party secp256k1 CGGMP24 wrapper, identities, and artifact envelopes
cosigner-protocol Wire-v1 DTOs, ceremony driver, MPC channels, encrypted records, and secure filesystem operations
cosigner-service Authenticated HTTP cosigner, authorization policy, ceremony supervision, persistence, and resource limits
cosigner-client Client orchestration, encrypted share storage, and loopback or STEVE transports
cosigner-uniffi Rust-to-Swift API, external-custody operations, and Base Sepolia transaction helpers

Apps

App Responsibility
apps/cosigner CLI containing the service and client commands, plus feature-gated local STEVE evaluation binaries
apps/cosigner-ios SwiftUI reference client for message signing and Base Sepolia native-ETH transfers through local synthetic or live Nitro profiles

Deploy on Caution and run the CLI

The repository includes the measured runtime profile in caution.hcl and the reproducible application image in Containerfile. Expected PCRs must come from fresh independent verification; never learn them from the target being verified or reuse them after a rebuild or redeployment.

Requirements: Rust 1.88, a Caution account, the caution CLI, Docker, curl, and jq. On Apple Silicon, the Caution build also needs an amd64-capable Docker environment.

1. Configure and build the deployment

Set network.http.domain in caution.hcl to a domain you control. Replace the example CORS origin if a browser origin will call the enclave, then initialize the Caution resource:

caution init
export STEVE_SERVER_COMMIT=367433a1db90e4da36b5e6edd1976253054697b7

Check that the managed Caution builder still resolves the STEVE revision required by this repository, then reproduce the enclave image from a clean checkout:

test "$(curl -fsS \
  https://dashboard.caution.co/.well-known/caution/build-inputs \
  | jq -r '.steve.commit')" = "$STEVE_SERVER_COMMIT"

git status --short
STEVE_COMMIT="$STEVE_SERVER_COMMIT" caution apps build --no-cache

git status --short must be empty. Save the build directory printed by caution apps build.

2. Deploy and verify the workload

Deploy the same commit, then verify fresh Nitro evidence against the reproduced PCRs:

export BUILD_DIR=<printed-build-directory>
git push caution HEAD:main
export COSIGNER_ORIGIN=https://<configured-domain>

caution verify \
  --attestation-url "$COSIGNER_ORIGIN/attestation" \
  --pcrs "$BUILD_DIR/output/enclave.pcrs"

A successful verification writes .caution/trusted_hashes.json. That file is the expected-PCR input to the cosigner client.

3. Run the live smoke

Build the CLI and prepare an unused client root. Use a new root when the ephemeral deployment has restarted or been redeployed because enrollment pins the cosigner application identity.

cargo build --locked --release -p cosigner --bin cosigner
export CLIENT_ROOT=.state/caution-client-new

target/release/cosigner client prepare \
  --state-dir "$CLIENT_ROOT/state" \
  --storage-key-file "$CLIENT_ROOT/storage.key"

./scripts/test-caution-live.sh \
  "$COSIGNER_ORIGIN" \
  .caution/trusted_hashes.json \
  "$CLIENT_ROOT"

The helper enrolls the client, creates a split key, signs a fixed message, and prints the key ID, public key, and signature. It changes local client state and creates shares in the deployed enclave. The first key creation generates safe primes and may take several minutes.

4. Use the CLI manually

Instead of the smoke helper, enroll and create a key with the same verified PCR profile:

target/release/cosigner client enroll \
  --transport steve \
  --server "$COSIGNER_ORIGIN" \
  --pcrs .caution/trusted_hashes.json \
  --state-dir "$CLIENT_ROOT/state" \
  --storage-key-file "$CLIENT_ROOT/storage.key"

target/release/cosigner client keygen \
  --transport steve \
  --server "$COSIGNER_ORIGIN" \
  --pcrs .caution/trusted_hashes.json \
  --state-dir "$CLIENT_ROOT/state" \
  --storage-key-file "$CLIENT_ROOT/storage.key"

Keep the key_id printed by keygen, then sign a file:

printf '%s' 'hello from the Caution cosigner' > /tmp/cosigner-message.bin
export KEY_ID=<key-id-from-keygen>

target/release/cosigner client sign \
  --transport steve \
  --server "$COSIGNER_ORIGIN" \
  --pcrs .caution/trusted_hashes.json \
  --state-dir "$CLIENT_ROOT/state" \
  --storage-key-file "$CLIENT_ROOT/storage.key" \
  --key-id "$KEY_ID" \
  --message-file /tmp/cosigner-message.bin

sign signs SHA-256(message). The advanced sign-digest command signs exactly 32 caller-supplied bytes; the caller is responsible for validating and approving the typed intent.

The included Caution profile is intentionally ephemeral and passes measured --allow-any-client. Use it only as a demo profile, not as production authorization or durable custody.

Run the protected demo locally

Run the real STEVE transport with signed-synthetic attestation:

./scripts/local-steve.sh run

The runner prepares and enrolls one local client and prints reusable client commands. In another terminal, for example:

target/debug/cosigner-local-eval client keygen \
  --transport steve \
  --server http://127.0.0.1:49500 \
  --pcrs .state/local-steve/trusted_hashes.json \
  --state-dir .state/local-steve/client/state \
  --storage-key-file .state/local-steve/client/storage.key

Ctrl-C stops both processes and preserves state. Remove stopped local state with:

./scripts/local-steve.sh reset --force

This lane proves protected-protocol interoperability with synthetic evidence. It does not prove Nitro, authentic PCRs, NSM, VSOCK isolation, or hostile-host resistance.

Run the iOS demo

make -C apps/cosigner-ios prepare
make -C apps/cosigner-ios build
make -C apps/cosigner-ios run-local
make -C apps/cosigner-ios test-local

make -C apps/cosigner-ios prepare-live
make -C apps/cosigner-ios build-live

LocalSynthetic runs only in the Apple-silicon Simulator with synthetic attestation. LiveNitro uses Nitro verification and requires a Secure Enclave P-256 identity at runtime. See the iOS README for the custody model, app flow, schemes, and Base Sepolia limits.

Run the raw loopback development profile

Raw HTTP is a local development seam and cannot bind outside loopback. Build the CLI, initialize both parties, and authorize the client:

cargo build --locked --release -p cosigner --bin cosigner

target/release/cosigner service init \
  --state-dir .state/http-server \
  --storage-key-file .state/http-server.key

target/release/cosigner client init \
  --state-dir .state/http-client \
  --storage-key-file .state/http-client.key \
  --server-public-key .state/http-server/server-public-key.hex

target/release/cosigner service authorize \
  --state-dir .state/http-server \
  --storage-key-file .state/http-server.key \
  --client-public-key .state/http-client/client-public-key.hex

Start the service:

target/release/cosigner service serve \
  --listen 127.0.0.1:9000 \
  --state-dir .state/http-server \
  --storage-key-file .state/http-server.key

In another terminal, create a key and sign a file:

target/release/cosigner client keygen \
  --server http://127.0.0.1:9000 \
  --state-dir .state/http-client \
  --storage-key-file .state/http-client.key

target/release/cosigner client sign \
  --server http://127.0.0.1:9000 \
  --state-dir .state/http-client \
  --storage-key-file .state/http-client.key \
  --key-id <key-id> \
  --message-file /tmp/cosigner-message.bin

Important boundaries

  • This is fixed additive two-party CGGMP24, not threshold MPC. There is no presignature, key import/export, refresh, resharing, recovery, or private-key reconstruction path.
  • Raw HTTP is plaintext and loopback-only. Remote use requires STEVE with a complete expected-PCR profile.
  • The included Caution profile accepts any authenticated P-256 client, so any reachable authenticated client can consume its finite resources.
  • The deployed cosigner identity and shares are ephemeral across enclave restart or redeployment. Existing client shares then become unusable.
  • Reviewed TOFU provides reviewed continuity, not independent workload approval; pinned PCR0/1/2 remains the stronger external-client policy.
  • Losing a client wrapping key permanently loses access to its encrypted shares.

See PROTOCOL.md for wire compatibility, ceremony lifecycle, storage, replay behavior, resource limits, and the complete trust model.

Verification

Run checks appropriate to the change:

cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace
./scripts/test-local-steve.sh
bash scripts/test-e2e.sh
make -C apps/cosigner-ios test-local
make -C apps/cosigner-ios build-live
./scripts/check-reproducible.sh
cargo audit

Safe-prime tests are slow and variable. Release E2E is needed only for release-profile, packaging, reproducibility, or deployment changes.