Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
122 commits
Select commit Hold shift + click to select a range
1fc8c7b
feat(kms): add stable MPC cluster identity v1
kvinwang Jul 30, 2026
ee6059a
style(kms): format MPC identity implementation
kvinwang Jul 30, 2026
5ea713f
chore(kms): remove unused MPC identity import
kvinwang Jul 30, 2026
94e2177
feat(kms): define canonical MPC epoch manifests
kvinwang Jul 30, 2026
1365f0b
refactor(kms): introduce root key backend boundary
kvinwang Jul 30, 2026
17b8a5a
refactor(kms): route CA derivation and key export through backend
kvinwang Jul 30, 2026
23599ac
chore(kms): remove superseded local key parsing
kvinwang Jul 30, 2026
c3861f6
feat(kms): add authenticated MPC session router
kvinwang Jul 30, 2026
27cd0c6
feat(kms): load and validate active MPC epoch manifest
kvinwang Jul 30, 2026
8490b4a
feat(kms): expose attested MPC peer transport
kvinwang Jul 30, 2026
2536276
fix(kms): implement generated MPC transport trait
kvinwang Jul 30, 2026
0b92ad0
build(kms): add audited CGGMP21 threshold ECDSA engine
kvinwang Jul 30, 2026
fb5c485
build(kms): lock CGGMP21 dependency graph
kvinwang Jul 30, 2026
df1240e
test(kms): enable deterministic MPC protocol simulation
kvinwang Jul 30, 2026
b711f3c
feat(kms): add CGGMP21 DKG context and validated share store
kvinwang Jul 30, 2026
1150535
test(kms): verify P-256 threshold DKG share persistence
kvinwang Jul 30, 2026
671fe8e
refactor(kms): move root CA private key ownership into backend
kvinwang Jul 30, 2026
b41fed1
feat(kms): load only threshold shares in MPC mode
kvinwang Jul 30, 2026
e0fecd9
fix(kms): expose CGGMP group public keys from shares
kvinwang Jul 30, 2026
20b12bd
build(kms): add async backend interface support
kvinwang Jul 30, 2026
27529dc
refactor(kms): make key backend operations asynchronous
kvinwang Jul 30, 2026
db1cca5
build(kms): lock async backend dependency
kvinwang Jul 30, 2026
87e3ad2
feat(kms): bind MPC peer endpoints and delivery type in epoch state
kvinwang Jul 30, 2026
9d685ee
feat(kms): drive round-based MPC over attested envelopes
kvinwang Jul 30, 2026
2ff0644
fix(kms): allow local CGGMP state-machine execution
kvinwang Jul 30, 2026
0da8fb5
feat(kms): add RA-TLS verified MPC HTTP delivery
kvinwang Jul 30, 2026
d8000f2
chore(kms): remove unused MPC certificate import
kvinwang Jul 30, 2026
73f1a64
feat(kms): add quorum operation start RPC
kvinwang Jul 30, 2026
e85fc93
build(kms): enable concurrent quorum coordination
kvinwang Jul 30, 2026
055a33d
feat(kms): define policy-scoped quorum operations
kvinwang Jul 30, 2026
bece330
feat(kms): drive CGGMP operations on dedicated threads
kvinwang Jul 30, 2026
13bb254
fix(kms): wire authenticated MPC operation dispatch
kvinwang Jul 30, 2026
af587c9
feat(kms): execute threshold K-256 signing quorums
kvinwang Jul 30, 2026
7e22abd
fix(kms): size threshold signatures by curve
kvinwang Jul 30, 2026
c8f9c72
build(kms): lock quorum coordination dependency
kvinwang Jul 30, 2026
c4a1b5c
fix(kms): make quorum operation starts replay-safe
kvinwang Jul 30, 2026
ff1c8d5
feat(kms): add verifiable threshold PRF primitive
kvinwang Jul 30, 2026
91cb1ae
fix(kms): interpolate owned PRF evaluations
kvinwang Jul 30, 2026
d06bcd9
feat(kms): integrate verified threshold key derivation
kvinwang Jul 30, 2026
e72f93a
fix(kms): dispatch derivation and signing payloads explicitly
kvinwang Jul 30, 2026
4238e67
build(kms): lock threshold PRF curve dependency
kvinwang Jul 30, 2026
4b2eb1b
feat(ra-tls): prepare certificates for threshold signers
kvinwang Jul 30, 2026
9d816ed
feat(kms): issue app CAs with threshold P-256 signing
kvinwang Jul 30, 2026
b6e3a38
fix(kms): validate certificate OIDs without error coercion
kvinwang Jul 30, 2026
2424ac4
test(ra-tls): verify externally signed certificate assembly
kvinwang Jul 30, 2026
ac5dc77
fix(ra-tls): encode external signature bit length
kvinwang Jul 30, 2026
85449c8
build(ra-tls): lock external certificate encoding dependency
kvinwang Jul 30, 2026
770b871
feat(kms): enforce threshold-signed monotonic epoch manifests
kvinwang Jul 30, 2026
463c769
fix(kms): bind root certificate to threshold P-256 key
kvinwang Jul 30, 2026
6231ba7
fix(kms): bind all local threshold shares to epoch membership
kvinwang Jul 30, 2026
717ad89
feat(kms): select live members for threshold quorums
kvinwang Jul 30, 2026
3b99e50
fix(kms): bound MPC liveness probes
kvinwang Jul 30, 2026
3aa8a05
fix(kms): keep MPC inboxes local to their recipient
kvinwang Jul 30, 2026
420ea2f
feat(kms): add verifiable threshold-preserving resharing
kvinwang Jul 30, 2026
986438f
fix(kms): construct resharing fixtures from core shares
kvinwang Jul 30, 2026
9d18edc
build(kms): lock resharing test dependency
kvinwang Jul 30, 2026
ab292d2
fix(kms): cap authenticated MPC operation concurrency
kvinwang Jul 30, 2026
4b9560f
feat(kms): materialize reshared CGGMP core shares
kvinwang Jul 30, 2026
ce1bec5
test(kms): exercise real CGGMP threshold signing transport
kvinwang Jul 30, 2026
d29a96b
fix(kms): import CGGMP share metadata in signing test
kvinwang Jul 30, 2026
1077dab
fix(kms): own signing fixtures across async test tasks
kvinwang Jul 30, 2026
a94078a
build(kms): confine trusted dealer support to tests
kvinwang Jul 30, 2026
3287d83
feat(kms): threshold-sign epoch transitions
kvinwang Jul 30, 2026
c84c669
feat(kms): expose threshold epoch authorization to admin
kvinwang Jul 30, 2026
035e9cd
feat(kms): authorize the unsigned genesis manifest once
kvinwang Jul 30, 2026
a761fc5
feat(ra-tls): prepare threshold self-signed root CAs
kvinwang Jul 30, 2026
4967459
fix(ra-tls): derive external root key identifiers from SPKI
kvinwang Jul 30, 2026
9580778
fix(kms): make threshold-derived signing keys total
kvinwang Jul 30, 2026
397874c
fix(kms): constrain threshold-signed app CA identity
kvinwang Jul 30, 2026
b604be5
fix(kms): bound threshold app CA validity from issuance
kvinwang Jul 30, 2026
02e0dbf
feat(kms): prepare threshold-preserving revoke epochs
kvinwang Jul 30, 2026
52cff72
fix(kms): reject recovery IDs for reshare results
kvinwang Jul 30, 2026
53d27bf
test(kms): separate revoke resharing from join authorization
kvinwang Jul 30, 2026
a69f23d
feat(kms): publish the signed active MPC epoch
kvinwang Jul 30, 2026
cb519aa
fix(kms): enforce canonical threshold-share topology
kvinwang Jul 30, 2026
97d546e
feat(kms): load generated MPC cluster identities
kvinwang Jul 30, 2026
d990b36
refactor(kms): generalize blocking MPC transports
kvinwang Jul 30, 2026
dfbdce8
feat(kms): add attested distributed genesis DKG service
kvinwang Jul 30, 2026
46a89e9
fix(kms): type genesis protocol failures explicitly
kvinwang Jul 30, 2026
e8cc7f2
fix(kms): retain genesis key share trait import
kvinwang Jul 30, 2026
a605c4c
feat(kms): launch attested MPC genesis mode
kvinwang Jul 30, 2026
1388e41
fix(kms): call generated genesis ping API
kvinwang Jul 30, 2026
5f0726c
feat(kms): threshold-finalize MPC genesis identity
kvinwang Jul 30, 2026
42988fe
fix(kms): make genesis finalization futures sendable
kvinwang Jul 30, 2026
1dacd9c
feat(kms): restart after durable MPC genesis
kvinwang Jul 30, 2026
7ac9608
fix(kms): ignite genesis server before shutdown handle
kvinwang Jul 30, 2026
ce51cdf
fix(kms): retain genesis state for lifecycle watcher
kvinwang Jul 30, 2026
905d0c5
feat(kms): make genesis operations replay safe
kvinwang Jul 30, 2026
057802d
feat(kms): atomically activate signed MPC epochs
kvinwang Jul 30, 2026
ff7449f
feat(kms): restart after MPC epoch activation
kvinwang Jul 30, 2026
6f5023c
test(kms): recover interrupted revoked epoch activation
kvinwang Jul 30, 2026
99aa071
feat(kms): threshold-authorize membership transitions
kvinwang Jul 30, 2026
3cb9135
fix(kms): classify reshare authorization signatures
kvinwang Jul 30, 2026
d95ad09
test(kms): bind joiners in threshold authorization
kvinwang Jul 30, 2026
ddaafea
refactor(kms): reshare from public key topology
kvinwang Jul 30, 2026
adc19f0
feat(kms): bind resharing dealers in join authorization
kvinwang Jul 30, 2026
73d0161
feat(kms): add attested authorized member join protocol
kvinwang Jul 30, 2026
ee60175
fix(kms): validate old shares during member join
kvinwang Jul 30, 2026
95b21d6
fix(kms): harden authorized join execution
kvinwang Jul 30, 2026
18869bc
test(kms): materialize joiner without old secret shares
kvinwang Jul 30, 2026
e1e4f92
fix(kms): type join test dealer indexes canonically
kvinwang Jul 30, 2026
c714586
feat(kms): recover crash-interrupted MPC genesis
kvinwang Jul 30, 2026
df966f2
feat(kms): threshold-issue attested genesis RPC certificates
kvinwang Jul 30, 2026
ac479a7
feat(kms): threshold-issue joined member RPC certificates
kvinwang Jul 30, 2026
e0a2a5b
feat(kms): publish quote-bound MPC node evidence
kvinwang Jul 30, 2026
6edec26
fix(kms): verify attestation before threshold RPC issuance
kvinwang Jul 30, 2026
64089ad
test(ra-tls): roundtrip transported external certificate TBS
kvinwang Jul 30, 2026
f13f568
test(kms): sign three-of-five with two MPC nodes offline
kvinwang Jul 30, 2026
6b9255a
test(kms): verify three-of-five threshold root certificate
kvinwang Jul 30, 2026
67e6cc7
fix(kms): validate reused P-256 test auxiliary info
kvinwang Jul 30, 2026
65c3a6e
test(kms): reject wrong epoch sender and recipient
kvinwang Jul 30, 2026
faf782f
fix(kms): avoid shadowing envelope test helper
kvinwang Jul 30, 2026
a889eb9
docs(kms): add MPC deployment and lifecycle runbook
kvinwang Jul 30, 2026
dd3ef78
fix(kms): preserve threshold-issued RPC certificates
kvinwang Jul 30, 2026
55e98da
fix(kms): forbid full-root onboarding in MPC mode
kvinwang Jul 30, 2026
02ec276
fix(kms): bound active operations without throttling replay cache
kvinwang Jul 30, 2026
8651673
fix(kms): refresh P-256 threshold test expiry
kvinwang Jul 30, 2026
799e438
fix(kms): separate MPC mutual TLS client identity
kvinwang Jul 30, 2026
4a62d24
fix(kms): durably reject epoch authorization forks
kvinwang Jul 30, 2026
be34040
fix(kms): serialize pending reshare generations
kvinwang Jul 30, 2026
0b56fcb
fix(kms): prevent maintenance join manifest forks
kvinwang Jul 30, 2026
fabf32d
test(kms): cover durable epoch authorization locks
kvinwang Jul 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
367 changes: 366 additions & 1 deletion dstack/Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions dstack/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ wavekv = "1.0.0"

