| Version |
|---|
| 0.4 |
Note
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in RFC 2119.
Note
This protocol is under active development.
SecureDrop Protocol is a first-contact messaging protocol between anonymous users (sources) and non-anonymous users (journalists) with a shared affiliation (newsroom). The design is largely motivated by the requirement that sources avoid local persistent state, for plausible deniability.
This specification describes:
- Each party (source, journalist, newsroom, FPF) and their setup
- Message encryption, retrieval, and decryption
- What type of security and confidentiality properties are provided
- What encryption algorithms and parameters are used
Throughout, the terms source, journalist and newsroom are used. These may be understood to mean: the anonymous users who initiate conversations (sources); the non-anonymous users who receive and reply to messages (journalists); and the public organization that manages the system and authorizes allowed journalists (newsroom).
The terms sender and recipient are also used, more abstractly, to refer to a user's role at a given point in the protocol's execution: when a source writes to a journalist, they are a sender, and when they receive a reply, they are a recipient, and vice-versa.
The protocol has:
- A limited, stateless, unauthenticated public API (
requestKeys,sendMessage,requestMessages,getMessage) used by all parties for message sending and retrieval - A limited, authenticated administrator API, used by newsrooms to enroll and unenroll journalists
- A limited, authenticated journalist API, used to replenish their message encryption keys
One of the system's goals is to consider real-world deployment scenarios and their risks. The choice of an unauthenticated API avoids a server-side "users" database.
- Prioritize the safety/anonymity of the source
- Do not require sources to use any specific software or download any applications to communicate; they should be able to use Tor Browser, visit a URL like
newsorg.securedrop.tor.onion, and begin messaging - Be maintainable: Use well-known encryption primitives and existing cryptography libraries
- Be readily deployable: Use a single-server design, and consider realistic threat models with respect to cloud deployments.
For further context, see Berra et al. (2026), "The SecureDrop Protocol: End-to-End Encrypted Whistleblowing for All" and our research page.
This diagram provides a high-level visual depiction of SecureDrop Protocol.
sequenceDiagram
actor Source
box News Organization
participant Server as Newsroom<br>(Server)
actor Journalist
participant Newsroom as Newsroom<br>(Signing)
end
participant FPF
activate FPF
Note over FPF: 1. FPF signing setup
activate Newsroom
Note over Newsroom, FPF: 2. Newsroom signing setup
deactivate FPF
activate Server
loop Each of m journalists
activate Journalist
Note over Journalist, Newsroom: 3.1. (offline) Journalist initial key setup
Note over Server, Journalist: 3.1. Journalist enrollment
deactivate Newsroom
Note over Journalist, Server: 3.2. Setup and periodic replenishment<br>of n signed key bundles
end
activate Source
Note over Source: 4. Source key setup
alt Source → Journalist
Note over Source, Server: 5. Sender fetches keys for m journalists<br>and verifies their authenticity
Note over Source, Server: 6. Sender submits a message<br>(m copies)
Note over Server, Journalist: 7. Receiver fetches and decrypts messages
else Journalist → Source
Note over Journalist, Server: 5. Sender fetches keys for m journalists<br>and verifies their authenticity
Note over Server, Journalist: 6. Sender submits a message<br>(m copies, reply case)
Note over Source, Server: 7. Receiver fetches and decrypts messages
end
deactivate Source
deactivate Journalist
deactivate Server
Protocol participants (sources, journalists) have separate keys for message encryption, metadata encryption and message fetching. Journalists also have signing keys and long-term message authentication keys, and generate a pool of usable message/metadata keys. The newsroom and FPF have signing keys to confer trust on journalists and newsrooms respectively.
Throughout this document, keys are notated as
-
$component \in \{sk, pk, vk\}$ for private ($sk$ ) or public ($pk$ or$vk$ ) components -
$owner \in \{FPF, NR, J, S\}$ for FPF, newsroom$NR$ , journalist$J$ , or source$S$ -
$scheme \in \{fetch, sig, APKE, PKE\}$ for:-
$fetch$ for message-fetching keys -
$sig$ for Ed25519 signatures -
$APKE = \text{SD-APKE}$ ($APKE_E$ if one-time) for message encryption keys -
$PKE = \text{SD-PKE}$ ($PKE_E$ if one-time) for metadata encryption keys
-
FPF keys:
| Private Key | Public Key | Purpose | Lifetime | Algorithm | Signed by | Bundled in |
|---|---|---|---|---|---|---|
| Signing | Long-term | Ed25519 |
Newsroom keys:
| Private Key | Public Key | Purpose | Lifetime | Algorithm | Signed by | Bundled in |
|---|---|---|---|---|---|---|
| Signing | Long-term | Ed25519 | Welcome bundle |
Journalist keys:
| Private Key | Public Key | Purpose | Lifetime | Algorithm | Signed by | Bundled in |
|---|---|---|---|---|---|---|
| Signing | Long-term | Ed25519 | Roster | |||
| Fetching | TBD1 | ristretto255 | Roster | |||
| Message (out) | Long-term | DHKEM(X25519, HKDF-SHA256) + ML-KEM-768 2 | Roster | |||
| Message (in) | One-time | DHKEM(X25519, HKDF-SHA256) + ML-KEM-768 2 | Signed key bundle | |||
| Metadata (in) | One-time | X-Wing(X25519, ML-KEM-768) | Signed key bundle |
Source keys:
| Private Key | Public Key | Purpose | Lifetime | Algorithm | Signed by | Bundled in |
|---|---|---|---|---|---|---|
| Fetching | Permanent3 | ristretto255 | ||||
| Message | Permanent3 | DHKEM(X25519, HKDF-SHA256) + ML-KEM-768 2 | Key bundle | |||
| Metadata | Permanent3 | X-Wing(X25519, ML-KEM-768) | Key bundle |
FPF (Freedom of the Press Foundation) serves as the root of trust for the SecureDrop ecosystem.4 FPF generates a long-term signing keypair whose verification key is pinned into client and server software. This key is used to sign newsroom verification keys, establishing a chain of trust: FPF signs newsrooms, and newsrooms sign journalists.
| FPF |
|---|
The server, the journalist client, and the source client SHOULD be built with
FPF's verification key
Each newsroom that operates a SecureDrop instance generates its own signing
keypair. The newsroom sends its verification key to FPF, which manually verifies
it (out of band) and signs it.4 The resulting signature
Given:
| FPF | |
|---|---|
| Holds | |
Then:
| Newsroom | FPF | |
|---|---|---|
| Verify manually | ||
The server MUST be deployed with the newsroom's verification key
Each journalist generates three long-term keypairs: a signing keypair, an SD-APKE keypair for message encryption (outgoing), and a fetching keypair. The journalist signs their SD-APKE and fetching public keys with their new signing key and sends the public keys to the newsroom along with the signature and verification key.
The newsroom manually verifies the journalist's verification key (out of band),
then signs it with the newsroom signing key to produce
Given:
| Newsroom | |
|---|---|
| Holds | |
Then:
| Journalist | Newsroom | |
|---|---|---|
|
|
||
| Verify |
||
| If |
Following enrollment, each journalist
The server verifies the signature, and stores the signed key bundle. These keys are used by other participants to address messages to the journalist.
For each key bundle
| Journalist | Server | |
|---|---|---|
| If |
A journalist rotates their long-term keys by re-executing the initial enrollment. After the newsroom manually verifies the journalist's new verification key and the signature over the new long-term keys, the newsroom MUST update the journalist's keys in the roster. This occurs after a scheduled rotation, lost or destroyed keys, or a key compromise.
To begin each session, a source MUST enter (on their first visit) or reenter
(on a subsequent visit) a
The 16-byte BIP39 entropy is used directly as the master key securedrop-source-v1 as
the salt parameter, the per-key label below as the info parameter, and an
output length of
- 256 bits for
$sk_S^{APKE}(\text{DH})$ and$sk_S^{PKE}$ - 512 bits for
$sk_S^{fetch}$ and$sk_S^{APKE}(\text{ML-KEM})$
All source keys are long-term and fully determined by the passphrase.
| Source |
|---|
The BIP39 wordlist is static; a source's mnemonic remains valid indefinitely.
BIP39 also defines wordlists for nine other languages; implementations MAY
support any. Note that
Note that info label.
As with the journalist,
The sending party begins the messaging protocol by fetching and verifying journalist keys. They then compose a message that is encrypted individually to each journalist, separately encrypt information required for message decryption ("metadata") and message delivery, and upload these to the server.
The protocol composes two modes of Hybrid Public-Key Encryption (RFC 9180):
- For message encryption, SD-APKE wraps HPKE
mode_auth_psk, following listing 17 of Alwen et al. (2023), "The Pre-Shared Key Modes of HPKE". - For metadata encryption, SD-PKE is an instantiation of HPKE
mode_base.
To check for messages, a recipient runs a challenge-based fetching protocol.
Note
In the listings that follow, mathematical syntax uses - for the empty
string, while Python pseudocode uses None. In tuples, _ denotes a value we
don't care about for the current operation.
| Scheme | Function | Use |
|---|---|---|
| Derive a key from input key |
||
SIG |
Signature scheme | |
| Generate keys | ||
| Sign a preimage7 |
||
| Verify signature |
||
AEAD |
Nonce-based authenticated encryption with associated data | |
| Encrypt a message |
||
| Decrypt a ciphertext |
||
SD-PKE |
Public-key encryption | |
| Generate keys | ||
| Encrypt a message |
||
| Decrypt a ciphertext |
||
SD-APKE |
Authenticated public-key encryption | |
| Generate keys | ||
| Encrypt a message |
||
| Decrypt a ciphertext |
||
Ristretto255 |
Generate a ristretto255 Diffie–Hellman keypair by sampling |
|
| Perform a Diffie–Hellman agreement between two ristretto255 keys, where |
SD-APKE is message encryption using Hybrid Public Key Encryption with an authenticated KEM (AuthEnc/AuthDec KGen() for details.
-
$\text{KEM}_{PQ} =$ ML-KEM-768 - HPKE's single-shot
SealAuthPSK()andOpenAuthPSK()APIs; see also Appendix: pskAPKE
via a wrapper function that connects them.
HPKE's SealAuthPSK()/OpenAuthPSK() use:
-
$\text{AKEM}$ , a (DH-based) Authenticated KEM with$\text{DHKEM}(\text{Group}, \text{KDF})$ = (X25519, HKDF-SHA256); see also Appendix: AKEM -
$\text{KS} =$ HPKE'sKeySchedule()with HKDF-SHA256 -
$\text{AEAD} =$ ChaCha20Poly1305
Senders and receivers MUST also possess a ristretto255 fetching keypair
| Syntax | Description |
|---|---|
| Generate keys | |
| Encrypt a message |
|
| Decrypt a message |
Concretely:
PSK_ID = "SD-pskAPKE"
def KGen():
(sk1, pk1) = AKEM.KGen()
(sk2, pk2) = KEM_PQ.KGen()
sk = (sk1, sk2)
pk = (pk1, pk2)
return (sk, pk)
def AuthEnc(
sk=(skS1, skS2), # NB. invalid Python syntax for parity with the mathematical signature
pk=(pkR1, pkR2),
m, ad, info_incl=pkR_fetch): # Sender commits to recipient fetch pubkey here
pkS = (skS1.public(), skS2.public())
(c2, K2) = KEM_PQ.Encap(pkR=pkR2)
# `info` parameter binds all of: c2, 'info_incl' (pkR_fetch), and pkS to encryption context
info_param = c2 + info_incl + pkS
(c1, cp) = HPKE.SealAuthPSK(skS=skS1, pkR=pkR1, psk=K2, psk_id=PSK_ID, m=m, ad=ad, info=info_param) # where cp = c'
return ((c1, cp), c2)
def AuthDec(
sk=(skR1, skR2), # NB. invalid Python syntax for parity with the mathematical signature
pk=(pkS1, pkS2),
c1, cp, c2, # where cp = c' in ((c1, cp), c2)
ad, info_incl=pkR_fetch):
K2 = KEM_PQ.Decap(skR=skR2, enc=c2)
# Reconstruct info parameter
info_reconstructed = c2 + info_incl + pk # c2 + pkR_fetch + pkS
m = HPKE.OpenAuthPSK(pkS=pkS1, skR=skR1, psk=K2, psk_id=PSK_ID, c1=c1, cp=cp, ad=ad, info=info_reconstructed)
return mThe info parameter commits to information not otherwise bound to the authenticated encrypted ciphertext. The sender supplies pkR_fetch (recipient's fetch public key) to the AuthEnc wrapper function, but the final info parameter passed to HPKE.SealAuthPSK includes: c2 (encapsulation of the PQ shared secret); pkS (sender's SD-APKE public key); and pkR_fetch.
This info parameter MUST NOT be transmitted with the ciphertext by the underlying AEAD, since it includes cleartext public keys, which are identifying.
Conformant implementations of HPKE pass the info parameter to KeySchedule() but do not transmit it with the ciphertext.
The receiver reconstructs the info parameter using: c2 (transmitted in encrypted message payload); pkS (by decrypting the SD-PKE ciphertext), and pkR_fetch (not transmitted, receiver knows their own key).
HPKE decryption will fail unless sender and receiver use the same values for all these components.
This info parameter is greater than 64 bytes. Implementors MUST ensure that the HPKE implementation and the underlying AEAD support a sufficiently long info parameter, or implement a modification to the protocol that hashes the concatenated values to the supported info length.
Why this info parameter? Via the info parameter, the sender binds material to the SD-APKE ciphertext:
| Component | Purpose | How transmitted | Where authenticated | Authentication prevents? |
|---|---|---|---|---|
| Sender |
Attach to receive replies | Inside SD-APKE (authenticated) | Transmitted inside PQ/T authenticated ciphertext | Key-swapping |
| Sender |
Sender authentication | Inside SD-PKE ct (SD-APKE.DH-AKEM via underlying AEAD, SD-APKE.MLKEM unauthenticated.) | Commit to in info |
Forged sender |
| Receiver fetch pubkey | Send to intended recipient | Not transmitted, but DH share used in message hint | Commit to in info |
Ciphertext relay/hint swap by impersonator |
PSK ciphertext (c2) |
Receiver runs Decap() to learn PSK |
Transmitted in message envelope (unauthenticated) | Commit to in info |
Re-encaps attacks |
Note
Although the DH-AKEM portion of pkS is already implicitly authenticated by its use in the DH-AKEM construction, the entire pkS is attached for clarity, parity with formal verification methods that treat the combined key as an opaque type, and ease of future drop-in replacement if a suitable PQ-authenticated construction to replace SD-APKE emerges.
mode_base with:
-
$\text{KEM}_H =$ X-Wing -
$\text{AEAD} =$ ChaCha20Poly1305 -
$\text{KS} =$ HPKE'sKeySchedule()with HKDF-SHA256
| Syntax | Description |
|---|---|
| Generate keys | |
Encrypt a message mode_base
|
|
Decrypt a message mode_base
|
Concretely, using HPKE's single-shot APIs:
def KGen():
(skS, pkS) = KEM_H.KGen()
return (skS, pkS)
def Enc(pkR, m):
c, cp = HPKE.SealBase(pkR=pkR, info=None, aad=None, pt=m) # where cp = c'
return (c, cp)
def Dec(skR, c, cp): # where cp = c' in (c, cp)
m = HPKE.OpenBase(enc=c, skR=skR, info=None, aad=None, ct=cp)
return mSources and journalists use different setup steps to manage their encryption keys. By contrast, messaging protocol steps are role-agnostic and turn-specific. Except where otherwise noted, sources and journalists execute the same fetching step (5), sending step (6), and receiving step (7), in any order.
Only a source can initiate a conversation. In other words, a source is always the first sender.
The server answers a sender's key request in two parts, which differ in lifetime:
- The welcome bundle, including the roster, is per-session, and the sender MAY cache it.
- Each journalist's one-time signed key bundle is consumed by the server once served. See "Known limitations" re: key exhaustion.
A sender MUST verify the welcome bundle before use. It MUST associate each
signed key bundle with a journalist in the roster by
Given:
| Anyone | |
|---|---|
| Pinned in client | |
| Published by server |
Then:
| Sender | Server | |
|---|---|---|
RequestKeys
|
||
| For all journalists |
||
|
|
||
|
|
||
| For all journalists |
||
| If |
||
| If |
||
| If |
||
| If |
Key exhaustion: If
For each recipient, a sender produces a message ciphertext (SD-APKE ciphertext), a metadata ciphertext (SD-PKE ciphertext), and a message delivery hint.
A sender knows their own keys, the newsroom's verification key
In addition, in the reply case, if the sender is a journalist replying to a source, they also already know their recipient's keys without further verification. In this case, they substitute the source's long-term keys for their own in the recipient list, and address the remaining slots to all other enrolled journalists.
The SD-APKE ciphertext is sender authenticated using classical DH-AKEM implicit authentication, and provides hybrid (post-quantum/traditional) message encryption by including a quantum-resistant secret in the encryption context using HPKE mode_auth_psk.
Despite the name, the "PSK" value is not a true 'pre-shared' key, and functions more like a KEM combiner.
Our terminology follows Alwen et al. (2023), "The Pre-Shared Key Modes of HPKE".
The PQ psk itself provides receiver authentication, but not sender authentication, due to the way it is constructed.
The SD-APKE ciphertext carries a structured plaintext message. Sources MUST include their long-term fetching and SD-PKE public keys in this plaintext in order to receive replies, since otherwise recipients cannot know all public key material required to reply to them. Since recipients always retrieve fresh public keys before responding to a journalist, long-term keys included by journalists inside the structured plaintext message are ignored by recipients during decryption, and journalists MAY therefore include placeholder values, particularly because a journalist does not have a "long-term" SD-PKE key. Journalists MUST always produce both the same size SD-APKE ciphertext and same size structured plaintext message as sources.
One-time drop mode extension: An extension implementation MAY omit the sender's fetching and SD-PKE keys from the plaintext message, offering improved deniability (no possibility for pending ciphertexts) but forgoing the sender's ability to receive replies. If this mode is implemented, the structured plaintext message length, or the ciphertext length, MUST NOT be shorter than a plaintext or ciphertext that includes reply keys.
Because decrypting the SD-APKE ciphertext requires the recipient to know the sender's long-term SD-APKE public key, an SD-PKE ciphertext (metadata ciphertext) delivers this SD-APKE public key, encrypted to the recipient's SD-PKE key, thus keeping the sender's identity hidden from the server, as described in HPKE's metadata protection guidance (RFC 9180 §9.9).
The SD-PKE (metadata) ciphertext is unauthenticated, so its contents MUST be committed to in the SD-APKE ciphertext's encryption context.
This is satisfied by the use of implicit authenticated encryption plus the info parameter, as described above.
The SD-PKE ciphertext MUST provide hybrid post-quantum/traditional confidentiality.
The sender also computes a hint from the recipient's fetching key: a fresh
ephemeral DH public key
The final message payload to the server includes: each ciphertext; the encapsulated shared secrets required to decrypt each of them; the encapsulated PQ secret; and the two components of the message delivery hint.
| All senders | Reply case | |
|---|---|---|
| Published by server | ||
| Holds | ||
|
Fetched for all |
||
|
Decrypted from a previous message sent by source |
||
Then, for some message
| Sender | Server | |
|---|---|---|
|
Reply case: A journalist |
||
| |
||
| |
||
|
|
||
| |
||
| |
||
| |
||
| |
||
| |
||
| |
||
|
|
||
| Store |
Note
info parameter to the underlying AEAD comprised of an encapsulated PQ secret,
The server will run a challenge-response protocol using the hint generated by the sender on message submission. This ensures that
- An external adversary does not obtain ciphertexts that may leak message content on future user compromise by only delivering ciphertexts to their intended recipients (with high probability).
- An honest server does not learn which client a message was intended for (unlinkability) by preventing an association between a recipient's public key and the ciphertext on the server.
- Avoids observing the volume or pattern of ciphertexts held by the server over time.
Challenge encryption protects each message ID using a key derived from the message’s delivery hint and a fresh per-message DH share. The server encrypts the message ID with AEAD, and sets the resulting ciphertext data as eid_k.
The server returns a fixed number of challenge pairs with the encryption ID and DH share (eid_k, Q_k). If fewer messages are available, the server generates random challenge pairs up to MAX_MESSAGES to ensure that the response size does not reveal how many messages are stored on the server.
A receiver knows their own keys, the newsroom's verification key
| Source | Journalist | |
|---|---|---|
| Published by server | ||
| Holds | ||
|
Fetched for all |
||
For some newsroom
| Server | Receiver | |
|---|---|---|
RequestMessages
|
||
|
|
||
| |
||
| If |
||
| |
||
Otherwise, pad with random values up to MAX_MESSAGES: |
||
| |
||
| |
||
| |
||
| |
||
| |
||
| |
||
| |
||
| |
||
| |
||
| |
||
| |
||
| If |
||
| If |
||
|
|
||
| If journalist, then |
||
| |
||
| |
||
| |
||
| If |
||
|
|
||
| If source: | ||
| If |
||
If RequestMessages
|
Note
info parameter used by the sender by concatenating the PQ encapsulated shared secret, decrypted
Implementors MUST mitigate timing attacks via the API that could leak the number of ciphertexts on the server, for example by ensuring that requestMessages is constant-time at the server.
Implementors MUST implement robust message-parsing and are expected to gracefully handle malformed plaintext and ciphertext messages, both at the server and on the client.
SENDER_FETCH_PUBKEY_BYTES || SENDER_PKE_PUBKEY_BYTES || structured_message_bytes || padding
Unpadded len: 32 bytes + 1216 bytes + len(structured_message)
= structured message size + 1248; messages will then be padded to a fixed message size before encryption.
SENDER_LONG_TERM_SD_APKE_DHAKEM_PUBKEY_BYTES || SENDER_LONG_TERM_SD_APKE_MLKEM768_PUBKEY_BYTES
Len: 32 + 1184 = 1216 bytes
Encrypting the SD-APKE (message) plaintext yields a ciphertext and encapsulated shared secret ciphertext:
CT_APKE = MLKEM768_CT_ENCAPS_SHARED_SECRET || DHAKEM_ENCAPS_SHARED_SECRET || CT_SD_APKE
Len: 1088 + 32 + ((fixed message size) + AEAD_TAG_LEN) = 1120 + (fixed message size + 16)
= fixed message size + 1136
Encrypting the SD-PKE (metadata) plaintext also yields a ciphertext and encapsulated shared secret ciphertext:
CT_PKE = CT_SD_PKE_ENCAPS_SS_CT || CT_SD_PKE
Len: XWING_SHARED_SECRET_ENCAPS_CT_LEN + (DHAKEM_PK_LEN + MLKEM768_PK_LEN + AEAD_TAG_LEN) = 1120 + 32 + 1184 + 16
= 2352
encrypted_envelope = X || Z || CT_APKE || CT_PKE = (ephemeral pk || dh(ephemeral pk, receiver fetch pk) || CT_APKE || CT_PKE)
Len: 32 + 32 + (fixed message size + 1136) + 2352
= fixed message size + 3552
Message tuples:
(id_k, encrypted_envelope, X, Z, timestamp) (server_generated_uuid, encrypted_envelope, ephemeral_pk, dh_share, server_generated_fuzzy_expiry_time)
Len: [16, len(encrypted_envelope), 32, 32, 12 ] per row; server pads to fixed number of messages (rows)
encrypted_uuid, message_challenge_3party)
where
eid_kis the ChaCha20-Poly1305 encryption of the 16-byte message ID with its 16-byte auth tagQ_kis a 32-byte Ristretto255 group element
Len: 16 + 16 + 32 = 64 bytes * n challenges; server pads to fixed number of challenges
- The protocol does not currently include a specification for transferring attachments.
- The protocol does not currently include a specification for journalist key replenishment.
- The protocol does not currently include a specification for rotation of the newsroom key. The relationship between the newsroom key and the server URL is not yet specified.
- The protocol is not designed for scalability. There is a maximum number of messages that can be held by the server, constrained by the number of per-request challenges that the server can reasonably perform during message-fetching without unacceptable latency for users, particularly over Tor. See benchmarks for more information.
- The use of HPKE's implicit authentication for message sending means that the protocol is vulnerable to key compromise impersonation.
- The protocol currently offers quantum-resistant message encryption, but not quantum-resistant message authentication or message-fetching.
A user's SD-APKE message key and SD-PKE metadata key. Sources' and journalists' key bundles have different lifetimes (see "Key hierarchy"):
-
A journalist's ephemeral, one-time key bundles are generated during their initial setup and then periodically refreshed (see step 3.2) and are consumed by the server when served to a sender.
-
A source's permanent key bundle is derived from their passphrase (see step 4) and included in each message they send to journalists.
The set of journalists currently enrolled by the newsroom. Each entry includes the journalist's verification key, long-term public keys, and their signatures.
A journalist's key bundle, signed under their long-term signing key.
The newsroom's verification key, FPF's signature over it, and the roster.
The following definitions are provided in "The SecureDrop Protocol: End-to-End Encrypted Whistleblowing for All". Implementors of this specification can rely on HPKE APIs without needing to implement the underlying constructions, but definitions are included for cross-referencing purposes.
Part of: SecureDrop APKE.
-
$\text{Group} =$ X25519 -
$\text{KDF} =$ HKDF-SHA256
| Syntax | Description |
|---|---|
| Generate keys; for DH-AKEM, |
|
| Encapsulate a ciphertext |
|
| Decapsulate a shared secret |
Concretely, these functions are used as specified in RFC 9180 §4.1.
Part of: SecureDrop APKE.
mode_auth_psk with:
-
$\text{AKEM}$ as above -
$\text{KS} =$ HPKE'sKeySchedule()with HKDF-SHA256 -
$\text{AEAD} =$ ChaCha20Poly1305
| Syntax | Description |
|---|---|
Encrypt a message mode_auth_psk
|
|
Decrypt a message mode_auth_psk
|
Concretely, using HPKE's single-shot APIs:
PSK_ID = "SD-pskAPKE"
def pskAEnc(skS, pkR, psk, m, ad, info):
c1, cp = HPKE.SealAuthPSK(pkR=pkR, info=info, aad=ad, pt=m, psk=psk, psk_id=PSK_ID, skS=skS) # where cp = c'
return (c1, cp)
def pskADec(pkS, skR, psk, c1, cp, ad, info): # where cp = c' in (c1, cp)
m = HPKE.OpenAuthPSK(enc=c1, skR=skR, info=info, aad=ad, ct=cp, psk=psk, psk_id=PSK_ID, pkS=pkS)
return mBeginning with v1, the protocol may adopt semantic versioning. For now,
versions like 0.x reflect coarse-grained milestones of the protocol's
development, with finer-grained changes reflected in individual Git commits.
All changes SHOULD be considered breaking.
Initial proof of concept.
As analyzed in Maier (2025), "A Formal Analysis of the SecureDrop
Protocol", using modified
As formalized in Berra et al. (2026), "The SecureDrop Protocol: End-to-End
Encrypted Whistleblowing for All", using standard HPKE modes
mode_base and mode_auth_psk.
Footnotes
-
TODO: https://github.com/freedomofpress/securedrop-protocol/blob/a0252a8ee7a6e4051c65e4e0c06b63d6ce921110/docs/wip-protocol-0.3.md?plain=1#L87 ↩
-
The SD-APKE key used for message encryption/decryption is composed of a classical and a quantum-resistant key. ↩ ↩2 ↩3
-
The source's keys are considered "permanent" because they are derived deterministically from the source's passphrase, which cannot be changed. ↩ ↩2 ↩3
-
See
draft-pki.mdfor further considerations. ↩ ↩2 ↩3 -
iis zero-indexed for idiomatic implementation. ↩ -
We refer to the "preimage" and the "preimage tag" given as input to the signature scheme in order to avoid confusion with "messages" in the sense of the overall messaging protocol. The notation
tag || mis encoded aslen(tag) || tag || m, wheretagis the variable-length tag as ASCII bytes,len(tag)is the length of the tag encoded as a single byte, andmis the preimage bytes. Tags MUST contain only ASCII characters and MUST be at most 255 bytes. ↩ ↩2 -
pksis assumed to have this arity and sequence for the remainder of this document. ↩ -
See "Configuration". ↩
-
Implementors SHOULD use a fresh scalar $
r_k$ for each challenge for challenge unlinkability. ↩ -
A zero-filled nonce does not affect the security of the AEAD encryption because the shared key is ephemeral. ↩