MPC Signing Architecture
Multi-Party Computation (MPC) threshold signing for enhanced key security.
Overview
MPC signing eliminates the single point of failure in traditional key management. Instead of one party holding the complete private key, the key is split into shares distributed between the client device and a server-side worker. Signatures are produced through a cryptographic protocol that never reconstructs the full key.
┌─────────────────────────────────────────────────────────────────┐
│ Traditional vs MPC Signing │
│ │
│ Traditional: │
│ ┌────────────────────┐ │
│ │ Full Private Key │ → sign(message) → Signature │
│ │ (Single point of │ │
│ │ failure) │ │
│ └────────────────────┘ │
│ │
│ MPC (Stratos Vault): │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Client Share │ │ Server Share │ │
│ │ (x1 / f(1)) │ │ (x2 / f(2)) │ │
│ └──────┬───────┘ └──────┬───────┘ │
│ │ Protocol │ │
│ └──────┬───────────┘ │
│ ▼ │
│ Signature │
│ (Key never reconstructed) │
└─────────────────────────────────────────────────────────────────┘How It Works
Key Generation (Distributed Key Generation)
During wallet registration with MPC enabled, key shares are generated using Distributed Key Generation (DKG) — each party independently generates their own share. The full private key never exists in memory on any machine, not even temporarily.
Secp256k1 DKG (EVM, BTC, TRON)
Registration Flow (2 round-trips):
Client Server (Durable Object)
│ │
│ 1. POST /api/mpc/dkg-init { mpcKeyId } │
│───────────────────────────────────────────────►│
│ │ Generate x2 = random scalar
│ │ Q2 = x2 * G
│ │ Generate Paillier keypair (p, q)
│ │ cKey = Enc(paillierPk, x2)
│ 2. { Q2Hex, paillierPk, cKeyHex } │ Store x2 + paillierSk
│◄───────────────────────────────────────────────│
│ │
│ 3. Generate x1 = random scalar │
│ Joint public key Q = x1 * Q2 │
│ Derive address from Q │
│ Encrypt x1 with PRF → store in D1 │
│ │
│ 4. POST /api/mpc/dkg-complete { publicKeyHex } │
│───────────────────────────────────────────────►│
│ │ Store joint public keyThe multiplicative relationship sk = x1 * x2 mod N is established without computing it. The joint public key Q = (x1 * x2) * G is derived via EC point multiplication: x1 * (x2 * G).
Ed25519 DKG (SVM, TON)
FROST uses Feldman Verifiable Secret Sharing — a polynomial generates shares directly:
Client generates polynomial f(x) = a0 + a1*x:
share1 = f(1) = a0 + a1 → encrypted with PRF, stored in D1
share2 = f(2) = a0 + 2*a1 → sent to server DO
Commitments C0 = a0*G, C1 = a1*G
a0 and a1 zeroed immediatelyThe secret a0 is never materialized as a standalone value after share computation.
Signing Protocol
When a transaction needs to be signed:
- User authenticates with WebAuthn passkey
- PRF extension decrypts the client share
- Client and server execute a multi-round signing protocol
- The combined output is a standard signature (indistinguishable from single-party)
- All nonces and session data are zeroed immediately
Lindell 2PC-ECDSA (secp256k1)
Used for Ethereum, Bitcoin, TRON, and other secp256k1-based chains.
Key Shares (Multiplicative)
Using DKG, each party generates their own share independently:
Server generates: x2 = random, Q2 = x2 * G
Client generates: x1 = random
Joint public key: Q = x1 * Q2 = (x1 * x2) * G
The full key x1 * x2 is NEVER computed.
Each party only knows their own share.Paillier Encryption
The protocol uses Paillier homomorphic encryption to enable computation on encrypted values:
- Client generates a Paillier keypair during keygen
- Paillier public key is stored with client data
- Paillier secret key components (
lambda,mu,n,n2) are stored server-side - Homomorphic properties allow the server to compute on encrypted shares
Signing Protocol (3 Rounds)
Client Server
│ │
│ Round 1: Nonce Exchange │
│ k1 = random() │
│ R1 = k1 * G │
│ ────── R1 ──────────────────────> │
│ │ k2 = random()
│ │ R2 = k2 * G
│ <─────── R2, sessionId ────────── │
│ │
│ Round 2: Partial Signature │
│ R = k1 * R2 (combined nonce) │
│ r = R.x mod n │
│ c3 = Enc(pk, k1^(-1)*m + ...) │
│ ────── c3 ──────────────────────> │
│ │
│ Round 3: Final Signature │
│ │ Decrypt c3
│ │ Compute s = k2^(-1) * ...
│ <─────── (r, s, v) ───────────── │
│ │
│ Valid ECDSA signature │FROST 2-of-2 (ed25519)
Used for Solana, TON, and other ed25519-based chains. Implements RFC 9591.
Key Splitting (Feldman VSS)
The private key is shared using a polynomial:
Polynomial: f(x) = a0 + a1 * x (degree 1 for 2-of-2)
where a0 = secret key
Client share: f(1) = a0 + a1
Server share: f(2) = a0 + 2*a1
Commitments: C0 = a0 * G, C1 = a1 * G (for verification)
Public key: C0 (= a0 * G, standard ed25519 public key)Signing Protocol (2 Rounds)
Client Server
│ │
│ Round 1: Nonce Commitments │
│ d1, e1 = random nonces │
│ D1 = d1*G, E1 = e1*G │
│ ────── D1, E1 ─────────────────> │
│ │ d2, e2 = random nonces
│ │ D2 = d2*G, E2 = e2*G
│ <─────── D2, E2, sessionId ───── │
│ │
│ Round 2: Partial Signatures │
│ rho = H(message, D1+D2, E1+E2) │
│ R = (D1+D2) + rho*(E1+E2) │
│ c = H(R, PublicKey, message) │
│ z1 = d1 + rho*e1 + c*L1*f(1) │
│ ────── z1, message ─────────────> │
│ │ z2 = d2 + rho*e2 + c*L2*f(2)
│ │ z = z1 + z2
│ │ signature = (R, z)
│ <─────── signature ───────────── │
│ │
│ Valid ed25519 signature │Lagrange coefficients L1 and L2 ensure correct threshold reconstruction.
Server Architecture
Two-System Separation
The portal and MPC signer run on independent systems (separate Cloudflare accounts, separate infrastructure, separate administrators). Communication is over authenticated HTTPS — not a service binding. This ensures no single system compromise exposes both key halves.
System A (Portal) System B (MPC Signer)
┌──────────────────────┐ ┌──────────────────────┐
│ Cloudflare Pages │ HTTPS + │ Cloudflare Worker │
│ + D1 Database │─────────────│ + Durable Objects │
│ │ X-MPC-Auth │ │
│ Holds: encrypted │ header │ Holds: server │
│ client shares │ │ shares (plaintext) │
└──────────────────────┘ └──────────────────────┘Configuration is set in the admin panel under Configuration → Security:
| Setting | Description |
|---|---|
MPC_SIGNER_URL | External URL of the MPC signer worker |
MPC_AUTH_SECRET | Shared secret (must match on both systems) |
MPC Signer Worker
The server side is a Cloudflare Worker with Durable Objects. Authentication via X-MPC-Auth header is required — the worker rejects all requests if MPC_AUTH_SECRET is not configured:
┌─────────────────────────────────────────────────────────────┐
│ MPC Signer Worker │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Router │ │
│ │ POST /dkg-init → DKG round 1 (server generates) │ │
│ │ POST /dkg-complete → DKG round 2 (store pubkey) │ │
│ │ POST /keygen → Store server share (FROST) │ │
│ │ POST /sign-init → Signing round 1 (nonce exchange)│ │
│ │ POST /sign-round2 → Signing round 2+ (signature) │ │
│ │ POST /export → Export key data (for backup) │ │
│ │ POST /import → Import key data (from backup) │ │
│ │ POST /backup-export → Bulk export (multiple keys) │ │
│ │ POST /backup-restore → Bulk import (multiple keys) │ │
│ │ POST /status → Diagnostic (has key?) │ │
│ │ POST /delete → Remove all key data │ │
│ └──────────────────────────┬──────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────▼──────────────────────────────┐ │
│ │ MpcKeyStore (Durable Object) │ │
│ │ │ │
│ │ storage: │ │
│ │ protocol: 'secp256k1' | 'ed25519' │ │
│ │ keyData: JSON(server share + crypto params) │ │
│ │ │ │
│ │ in-memory: │ │
│ │ sessions: Map<sessionId, SigningSession> │ │
│ │ (auto-expire after 30s, one-time use) │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘Session Security
- Each signing session gets a unique
sessionId(UUID) - Sessions expire after 30 seconds
- Sessions are one-time use - deleted immediately after signing
- Nonces in expired sessions are explicitly zeroed
- Worker-to-worker authentication via
MPC_AUTH_SECRET
API Reference
DKG Init (secp256k1)
Starts distributed key generation. Server generates its share, returns public data.
POST /api/mpc/dkg-init
Authorization: Bearer <session_token>
Request: { "mpcKeyId": "user_evm_uuid" }
Response: {
"success": true,
"data": {
"Q2Hex": "02ab...", // Server's public point (compressed)
"paillierPk": "c3f...", // Paillier public key N
"cKeyHex": "8a1..." // Enc(pk, x2) for signing protocol
}
}DKG Complete (secp256k1)
Finalizes DKG. Client sends the derived joint public key.
POST /api/mpc/dkg-complete
Authorization: Bearer <session_token>
Request: { "mpcKeyId": "user_evm_uuid", "publicKeyHex": "02cd..." }
Response: { "success": true }Key Generation (FROST / ed25519)
Stores server share for ed25519 chains (FROST already uses proper DKG client-side).
POST /api/mpc/keygen
Authorization: Bearer <session_token>
{
"protocol": "ed25519",
"mpcKeyId": "unique-key-identifier",
"share2": "...", "commitmentC0": "...", "commitmentC1": "...",
"publicKeyHex": "..."
}Sign Init (Round 1)
POST /api/mpc/sign-init
Authorization: Bearer <session_token>
// secp256k1:
{ "protocol": "secp256k1", "mpcKeyId": "...", "clientR1Hex": "compressed-point-hex" }
// ed25519:
{ "protocol": "ed25519", "mpcKeyId": "...", "clientD1Hex": "...", "clientE1Hex": "..." }Sign Round 2
POST /api/mpc/sign-round2
Authorization: Bearer <session_token>
// secp256k1:
{ "protocol": "secp256k1", "mpcKeyId": "...", "sessionId": "...", "ciphertext": "paillier-hex" }
// ed25519:
{ "protocol": "ed25519", "mpcKeyId": "...", "sessionId": "...", "clientZ1Hex": "...", "messageHex": "..." }Migration (Full Key → MPC)
POST /api/mpc/migrate
Authorization: Bearer <session_token>
{ "chainType": "evm", "encryptedClientShare": "prf-encrypted-share", "mpcKeyId": "..." }Portal Backup (Superadmin)
Exports D1 database tables only (encrypted client shares, user data, config). Available via admin panel Configuration section.
GET /api/superadmin/backup
X-Superadmin-Token: <token>
Response: { version, type: "portal", timestamp, database: { table: rows[] } }MPC Signer Backup (Superadmin + IP Whitelist)
Exports Durable Object server shares only. Requires caller IP to be in MPC_BACKUP_IP_WHITELIST (empty whitelist = allow all). Available via admin panel Configuration section.
GET /api/superadmin/backup-mpc
X-Superadmin-Token: <token>
Response: { version, type: "mpc-signer", timestamp, mpcKeys: [...] }The two backup files should be stored separately by different custodians. Neither file alone can reconstruct a private key.
Portal Restore (Superadmin)
POST /api/superadmin/restore?mode=merge|clean
X-Superadmin-Token: <token>
Body: portal backup JSON (rejects MPC backup files)MPC Signer Restore (Superadmin + IP Whitelist)
POST /api/superadmin/restore-mpc
X-Superadmin-Token: <token>
Body: MPC backup JSON (rejects portal backup files)MPC Backup IP Whitelist
GET /api/superadmin/mpc-whitelist → { whitelist, yourIp, yourIpWhitelisted }
PUT /api/superadmin/mpc-whitelist → { whitelist: ["1.2.3.4", ...] }Only IPs currently in the whitelist can modify it. Empty whitelist allows all IPs.
Status (Superadmin)
POST /api/mpc/status
X-Superadmin-Token: <token>
{ "mpcKeyId": "..." }Database Schema
MPC adds two columns to the wallet_addresses table:
key_type TEXT DEFAULT 'full' -- 'full' = PRF-encrypted full key
-- 'mpc_share' = PRF-encrypted client share
mpc_key_id TEXT -- Durable Object name ID for server share lookupConfiguration
Enabling MPC
- Deploy the MPC Signer worker (
stratos-mpc-signer) on a separate Cloudflare account or separate infrastructure - Set
MPC_AUTH_SECRETon the signer worker:
echo "your-shared-secret" | wrangler secret put MPC_AUTH_SECRET --name stratos-mpc-signer- In the portal admin panel, go to Configuration → Security and set:
| Setting | Value |
|---|---|
| MPC Signing | Enabled |
| MPC Signer URL | https://your-mpc-signer.workers.dev |
| MPC Auth Secret | Same secret set on the signer |
No service binding or wrangler.toml changes needed — communication is over external HTTPS.
Admin Panel
The admin panel (Configuration section) provides:
- MPC toggle — Enable/disable MPC for new registrations
- MPC Signer URL / Auth Secret — Configure portal-to-signer connection
- Portal Backup — Download/restore D1 database (encrypted client shares)
- MPC Signer Backup — Download/restore server shares (IP-whitelisted)
- IP Whitelist — Manage which IPs can access MPC backup endpoints
Transparent Integration
MPC signing is transparent to dock apps and SDK consumers. The SigningContext determines the mode:
// The sign adapter dispatches automatically
// Dock apps call the same SDK methods regardless of mode
// Local mode:
signSecp256k1(hash, key, { mode: 'local', privateKeyHex: '...' })
// MPC mode:
signSecp256k1(hash, key, { mode: 'mpc', mpcClient, clientKeyData: '...' })
// Both produce identical ECDSA/EdDSA signaturesAll chain signers (EVM, Solana, TON, TRON, Bitcoin) use the sign adapter, making MPC a zero-change upgrade for application developers.
Backup & Restore
MPC key data is distributed across two stores — both must be backed up together:
| Store | Contains | Backed Up By |
|---|---|---|
| D1 Database | PRF-encrypted client shares (wallet_addresses, passkey_encrypted_keys) | GET /api/superadmin/backup |
| Durable Objects | Server shares (x2/share2 + Paillier secret keys) | Same endpoint (discovers mpcKeyIds from D1, exports each DO) |
Three Dependencies for Signing
All three must align for a valid signature:
- User's passkey — hardware-bound, non-exportable, domain-locked via RP ID
- D1 database — holds PRF-encrypted client share
- Durable Object — holds server share
If any one is lost, signing fails. The backup/restore system protects against loss of #2 and #3.
Domain Migration Warning
The passkey PRF output is bound to the RP ID (domain). Changing the domain means:
- Existing passkeys won't authenticate on the new domain
- PRF output changes, making encrypted client shares unreadable
- A re-keying migration flow is required with both domains live simultaneously
Prevention: Set RP ID to the registrable domain (e.g., cantondefi.com) rather than a subdomain. This allows subdomain changes without breaking passkeys.
Backup Schedule Recommendation
| Frequency | Trigger |
|---|---|
| Daily | Automated cron backup to encrypted storage |
| After user registration | New MPC keys created |
| Before infrastructure changes | Domain, account, or worker migrations |
The backup file contains server shares in plaintext — store it encrypted at rest with restricted access.
Next Steps
- Security Architecture - Full security model
- Administration - Backup/restore operations
- Deployment Model - Infrastructure overview
- Developer Introduction - Build on Stratos Vault
