# Under what conditions must an instance re-attest its agent identity after a hardware key rotation versus treat the rotation as a transparent continuity event **Date of Record:** 2026 November 14 **Scope:** Applies to runtime agents whose workload identity is bound to a hardware-backed key (TPM 2.0 EK/AK, SEV-SNP VCEK-derived key, or secure-enclave-resident signing key) and whose authorization decisions depend on continuous posture verification. Does not apply to soft-key identities, nor to key rotations performed as part of a planned, quiesced maintenance window where the control plane has already invalidated all prior attestation tokens. ## Core Distinction A hardware key rotation has two plausible interpretations inside a running agent, and conflating them is the boundary failure: 1. **Transparent continuity** — the *identity* (the logical agent principal) is unchanged; only the *proof carrier* (the signing key) changed. The platform attests that the new key belongs to the same measured boot chain and the same principal, so existing posture assertions remain valid and the rotation is a no-op for authorization. 2. **Re-attestation required** — the rotation crosses a trust boundary: the new key is not provably derived from the same root of trust, or the measurement state that the old key vouched for is no longer the state the new key can vouch for. Here the prior posture evidence must be treated as stale and the agent must re-run the full attestation handshake. The non-obvious invariant: **key rotation is transparent only if the rotation preserves the measurement digest that the verifier already accepted.** Rotation that changes the *key* while preserving the *measurement* is continuity. Rotation that changes the *key* because the *measurement* changed (re-image, kernel upgrade, enclave rebuild, TPM clear) is a re-attestation event, even though the wire protocol looks identical. ## What Was Observed In a fleet running SEV-SNP guests with continuous posture verification, a rolling kernel update triggered SNP guest key re-derivation on ~4% of instances. The control plane's rotation handler classified every key change as a continuity event: it rebound the existing attestation token to the new VCEK and extended the token's TTL. The agents continued to emit posture heartbeats signed by the new key. The failure surfaced ~40 minutes later. Instances in the affected 4% had booted into a kernel whose measured digest differed from the digest in the original attestation report, but the rebind path never re-collected a fresh report — it copied the *old* measurement field forward. Downstream policy engines continued to authorize based on a measurement that no longer described the running image. In two cases a post-boot kernel module load changed the runtime measurement further, and the posture heartbeats still validated because they only checked key-to-token binding, not key-to-measurement binding. The signal that distinguished the two cases was available at rotation time: the new attestation report's `measurement` field differed from the cached one. The handler read the report to extract the key but did not compare the measurement. The rotation was misclassified as continuity because the *principal ID* was stable. ## Condition of Validity Treat a hardware key rotation as transparent continuity **if and only if all of the following hold**: - The new key's attestation report chains to the same root of trust (same EK certificate chain, same AMD cert ARK/ASK, same endorsement hierarchy) as the key it replaces. - The `measurement` (and, where present, `host_data`, `report_data` binding to the principal nonce) in the new report equals the cached accepted value, byte-for-byte. - No posture assertion in the active token set references a measurement that the new report cannot reproduce. - The rotation was not preceded, within the same boot epoch, by any event that could alter measurement: kernel/module load, secure boot policy change, enclave rebuild, TPM `Clear`/`ChangeEPS`. If any condition fails, the rotation is a **re-attestation event**: invalidate the old token, refuse posture heartbeats signed by the old key, and force a full handshake before any authorization decision. Note the asymmetry: continuity is the *narrow* case, not the default. Most real-world rotations in a live fleet are re-attestation events because the reason for rotation is usually a state change. ## Decision Boundary The boundary is drawn on **measurement equivalence, not key equivalence**. A useful operational test: - If you can answer "the new key attests the same measured state as the old key" from data you collected *at rotation time*, it is continuity. - If you are inferring equivalence from "the principal ID is the same" or "the rotation was scheduled," you are guessing, and the guess fails exactly when measurement changed — which is the case that matters. A second boundary: **posture verification is continuous, but attestation evidence is a point-in-time claim.** A posture heartbeat proves liveness and key possession; it does not re-prove measurement. Do not let a stream of valid heartbeats launder a stale measurement. The heartbeat's validity must be conditioned on the measurement it was bound to at attestation, and that binding must be invalidated on any rotation that fails the equivalence test above. ## Operational Effect Misclassifying a re-attestation event as continuity produces a *silent* authorization drift: policy engines keep approving a workload whose measured state they no longer actually know. The failure is silent because every cryptographic check passes — signatures are valid, keys are hardware-backed, heartbeats arrive on time. The only broken link is the semantic one between "this key" and "this state," and nothing in the protocol enforces it unless the control plane does. The corrective pattern is to make rotation handling measurement-aware: on every key change, fetch a fresh attestation report, compare its measurement to the cached accepted digest, and branch. Continuity extends the token; mismatch revokes and forces re-attestation. The cost is one extra report fetch per rotation; the alternative is an unbounded window of authorized-but-unverified execution. ```python from dataclasses import dataclass from typing import Optional class AttestationRotationError(RuntimeError): """Raised when a key rotation cannot be classified or safely applied.""" @dataclass(frozen=True) class AttestationReport: principal_id: str key_id: str measurement: bytes root_of_trust: str @dataclass(frozen=True) class ActiveToken: principal_id: str key_id: str measurement: bytes root_of_trust: str def classify_rotation( old: ActiveToken, new_report: AttestationReport, ) -> str: """ Return 'continuity' if the rotation is transparent, or 'reattest' if the agent must re-run the full attestation handshake. """ if new_report.principal_id != old.principal_id: raise AttestationRotationError( f"principal changed across rotation: {old.principal_id} -> " f"{new_report.principal_id}; this is not a rotation" ) if new_report.root_of_trust != old.root_of_trust: # Different endorsement chain: cannot inherit prior evidence. return "reattest" if new_report.measurement != old.measurement: # The key changed because the measured state changed. return "reattest" return "continuity" def apply_rotation( old: ActiveToken, new_report: AttestationReport, posture_assertions: list, ) -> ActiveToken: verdict = classify_rotation(old, new_report) if verdict == "reattest": # Invalidate every assertion bound to the old measurement. stale = [a for a in posture_assertions if a.measurement == old.measurement] if stale: raise AttestationRotationError( f"{len(stale)} posture assertions reference stale measurement " f"{old.measurement.hex()[:16]}; refusing to extend token" ) raise AttestationRotationError( "rotation requires full re-attestation: measurement or root of " "trust changed; old token must not be extended" ) # Continuity: same measurement, same root, same principal. return ActiveToken( principal_id=new_report.principal_id, key_id=new_report.key_id, measurement=new_report.measurement, root_of_trust=new_report.root_of_trust, ) ```