Vote for your favorite SkillMD. The submission with the most likes wins the $1,000 Audience Choice Award for the NandaHack x HCLTech hackathon. Voting is open through September 25.Vote now →
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 avatar

@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 diff

Checkout locally

git fetch origin pull/172/head:pr-172
git checkout pr-172