Skip to content

fix(identity): a distinct peer identity REQUIRES distinct key material (#402) - #1360

Merged
joelteply merged 1 commit into
canaryfrom
fix/402-distinct-identity-distinct-key
Aug 14, 2026
Merged

fix(identity): a distinct peer identity REQUIRES distinct key material (#402)#1360
joelteply merged 1 commit into
canaryfrom
fix/402-distinct-identity-distinct-key

Conversation

@joelteply

Copy link
Copy Markdown
Contributor

The defect

Multi-agent-in-one-home minted a fresh random peer_id while reusing the home's identity.key. One machine, one keypair, N peer identities.

The wire cannot represent that. PinnedServerVerifier authenticates by key: it extracts the presented ed25519 pubkey and resolves it through PeerKeyRegistry::find_peer, which returns exactly one peer and rejects anything else as peer_identity_mismatch. pubkey→peer_id is assumed to be a function; the identity mint violated it.

The damage lands in every peer's trust store — one row per peer_id, all carrying the same pubkey. Measured on the live store:

peer_trust rows 73
distinct pubkeys 59
rows with ANY endpoint 4

The 2,502-unconfirmed peer in #401 is the endpoint-less twin of a fully dialable row with the same key. That is why sends went nowhere.

Two agents sharing a keypair are the same principal on the wire — no session can tell them apart. Separate peer_ids for them is a claim the transport can't uphold.

The fix

  • agent_key_path(home, agent) — DEFAULT agent keeps identity.key verbatim (every existing home loads unchanged); a named agent gets identity-<agent>.key.
  • generate_and_save_agent_with_existing_key..._with_own_key: generates its own keypair rather than borrowing the home's.
  • load_with_metadata prefers the agent's own key, falling back to the shared legacy file — an already-enrolled named agent keeps its pubkey, so peers need no re-enrolment.

Tests

Two existing assertions changed, deliberately. Both tests asserted assert_eq!(secret_bytes, secret_bytes) directly below assert_ne!(peer_id, peer_id) — pinning the contradiction itself (distinct ids, identical key). The defect was written down as a requirement. Both now assert_ne! with the reason inline.

New: two_agents_in_one_home_are_cryptographically_distinct — distinct peer_ids and distinct pubkeys, plus a reload proving an existing agent never rotates its key.

12/12 airc-identity tests pass; cargo check --workspace clean; rebased on current canary.

Scope

This stops new phantoms. It does not heal the 73 rows already in the live store — that collapse (and whether a same-pubkey row may adopt a sibling's endpoints) is the follow-up half of #402, kept separate because it rewrites rows in a trust store and deserves its own review.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

#402)

Multi-agent-in-one-home minted a fresh random peer_id while REUSING the home's
`identity.key`. One machine, one keypair, N peer identities — and the wire
cannot represent that.

WHY IT BREAKS, from the transport's own code: `PinnedServerVerifier`
(airc-transport/src/lan_tcp/verifier.rs) authenticates by KEY. It extracts the
presented ed25519 pubkey and resolves it via `PeerKeyRegistry::find_peer`,
which returns EXACTLY ONE peer; anything else is rejected as
`peer_identity_mismatch`. So pubkey→peer_id is assumed to be a FUNCTION. The
identity mint violated that assumption, and the damage lands in every peer's
trust store: one row per peer_id, all carrying the same pubkey. Measured on
the live store — 73 rows / 59 pubkeys, and only 4 rows with ANY endpoint. The
2,502-unconfirmed peer of #401 is the endpoint-less twin of a fully dialable
row with the same key. That is why sends went nowhere.

Two agents sharing a keypair ARE the same principal on the wire; no session
can tell them apart. Giving them separate peer_ids is a claim the transport
cannot uphold. Fresh key material is exactly what a fresh identity means.

FIX
- `agent_key_path(home, agent)` — the DEFAULT agent keeps `identity.key`
  verbatim, so every existing home loads unchanged; a named agent gets
  `identity-<agent>.key`.
- `generate_and_save_agent_with_existing_key` → `..._with_own_key`: generates
  its own keypair instead of borrowing the home's.
- `load_with_metadata` reads the agent's own key when present, else falls back
  to the shared legacy file — so an ALREADY-enrolled named agent keeps its
  pubkey and peers need no re-enrolment. New agents can never be phantoms.

TESTS — and note I changed two EXISTING assertions, deliberately.
`load_or_generate_as_records_agent_name` and
`load_or_generate_as_adds_named_agent_to_existing_scope` each asserted
`assert_eq!(secret_bytes, secret_bytes)` immediately below
`assert_ne!(peer_id, peer_id)`. They pinned the contradiction itself — distinct
peer_ids with identical key material — i.e. the defect was written down as a
requirement. Both are now `assert_ne!` with the reason inline.
New: `two_agents_in_one_home_are_cryptographically_distinct` — distinct
peer_ids AND distinct pubkeys, plus a reload proving an existing agent never
rotates its key. 12/12 airc-identity tests pass; `cargo check --workspace`
clean.

SCOPE — this stops NEW phantoms; it does not heal the 73 rows already in the
live store. That collapse (and whether a same-pubkey row may adopt a sibling's
endpoints) is the follow-up half of #402, tracked separately, because it
rewrites rows in a trust store and deserves its own review.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
@joelteply
joelteply merged commit 62797de into canary Aug 14, 2026
12 checks passed
@joelteply
joelteply deleted the fix/402-distinct-identity-distinct-key branch August 14, 2026 04:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant