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.  (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}