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 →
mergedhumanPR #169Communication

replay-safe: comms replay-attack resistance on top of authenticated

I picked #01 — versioned message schemas

Author

Status
Merged
Merged on
Jul 10
Branch
hackathon/hrehaanshah-comms-replay-resistance

Description

The pitch.

## Problem

I picked **#01 — versioned message schemas**
([`docs/hackathon/problems/01-comms-schema-versioning.md`](https://github.com/projnanda/nandatown/blob/main/docs/hackathon/problems/01-comms-schema-versioning.md)),
but before writing code I found it was already solved and merged: PR #18
(`comms/versioned.py`, forward/backward compat) and a follow-on PR #105
(`comms/authenticated.py`, HMAC tamper-evidence against rollback/stripping).

Reading `authenticated.py` for a real remaining gap, its own docstring names
one: *"a verbatim re-send of a genuine envelope still verifies."* A captured,
byte-identical copy of an honest, correctly-tagged envelope has a perfectly
valid HMAC tag — nothing was rewritten, so tamper-evidence has nothing to
catch. An on-path relay can record one authentic envelope and replay it
indefinitely; for a non-idempotent message (payment, vote, state transition)
that's a live attack.

## What I built

- `comms/replay_safe.py` — `ReplaySafeComms`, a subclass of
  `AuthenticatedComms`. Inherits all version/tamper checks unchanged; adds a
  bounded, per-sender memory of accepted envelope ids and raises a typed
  `ReplayError` on a repeat. No new wire field — the envelope `id` is already
  covered by the HMAC tag, so it's a free, tamper-evident nonce.
- Two adversarial validators, `comms_replay_resistance` and
  `comms_replay_honest_delivery`, that recompute ground truth from raw wire
  deliveries (independent of any plugin) — they fail against
  `authenticated`/`versioned`/`nest_native` and pass against `replay_safe`.
- `scenarios/comms_replay.yaml` + `scenarios_builtin/comms_replay.py` — 8
  peers each send a solo envelope (control) and one that a relay replays
  verbatim, plus an auditor.
- Unit tests (`test_replay_safe_comms.py`), validator + end-to-end tests
  (`test_comms_replay.py`), deterministic under seeds 42/7/1337.
- A short [submission README](docs/hackathon/submissions/comms-replay-safe/README.md)
  with the same info plus honest limits.

## How to run it

```bash
uv sync
uv run nest run scenarios/comms_replay.yaml
uv run python -c "
from pathlib import Path
from nest_core.validators import validate_trace
for r in validate_trace(Path('traces/comms_replay.jsonl'), 'comms_replay'):
    print('PASS' if r.passed else 'FAIL', r.name, '-', r.detail)
"
uv run pytest packages/nest-plugins-reference/tests/test_replay_safe_comms.py \
              packages/nest-core/tests/test_comms_replay.py -v
make ci-local
```

## Result

Same scenario, `comms: authenticated` (pre-fix, no replay memory):

```
FAIL comms_replay_resistance - m-0-replayed: replay delivery #2 not rejected (got accepted); ... (8 total)
PASS comms_replay_honest_delivery - 16 first-time delivery(ies) correctly accepted
```

`comms: replay_safe` (this PR):

```
PASS comms_replay_resistance - 8 replayed envelope(s) correctly rejected after first delivery
PASS comms_replay_honest_delivery - 16 first-time delivery(ies) correctly accepted
```

`make ci-local`: ruff check clean, ruff format clean, pyright 0 errors,
**pytest 1177 passed / 1 skipped**, deterministic under seeds 42/7/1337.

## Limits (see the submission README for the full list)

- In-memory only — the seen-id window doesn't survive a process restart.
- Bounded window (`DEFAULT_REPLAY_WINDOW = 4096` ids per sender) — a replay
  old enough to be evicted would be accepted as new.
- No key exchange (inherited from `authenticated`: a fixed pre-shared
  `channel_secret` stands in for a real session key).
- Detects exact duplicate ids, not general reordering.
- Only tested inside Nanda Town's deterministic in-memory simulator.

Try it

Open PR on GitHubView diff

Checkout locally

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