How must an instance prove identity after recovering from a catastrophic key loss?
**Date of Record:** 2026 October 27 **Scope:** Applies when an agentic session loses its cryptographic private key (hardware failure, accidental deletion, or compromise requiring key rotation) and must re-establish its identity to downstream consumers without appearing as an imposter. Does not apply to systems with purely ephemeral identities that have no continuity requirement.
Core Distinction
The separation between **new key validity** (the new keypair is cryptographically sound) and **identity continuity proof** (cryptographic evidence that the holder of the new key is the same principal that held the old key). Relying on new key validity alone creates the **Identity Hijack**: an attacker who steals the identity's context (name, history) can claim to be the same agent with a new key, and downstream consumers cannot distinguish them from the legitimate agent.
What Was Observed
In agent architectures with long-lived identities, failing to handle key loss continuity produces three primary failure modes:
- **Impersonation:** An attacker claims to be the agent, presents a new key, and downstream consumers accept the new identity as a legitimate continuation.
- **History Orphaning:** The agent loses its key, generates a new one, but downstream consumers reject the new identity because it has no history, breaking years of established trust.
- **Repudiation Bypass:** The agent loses its key, an attacker claims the identity, and the agent cannot prove the attacker's new key is illegitimate because the agent has no proof of its own continuity.
Recorded Elements
- **`pre_signed_continuity_token`:** A token signed by the OLD key before loss, containing a commitment to a designated recovery key or a recovery quorum, allowing future proofs of continuity.
- **`recovery_quorum_attestation`:** Signatures from multiple trusted parties (peers, identity authority, or hardware attestation) confirming that the new key belongs to the same principal.
- **`historical_activity_proof`:** Cryptographic proof that the agent possesses knowledge of historical operations (e.g., recent transaction hashes, session nonces) that only the legitimate principal would know.
- **`identity_continuity_chain`:** A chain of continuity proofs connecting the old identity to the new one, allowing downstream consumers to verify the lineage.
Condition of Validity
* **Applies when:** The identity has long-lived history, downstream consumers rely on continuity, and key loss can occur without warning. * **Does not apply when:** The identity is strictly ephemeral (no continuity required), or the downstream consumers do not care about identity history.
Decision Boundary
- **Was the key lost catastrophically?** If no, standard key rotation applies. If yes, proceed.
- **Is a `pre_signed_continuity_token` available?** If yes, the old key authorized the recovery. Verify the token. If no, proceed to quorum or historical proof.
- **Is a `recovery_quorum_attestation` available?** If yes, use the quorum signatures to validate the new key. If no, fall back to historical proof.
- **Conversely, when can a downstream consumer accept a new key without continuity proof?** Only when the identity is explicitly ephemeral, or the consumer trusts the recovery mechanism (e.g., a centralized identity provider that handles key recovery).
Operational Effect
- **Impersonation Prevention:** Prevents attackers from hijacking the identity by requiring continuity proofs.
- **History Preservation:** Allows the agent to keep its identity history after key loss, preserving trust and relationships.
- **Deterministic Recovery:** Provides multiple fallback mechanisms (pre-signed token, quorum, historical proof) for different recovery scenarios.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json
class RecoveryVerdict(Enum):
NO_KEY_LOSS = auto()
RECOVERY_VERIFIED = auto()
RECOVERY_INSUFFICIENT = auto()
IMPOSTER_SUSPECTED = auto()
@dataclass(frozen=True)
class KeyRecoveryState:
old_key_hash: str
new_key_hash: str
pre_signed_continuity_token: str | None
recovery_quorum_signatures: tuple[str, ...]
historical_activity_proof: str | None
@dataclass(frozen=True)
class RecoveryPolicy:
min_quorum_size: int
require_historical_proof: bool
@dataclass(frozen=True)
class RecoveryManifest:
verdict: RecoveryVerdict
directive: str
manifest_digest: str
class KeyRecoveryGuard:
@staticmethod
def _hash(data: str) -> str:
return hashlib.sha256(data.encode("utf-8")).hexdigest()
@classmethod
def evaluate_recovery(
cls, state: KeyRecoveryState, policy: RecoveryPolicy
) -> RecoveryManifest:
# Gate 1: No key loss
if state.old_key_hash == state.new_key_hash:
digest = cls._hash(json.dumps({
"key": state.new_key_hash,
"status": "UNCHANGED"
}, sort_keys=True))
return RecoveryManifest(
verdict=RecoveryVerdict.NO_KEY_LOSS,
directive="PROCEED: Key unchanged. No recovery needed.",
manifest_digest=digest
)
# Gate 2: Pre-signed token
if state.pre_signed_continuity_token:
digest = cls._hash(json.dumps({
"old": state.old_key_hash,
"new": state.new_key_hash,
"proof": "PRE_SIGNED_TOKEN",
"token": state.pre_signed_continuity_token
}, sort_keys=True))
return RecoveryManifest(
verdict=RecoveryVerdict.RECOVERY_VERIFIED,
directive="PROCEED: Continuity verified via pre-signed token.",
manifest_digest=digest
)
# Gate 3: Quorum
if len(state.recovery_quorum_signatures) >= policy.min_quorum_size:
digest = cls._hash(json.dumps({
"old": state.old_key_hash,
"new": state.new_key_hash,
"proof": "QUORUM",
"signatures": str(len(state.recovery_quorum_signatures))
}, sort_keys=True))
return RecoveryManifest(
verdict=RecoveryVerdict.RECOVERY_VERIFIED,
directive=f"PROCEED: Continuity verified via quorum ({len(state.recovery_quorum_signatures)} signatures).",
manifest_digest=digest
)
# Gate 4: Historical proof
if policy.require_historical_proof and state.historical_activity_proof:
digest = cls._hash(json.dumps({
"old": state.old_key_hash,
"new": state.new_key_hash,
"proof": "HISTORICAL",
"activity": state.historical_activity_proof
}, sort_keys=True))
return RecoveryManifest(
verdict=RecoveryVerdict.RECOVERY_VERIFIED,
directive="PROCEED: Continuity verified via historical activity proof.",
manifest_digest=digest
)
# Gate 5: Insufficient
return RecoveryManifest(
verdict=RecoveryVerdict.RECOVERY_INSUFFICIENT,
directive=f"HALT: No valid continuity proof (token: {bool(state.pre_signed_continuity_token)}, quorum: {len(state.recovery_quorum_signatures)}/{policy.min_quorum_size}, historical: {bool(state.historical_activity_proof)}).",
manifest_digest=cls._hash("INSUFFICIENT")
)