Disclaimer: This issue is highly exploratory and may be off-base, ignorant of prior discussion or generally reflective of Ark protocol newb status. Actual consideration would require non-trivial vetting and feasibility analysis with the introspector team, so wanted to ask/explore here before bringing the topic to broader attention. High level idea is to generate a new key for each introspector version inside the enclave and rely more on nitriding to propagate all the keys natively, using its horizontal scaling pattern.
What does this break? What is it missing?
Per usual, research and requirement generation via AI (arkadian) is over-confident and subject to mistake and hallucination ;)
Summary
Instead of carrying the same introspector signing key across enclave code updates (which requires the 9-step locked-key migration ceremony), each new enclave version should generate a fresh signing key inside the enclave and retain old keys only until existing VTXOs expire.
The Ark protocol's natural lifecycle makes this possible: VTXOs are ephemeral with bounded lifetimes. Every VTXO referencing the old key has a timelock expiry. Once all old-key VTXOs expire or are refreshed into rounds using the new key, the old key is retired.
Motivation
The current deployment model requires a 9-step locked-key migration (POST /migrate) every time the enclave binary changes, because any code change produces a new PCR0 and the KMS key policy is locked to the old PCR0. Steps 2-6 exist solely to re-seal the signing key under a new KMS key for the new measurement. This is the single largest operational burden for enclave upgrades.
With per-version keys, the KMS key can remain PCR0-locked and immutable — because the signing key is expected to die with the version. The immutable lock becomes a feature, not a constraint. The migration ceremony shrinks from 9 steps to ~4 (only the storage DEK migration remains).
Feasibility Evidence
This proposal has been validated against three codebases (enclave, introspector, arkd) with code-level analysis.
1. DeprecatedSigner proto already exists
The protocol already anticipated key rotation:
// ark/v1/types.proto — already compiled into types.pb.go:817-823
message DeprecatedSigner {
string pubkey = 1;
int64 cutoff_date = 2;
}
// GetInfoResponse field 17 — already exists in service.pb.go:79
repeated DeprecatedSigner deprecated_signers = 17;
The go-sdk client already reads this field (client/grpc/client.go:101-131). Only the server-side population in arkd/internal/core/application/service.go:GetInfo() is missing — a 1-2 hour wiring task.
2. Enclave dynamic secrets provide retired key persistence
The enclave's existing infrastructure solves the key persistence problem:
sdk/secrets.go — Dynamic secrets layer stores arbitrary strings (up to 64KB) in S3, encrypted with AES-256-GCM using the storage DEK
sdk/storage.go:65-87 — DEK migration handles continuity automatically across enclave versions
sdk/migrate.go:128-154 — export-key only iterates static KMS secrets; dynamic secrets survive migration via DEK, requiring no additional export logic
Retired keys would be stored as named dynamic secrets (e.g., signing-key-v1), persist across enclave versions automatically, and get GC'd when their retention window expires.
3. Nitriding leader/worker sync handles horizontal scaling
Per-version key generation must be leader-level, not per-instance. Nitriding's key synchronization protocol provides exactly this:
- Leader generates the signing key once via NSM hardware RNG
- Workers connect and perform mutual NSM attestation (verifying PCR values match)
- Leader encrypts key material with worker's ephemeral NaCl public key
- Worker decrypts and installs; heartbeat loop detects drift
The enclave SDK already integrates with nitriding via generateAttestationKey() posting to POST /enclave/hash. The signing key needs to be included in nitriding's synchronized state bundle.
4. Migration ceremony simplification
| Step |
Current purpose |
With per-version keys |
| 0 |
Cooldown/abort window |
Retain |
| 1 |
Read current KMSKeyID |
DEK only |
| 2 |
Create new KMS key |
DEK only |
| 3 |
Apply transitional KMS policy |
DEK only |
| 4 |
Store migration KMS key IDs |
DEK only |
| 5 |
Call /v1/export-key (re-encrypt secrets) |
Eliminated |
| 6 |
Poll SSM for migration ciphertexts |
Eliminated |
| 7 |
Adopt ciphertexts |
DEK only |
| 8 |
Stop old, start new enclave |
Retain |
| 9 |
Clean up migration params |
Simplified |
Steps 5 and 6 (the most operationally fragile — they require the old enclave to cooperate in real-time) are fully eliminated for the signing key.
How It Would Work
- New enclave version boots, supervisor generates fresh secp256k1 keypair via
secureRandom (NSM hardware RNG)
- New signing key stored as dynamic secret (
signing-key-current), injected as env var to introspector child process
- New key's public key published via
GetInfo, old key's pubkey added to deprecated_signers with cutoff date
- Old VTXOs still reference old tweaked public key — introspector loads retired keys from dynamic secrets at startup and iterates candidates when signing
- New VTXOs use the new tweaked public key
- Over time, old VTXOs expire or get refreshed in rounds (which naturally uses the current key)
- Once retention window expires (
max(VtxoTreeExpiry, BoardingExitDelay)), retired key's dynamic secret is deleted
Architectural Decisions
PCR16 commitment trade-off
Currently, extendPCRsWithSecretPubkeys (sdk/enclave.go:380-415) commits the signing key's pubkey hash to PCR16+, making it part of the attestation measurement. With runtime-generated per-version keys, this commitment would be dropped.
The trust anchor shifts from "KMS policy proves the key" to "NSM attestation proves the enclave that generated the key." This is security-equivalent within the Nitro threat model — the PCR0 measurement already proves the code integrity of the enclave that generated the key.
Decision needed: Accept this trade-off, or explore first-boot PCR16 extension (generate key, extend PCR16, then start serving).
KMS role reduction
| Secret |
Current |
Per-version model |
| Signing key |
KMS-encrypted, PCR0-locked |
Generated inside enclave, stored as dynamic secret |
| Storage DEK |
KMS-encrypted, PCR0-locked |
Unchanged |
| Other app secrets |
KMS-encrypted |
Unchanged |
KMS remains necessary for the storage DEK. The signing key exits the KMS lifecycle entirely.
Implementation Plan
Phase 1: Key generation inside enclave (enclave repo)
- Remove signing key from
ENCLAVE_SECRETS_CONFIG
- Generate fresh keypair via
secureRandom in supervisor boot
- Store as dynamic secret, inject as env var before child start
Phase 2: Multi-key signer (introspector repo)
- Extend
signer struct: current *btcec.PrivateKey + retired []versionedKey
- Iterate candidates in
signInput, SubmitTx, SubmitFinalization
- Load retired keys from
/v1/secrets/signing-key-v* at startup
Phase 3: Key lifecycle management (introspector repo)
- GC routine deletes retired keys past retention window
- Query
arkdInfo.BoardingExitDelay (or use configured maximum) for retention calculation
Phase 4: Wire deprecated_signers (arkd repo)
- Populate
deprecated_signers field in GetInfo() response
- Add config option or provider interface for deprecated signer list
Phase 5: Nitriding state registration (enclave repo + nitriding config)
- Include signing key in nitriding's synchronized state bundle
- Document leader-election assumption in operational runbook
Open Questions
-
PCR16 commitment — Should we accept the trade-off (drop PCR16 for signing key) or implement first-boot extension? The former is simpler; the latter preserves the current attestation guarantee.
-
Boarding exit delay — The maximum key retention window is driven by BoardingExitDelay (default 90 days). Is 90 days an acceptable retention window, or should introspector-backed boarding inputs use a shorter timelock?
-
First-boot idempotency — If the supervisor generates a key but crashes before the introspector starts, the key is orphaned in dynamic secrets. The boot logic must be idempotent: check for signing-key-current, reuse if present, generate only if absent.
-
Nitriding state bundle scope — What exactly goes into the synchronized state? Just the current signing key, or current + all retired keys? Workers need all keys to sign for old VTXOs.
Files Affected
| File |
Change |
enclave/sdk/cmd/enclave-supervisor/main.go |
Generate signing key at boot, store as dynamic secret |
introspector/internal/application/signer.go |
Multi-key struct, candidate iteration |
introspector/internal/application/service.go |
Load retired keys from management API |
introspector/internal/application/tx.go |
Iterate candidates in SubmitTx |
introspector/internal/application/finalization.go |
Iterate candidates in signing |
introspector/internal/config/config.go |
Remove INTROSPECTOR_SECRET_KEY requirement |
arkd/internal/core/application/service.go |
Populate deprecated_signers in GetInfo |
arkd/internal/core/application/types.go |
Add DeprecatedSigners to ServiceInfo |
enclave/enclave.yaml (introspector deployment) |
Remove signing key from secrets[] |
Disclaimer: This issue is highly exploratory and may be off-base, ignorant of prior discussion or generally reflective of Ark protocol newb status. Actual consideration would require non-trivial vetting and feasibility analysis with the
introspectorteam, so wanted to ask/explore here before bringing the topic to broader attention. High level idea is to generate a new key for eachintrospectorversion inside the enclave and rely more onnitridingto propagate all the keys natively, using its horizontal scaling pattern.What does this break? What is it missing?
Per usual, research and requirement generation via AI (
arkadian) is over-confident and subject to mistake and hallucination ;)Summary
Instead of carrying the same introspector signing key across enclave code updates (which requires the 9-step locked-key migration ceremony), each new enclave version should generate a fresh signing key inside the enclave and retain old keys only until existing VTXOs expire.
The Ark protocol's natural lifecycle makes this possible: VTXOs are ephemeral with bounded lifetimes. Every VTXO referencing the old key has a timelock expiry. Once all old-key VTXOs expire or are refreshed into rounds using the new key, the old key is retired.
Motivation
The current deployment model requires a 9-step locked-key migration (
POST /migrate) every time the enclave binary changes, because any code change produces a new PCR0 and the KMS key policy is locked to the old PCR0. Steps 2-6 exist solely to re-seal the signing key under a new KMS key for the new measurement. This is the single largest operational burden for enclave upgrades.With per-version keys, the KMS key can remain PCR0-locked and immutable — because the signing key is expected to die with the version. The immutable lock becomes a feature, not a constraint. The migration ceremony shrinks from 9 steps to ~4 (only the storage DEK migration remains).
Feasibility Evidence
This proposal has been validated against three codebases (enclave, introspector, arkd) with code-level analysis.
1.
DeprecatedSignerproto already existsThe protocol already anticipated key rotation:
The go-sdk client already reads this field (
client/grpc/client.go:101-131). Only the server-side population inarkd/internal/core/application/service.go:GetInfo()is missing — a 1-2 hour wiring task.2. Enclave dynamic secrets provide retired key persistence
The enclave's existing infrastructure solves the key persistence problem:
sdk/secrets.go— Dynamic secrets layer stores arbitrary strings (up to 64KB) in S3, encrypted with AES-256-GCM using the storage DEKsdk/storage.go:65-87— DEK migration handles continuity automatically across enclave versionssdk/migrate.go:128-154—export-keyonly iterates static KMS secrets; dynamic secrets survive migration via DEK, requiring no additional export logicRetired keys would be stored as named dynamic secrets (e.g.,
signing-key-v1), persist across enclave versions automatically, and get GC'd when their retention window expires.3. Nitriding leader/worker sync handles horizontal scaling
Per-version key generation must be leader-level, not per-instance. Nitriding's key synchronization protocol provides exactly this:
The enclave SDK already integrates with nitriding via
generateAttestationKey()posting toPOST /enclave/hash. The signing key needs to be included in nitriding's synchronized state bundle.4. Migration ceremony simplification
/v1/export-key(re-encrypt secrets)Steps 5 and 6 (the most operationally fragile — they require the old enclave to cooperate in real-time) are fully eliminated for the signing key.
How It Would Work
secureRandom(NSM hardware RNG)signing-key-current), injected as env var to introspector child processGetInfo, old key's pubkey added todeprecated_signerswith cutoff datemax(VtxoTreeExpiry, BoardingExitDelay)), retired key's dynamic secret is deletedArchitectural Decisions
PCR16 commitment trade-off
Currently,
extendPCRsWithSecretPubkeys(sdk/enclave.go:380-415) commits the signing key's pubkey hash to PCR16+, making it part of the attestation measurement. With runtime-generated per-version keys, this commitment would be dropped.The trust anchor shifts from "KMS policy proves the key" to "NSM attestation proves the enclave that generated the key." This is security-equivalent within the Nitro threat model — the PCR0 measurement already proves the code integrity of the enclave that generated the key.
Decision needed: Accept this trade-off, or explore first-boot PCR16 extension (generate key, extend PCR16, then start serving).
KMS role reduction
KMS remains necessary for the storage DEK. The signing key exits the KMS lifecycle entirely.
Implementation Plan
Phase 1: Key generation inside enclave (enclave repo)
ENCLAVE_SECRETS_CONFIGsecureRandomin supervisor bootPhase 2: Multi-key signer (introspector repo)
signerstruct:current *btcec.PrivateKey+retired []versionedKeysignInput,SubmitTx,SubmitFinalization/v1/secrets/signing-key-v*at startupPhase 3: Key lifecycle management (introspector repo)
arkdInfo.BoardingExitDelay(or use configured maximum) for retention calculationPhase 4: Wire
deprecated_signers(arkd repo)deprecated_signersfield inGetInfo()responsePhase 5: Nitriding state registration (enclave repo + nitriding config)
Open Questions
PCR16 commitment — Should we accept the trade-off (drop PCR16 for signing key) or implement first-boot extension? The former is simpler; the latter preserves the current attestation guarantee.
Boarding exit delay — The maximum key retention window is driven by
BoardingExitDelay(default 90 days). Is 90 days an acceptable retention window, or should introspector-backed boarding inputs use a shorter timelock?First-boot idempotency — If the supervisor generates a key but crashes before the introspector starts, the key is orphaned in dynamic secrets. The boot logic must be idempotent: check for
signing-key-current, reuse if present, generate only if absent.Nitriding state bundle scope — What exactly goes into the synchronized state? Just the current signing key, or current + all retired keys? Workers need all keys to sign for old VTXOs.
Files Affected
enclave/sdk/cmd/enclave-supervisor/main.gointrospector/internal/application/signer.gointrospector/internal/application/service.gointrospector/internal/application/tx.gointrospector/internal/application/finalization.gointrospector/internal/config/config.goINTROSPECTOR_SECRET_KEYrequirementarkd/internal/core/application/service.godeprecated_signersin GetInfoarkd/internal/core/application/types.goDeprecatedSignerstoServiceInfoenclave/enclave.yaml(introspector deployment)secrets[]