in reviewhumanPR #289Transport
[Fellowship Challenge] Corrupt fault: prove signed message bodies survive tampering in transit
Submission for the Hexaware x Project NANDA Fellowship Technical Challenge
Author
@MohitGoyal09
github profile →- Status
- In review
- Opened on
- Sep 30
- Branch
- challenge/MohitGoyal09-corrupt-fault
Description
The pitch.
Submission for the Hexaware x Project NANDA Fellowship Technical Challenge
**Track:** Lab · **Layer:** transport (`memory.v1` fault handling) · `auth` is used as-is, not changed.
## The gap
Every Lab message body is signed by its sender (`sim/api.py:44`) and checked on delivery; a mismatch is refused as `delivery_failed {reason: "bad signature"}` (`sim/engine.py:68`). But the only faults the town can inject are `drop`, `duplicate`, `delay`, and `drop_rate`. **Nothing can change a message after it is signed**, so no run and no test has ever reached that rejection path (`grep -rln "bad signature" tests src` finds only `engine.py`). The town's message-integrity check existed but was never exercised.
## The fault
A new `FaultRule` action, `corrupt`, rewrites one body field after signing and keeps the original signature (the attacker cannot re-sign):
```yaml
faults:
- {action: corrupt, kind: commit, nth: 2, field: value, value: v666}
```
- The transport delivers a **new** envelope; the original is never changed in place, so `message_sent` and the intent record keep the true body.
- It emits `message_corrupted {to, kind, field}` (the field name only, never the value).
- It is deterministic: it uses the existing `nth` counter, not the RNG.
- `corrupt` without `field` is refused at load (fails closed, like #255). A no-op rewrite (the value is already equal) is skipped, so the result is an honest `not_enough_evidence`, never a misleading failure.
## The invariant
A new scenario, `consensus_corrupt`, rewrites the 2nd commit message (to acceptor-2) from `v42` to `v666`. The new `consensus_corrupt` validator adds three stages to the existing `quorum_commit` and `agreement`:
| Stage | Passes when |
|---|---|
| `corruption_injected` | a `message_corrupted` event exists (otherwise `not_enough_evidence`) |
| `corruption_rejected` | every corrupted message id has a `delivery_failed` with `reason: bad signature` |
| `corruption_recovered` | the proposer retried the commit (`commit_retry`) and re-sent it to the same acceptor |
Recovery needs a commit acknowledgement, which the consensus roles did not have. It is added behind `confirm_commit` (default off), with a bounded re-send (`max_commit_retries`, default 3). The existing prepare retry and the new commit retry share one helper. The existing `consensus` scenario is unchanged, and a test pins its events byte for byte against `main`.
## The negative control
`consensus_corrupt_weak_auth` is the same run with `layers: {auth: plain.v1}`, the same pattern as `capability_spoofing_weak_auth`. The rewritten commit is accepted, **acceptor-2 commits `v666` while acceptors 1 and 3 commit `v42`**, and the run fails on exactly the stages predicted before it was run:
| Stage | `consensus_corrupt` (hmac.v1) | `consensus_corrupt_weak_auth` (plain.v1) |
|---|---|---|
| quorum_commit | passed | passed |
| agreement | passed | **failed** (consensus split) |
| corruption_injected | passed | passed |
| corruption_rejected | passed | **failed** (no signature rejection) |
| corruption_recovered | passed | **failed** (acceptor-2 acked the fake commit, so no retry) |
| ledger_conserved | passed | passed |
| privacy | not_tested (no `redact_fields`) | not_tested |
| **verdict** | **passed** | **failed** |
## Bundle fingerprints
Both bundles pass `nandatown verify` ("hashes match and the evaluator reproduces the recorded result"):
- `consensus_corrupt`: `sha256:ad99f5d4af9b46b413854a0b87678431f1c9073550dfc01d6be45ff5dabe4adc`
- `consensus_corrupt_weak_auth`: `sha256:f77397ea2c78dc7df5f544781cb845e6c9f655e80a7ee99097b358c12298f4fb`
The Lab run id is random, so a fresh run gets a fresh fingerprint with the same stage results.
Reproduce:
```bash
nandatown run consensus_corrupt --out runs/demo
nandatown replay runs/demo/<id> --kind delivery_failed
nandatown run consensus_corrupt_weak_auth --out runs/demo
nandatown replay runs/demo/<id> --kind value_committed
nandatown verify runs/demo/<id>
```
## Tests
…Try it
Open PR on GitHubView diffCheckout locally
git fetch origin pull/289/head:pr-289
git checkout pr-289