Skip to main content

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    client::challenge::solve_fetch_challenges,
21    client::message::encrypt,
22    traits::RestrictedApi,
23    wire::bundle::SignedKeyBundlePublic,
24    wire::{
25        core::{
26            JournalistEphemeralKeys, JournalistLongTermView, MessageChallengeFetchRequest,
27            MessageFetchRequest, WelcomeBundle,
28        },
29        setup::{JournalistEphemeralKeyRequest, JournalistSetupRequest},
30    },
31};
32use alloc::vec::Vec;
33use anyhow::Error;
34use rand_core::{CryptoRng, RngCore};
35use uuid::Uuid;
36
37/// Clients hold a reference to the newsroom [`VerifyingKey`](VerifyingKey)
38/// of the instance they are interacting with.
39pub trait Client {
40    /// Returns the stored newsroom verifying key, if one has been verified.
41    fn newsroom_verifying_key(&self) -> Option<&VerifyingKey>;
42
43    /// Stores a verified newsroom verifying key.
44    fn set_newsroom_verifying_key(&mut self, key: VerifyingKey);
45}
46
47/// Common API shared by sources and journalists. [`Api`](Api) users must provide
48/// a Client implementation (local storage abstraction).
49/// All users use the same API, but hax does not support default trait implementations
50/// (cryspen/hax/issues/888) so the trait is defined separately.
51pub trait Api: Client {
52    /// Creates a request to fetch encrypted message IDs from the server.
53    ///
54    /// Corresponds to step 7 in the protocol spec. The server returns a
55    /// fixed-size set of challenges (encrypted message IDs) that the client
56    /// must solve using [`solve_fetch_challenges`](Api::solve_fetch_challenges).
57    fn fetch_message_ids<R: RngCore + CryptoRng>(
58        &self,
59        _rng: &mut R,
60    ) -> MessageChallengeFetchRequest;
61
62    /// Solves the encrypted message-ID challenges returned by the server.
63    ///
64    /// Each [`FetchResponse`] contains an encrypted message ID and a
65    /// per-request DH share. The client uses its fetch keypair to recover
66    /// message IDs that were addressed to it, discarding the rest.
67    ///
68    /// Returns the set of [`Uuid`]s for messages belonging to this client.
69    fn solve_fetch_challenges(&self, challenges: &[FetchResponse]) -> Result<Vec<Uuid>, Error>
70    where
71        Self: Sized + UserSecret;
72
73    /// Creates a request to fetch a specific message by its ID.
74    ///
75    /// Corresponds to steps 8 and 10 in the protocol spec. Returns `None`
76    /// if the request cannot be constructed (the default implementation
77    /// always returns `Some`).
78    fn fetch_message(&self, message_id: Uuid) -> Option<MessageFetchRequest>;
79
80    /// Encrypts and submits a message from `sender` to `recipient`.
81    ///
82    /// Handles padding, plaintext construction (including sender reply keys),
83    /// and hybrid encryption. This covers step 6 (source submissions) and
84    /// step 9 (journalist replies) in the protocol spec.
85    ///
86    /// # Errors
87    ///
88    /// Returns an error if encryption fails.
89    fn submit_message<R, S, P>(
90        &self,
91        rng: &mut R,
92        message: &[u8],
93        sender: &S,
94        recipient: &P,
95    ) -> Result<Envelope, Error>
96    where
97        R: RngCore + CryptoRng,
98        S: UserSecret,
99        P: UserPublic;
100
101    /// Verifies a newsroom [`WelcomeBundle`] and stores the newsroom key (step 5).
102    ///
103    /// We check the FPF signature using the newsroom verifying key, then for every
104    /// journalist in the roster we verify:
105    /// * the newsroom's signature over the journalist's verifying key
106    /// * the journalist's signature over their long term keys
107    ///
108    /// On success the newsroom key is stored, and the long term views can be
109    /// cached and reused.
110    ///
111    /// # Errors
112    ///
113    /// Returns an error if the FPF signature or any journalist signature is invalid.
114    fn handle_welcome(
115        &mut self,
116        welcome: &WelcomeBundle,
117        fpf_verifying_key: &VerifyingKey,
118    ) -> Result<(), Error>;
119
120    /// Verifies one journalist's long-term view against a trusted newsroom verifying key,
121    /// the newsroom's signature over the journalist's verifying key, and the
122    /// journalist's signature over their long term keys.
123    ///
124    /// # Errors
125    ///
126    /// Returns an error if either signature is invalid.
127    fn verify_long_term(
128        &self,
129        journalist: &JournalistLongTermView,
130        newsroom_verifying_key: &VerifyingKey,
131    ) -> Result<(), Error>;
132
133    /// Verifies a list of journalists' one-time bundles against their already verified long-term view and assembles a list of `JournalistPublicView` for encryption. Discards any bundles that do not match a verification key in the roster.
134    ///
135    /// # Errors
136    ///
137    /// Returns an error if the signature on the one-time bundle is invalid, or if the same journalist appears multiple times in the list.
138    fn verify_ephemeral_keys(
139        &self,
140        roster: &[JournalistLongTermView],
141        ephemeral: &[JournalistEphemeralKeys],
142    ) -> Result<Vec<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::schemes::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_bundle.bundle_bytes(),
242                &journalist.signed_longterm_key_bundle.selfsig,
243            )
244            .map_err(|_| anyhow::anyhow!("invalid journalist self-signature on long-term keys"))?;
245        Ok(())
246    }
247
248    fn verify_ephemeral_keys(
249        &self,
250        roster: &[JournalistLongTermView],
251        ephemeral: &[JournalistEphemeralKeys],
252    ) -> Result<Vec<JournalistPublicView>, Error> {
253        let mut views = Vec::new();
254        let mut seen: Vec<[u8; 32]> = Vec::new();
255
256        for eph in ephemeral {
257            let vk = eph.vk.into_bytes();
258            let Some(long_term) = roster.iter().find(|j| j.vk.into_bytes() == vk) else {
259                continue; // Discard if no match in roster
260            };
261            if seen.contains(&vk) {
262                return Err(anyhow::anyhow!(
263                    "detected multiple ephemeral key bundles for the same journalist signing key"
264                ));
265            }
266            seen.push(vk);
267
268            let signed_bundle = &eph.ephemeral;
269            long_term
270                .vk
271                .verify(&signed_bundle.0.as_bytes(), &signed_bundle.1)
272                .map_err(|_| {
273                    anyhow::anyhow!("invalid journalist self-signature on one-time keys")
274                })?;
275
276            views.push(JournalistPublicView::new(
277                long_term.vk,
278                long_term.signed_longterm_key_bundle.clone(),
279                signed_bundle.clone(),
280            ));
281        }
282
283        Ok(views)
284    }
285}
286
287/// Provide generic implementation, restricted to implementors RestrictedApi trait and
288/// the Enrollable trait. Implementors of both those will automatically be able to use
289/// this generic JournalistApi implementation, but downstream crates will be unable to
290/// implement RestrictedApi. Originally this was defined at the trait level
291/// (`pub trait JournalistApi: Api + restricted::RestrictedApi`), but hax was unable
292/// to extract the trait.
293impl<T> JournalistApi for T
294where
295    T: Api + Enrollable + RestrictedApi,
296{
297    fn create_setup_request(&self) -> Result<JournalistSetupRequest, Error> {
298        Ok(JournalistSetupRequest {
299            enrollment: self.enroll(),
300        })
301    }
302
303    fn create_ephemeral_key_request(&self) -> JournalistEphemeralKeyRequest {
304        JournalistEphemeralKeyRequest {
305            verifying_key: self.signing_key().clone(),
306            bundles: self.signed_keybundles(),
307        }
308    }
309}
310
311/// Journalist-specific API operations.
312///
313/// Extends [`Api`] with enrollment and ephemeral key management.
314pub trait JournalistApi {
315    /// Creates an enrollment request for initial journalist onboarding.
316    ///
317    /// Packages the journalist's self-signed long-term key bundle into a
318    /// [`JournalistSetupRequest`] for submission to the newsroom (step 3.1).
319    ///
320    /// # Errors
321    ///
322    /// Returns an error if enrollment data cannot be constructed.
323    fn create_setup_request(&self) -> Result<JournalistSetupRequest, Error>;
324
325    /// Creates a request to replenish ephemeral key bundles on the server.
326    ///
327    /// Collects all current signed key bundles and packages them into a
328    /// [`JournalistEphemeralKeyRequest`] for upload to the server (step 3.2).
329    fn create_ephemeral_key_request(&self) -> JournalistEphemeralKeyRequest;
330}