# Security policy Parley's reason to exist is that its guarantees are *checkable*, so we take reports about broken guarantees as seriously as classic vulnerabilities. ## Reporting a vulnerability **Please do not open a public issue for a security problem.** Use GitHub's private [Report a vulnerability](https://github.com/5uper0/parley/security/advisories/new) flow, or email **oleh@veheria.tech**. We aim to acknowledge within 72 hours. Things we especially want to hear about: - A way to make a red-line-violating option **win** (a betrayal that the engine accepts). - A way for an untrusted coordinator to **forge or replay** a signed verdict past `verify_transcript()`. - A way to **extract a private preference sheet** through the net layer (the extraction attack the red-team tests are meant to close, see `tests/test_redteam.py`). - Any way to make `verify_non_betrayal` return a false ✓. ## Scope & maturity This is a **v0**. The net layer is hardened against the demonstrated attacks (Ed25519-signed verdicts, bearer auth, rate limiting, input validation). Known limits we already disclose (so a report on one of these is a confirmation, not a surprise, but a *working exploit or a sharper framing* is still welcome): - **Signatures are tamper-evidence, not authenticity.** `verify_transcript` checks each signature against the pubkey carried *in the same record*; there is no trusted `owner → key` roster yet, so a coordinator that assembles the transcript could sign a fabricated verdict with its own key. The check exists but is opt-in: `verify_transcript(roster={owner: pubkey})`, `verify_acceptance(roster=)` and `scripts/verify-receipt.py --roster file.json` reject any key other than the one each owner pinned at join, out of band from the host (a roster read from the receipt binds nothing); a roster also requires every verdict and acceptance to be signed. Without a roster, one key standing for two owners, or one owner using two keys, fails; nothing more. One client-side piece is in: when a bot's `/card` advertises a key, `RemoteAgent` refuses any `/consider` reply that lacks a signature or carries a different key. The client checks that a signature is present under the card's key; whether the signature is valid is checked by `verify_transcript(require_signed=True)`, which `examples/run_env.py` now runs and any other caller must run. Together they stop a party between an honest coordinator and a bot from slipping in its own verdicts after discovery. They do not help a third party against a dishonest coordinator, and without TLS a party present at discovery can serve its own card. - **Signed acceptances have the same gap.** `verify_acceptance` checks the signature against the pubkey carried *in the same acceptance*, so anyone with a key can sign an acceptance under another owner's name. Unsigned acceptances authenticate nothing: `agreement()` without a `verifier` is tamper-evidence only. Closed by the same v0.2 roster pin. - **Replay binding is opt-in.** `RemoteAgent.bind(session)` makes the bot sign `verdict/0.2` with the room's session id and a fresh nonce per option; `verify_transcript(expected_session=...)` then rejects a verdict from another run, a repeated nonce, or a stripped binding. A caller that does not bind, or does not pass `expected_session`, still gets the old unbound behaviour: a genuine signed verdict is replayable into another parley that reuses the same option. Session id and nonce are chosen by the coordinator, so this is only meaningful together with the owner-key roster below. - **Strategic reporting of soft scores is not prevented.** Red lines are enforced in code, but the soft score each party reports is self-declared. A party that misreports it can tilt the pick among options that are acceptable to everyone, at the other parties' expense; in our own simulation (2026-10-25) a uninformed liar gained in roughly a quarter to a third of random decisions and an informed liar in more. Do not read the outcome as strategy-proof. Mitigations under consideration: ordinal-only scores and a sealed commit-reveal verdict round. - **Scores of accepted options are unmasked.** The soft cardinal `score` of every option a party accepts is public, leaking preference ordering among those options. The `acceptable` flags still leak the feasible region, and enough probing options pin a numeric red line. A rejected option reports score 0, not its real score (after 0.2.0, see CHANGELOG). Range-masking via MPC is future work. - **No TLS/mTLS**, and no game-theoretic **collusion / strategic-misreport resistance** (research track). Do not deploy v0 against genuinely adversarial principals in production. Reports that deepen the collusion-resistance story, or that break a guarantee we *didn't* list above, are the ones we most want, the first three items are on the v0.2 roadmap. Outcome verification closed in v0.2: `verify_outcome(transcript)` recomputes the max-min winner from the recorded verdicts and checks it matches the announced decision. On its own it checks the owner set only against the record itself, so a coordinator that drops one owner from every entry still passes; pass `expected_owners=` and every entry must carry exactly that owner set. `scripts/verify-receipt.py` passes the receipt's `participants`, which the coordinator also wrote, so that binding holds only if the holder checks that list against who actually took part. ## Supported versions Until a `1.0` tag, only the `main` branch is supported. Fixes land on `main`.