# ADR-005: Relay access control + anti-abuse (shared capability + proof-of-work, not per-identity signatures)

## Status

Accepted

## Date

2026-05-31

## Context

The oblivious store-and-forward relay (`pvtcoms_core::relay`, deployed per `deploy/`) must keep
**strangers out** of a private/family deployment (the user's "without other people having access"
requirement) and **resist flooding**, while preserving its single most important property:
**obliviousness** — the relay must not learn who is talking to whom, nor link tokens to identities.

The obvious way to gate access is to make each request carry an **Ed25519 signature from the sending
identity**, checked against an allowlist of identity public keys. This is a trap: it tells the relay
*which identity* deposited or pulled under *which token*, directly linking identities ↔ tokens ↔
traffic timing. That destroys obliviousness — the relay becomes a metadata oracle. A per-identity
allowlist and an oblivious relay are fundamentally in tension.

We also need anti-abuse that does **not** require identifying the caller (so it composes with
obliviousness and with an eventual public/open mode).

## Decision

Two independent, identity-free gates on every request:

1. **Access capability (gated mode).** A single **shared bearer secret** (`access_key`, 32 bytes) is
   distributed to the invited circle (out-of-band / baked into the build). Each request carries
   `HMAC(access_key, ctx ‖ op ‖ token ‖ timestamp ‖ salt ‖ pow_nonce ‖ blob_hash)`, verified in
   constant time. All invited users present the **same** capability, so the relay **cannot
   distinguish or identify them** — obliviousness is preserved while strangers (who lack the key) are
   rejected. `Open` mode skips this gate entirely (flip via config / unset `PVTCOMS_RELAY_KEY`).

2. **Proof of work.** A hashcash stamp (`pvtcoms_core::pow`, SHA-256, leading-zero-bit difficulty)
   **bound to the request** (op, token, timestamp, salt, payload). Flooding costs CPU; verification is
   one hash. Identity-free, so it works in both `Open` and `Gated` modes.

Supporting rules: **freshness** (timestamp within a window), **replay suppression** (per-request
fingerprint remembered for the window), a random **per-request salt** bound into the PoW + capability
(so a legitimate same-second re-pull is not a false replay, while a verbatim or salt-mutated replay is
still caught), and **TTL** expiry of undelivered mail.

## Consequences

**Positive**
- Obliviousness preserved: the relay sees opaque tokens + ciphertext + a shared capability it cannot
  attribute to an individual. Seizing it yields nothing linkable.
- Strangers are kept out (gated) without accounts, identities, or IP logging.
- Anti-abuse (PoW) is identity-free and tunable; composes with a future public/open relay.
- One mechanism, config-flippable between private (gated) and public (open) — no rewrite.

**Negative / trade-offs**
- A shared bearer secret is **not individually revocable**: removing one user means **rotating the
  key and redistributing** to the rest. Acceptable for a family/private circle; weak for large/public.
- A shared secret can leak (one careless member exposes access for all until rotation). PoW still caps
  the blast radius of a leaked key.
- PoW taxes legitimate senders (kept modest, ~18–22 bits) and is not a hard DoS bound.

## Alternatives considered

- **Per-identity signature + allowlist** — rejected: deanonymises (links identity ↔ token), the exact
  thing the relay must not learn.
- **Per-user random access tokens** — individually revocable, but each token is a stable pseudonym the
  relay can use to link a user's activity over time (partial obliviousness loss). Reconsider if
  per-user revocation becomes a hard requirement.
- **Open + PoW only** — simplest, fully oblivious, but no "invited only" gate (anyone may use it).
  Retained as the `Open` mode for a future public deployment.

## Future work

Per-user **revocable *and* unlinkable** access needs **anonymous credentials / blind tokens**
(Privacy-Pass-style VOPRF, or a blind-signature capability). That is a new primitive — it will get its
own ADR (and must use an audited implementation, per `coding-standards.md`: never roll our own crypto).
