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_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}