This is a research prototype. It is not audited and not ready for real-world whistleblowing.
Published as a research artifact alongside the Applied Social Media Lab post Selfhood in the Shadows: Rethinking Whistleblowing Infrastructure with PSST.
Rendezvous is a privacy-preserving protocol and prototype for whistleblowing that ensures disclosures are only released when a safety threshold is met – meaning enough independent disclosures from the same organization have accumulated for the same recipient. It provides anonymity, security, and accountability through:
- IP-based organization verification
- Domain fronting to stay hidden
- Threshold-gated release via distributed crypto
- End-to-end encrypted to your recipient
- Whistleblower: Submits disclosures encrypted under recipient keys and verifiably split across servers.
- Recipient: Registered endpoint authorized to retrieve disclosures.
- Rendezvous Point: Independently operated server that holds encrypted disclosure shares and releases them when a threshold is met.
Two distinct threshold parameters are used in the protocol, named separately throughout this README:
- Release threshold (server-side): the minimum number of distinct disclosures that must accumulate for a
(recipient, organization)pair before any rendezvous point will serve shares to the recipient. Hardcoded to 3 in the demo; must agree across deployments. - Reconstruction threshold (client-side, Shamir): the minimum number of secret shares the recipient needs to reconstruct any single encrypted disclosure. Currently
⌈2N/3⌉where N is the number of rendezvous points (so 2-of-3 in the default deployment).
-
Whistleblower
- Discovers the available recipients by calling
GET /recipientson each rendezvous point and intersecting the responses (so a single dishonest server can't unilaterally inject a recipient). - Fetches a JWT credential from each rendezvous point via
GET /credential. - Encrypts their disclosure using ephemeral Curve25519 + AES-GCM derived from the recipient's public key.
- Splits the encrypted disclosure using Shamir Secret Sharing.
- For each share, computes a per-recipient commitment:
HMAC-SHA256(symmetricKey, disclosureID || share), wheresymmetricKeyis the AES-GCM key derived from the ephemeral X25519 agreement with the recipient. - Submits one share, its commitment, and the ephemeral key to each rendezvous point using
POST /disclose.
- Discovers the available recipients by calling
-
Recipient
- Registers their public key with rendezvous points via
POST /register. - Requests inbox authentication via
GET /inbox/:publicKey/challenge. - Receives a random challenge + server ephemeral key.
- Performs X25519 key agreement and returns the AES-GCM encrypted challenge.
- Retrieves shares once the threshold is met with
GET /inbox/:publicKey. - Verifies each share against its commitment using the ephemeral key and reconstructed shared secret, rejecting any unverifiable shares.
- Reconstructs and decrypts the disclosure.
- Optionally deletes the received share with
DELETE /inbox/:publicKey/:id.
- Registers their public key with rendezvous points via
The protocol's guarantees are stated relative to the parties and trust assumptions described here.
Parties and trust:
- Whistleblower: assumed honest, on an uncompromised device, with no long-term identity registered with the system.
- Recipient: assumed honest after registration, and trusted to safeguard their long-term private key.
- Rendezvous Points: mutually distrusting and independently operated. Confidentiality relies on at most
(reconstruction_threshold - 1)being malicious (one out of three at default settings). - Employer and other passive network observers: full visibility into the whistleblower's outbound traffic, but cannot break TLS, run a rendezvous point, or compromise the recipient's device.
What the protocol defends against:
- Network observation linking the whistleblower to the rendezvous service. With domain fronting enabled, outbound traffic is indistinguishable from ordinary CDN traffic.
- Less-than-threshold collusion among rendezvous points. Each holds a single Shamir share, insufficient on its own to recover plaintext.
- Linking two submissions to the same whistleblower. Each uses a fresh ephemeral keypair with no long-lived identifier.
- Forgery or tampering of shares by a malicious rendezvous point. Each share is bound to an HMAC commitment keyed by the recipient-derived ECDH secret, which the server cannot compute.
For threats outside this model, see Known Limitations & Future Directions.
Under the threat model above, the Rendezvous protocol provides:
- Ephemeral Submission Keys: Each disclosure is encrypted under a fresh whistleblower-side Curve25519 keypair. The whistleblower never persists a long-term secret, so there is no client-side long-term key whose later compromise could decrypt past submissions.
- Sender Deniability: Disclosures are encrypted under a symmetric key derived via ECDH between the whistleblower's ephemeral keypair and the recipient's long-term key, with no signature on the message itself. The recipient can decrypt but cannot cryptographically prove to a third party who authored a given disclosure.
- Anonymity from the Employer: Whistleblowers use only ephemeral keys and never register or reuse long-lived identifiers, and traffic is domain-fronted through a benign-looking CDN, so an employer monitoring its network sees only ordinary requests to that CDN rather than a rendezvous point. The whistleblower's IP is necessarily visible to each rendezvous point (each RP performs an RDAP lookup against it to derive the organization), but the recipient never sees the IP itself: only the resulting organization label, which is what gates the safety threshold.
- Threshold Privacy: No single server can reconstruct an encrypted disclosure or unilaterally determine an organization. Both disclosure recovery and IP-based organization verification require cooperation from a threshold of independent servers.
- Verifiable Shares: Each share is individually verifiable by the recipient using a keyed commitment, preventing forgery or tampering by malicious servers.
- End-to-end Encryption: Disclosures are encrypted directly to the recipient’s registered public key, and only the holder of the corresponding private key can decrypt them.
The demo defaults to running entirely against locally hosted servers.
From the server directory, in three separate terminals:
cd server
go run . -port 8080 -remote-ip-override 8.8.8.8
go run . -port 8081 -remote-ip-override 8.8.8.8
go run . -port 8082 -remote-ip-override 8.8.8.8-remote-ip-override is required when running on localhost because the server uses RDAP to look up the requestor's organization, and that lookup needs a real, public IP. Pick whatever IP corresponds to the "organization" you want the demo to attribute disclosures to (e.g. 8.8.8.8 for Google).
Open ios/Rendezvous.xcworkspace in Xcode and run on a simulator. By default it points at 127.0.0.1:8080–8082 with domain fronting disabled. Configure both via ios/Rendezvous/Config.swift:
static let rendezvousPointURLs: [URL] = [
URL(string: "https://your-rp1.example.com")!,
URL(string: "https://your-rp2.example.com")!,
URL(string: "https://your-rp3.example.com")!,
]
static let useDomainFronting = trueTo run on a real device against a Mac on the same LAN, replace 127.0.0.1 with your Mac's LAN IP. Domain fronting requires the rendezvous points to be deployed behind a CDN that accepts fronted traffic (e.g. Google Cloud Run reachable via Google-fronted SNI); leave it off for plain HTTPS.
The demo Swift client provides:
-
Whistleblower
- Automatic credential retrieval across all rendezvous points
- Encryption, secret sharing, and threshold submission of disclosures
- Domain-fronted HTTPS via Google's fronting infrastructure
-
Recipient
- Key generation and registration
- Challenge-response inbox access
- Decryption and automatic deletion of processed disclosures
The demo Go server implements the following API, with in memory storage.
Issues a JWT credential tied to the requestor’s IP and organization (via RDAP).
Response:
{
"organization": "Example Corp",
"credential": "<jwt-signed-token>"
}Authenticated with JWT credential.
Body:
{
"id": "UUID",
"recipient": "<base64 Curve25519 public key>",
"verifiableShare": {
"ephemeralKey": "<base64 Curve25519 ephemeral public key>",
"data": "<base64 secret share>",
"commitment": "<base64 HMAC-SHA256(symmetricKey, id || share)>"
}
}Registers a recipient to receive disclosures.
Body:
{
"name": "Legal Team",
"publicKey": "<base64 Curve25519 public key>"
}Returns a list of registered recipients.
Body:
[
{
"name": "Legal Team",
"publicKey": "<base64 Curve25519 public key>"
},
...
]Requests a challenge token for a given public key initiate inbox authentication.
Response:
{
"token": "<base64 random challenge>",
"publicKey": "<base64 server ephemeral key>",
"nonce": "<base64 server random nonce>",
}Authenticated with AES-GCM encryptedToken and nonce.
Returns shares for any organization where a threshold has been met.
Response:
[
{
"id": "UUID",
"org": "Harvard University",
"verifiableShare": {
"ephemeralKey": "<base64 Curve25519 ephemeral public key>",
"data": "<base64 secret share>",
"commitment": "<base64 HMAC-SHA256(symmetricKey, id || share)>"
}
}
]Authenticated with AES-GCM encryptedToken and nonce.
Deletes a disclosure share by id.
Beyond the broad "this is a research prototype" caveat at the top of this README, several specific gaps are worth calling out, each paired with a sketch of how it could be addressed in a follow-on design.
-
POST /registeris unauthenticated and last-write-wins. Anyone who can reach a rendezvous point can register an arbitrary(name, publicKey)pair (including squatting on a display name with a public key whose private half they don't control), and a subsequent registration silently overwrites a prior one.Future direction: Require the registrant to produce a signature over
(name, publicKey), validated by the server on register and re-validated by the client when fetching the recipient list. Because the recipient's current key is a Curve25519 X25519 key (used for ECDH; X25519 keys cannot sign), this means either introducing a second Ed25519 identity key per recipient, or switching the recipient's primary identity to Ed25519 (and converting / pairing it with an X25519 key for ECDH). -
Recipient long-term key compromise decrypts past disclosures. A captured ciphertext together with the recipient's long-term private key are sufficient to recover the plaintext, since per-disclosure ECDH is computed against the recipient's long-term public key (not an ephemeral prekey).
Future direction: Adopt an X3DH-style prekey handshake (à la Signal). Recipients publish a pool of one-time prekeys signed by their long-term identity key, the whistleblower picks one and uses it for ECDH, and the recipient deletes the corresponding prekey after first use, making past ciphertexts unrecoverable even given a later compromise of the recipient's identity key.
-
Recipient discovery trusts non-collusion of rendezvous points. The whistleblower client identifies recipients by intersecting
/recipientsacross rendezvous points and matching on public-key bytes. End-to-end encryption only holds against an adversary who hasn't compromised every rendezvous point. A fully colluding set could substitute an attacker's public key under any name. The intersection also matches only onpublicKey, not on the full(name, publicKey)tuple, so a single dishonest server can change the displayed name attached to a key without dropping out of the intersection.Future direction: Combined with authenticated registration (above), the client can verify each registration's signature itself rather than relying on the rendezvous points to honestly relay the registry. The intersection should additionally match on the full
(name, publicKey, signature)tuple so any server-side mutation drops the entry from the common set. -
Release threshold counts disclosures, not distinct submitters. The release threshold gates on N disclosures from the same
(recipient, organization)rather than on N distinct submitters. A single whistleblower with one org credential could in principle submit multiple disclosures with distinct IDs and trigger release on their own.Future direction: Use anonymous credentials that prove "I am one person from organization X" while remaining unlinkable across uses (e.g. group signatures, BBS+, anonymous tokens), paired with a double-spend / linkability check at the rendezvous points to prevent the same submitter from inflating the count.
-
In-memory server state. Restart of any rendezvous point wipes its registered recipients, all pending disclosure shares, and rotates its JWT signing key, so every credential it has previously issued silently becomes invalid. There is no persistence layer.
Future direction: Add a persistent store for the recipient registry, share state, and JWT signing key. Persisting the disclosure store also enables an explicit retention policy (e.g. discarding shares after some TTL when the threshold is never reached).
-
No rate limiting or per-key caps. The challenge map (
/inbox/.../challenge), recipient registry, and disclosure store all grow without bound; trivial to exhaust memory.Future direction: Bound each map: TTL on challenges, per-recipient and global caps on registry entries, and per-
(recipient, org)caps on pending disclosures. Add per-IP rate limits to/credentialand/disclose.
This project is released under the MIT License.
Maintained by Nora Trapp and the Applied Social Media Lab at the Berkman Klein Center.
