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