# Core dependencies
anyhow = { version = "1.0.97", default-features = false }
async-trait = "0.1.89"
binrw = { version = "0.15.1", default-features = false, features = ["std"] }
arc-swap = "1"
errify = { version = "0.3.0", features = ["anyhow"] }
Expand Down Expand Up @@ -185,6 +186,8 @@ object = "0.36.4"
fatfs = "0.3.6"
fscommon = "0.1.1"
ciborium = "0.2"
cggmp21 = { version = "0.6.3", features = ["curve-secp256k1", "curve-secp256r1", "state-machine"] }
round-based = { version = "0.4.1", features = ["state-machine"] }

# Networking/HTTP
bollard = "0.18.1"
Expand Down
6 changes: 5 additions & 1 deletion dstack/dstack-util/src/system_setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2090,7 +2090,11 @@ impl<'a> Stage0<'a> {
gateway_app_id: response.gateway_app_id,
key_provider: KeyProvider::Kms {
url: kms_url,
pubkey: root_pubkey,
pubkey: if response.key_provider_id.is_empty() {
root_pubkey
} else {
response.key_provider_id
},
tmp_ca_key: tmp_ca.temp_ca_key,
tmp_ca_cert: tmp_ca.temp_ca_cert,
},
Expand Down
9 changes: 9 additions & 0 deletions dstack/kms/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ license.workspace = true

[dependencies]
anyhow.workspace = true
async-trait.workspace = true
chrono.workspace = true
clap.workspace = true
fs-err.workspace = true
futures.workspace = true
dstack-build-info.workspace = true
hex.workspace = true
hex_fmt.workspace = true
Expand All @@ -33,6 +35,7 @@ reqwest = { workspace = true, features = ["json"] }
sha2.workspace = true
sha3.workspace = true
k256.workspace = true
p256 = { workspace = true, features = ["hash2curve"] }
rand.workspace = true
dstack-guest-agent-rpc.workspace = true
http-client = { workspace = true, features = ["prpc"] }
Expand All @@ -41,6 +44,7 @@ x509-parser = { workspace = true, features = ["verify"] }
ring.workspace = true
safe-write.workspace = true
serde_json.workspace = true
serde_jcs.workspace = true
dstack-types.workspace = true
tokio = { workspace = true, features = ["process"] }
tempfile.workspace = true
Expand All @@ -49,9 +53,14 @@ dstack-verifier = { workspace = true, default-features = false }
dstack-mr.workspace = true
dstack-attest.workspace = true
dstack-api-auth.workspace = true
cggmp21.workspace = true
round-based.workspace = true

[dev-dependencies]
cc-eventlog.workspace = true
cggmp21 = { workspace = true, features = ["spof"] }
round-based = { workspace = true, features = ["sim"] }
key-share = { version = "0.6.1", features = ["spof"] }

[features]
default = []
5 changes: 5 additions & 0 deletions dstack/kms/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,8 @@ The `SignCert` RPC is used by the dstack app to sign a TLS certificate. In this
- Verify the CSR signature
- Query the smart contract to check if the app is authorized
- If authorized, sign the CSR with the CA root key and return the certificate chain to the app

## Multi-instance MPC mode

See [docs/mpc.md](docs/mpc.md) for the identity chain, genesis, membership
lifecycle, crash recovery, deployment procedure, and migration constraints.
183 changes: 183 additions & 0 deletions dstack/kms/docs/mpc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# Multi-instance MPC KMS

## Security identity

MPC mode never loads or reconstructs a complete root private key. Its stable
`key_provider_id` is the SHA-256 domain-separated commitment to:

- the threshold P-256 root CA public key;
- the threshold K-256 signing public key;
- the threshold P-256 derivation public key; and
- the MPC protocol version and cluster ID.

Membership is deliberately excluded from this identity. A membership epoch is
a canonical manifest signed by the threshold K-256 key. Each member entry binds
the node ID, endpoint, quote-bound RA-TLS SubjectPublicKeyInfo, and a commitment
to that node's three public shares.

The verification chain is:

1. an application pins `key_provider_id` in its measured compose configuration;
2. `GetMeta` publishes the cluster identity, signed active manifest, and local
node evidence;
3. the manifest signature verifies under the pinned K-256 group key;
4. the manifest binds the serving node's RA-TLS key and public-share
commitment; and
5. the TEE quote binds the live TLS key to the measured KMS instance.

The threshold P-256 key signs the root certificate and all member RPC
certificates. A member certificate is issued only after every signer verifies
that its embedded attestation is valid and bound to the authorized TLS key.

## Genesis

Genesis is an all-member maintenance ceremony. Before starting it, provision
only transport material (`rpc.crt`, `rpc.key`, the temporary CA files), an
operator-reviewed genesis plan, and a CA used by the provisional transport.
Do not provision a root private key.

Example MPC configuration:

```toml
[core.mpc]
enabled = true
protocol_version = 1
cluster_id = "production-kms"
node_id = "kms-1"
identity_file = "/var/lib/dstack-kms/mpc-identity.json"
genesis_plan_file = "/var/lib/dstack-kms/genesis-plan.json"
genesis_tls_ca_cert = "/var/lib/dstack-kms/genesis-transport-ca.crt"
join_authorization_file = "/var/lib/dstack-kms/join-authorization.json"
client_cert_file = "/var/lib/dstack-kms/mpc-client.crt"
client_key_file = "/var/lib/dstack-kms/mpc-client.key"
manifest_file = "/var/lib/dstack-kms/epoch-manifest.json"
checkpoint_file = "/var/lib/dstack-kms/epoch-checkpoint.json"
p256_share_file = "/var/lib/dstack-kms/p256.share"
k256_share_file = "/var/lib/dstack-kms/k256.share"
derivation_share_file = "/var/lib/dstack-kms/derivation.share"
max_sessions = 128
session_ttl = "5m"
```

The canonical JSON genesis plan contains sorted members:

```json
{
"protocol_version": 1,
"cluster_id": "production-kms",
"threshold": 3,
"coordinator": "kms-1",
"members": [
{
"node_id": "kms-1",
"endpoint": "https://kms-1.example/prpc",
"attestation_pubkey": "<hex DER SubjectPublicKeyInfo>"
}
]
}
```

All members must receive the identical plan. The coordinator waits for every
attested peer and then runs distributed auxiliary generation and DKG for all
three keys. It threshold-signs the root CA, reissues each attested RPC
certificate under that root, signs epoch 1, and distributes the final bundle.
A durable genesis journal makes every node finish a crash-interrupted commit.
Processes shut down after commit and must be restarted by their supervisor.

`mpc-client.crt` must be accepted by `[rpc.tls.mutual].ca_certs`, contain a
valid RA-TLS attestation, and use the same key as `rpc.crt`. The server
certificate is reissued by the threshold root; the client certificate remains
under the mutual-TLS CA so Rocket can authenticate peer connections before the
RPC quote and manifest checks run.

Record and independently compare `key_provider_id` from multiple members before
placing it in application compose files.

## Normal operation

Signing uses an exact live threshold selected in canonical manifest order.
Two offline nodes are tolerated by a 3-of-5 cluster. Every protocol envelope is
bound to the epoch, protocol, session, request hash, sender, recipient, sequence
number, and expiry. Raw arbitrary-digest signing is not exposed.

Derivation is a verifiable threshold PRF. Each response carries a DLEQ proof
against the member's public derivation share; the coordinator combines only
verified partials. Disk, environment, application K-256, and application CA
keys use separate semantic domains.

## Revocation and subset resharing

For a target consisting only of current members:

1. call `Admin.PrepareReshare` with a canonical next-epoch `ResharePlan`;
2. construct an `EpochManifest` using the returned commitment for every target
member;
3. call `Admin.SignEpochManifest`;
4. send the signed result to `Admin.ActivateEpoch` on every target and removed
member; and
5. let the supervisor restart each process.

Exactly an old threshold acts as dealers. Their Lagrange-weighted random
polynomials preserve every group public key. Private evaluations are sent
point-to-point over RA-TLS and are verified against coefficient commitments.

Activation validates the signed chain, local pending share topology, stable
group keys, and local share commitment. It writes an activation journal before
changing active files. Startup completes an interrupted transaction before it
loads a manifest or share. Removed nodes install the signed epoch and then fail
closed because they are no longer members.

## Adding members

Adding a member is an explicitly authorized maintenance operation:

1. Create a sorted `ResharePlan` containing the target membership and exactly
the old threshold in `dealers`. Existing node endpoints and attestation keys
must remain unchanged. New members use freshly attested TLS keys.
2. Call `Admin.AuthorizeReshare`. The result is signed by the active threshold
K-256 key and binds the predecessor, target epoch, threshold, dealers,
endpoints, and quote-bound keys.
3. Provision the signed authorization, active signed manifest, cluster
identity, root certificate, and provisional attested transport key on every
target node. Place the authorization at `join_authorization_file`.
4. Restart every target. They enter maintenance join mode. The first authorized
dealer coordinates fresh all-target auxiliary generation, point-to-point
verifiable resharing, target manifest signing, and threshold issuance of
each target RPC certificate.
5. Each target atomically activates the signed epoch, removes the authorization,
and shuts down for a normal-mode restart.

The coordinator never receives all private recipient evaluations. A joining
node receives only the old public topology and its own evaluation from each
old dealer. The old root private key is never reconstructed.

## Migration and compatibility

The public KMS RPCs and guest key-delivery formats remain unchanged. MPC mode
adds metadata fields and admin methods without removing legacy fields.

Moving an existing single-root deployment to a newly generated MPC cluster is
a provider rotation: the `key_provider_id` and deterministically derived
application keys change. Existing encrypted disks therefore require an
application-level migration ceremony. Do not silently switch an existing
compose pin. Run old and new providers in parallel, migrate protected data,
then update and re-measure the compose pin.

MPC members reject legacy root-key export and handover. There is intentionally
no API that reconstructs threshold roots.

## Operational requirements

- Keep share, checkpoint, journal, identity, authorization, and manifest files
on durable storage; secret shares and journals must be mode `0600`.
- Use authenticated admin listeners. Never enable `insecure_no_auth` in a
network-reachable deployment.
- Require at least threshold 2; production deployments should use 3-of-5 or
stronger.
- Monitor quorum availability and restart completion. Do not activate a target
manifest until every retained member reports its expected commitment.
- Back up shares independently. A backup set smaller than the threshold cannot
recover the root; losing threshold shares permanently loses the provider.
- Treat a same-epoch fork, skipped epoch, checkpoint permission failure, quote
mismatch, share commitment mismatch, or group-key change as a security
incident. The implementation fails closed for all of them.
26 changes: 26 additions & 0 deletions dstack/kms/kms.toml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,32 @@ insecure_no_auth = false
# or another access-control layer.
enabled = true

# MPC cluster identity preview. When enabled, applications pin a stable hash of
# the group public keys rather than one KMS node. The local keys must match the
# configured signing group keys until the threshold key backend is enabled.
[core.mpc]
enabled = false
protocol_version = 1
cluster_id = ""
identity_file = ""
genesis_plan_file = ""
genesis_tls_ca_cert = ""
join_authorization_file = ""
client_cert_file = ""
client_key_file = ""
p256_group_pubkey = ""
k256_group_pubkey = ""
derivation_group_pubkey = ""
node_id = ""
manifest_file = ""
checkpoint_file = ""
allow_unsigned_manifest = false
max_sessions = 128
session_ttl = "5m"
p256_share_file = ""
k256_share_file = ""
derivation_share_file = ""

[core.auth_api]
type = "webhook"

Expand Down
Loading
Loading