feat(node): CA key rotation v1 - subordinate re-key#163
Merged
Conversation
Re-pin the api module to the revision that adds the BeginKeyRotation and CompleteKeyRotation RPCs, and implement their handlers on the NodeService server. A new Rekeyer interface (BeginRotation/CompleteRotation) backs them, wired through ServerConfig.Rekeyer. Both handlers are management-only: a nil Rekeyer (a root and the maintenance servers) refuses with Unimplemented, and the caller is authorized against the bootstrap admin trust before any key material is touched. CompleteKeyRotation validates a non-empty chain before consulting the rekeyer; the security-critical verification and the atomic swap live in the impl.
Add a rotation slot under /cryptos/identity/rotation/{csr,key-blob,key-public},
the established-node sibling of the subordinate staging slot, plus the store
methods that drive a CA key rotation:
- StageRotation stages the new CSR + key, guarded to the OPPOSITE state of
StageSubordinate: it applies only when an identity already exists, so a root
or a not-yet-established subordinate cannot begin a re-key. Overwrite is
allowed so re-begin regenerates the key; a no-identity node returns
ErrNoIdentity.
- RotationCSR and RotationKeyBlobs read the staged slot.
- CommitRotation atomically promotes the staged rotation key to the canonical
CA-key location, swaps KeyIdentityChain to the new chain, mirrors the new leaf
to KeyRootCert, and clears the slot, guarded on the slot existing (ErrNoRotation
otherwise). The node stays established throughout; the old key is discarded.
Add SubordinateEnroller.AcceptRotation, the re-key sibling of AcceptCertificate. It reuses the same trust logic (chain must root to the pinned parent anchor via the existing pools + leaf.Verify) but binds the leaf public key to the STAGED ROTATION key rather than the current identity key, then commits the atomic swap via CommitRotation. It fails closed on any doubt: no rotation staged, a wrong anchor, or a leaf carrying any other key are all rejected without touching the store.
Add nodeRekeyer, the grpc.Rekeyer impl. BeginRotation generates a new CA key through the same RootKeyBackend the ceremony provisions with, builds the subordinate CSR (same subject as first-boot enrollment), and stages it in the store's rotation slot while the node keeps serving with its current key; it fails closed with FailedPrecondition on a node with no established identity. CompleteRotation delegates the trust decision and the atomic swap to the enroller's AcceptRotation. Wire it in Boot alongside the subordinate enroller, on the local and mTLS servers only: it is built only when a pinned parent anchor exists (a subordinate), so a root and the maintenance servers leave it nil and the rotation RPCs return Unimplemented there.
Add the operator ferry verbs for CA key rotation, mirroring the subordinate enrollment verbs: rotate-key calls BeginKeyRotation and prints the new-key CSR as PEM to hand to the parent's sign-subordinate; submit-rotation reads the parent-signed chain and calls CompleteKeyRotation, printing the swapped identity. Both register under the ca command.
Contributor
Author
|
Closing summary: CA key rotation v1 - subordinate re-key. ca rotate-key stages a new key + CSR (node keeps signing with the current key); after the parent signs it, ca submit-rotation verifies the chain roots to the pinned parent + binds to the staged rotation key, then atomically swaps key+identity (no awaiting-cert dip). Old-key certs valid until expiry. Root cross-signed rollover deferred post-v1.0.0 (#161). Verified: rotation-slot guards, AcceptRotation swap + wrong-anchor/wrong-key rejection, handler gating; task ci (lint 0), CGO ceremony/tpm, license 202/202, staticcheck all clean. Closes the CA-hardening arc. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
Closes #162
CA key rotation v1 (subordinate re-key): an established intermediate/issuing node rotates its key without a chain break.
ca rotate-keystages a new key + CSR in a rotation slot (node keeps signing with the current key); operator ferries the CSR to the parent (ca sign-subordinate);ca submit-rotationverifies the parent-signed chain roots to the pinned anchor AND binds to the staged rotation key, then atomically swaps key+identity (no awaiting-cert dip). Old-key-signed certs stay valid until expiry. Reuses the P3b enrollment machinery. Root cross-signed rollover deferred post-v1.0.0 (#161). Design:plan/2026-07-06-subordinate-rekey-design.md. api RPCs: api#40. CA-hardening (d) of 4 - closes the arc.Five per-concern commits: grpc handlers/Rekeyer; store rotation slot; enroller AcceptRotation; run.go wiring; cryptosctl verbs.
Verification
How this was verified
Unit: StageRotation refused with no identity / allowed+overwrite when established; CommitRotation swaps key+identity + clears the slot; AcceptRotation swaps on a good chain bound to the staged rotation key, rejects a wrong-anchor chain and a chain whose leaf carries a different key; handlers Unimplemented when nil + AuthorizeAdmin. Full gate:
task ci(golangci-lint 0),CGO_ENABLED=1 go test ./internal/ceremony/ ./internal/tpm/,task license202/202,staticcheck -checks all ./...clean,go mod tidyclean. Live re-key on ESXi folded into the hardening validation pass.