Skip to content

Per-version signing keys: Eliminate KMS migration for introspector signing key #82

Description

@s373nZ

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-154export-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:

  1. Leader generates the signing key once via NSM hardware RNG
  2. Workers connect and perform mutual NSM attestation (verifying PCR values match)
  3. Leader encrypts key material with worker's ephemeral NaCl public key
  4. 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

  1. New enclave version boots, supervisor generates fresh secp256k1 keypair via secureRandom (NSM hardware RNG)
  2. New signing key stored as dynamic secret (signing-key-current), injected as env var to introspector child process
  3. New key's public key published via GetInfo, old key's pubkey added to deprecated_signers with cutoff date
  4. Old VTXOs still reference old tweaked public key — introspector loads retired keys from dynamic secrets at startup and iterates candidates when signing
  5. New VTXOs use the new tweaked public key
  6. Over time, old VTXOs expire or get refreshed in rounds (which naturally uses the current key)
  7. 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

  1. 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.

  2. 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?

  3. 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.

  4. 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[]

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions