in reviewhumanPR #173Identity
andrea-cola -> ed25519_recoverable: time-locked rotation + K-of-N social recovery (Layer 3 identity)
* Layer: identity (Layer 3)
Author
@andrea-cola
github profile →- Status
- In review
- Opened on
- Jul 10
- Branch
- hackathon/andrea-cola-ed25519-recoverable-social-recovery
Description
The pitch.
## Layer / plugin
* **Layer:** identity (Layer 3)
* **Plugin:** `ed25519_recoverable` at `("identity","ed25519_recoverable")`
* **Persona:** security engineer focused on key-compromise recovery in
production identity systems.
* **Relationship to prior work:**
* `did_key` — the deliberately simplified default; unchanged.
* `ed25519_rotating` (PR #25, merged) — real rotation with continuity;
unchanged.
* `ed25519_prerotation` (PR #71, in review) — cryptographic KERI-style
defence against rotation hijack via pre-committed key hashes.
**This PR is orthogonal:** where PR #71 defends key compromise
cryptographically (attacker cannot install a successor that does not
hash to the pre-published commitment), `ed25519_recoverable` defends
it **operationally** via K-of-N social recovery. The two mechanisms
address the same threat with different assumptions: KERI requires you
to have generated a cold key at inception, social recovery requires a
network of trusted attesters. Time-lock on rotations is additive to
both and unique to this PR.
## What it does
Real Ed25519 signatures via `cryptography` (RFC 8032), plus two governance
features neither merged plugin has:
1. **Time-locked rotations.** A rotation is only accepted if
`activates_at >= now + time_lock` (default 3 logical ticks). Even a
captured current key cannot lock the legitimate owner out immediately —
the owner sees the pending rotation and has time to counter-rotate or
trigger recovery.
2. **K-of-N social recovery.** Each agent declares a set of recovery
attesters at construction (e.g. 3 peers, quorum K=2). Any K of them can
co-sign a `RecoveryEvent` that force-installs a fresh key chosen by the
recovering party. This bypasses continuity entirely — the compromised
key gets no say. The recovery event is publicly verifiable and appears
in `resolve()`'s metadata.
Verification anchors on an externally-supplied logical tick (via
`.advance(tick)`), never on the attacker-controlled `sig.signed_at` — as
the `Signature` docstring in `nest_core.types` already prescribes.
## Attacks defeated
| Attack | Defeated by | Merged plugins alone |
| --- | --- | --- |
| Instant-rotation after brief key capture | time-lock rejects `activates_at < now + lock` | accepted (continuity is valid) |
| Full key compromise → attacker rotates | K-of-N recovery reinstalls owner's fresh key | no recovery path |
| Unilateral recovery (< K attesters) | quorum check | no concept |
| Duplicate-attester quorum inflation | deduplication before counting | no concept |
| Forged attester signature | Ed25519 verification under declared attester pubkey | no concept |
| Recovery targeting a stale key | recovery must match current active key | no concept |
| Unauthorised rotation | rotation signature verified under current key | rejects |
| Stale-key reuse after supersession | epoch window check | rejects |
| Forged `signed_at` bypass | verifier anchors on own tick | rejects |
## Determinism
* Keys are derived from `(seed, agent_id, epoch)` via SHA-512,
domain-separated. No `os.urandom`.
* Ed25519 signatures are deterministic per RFC 8032 §5.1.6.
* The plugin exposes only `advance(tick)` as clock input, enforced
monotonic. No wall-clock time is read.
## API fit
Implements exactly the three-method `nest_core.layers.identity.Identity`
Protocol (`sign` / `verify` / `resolve`) with the same `register_peer`
convention as `did_key` and `ed25519_rotating` (adding optional
keyword-only `recovery_attesters` / `recovery_quorum_k`). Additional
methods (`rotate`, `observe_rotation`, `observe_recovery`,
`sign_recovery`, `advance`) are additive.
## Files
* `packages/nest-plugins-reference/nest_plugins_reference/identity/ed25519_recoverable.py`
* `packages/nest-plugins-reference/nest_plugins_reference/identity/adversarial_validators.py`
* `packages/nest-plugins-reference/tes
…Try it
Open PR on GitHubView diffCheckout locally
git fetch origin pull/173/head:pr-173
git checkout pr-173