- Rust 54.4%
- Swift 40.1%
- Shell 5.2%
- Dockerfile 0.1%
- Makefile 0.1%
| apps | ||
| crates | ||
| scripts | ||
| .dockerignore | ||
| .gitignore | ||
| AGENTS.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| caution.hcl | ||
| Containerfile | ||
| LICENSE | ||
| PROTOCOL.md | ||
| README.md | ||
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.
- Protocol and security model:
PROTOCOL.md - iOS app:
apps/cosigner-ios/README.md - Swift bindings:
crates/cosigner-uniffi/README.md
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.