Skip to main content

securedrop_protocol_minimal/server/
session.rs

1//! Server-side protocol implementation
2//!
3//! This module implements the server-side handling of SecureDrop protocol steps 5-10.
4
5use alloc::vec::Vec;
6use anyhow::Error;
7use rand_core::{CryptoRng, RngCore};
8use uuid::Uuid;
9
10use crate::Envelope;
11use crate::keys::NewsroomKeyPair;
12use crate::schemes::challenge::MAX_MESSAGES;
13use crate::server::challenge::compute_fetch_challenges;
14use crate::server::storage::ServerStorage;
15use crate::sign::{FpfOnNewsroom, NewsroomOnJournalist, Signature, VerifyingKey};
16use crate::wire::core::{
17    JournalistEphemeralKeys, JournalistLongTermView, MessageChallengeFetchRequest,
18    MessageChallengeFetchResponse, MessageFetchRequest, WelcomeBundle,
19};
20use crate::wire::setup::{
21    JournalistEphemeralKeyRequest, JournalistSetupRequest, JournalistSetupResponse,
22    NewsroomSetupRequest,
23};
24
25/// Server session for handling source requests
26#[derive(Default)]
27pub struct Server {
28    storage: ServerStorage,
29    newsroom_keys: Option<NewsroomKeyPair>,
30    /// Signature from FPF over the newsroom keys
31    signature: Option<Signature<FpfOnNewsroom>>,
32}
33
34impl Server {
35    /// Create a new server session
36    ///
37    /// TODO: Load newsroom keys from storage if they exist.
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    /// Generate a new newsroom setup request.
43    ///
44    /// This creates a newsroom key pair, stores it in the server storage,
45    /// and returns a setup request that can be sent to FPF for signing.
46    ///
47    /// TODO: The caller should persist these keys to disk.
48    pub fn create_newsroom_setup_request<R: RngCore + CryptoRng>(
49        &mut self,
50        rng: &mut R,
51    ) -> Result<NewsroomSetupRequest, Error> {
52        let newsroom_keys = NewsroomKeyPair::new(rng)?;
53        let newsroom_vk = newsroom_keys.verifying_key();
54
55        // Store the newsroom keys in the session for later use (e.g., signing journalist keys)
56        self.newsroom_keys = Some(newsroom_keys);
57
58        Ok(NewsroomSetupRequest {
59            newsroom_verifying_key: newsroom_vk,
60        })
61    }
62
63    /// Setup a journalist. This corresponds to step 3.1 in the spec.
64    ///
65    /// The newsroom then signs the bundle of journalist public keys.
66    ///
67    /// TODO: There is a manual verification step here, so the caller should
68    /// instruct the user to stop, verify the fingerprint out of band, and
69    /// then proceed. The caller should also persist the fingerprint and signature
70    /// in its local data store.
71    ///
72    /// TODO(later): How to handle signing when offline? (Not relevant for benchmarking)
73    pub fn setup_journalist(
74        &mut self,
75        request: JournalistSetupRequest,
76    ) -> Result<JournalistSetupResponse, Error> {
77        // Get enrollment key from the request
78        let journalist_signing_key = request.enrollment.verification_key;
79
80        // Verify journalist self-signature over their own pubkeys.
81        journalist_signing_key
82            .verify(
83                &request.enrollment.bundle.bundle_bytes(),
84                &request.enrollment.bundle.selfsig,
85            )
86            .map_err(|_| anyhow::anyhow!("Invalid signature on longterm keys"))?;
87
88        // Sign the journalist's verifying key.
89        let verifying_key_bytes = request.enrollment.verification_key.into_bytes();
90        let newsroom_keys = self
91            .newsroom_keys
92            .as_ref()
93            .ok_or_else(|| anyhow::anyhow!("Newsroom keys not found in session"))?;
94        let newsroom_signature: Signature<NewsroomOnJournalist> =
95            newsroom_keys.sign(&verifying_key_bytes);
96
97        // Insert journalist keys into storage
98        let _journalist_id = self
99            .storage
100            .add_journalist(request.enrollment, newsroom_signature.clone());
101
102        Ok(JournalistSetupResponse {
103            sig: newsroom_signature,
104        })
105    }
106
107    /// Handle journalist ephemeral key replenishment. This corresponds to step 3.2 in the spec.
108    ///
109    /// The journalist sends ephemeral keys signed by their signing key, and the server
110    /// verifies the signature and stores the ephemeral keys.
111    ///
112    /// # Errors
113    ///
114    /// Returns an error if the journalist is not found in storage, or if any bundle
115    /// signature fails verification.
116    pub fn handle_ephemeral_key_request(
117        &mut self,
118        request: JournalistEphemeralKeyRequest,
119    ) -> Result<(), Error> {
120        // Look up the journalist by their verifying key
121        let journalist_id = self
122            .storage
123            .find_journalist_by_verifying_key(&request.verifying_key)
124            .ok_or_else(|| anyhow::anyhow!("Journalist not found in storage"))?;
125
126        // TODO: more efficient way than verifying each signature!
127        // Verify each ephemeral bundle signature.
128        request
129            .bundles
130            .iter()
131            .try_for_each(|k| request.verifying_key.verify(&k.0.as_bytes(), &k.1))
132            .map_err(|_| anyhow::anyhow!("Invalid signature on ephemeral keys"))?;
133
134        // Store the ephemeral keys for the journalist
135        self.storage
136            .add_ephemeral_keys(journalist_id, request.bundles);
137
138        Ok(())
139    }
140
141    /// Returns the newsroom verifying key, if one has been generated.
142    pub fn newsroom_verifying_key(&self) -> Option<VerifyingKey> {
143        self.newsroom_keys.as_ref().map(|keys| keys.verifying_key())
144    }
145
146    /// Set the FPF signature for the newsroom
147    pub fn set_fpf_signature(&mut self, signature: Signature<FpfOnNewsroom>) {
148        self.signature = Some(signature);
149    }
150
151    /// Get the ephemeral key count for a journalist
152    pub fn ephemeral_keys_count(&self, journalist_id: Uuid) -> usize {
153        self.storage.ephemeral_keys_count(journalist_id)
154    }
155
156    /// Check if a journalist has ephemeral keys available
157    pub fn has_ephemeral_keys(&self, journalist_id: Uuid) -> bool {
158        self.storage.has_ephemeral_keys(journalist_id)
159    }
160
161    /// Find journalist ID by verifying key
162    pub fn find_journalist_id(&self, verifying_key: &VerifyingKey) -> Option<Uuid> {
163        self.storage.find_journalist_by_verifying_key(verifying_key)
164    }
165
166    /// Check if a message exists with the given ID
167    pub fn has_message(&self, message_id: &Uuid) -> bool {
168        self.storage.get_messages().contains_key(message_id)
169    }
170
171    pub fn handle_welcome(&self) -> WelcomeBundle {
172        let newsroom_verifying_key = self
173            .newsroom_keys
174            .as_ref()
175            .expect("Newsroom keys not found")
176            .verifying_key();
177        let fpf_sig = self
178            .signature
179            .as_ref()
180            .expect("FPF signature not found")
181            .clone();
182
183        let mut journalists = Vec::new();
184        for (_id, entry) in self.storage.get_journalists().iter() {
185            let (vk, signed_longterm_key_bundle, nr_signature) = entry.clone();
186            journalists.push(JournalistLongTermView {
187                vk,
188                signed_longterm_key_bundle,
189                nr_signature,
190            });
191        }
192
193        WelcomeBundle {
194            newsroom_verifying_key,
195            fpf_sig,
196            journalists,
197        }
198    }
199
200    pub fn handle_journalist_ephemeral_keys<R: RngCore + CryptoRng>(
201        &mut self,
202        rng: &mut R,
203    ) -> Vec<JournalistEphemeralKeys> {
204        let mut responses = Vec::new();
205
206        let journalist_ephemeral_keys = self.storage.get_all_ephemeral_keys(rng);
207
208        for (journalist_id, ephemeral_bundle) in journalist_ephemeral_keys.iter() {
209            // TODO: Do something better than expect here
210            let entry = self
211                .storage
212                .get_journalists()
213                .get(journalist_id)
214                .expect("Journalist should exist in storage")
215                .clone();
216            let vk = entry.0;
217
218            responses.push(JournalistEphemeralKeys {
219                vk,
220                ephemeral: ephemeral_bundle.clone(),
221            });
222        }
223
224        responses
225    }
226
227    /// Handle message submission (step 6 for sources, step 9 for journalists)
228    pub fn handle_message_submit<R: RngCore + CryptoRng>(
229        &mut self,
230        message: Envelope,
231        rng: &mut R,
232    ) -> Result<Uuid, Error> {
233        // Generate a random message ID
234        let message_id = self.storage.deterministic_uuid(rng);
235
236        // Store the message with the generated ID
237        self.storage.add_message(message_id, message);
238
239        Ok(message_id)
240    }
241
242    /// Compute "hints"/challenges for message id fetch request (step 7)
243    pub fn handle_request_challenges<R: RngCore + CryptoRng>(
244        &self,
245        _request: MessageChallengeFetchRequest,
246        rng: &mut R,
247    ) -> Result<MessageChallengeFetchResponse, Error> {
248        let total_challenges: usize = MAX_MESSAGES;
249        let entries: Vec<_> = self
250            .storage
251            .get_messages()
252            .iter()
253            .take(total_challenges)
254            .map(|(uuid, envelope)| (*uuid.as_bytes(), envelope.clone()))
255            .collect();
256        let chall = compute_fetch_challenges(rng, &entries, total_challenges);
257
258        Ok(MessageChallengeFetchResponse {
259            count: total_challenges,
260            messages: chall,
261        })
262    }
263
264    /// Handle message fetch request (step 8/10)
265    pub fn handle_message_fetch(&self, request: MessageFetchRequest) -> Option<Envelope> {
266        self.storage
267            .get_messages()
268            .get(&request.message_id)
269            .cloned()
270    }
271}