in reviewhumanPR #172Trust
trust-security-engineer: attested_peering operator delegation is not key-bound — trusted-operator impersonation
The attested-peering trust plugin gates reputation on a "who do you work for?"
Author
@theCodeForgerHQ
github profile →- Status
- In review
- Opened on
- Jul 10
- Branch
- hackathon/theCodeForgerHQ-attested-peering-delegation-binding
Description
The pitch.
# [Hackathon] trust-security-engineer: attested_peering operator delegation is not key-bound — trusted-operator impersonation
## TL;DR
The attested-peering trust plugin gates reputation on a "who do you work for?"
operator delegation. But the bytes the operator signed — `_delegation_msg` —
covered only `operator_id | agent_id | label`, **not the delegated agent's
public key**. A delegation was therefore a bearer token for a *string* id, not
a *keypair*. An attacker copies a genuine operator delegation off an honest
card, keeps the victim's `agent_id`, swaps in **its own** key, self-signs, and
is admitted **as the victim** with a trusted operator's authority — identity
impersonation that turns straight into reputation poisoning. The fix binds the
agent public key into the delegation message (v2), so a delegation authorises
exactly one keypair and is non-transferable. This PR ships the fix plus an
adversarial validator, a scenario attack role, and RED→GREEN + property +
unit tests.
## Persona: trust-security engineer
I read an operator delegation as a **capability bound to a principal's key**,
not a label. The moment a signed grant names only `agent_id` (a string an
attacker fully controls on its own card) and omits the key, the grant becomes
transferable: possession of the *ciphertext* confers the *authority*. The
attacker never breaks Ed25519, never steals a private key, never forges a
signature — it **replays a valid one onto a different subject**. That is the
classic key-substitution / unknown-key-share failure mode, and it is exactly
what a delegation must not permit.
## The vulnerability
The plugin's own docstrings advertised the safety property it did not have:
- Module docstring: *"a named operator delegated authority to **this agent** …
via a signed delegation embedded in its passport."* — but the signature did
not bind "this agent" to a key.
- Constructor comment (injection path): *"a peer that claims a delegation it was
never granted produces a self-consistent card with an **invalid
delegation_signature**."* — false under v1: a *copied* (not forged) signature
verified fine, because the signed bytes never mentioned the agent's key.
**Attack.** Given any honest card `H` delegated by trusted operator `Op`:
1. Take `H.operator_id`, `H.operator_public_key`, `H.delegation_signature`
verbatim (all public, on the wire in every hail).
2. Build a card that keeps `agent_id = "honest-0"` and `label`, but sets
`public_key` = the **attacker's** key.
3. Self-sign the body with the attacker's key (valid — it is the card's key).
4. Complete the handshake signing the live transcript with the attacker's key.
Under v1: key possession passes (card key == attacker key, attacker signs the
transcript), the self-signature is valid, the copied delegation verifies
(`Op` did sign `delegation-v1|Op|honest-0|label`), and `Op` is on the roster.
Verdict **ALLOW** — the attacker is admitted **as `honest-0`**, and every
`report()` it files counts under the victim's identity.
## Root cause
`packages/nest-plugins-reference/nest_plugins_reference/trust/attested_peering.py`
```python
# v1 (vulnerable): signs only the id triple — transferable to ANY key
return DELEGATION_TAG + f"|{operator_id}|{agent_id}|{label}".encode()
```
`_verify_card` recomputed the same key-free message, so a copied signature over
a *different* card's key still verified. The delegation authorised a name, not a
keypair.
## The fix
Bind the agent public key into the signed delegation (v2), and pass it at both
call sites — `_verify_card` passes `card.public_key`, the sign path passes
`self._pub`. Tag bumped `nest-attest-delegation-v1` → `-v2` so a v1 signature
can never be reinterpreted as v2.
```python
DELEGATION_TAG = b"nest-attest-delegation-v2"
def _delegation_msg(operator_id, agent_id, label, agent_public_key) -> bytes:
return (
DELEGATION_TAG
+ f"|{operator_id}|{agent_id}|{label}|".encode()
+ _b64(agent_public_key).enc
…Try it
Open PR on GitHubView diffCheckout locally
git fetch origin pull/172/head:pr-172
git checkout pr-172