securedrop_protocol_minimal/api.rs
1//! Client API traits for the SecureDrop protocol.
2//!
3//! This module defines the shared API surface for both source and journalist
4//! clients. The [`Api`] trait provides common operations such as key fetching,
5//! signature verification, and message submission. The [`JournalistApi`] trait
6//! extends [`Api`] with journalist-specific operations like enrollment and
7//! ephemeral key management.
8//!
9//! # Trust model
10//!
11//! Key verification follows a chain of trust:
12//! 1. The FPF signing key is a trust anchor (pre-distributed out of band).
13//! 2. The newsroom's verifying key is signed by FPF. (This is not yet verified by `handle_journalist_key_response()`.)
14//! 3. Each journalist's signing key is signed by the newsroom.
15//! 4. Each journalist's key bundles are self-signed.
16
17use crate::{
18 Enrollable, Envelope, FetchResponse, JournalistPublic, UserPublic, UserSecret, VerifyingKey,
19 encrypt_decrypt::{encrypt, solve_fetch_challenges},
20 traits::RestrictedApi,
21 wire::{
22 core::{
23 MessageChallengeFetchRequest, MessageFetchRequest, SourceJournalistKeyRequest,
24 SourceJournalistKeyResponse, SourceNewsroomKeyRequest, SourceNewsroomKeyResponse,
25 },
26 setup::{JournalistEphemeralKeyRequest, JournalistSetupRequest},
27 },
28};
29use alloc::vec::Vec;
30use anyhow::Error;
31use rand_core::{CryptoRng, RngCore};
32use uuid::Uuid;
33
34/// Clients hold a reference to the newsroom [`VerifyingKey`](VerifyingKey)
35/// of the instance they are interacting with.
36pub trait Client {
37 /// Returns the stored newsroom verifying key, if one has been verified.
38 fn newsroom_verifying_key(&self) -> Option<&VerifyingKey>;
39
40 /// Stores a verified newsroom verifying key.
41 fn set_newsroom_verifying_key(&mut self, key: VerifyingKey);
42}
43
44/// Common API shared by sources and journalists. [`Api`](Api) users must provide
45/// a Client implementation (local storage abstraction).
46/// All users use the same API, but hax does not support default trait implementations
47/// (cryspen/hax/issues/888) so the trait is defined separately.
48pub trait Api: Client {
49 /// Creates a request to fetch the newsroom's public keys from the server.
50 ///
51 /// This is the first part of step 5 in the protocol spec.
52 fn fetch_newsroom_keys(&self) -> SourceNewsroomKeyRequest;
53
54 /// Creates a request to fetch journalist public keys from the server.
55 ///
56 /// This is the second part of step 5 in the protocol spec. The server
57 /// responds with long-term keys and a one-time ephemeral key bundle
58 /// for each available journalist.
59 fn fetch_journalist_keys(&self) -> SourceJournalistKeyRequest;
60
61 /// Creates a request to fetch encrypted message IDs from the server.
62 ///
63 /// Corresponds to step 7 in the protocol spec. The server returns a
64 /// fixed-size set of challenges (encrypted message IDs) that the client
65 /// must solve using [`solve_fetch_challenges`](Api::solve_fetch_challenges).
66 fn fetch_message_ids<R: RngCore + CryptoRng>(
67 &self,
68 _rng: &mut R,
69 ) -> MessageChallengeFetchRequest;
70
71 /// Solves the encrypted message-ID challenges returned by the server.
72 ///
73 /// Each [`FetchResponse`] contains an encrypted message ID and a
74 /// per-request DH share. The client uses its fetch keypair to recover
75 /// message IDs that were addressed to it, discarding the rest.
76 ///
77 /// Returns the set of [`Uuid`]s for messages belonging to this client.
78 fn solve_fetch_challenges(&self, challenges: &[FetchResponse]) -> Result<Vec<Uuid>, Error>
79 where
80 Self: Sized + UserSecret;
81
82 /// Creates a request to fetch a specific message by its ID.
83 ///
84 /// Corresponds to steps 8 and 10 in the protocol spec. Returns `None`
85 /// if the request cannot be constructed (the default implementation
86 /// always returns `Some`).
87 fn fetch_message(&self, message_id: Uuid) -> Option<MessageFetchRequest>;
88
89 /// Encrypts and submits a message from `sender` to `recipient`.
90 ///
91 /// Handles padding, plaintext construction (including sender reply keys),
92 /// and hybrid encryption. This covers step 6 (source submissions) and
93 /// step 9 (journalist replies) in the protocol spec.
94 ///
95 /// # Errors
96 ///
97 /// Returns an error if encryption fails.
98 fn submit_message<R, S, P>(
99 &self,
100 rng: &mut R,
101 message: &[u8],
102 sender: &S,
103 recipient: &P,
104 ) -> Result<Envelope, Error>
105 where
106 R: RngCore + CryptoRng,
107 S: UserSecret,
108 P: UserPublic;
109
110 /// Verifies and stores the newsroom's verifying key from a server response.
111 ///
112 /// Checks the FPF signature over the newsroom verifying key, and if valid,
113 /// stores it for subsequent journalist key verification.
114 ///
115 /// # Errors
116 ///
117 /// Returns an error if the FPF signature is invalid.
118 fn handle_newsroom_key_response(
119 &mut self,
120 response: &SourceNewsroomKeyResponse,
121 fpf_verifying_key: &VerifyingKey,
122 ) -> Result<(), Error>;
123
124 /// Verifies a journalist's key response against the newsroom's signature.
125 ///
126 /// Performs three signature checks:
127 /// 1. The newsroom's signature over the journalist's verifying key.
128 /// 2. The journalist's self-signature over their long-term key bundle.
129 /// 3. The journalist's self-signature over their one-time keys.
130 ///
131 /// # Errors
132 ///
133 /// Returns an error if any signature check fails.
134 fn handle_journalist_key_response(
135 &self,
136 response: &SourceJournalistKeyResponse,
137 newsroom_verifying_key: &VerifyingKey,
138 ) -> Result<(), Error>;
139}
140
141impl<T> Api for T
142where
143 T: Client,
144{
145 /// Creates a request to fetch the newsroom's public keys from the server.
146 ///
147 /// This is the first part of step 5 in the protocol spec.
148 fn fetch_newsroom_keys(&self) -> SourceNewsroomKeyRequest {
149 SourceNewsroomKeyRequest {}
150 }
151
152 /// Creates a request to fetch journalist public keys from the server.
153 ///
154 /// This is the second part of step 5 in the protocol spec. The server
155 /// responds with long-term keys and a one-time ephemeral key bundle
156 /// for each available journalist.
157 fn fetch_journalist_keys(&self) -> SourceJournalistKeyRequest {
158 SourceJournalistKeyRequest {}
159 }
160
161 /// Creates a request to fetch encrypted message IDs from the server.
162 ///
163 /// Corresponds to step 7 in the protocol spec. The server returns a
164 /// fixed-size set of challenges (encrypted message IDs) that the client
165 /// must solve using [`solve_fetch_challenges`](Api::solve_fetch_challenges).
166 fn fetch_message_ids<R: RngCore + CryptoRng>(
167 &self,
168 _rng: &mut R,
169 ) -> MessageChallengeFetchRequest {
170 MessageChallengeFetchRequest {}
171 }
172
173 /// Solves the encrypted message-ID challenges returned by the server.
174 ///
175 /// Each [`FetchResponse`] contains an encrypted message ID and a
176 /// per-request DH share. The client uses its fetch keypair to recover
177 /// message IDs that were addressed to it, discarding the rest.
178 ///
179 /// Returns the set of [`Uuid`]s for messages belonging to this client.
180 fn solve_fetch_challenges(&self, challenges: &[FetchResponse]) -> Result<Vec<Uuid>, Error>
181 where
182 Self: Sized + UserSecret,
183 {
184 Ok(solve_fetch_challenges(self, challenges))
185 }
186
187 /// Creates a request to fetch a specific message by its ID.
188 ///
189 /// Corresponds to steps 8 and 10 in the protocol spec. Returns `None`
190 /// if the request cannot be constructed (the default implementation
191 /// always returns `Some`).
192 fn fetch_message(&self, message_id: Uuid) -> Option<MessageFetchRequest> {
193 Some(MessageFetchRequest { message_id })
194 }
195
196 /// Encrypts and submits a message from `sender` to `recipient`.
197 ///
198 /// Handles padding, plaintext construction (including sender reply keys),
199 /// and hybrid encryption. This covers step 6 (source submissions) and
200 /// step 9 (journalist replies) in the protocol spec.
201 ///
202 /// # Errors
203 ///
204 /// Returns an error if encryption fails.
205 fn submit_message<R, S, P>(
206 &self,
207 rng: &mut R,
208 message: &[u8],
209 sender: &S,
210 recipient: &P,
211 ) -> Result<Envelope, Error>
212 where
213 R: RngCore + CryptoRng,
214 S: UserSecret,
215 P: UserPublic,
216 {
217 // TODO: review padding
218 let padded_message = crate::primitives::pad::pad_message(message);
219 let plaintext = sender.build_message(padded_message);
220 let envelope = encrypt(rng, sender, &plaintext, recipient);
221 Ok(envelope)
222 }
223
224 /// Verifies and stores the newsroom's verifying key from a server response.
225 ///
226 /// Checks the FPF signature over the newsroom verifying key, and if valid,
227 /// stores it for subsequent journalist key verification.
228 ///
229 /// # Errors
230 ///
231 /// Returns an error if the FPF signature is invalid.
232 fn handle_newsroom_key_response(
233 &mut self,
234 response: &SourceNewsroomKeyResponse,
235 fpf_verifying_key: &VerifyingKey,
236 ) -> Result<(), Error> {
237 let newsroom_vk_bytes = response.newsroom_verifying_key.into_bytes();
238 fpf_verifying_key
239 .verify(&newsroom_vk_bytes, &response.fpf_sig)
240 .map_err(|_| anyhow::anyhow!("invalid FPF signature on newsroom verifying key"))?;
241
242 self.set_newsroom_verifying_key(response.newsroom_verifying_key);
243 Ok(())
244 }
245
246 /// Verifies a journalist's key response against the newsroom's signature.
247 ///
248 /// Performs three signature checks:
249 /// 1. The newsroom's signature over the journalist's verifying key.
250 /// 2. The journalist's self-signature over their long-term key bundle.
251 /// 3. The journalist's self-signature over their one-time keys.
252 ///
253 /// # Errors
254 ///
255 /// Returns an error if any signature check fails.
256 fn handle_journalist_key_response(
257 &self,
258 response: &SourceJournalistKeyResponse,
259 newsroom_verifying_key: &VerifyingKey,
260 ) -> Result<(), Error> {
261 // 1. Verify newsroom signature on journalist's verifying key.
262 newsroom_verifying_key
263 .verify(
264 &response.journalist.verifying_key().into_bytes(),
265 &response.nr_signature,
266 )
267 .map_err(|_| anyhow::anyhow!("invalid newsroom signature on journalist signing key"))?;
268
269 // 2. Verify journalist's self-signature on long-term key bundle.
270 let vk = response.journalist.verifying_key();
271 vk.verify(
272 response.journalist.signed_keybytes().as_bytes(),
273 response.journalist.self_signature(),
274 )
275 .map_err(|_| anyhow::anyhow!("invalid journalist self-signature on long-term keys"))?;
276
277 // 3. Verify journalist's self-signature on one-time ephemeral key bundle.
278 vk.verify(
279 &response.journalist.ephemeral_bundle().as_bytes(),
280 response.journalist.ephemeral_signature(),
281 )
282 .map_err(|_| anyhow::anyhow!("invalid journalist self-signature on one-time keys"))?;
283
284 Ok(())
285 }
286}
287
288/// Provide generic implementation, restricted to implementors RestrictedApi trait and
289/// the Enrollable trait. Implementors of both those will automatically be able to use
290/// this generic JournalistApi implementation, but downstream crates will be unable to
291/// implement RestrictedApi. Originally this was defined at the trait level
292/// (`pub trait JournalistApi: Api + restricted::RestrictedApi`), but hax was unable
293/// to extract the trait.
294impl<T> JournalistApi for T
295where
296 T: Api + Enrollable + RestrictedApi,
297{
298 fn create_setup_request(&self) -> Result<JournalistSetupRequest, Error> {
299 Ok(JournalistSetupRequest {
300 enrollment: self.enroll(),
301 })
302 }
303
304 fn create_ephemeral_key_request(&self) -> JournalistEphemeralKeyRequest {
305 JournalistEphemeralKeyRequest {
306 verifying_key: self.signing_key().clone(),
307 bundles: self.signed_keybundles(),
308 }
309 }
310}
311
312/// Journalist-specific API operations.
313///
314/// Extends [`Api`] with enrollment and ephemeral key management.
315pub trait JournalistApi {
316 /// Creates an enrollment request for initial journalist onboarding.
317 ///
318 /// Packages the journalist's self-signed long-term key bundle into a
319 /// [`JournalistSetupRequest`] for submission to the newsroom (step 3.1).
320 ///
321 /// # Errors
322 ///
323 /// Returns an error if enrollment data cannot be constructed.
324 fn create_setup_request(&self) -> Result<JournalistSetupRequest, Error>;
325
326 /// Creates a request to replenish ephemeral key bundles on the server.
327 ///
328 /// Collects all current signed key bundles and packages them into a
329 /// [`JournalistEphemeralKeyRequest`] for upload to the server (step 3.2).
330 fn create_ephemeral_key_request(&self) -> JournalistEphemeralKeyRequest;
331}