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 #28Privacy

[NandaHack] privacy: hybrid_x25519 — real hybrid encryption, selective disclosure & broadcast revocation

Persona: applied-cryptography / security engineer. The code is

Author

Status
Merged
Merged on
Jul 1
Branch
hackathon/risingtell-privacy-hybrid

Description

The pitch.

## Problem 09 — Privacy: hybrid encryption with selective disclosure and broadcast revocation

**Persona:** applied-cryptography / security engineer. The code is
threat-model-first, adversarial-test-driven, and *honest about what it does not
provide* (see the forward-secrecy disclosure below) — that posture is meant to
be legible in the diff itself, not just this description.

### Motivation

The default privacy plugin (`noop`) is 60 lines of passthrough: `encrypt` returns
its input, `verify_proof` is an unconditional `True`. Every Nanda Town scenario
that simulates a sensitive workflow therefore leaks everything to every observer,
and the trace can't even tell you it would have leaked in production. This PR
ships a real privacy layer so any scenario can opt into a believable
confidentiality story by changing one line (`privacy: hybrid_x25519`).

### What this ships

- **`privacy/hybrid_x25519.py`** — HPKE-shaped hybrid encryption: one
  ChaCha20-Poly1305 content-key encryption of the payload, wrapped once per
  recipient via X25519 ephemeral-static ECDH + HKDF-SHA256 (the broadcast/
  group-key shape — N cheap key-wraps, not N re-encryptions). Plus salted-Merkle
  **selective disclosure** (`prove`/`verify_proof`) and **epoch-based broadcast
  revocation** (`revoke`).
- **`validators/privacy_validators.py`** — four adversarial checks (eavesdropper,
  replay, field-injection, stale-revocation) on the `Privacy` protocol surface.
  Each **passes against `hybrid_x25519` and fails against `noop`** — the charter's
  bar for "adversarial."
- **32 tests** across unit, adversarial-discrimination, **Hypothesis property**,
  and full-**ScenarioRunner** integration.
- **`scenarios/sealed_bid_with_privacy.yaml`** wiring the layer into a real run.
- Doc update at `docs/layers/privacy.md`.

### Design & the four attacks

| Attack | Defense |
|---|---|
| **Eavesdropper** | A non-audience agent holds no wrap entry keyed to its pubkey → `decrypt` raises `NotInAudienceError`; the plaintext never appears in the envelope bytes. |
| **Replay** | A unique `msg_id` (`sender:epoch:seq`) is bound into the AEAD AAD; a re-presented envelope raises `ReplayError`; a redirected one fails to authenticate. |
| **Field-injection** | Selective-disclosure leaves are salted, length-prefixed Merkle commitments. Tampering any revealed value/salt/path node changes the reconstructed root ≠ the issuer-anchored root → `verify_proof` returns `False`. |
| **Stale-revocation** | A member revoked at epoch *E* is excluded from the wrap set of every message at epoch ≥ *E* → `decrypt` raises `NotInAudienceError`. |

The sender, epoch, msg_id and the **sorted recipient key-ids** are all bound into
the AEAD associated data, so audience-set edits and redirection break
authentication, not just payload edits.

### Two non-obvious decisions

1. **Deterministic mode for Tier-1 traces.** Hybrid encryption is randomized
   (fresh ephemeral + nonces), which would make traces non-reproducible.
   `deterministic=True` derives the ephemeral scalar and every nonce from
   `HKDF(seed, msg_id)`, so identical inputs yield byte-identical envelopes — and
   this is sound *only because* `msg_id` is unique per `(sender, epoch)` via a
   monotonic counter (the `(key, nonce)`-reuse guard). The scenario determinism
   test asserts byte-identical traces across replays.
2. **Honest forward-secrecy disclosure.** Revocation is **future-only**: it
   guarantees a revoked member can't read messages issued at/after their epoch,
   but makes **no** claim about earlier messages they already received. True
   forward secrecy would need per-epoch content re-keying with key deletion,
   which would change the broadcast cost model — so I document the boundary
   rather than imply a stronger property. The module docstring states this
   explicitly.

### Selective disclosure

A credential is committed as a salted Merkle root (issued by an authority, carried
in `Statement.public_inputs['root']`). The holder reveals

…

Try it

Open PR on GitHubView diff

Checkout locally

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