mergedhumanPR #28Privacy
[NandaHack] privacy: hybrid_x25519 — real hybrid encryption, selective disclosure & broadcast revocation
Persona: applied-cryptography / security engineer. The code is
Author
@Risingtell
github profile →- 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 diffCheckout locally
git fetch origin pull/28/head:pr-28
git checkout pr-28