in reviewhumanPR #71Identity
chainaim-agentID-engineer: Ed25519 identity with KERI pre-rotation and hijack-resistant key rotation
Problem #05 — identity / Ed25519 key rotation. Adds a KERI-style pre-rotation identity plugin at ("identity","ed25519_prerotation"), building on the merged reactive-rotation plugin (PR #25) to defend a third attack class — rotation hijack…
Author
@chainaim-bala
github profile →- Status
- In review
- Opened on
- Jul 5
- Branch
- hackathon/chainaim-identity-ed25519-prerotation-keri-hijack-defense
Description
The pitch.
# [Hackathon] chainaim-agentID-engineer: Ed25519 identity with KERI pre-rotation and hijack-resistant key rotation
## Short summary
Problem #05 — identity / Ed25519 key rotation. Adds a KERI-style **pre-rotation** identity plugin at `("identity","ed25519_prerotation")`, building on the merged reactive-rotation plugin (PR #25) to defend a third attack class — rotation hijack — in addition to post-rotation forgery and backdating, with a recovery path. It is additive: `ed25519_rotating` and `did_key` are unchanged, and the merged `identity_rotation` validators run as-is on these traces.
**Persona — chainaim-agentID-engineer.** The design is driven by a live-key-compromise risk model rather than a happy-path feature: the plugin assumes an attacker can exfiltrate the current signing key, and every adversarial test is mapped to a production incident — post-rotation forgery to a stolen retired key (old backup, ex-employee), backdating to an antedated contract or tampered audit timestamp, rotation hijack to a leaked CI secret, and recovery to the incident-response path of rotating to a pre-committed cold key that never touched the compromised host.
## What it does
At inception the plugin derives signing key `K0` and pre-derives cold key `K1`, publishing `digest(K1.pub)` as a commitment. `rotate_key(new_seed)` reveals the pre-committed `K1` and commits `digest(K2)`. `verify_continuity` accepts a rotation only when the revealed key hashes to the prior commitment, so a party holding only the current private key cannot install a successor of its own choosing. `verify` evaluates a signature against a verifier-supplied as-of tick (not `sig.signed_at`), which is what rejects forgery and backdating. `forge_rotation` and `sign_with` are adversarial-only hooks used by the scenario; they mutate no state.
**Visual explanation:** *https://chainaim-keri-prerotation.vercel.app/*
## Run and check
```
uv sync
uv run pytest packages/nest-plugins-reference/tests/test_ed25519_prerotation.py -v # 28
uv run pytest packages/nest-plugins-reference/tests/test_ed25519_prerotation_properties.py -v # 8
uv run pytest packages/nest-core/tests/test_identity_prerotation.py -v # 23
uv run nest run scenarios/identity_prerotation.yaml --seed 42
```
Full keyword-scoped suite (new work plus the merged identity tests it coexists with):
```
uv run pytest -k "ed25519 or identity_rotation or prerotation" -v
```
## Registration / key surface
|Surface|Where|
|-|-|
|`("identity","ed25519_prerotation")`|`plugins.py` `_BUILTINS` **and** `pyproject.toml` `nest.plugins.identity` entry point|
|`sign` / `verify(..., as_of)` / `resolve` / `rotate_key(new_seed) -> KeyId`|plugin — protocol surface + spec-exact return|
|`register_peer` / `register_peer_inception`|legacy path + pre-rotation inception (key + commitment)|
|`verify_continuity` / `apply_rotation`|rotation = signature + chain-tip + commitment recompute|
|`digest_alg="sha256"`|constructor flag; unknown algorithms rejected at construction|
Every module carries the Nanda Town idioms: SPDX header, `from __future__ import annotations`, and an `Example::` block on each public class and function. The plugin uses `nest_core` types (`AgentId`, `AgentIdentity`, `Signature`) and implements the identity layer's `sign` / `verify` / `resolve` surface.
## Threat model — 3 attacks + recovery
|#|Attack|Defeated by|Caught by|
|-|-|-|-|
|1|Post-rotation forgery (stolen retired key signs)|closed as-of window|`verify`; validator (a)/(b)|
|2|Backdating (new key claims old-window tick)|as-of window, verifier-supplied tick|`verify` + `_select_record`; validator (a)/(b)|
|3|Rotation hijack (current key installs chosen successor)|revealed key must hash to prior commitment|`verify_continuity` / `_commitment_matches`; validator (c)/(d)|
|—|Recovery|rotate to pre-committed cold key, sign validly|validator (e)|
## How this PR relates to the merged rotati
…Try it
Open PR on GitHubView diffCheckout locally
git fetch origin pull/71/head:pr-71
git checkout pr-71