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    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_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(
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.signed_longterm_key_bundle.clone(),
261            ephemeral.clone(),
262        ))
263    }
264}
265
266/// Provide generic implementation, restricted to implementors RestrictedApi trait and
267/// the Enrollable trait. Implementors of both those will automatically be able to use
268/// this generic JournalistApi implementation, but downstream crates will be unable to
269/// implement RestrictedApi. Originally this was defined at the trait level
270/// (`pub trait JournalistApi: Api + restricted::RestrictedApi`), but hax was unable
271/// to extract the trait.
272impl<T> JournalistApi for T
273where
274    T: Api + Enrollable + RestrictedApi,
275{
276    fn create_setup_request(&self) -> Result<JournalistSetupRequest, Error> {
277        Ok(JournalistSetupRequest {
278            enrollment: self.enroll(),
279        })
280    }
281
282    fn create_ephemeral_key_request(&self) -> JournalistEphemeralKeyRequest {
283        JournalistEphemeralKeyRequest {
284            verifying_key: self.signing_key().clone(),
285            bundles: self.signed_keybundles(),
286        }
287    }
288}
289
290/// Journalist-specific API operations.
291///
292/// Extends [`Api`] with enrollment and ephemeral key management.
293pub trait JournalistApi {
294    /// Creates an enrollment request for initial journalist onboarding.
295    ///
296    /// Packages the journalist's self-signed long-term key bundle into a
297    /// [`JournalistSetupRequest`] for submission to the newsroom (step 3.1).
298    ///
299    /// # Errors
300    ///
301    /// Returns an error if enrollment data cannot be constructed.
302    fn create_setup_request(&self) -> Result<JournalistSetupRequest, Error>;
303
304    /// Creates a request to replenish ephemeral key bundles on the server.
305    ///
306    /// Collects all current signed key bundles and packages them into a
307    /// [`JournalistEphemeralKeyRequest`] for upload to the server (step 3.2).
308    fn create_ephemeral_key_request(&self) -> JournalistEphemeralKeyRequest;
309}