SECURITY.md
Rama main · réplica de lectura del repositorio original.
# 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=<the roster you expect>` 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`.