Skip to main content

securedrop_protocol_minimal/
journalist.rs

1use crate::VerifyingKey;
2use crate::api::Client;
3use crate::message::{MessageKeyPair, MessagePublicKey, keygen as message_keygen};
4use crate::metadata::{MetadataPublicKey, keygen as metadata_keygen};
5use crate::primitives::x25519::DHPrivateKey;
6use crate::primitives::x25519::DHPublicKey;
7use crate::primitives::x25519::generate_dh_keypair;
8use crate::sign::{JournalistEphemeralKey, JournalistLongTermKey, Signature, SigningKey};
9use alloc::vec::Vec;
10use rand_core::{CryptoRng, RngCore};
11
12use crate::ciphertext::Plaintext;
13use crate::keys::*;
14use crate::traits::{Enrollable, JournalistPublic, RestrictedApi, UserPublic, UserSecret};
15
16// caution: do not re-export!
17use crate::sealed;
18impl sealed::Sealed for Journalist {}
19impl RestrictedApi for Journalist {}
20
21/// Journalists: ingredients.
22/// Journalists have a signing/verifying key, a reply key,
23/// a fetch key, and a collection of one-time signed key bundles
24pub struct Journalist {
25    signing_key: SigningKeyPair,
26    fetch_key: DhFetchKeyPair,
27    message_keys: Vec<SignedMessageKeyBundle>,
28    /// Long-term SD-APKE key tuple `(sk_J^APKE, pk_J^APKE)`
29    reply_apke: MessageKeyPair,
30    self_signature: Signature<JournalistLongTermKey>,
31    signed_longterm_key_bytes: SignedLongtermPubKeyBytes,
32    session_storage: SessionStorage,
33}
34
35// Public-facing representation of a journalist
36// used to send them a message
37pub struct JournalistPublicView {
38    vk: VerifyingKey,
39    fetch_pk: DHPublicKey,
40    reply_apke_pk: MessagePublicKey,
41    signed_longterm_key_bytes: SignedLongtermPubKeyBytes,
42    selfsig: Signature<JournalistLongTermKey>,
43    kb: SignedKeyBundlePublic,
44}
45
46impl JournalistPublicView {
47    pub fn new(
48        vk: VerifyingKey,
49        fetch: DHPublicKey,
50        reply_apke: MessagePublicKey,
51        selfsig: Signature<JournalistLongTermKey>,
52        signed_longterm_key_bytes: SignedLongtermPubKeyBytes,
53        kb: SignedKeyBundlePublic,
54    ) -> Self {
55        Self {
56            vk,
57            fetch_pk: fetch,
58            reply_apke_pk: reply_apke,
59            selfsig,
60            signed_longterm_key_bytes,
61            kb,
62        }
63    }
64}
65
66impl UserPublic for JournalistPublicView {
67    fn fetch_pk(&self) -> &DHPublicKey {
68        &self.fetch_pk
69    }
70
71    fn message_auth_pk(&self) -> &MessagePublicKey {
72        &self.reply_apke_pk
73    }
74
75    fn message_metadata_pk(&self) -> &MetadataPublicKey {
76        &self.kb.0.metadata_pk
77    }
78
79    fn message_enc_pk(&self) -> &MessagePublicKey {
80        &self.kb.0.apke_pk
81    }
82}
83
84impl JournalistPublic for JournalistPublicView {
85    fn verifying_key(&self) -> &VerifyingKey {
86        &self.vk
87    }
88
89    fn self_signature(&self) -> &Signature<JournalistLongTermKey> {
90        &self.selfsig
91    }
92
93    fn signed_keybytes(&self) -> &SignedLongtermPubKeyBytes {
94        &self.signed_longterm_key_bytes
95    }
96
97    fn ephemeral_bundle(&self) -> &KeyBundlePublic {
98        &self.kb.0
99    }
100
101    fn ephemeral_signature(&self) -> &Signature<JournalistEphemeralKey> {
102        &self.kb.1
103    }
104}
105
106impl Client for Journalist {
107    fn newsroom_verifying_key(&self) -> Option<&VerifyingKey> {
108        self.session_storage.nr_key.as_ref()
109    }
110
111    fn set_newsroom_verifying_key(&mut self, key: VerifyingKey) {
112        self.session_storage.nr_key = Some(key);
113    }
114}
115
116/// Private, common to all users, implemented for Journalists
117impl UserSecret for Journalist {
118    fn num_bundles(&self) -> usize {
119        self.message_keys.len()
120    }
121
122    fn fetch_keypair(&self) -> (&DHPrivateKey, &DHPublicKey) {
123        (&self.fetch_key.sk, &self.fetch_key.pk)
124    }
125
126    fn message_auth_key(&self) -> &crate::message::MessagePrivateKey {
127        self.reply_apke.private_key()
128    }
129
130    fn message_auth_pk(&self) -> &MessagePublicKey {
131        self.reply_apke.public_key()
132    }
133
134    fn build_message(&self, message: Vec<u8>) -> Plaintext {
135        // TODO: the journalist doesn't attach their own keys,
136        // because the source pulls a fresh set of keys and verifies them
137        // in order to reply. either fill with random bytes or use
138        // another scheme (fixme)
139        Plaintext {
140            sender_fetch_key: [0u8; crate::primitives::x25519::DH_PUBLIC_KEY_LEN],
141            sender_reply_pubkey_hybrid: [0u8; crate::primitives::xwing::XWING_PUBLIC_KEY_LEN],
142            msg: message,
143        }
144    }
145
146    fn keybundles(&self) -> Vec<&MessageKeyBundle> {
147        self.message_keys
148            .iter()
149            .map(|signed| &signed.bundle)
150            .collect()
151    }
152}
153
154impl Enrollable for Journalist {
155    fn enroll(&self) -> Enrollment {
156        Enrollment {
157            bundle: self.signed_longterm_key_bytes.clone(),
158            selfsig: self.self_signature,
159            keys: (
160                self.signing_key.pk,
161                self.fetch_key.pk.clone(),
162                self.reply_apke.public_key().clone(),
163            ),
164        }
165    }
166
167    fn signed_keybundles(&self) -> Vec<SignedKeyBundlePublic> {
168        fn extract_public_bundle(signed: &SignedMessageKeyBundle) -> SignedKeyBundlePublic {
169            (signed.bundle.public(), signed.selfsig)
170        }
171
172        self.message_keys
173            .iter()
174            .map(extract_public_bundle)
175            .collect()
176    }
177
178    fn signing_key(&self) -> &VerifyingKey {
179        &self.signing_key.pk
180    }
181}
182
183impl Journalist {
184    pub fn new<R: RngCore + CryptoRng>(rng: &mut R, num_keybundles: usize) -> Self {
185        let mut key_bundles: Vec<SignedMessageKeyBundle> = Vec::with_capacity(num_keybundles);
186
187        let signing_key = SigningKey::new(&mut *rng).expect("Signing keygen failed");
188        let verifying_key = signing_key.vk;
189
190        let (sk_fetch, pk_fetch) =
191            generate_dh_keypair(&mut *rng).expect("DH Keygen (Fetch) failed");
192
193        let reply_apke = message_keygen(&mut *rng).expect("SD-APKE Keygen (Reply) failed");
194
195        // Self-sign long-term pubkeys (for enrollment).
196        // Covers pk_J^APKE = (pk_J^AKEM, pk_J^PQ) and pk_J^fetch
197        let selfsigned_pubkeys =
198            SignedLongtermPubKeyBytes::from_keys(reply_apke.public_key(), &pk_fetch);
199        let self_signature: Signature<JournalistLongTermKey> =
200            signing_key.sign(selfsigned_pubkeys.as_bytes());
201
202        // Generate one-time/short-lived keybundles.
203        for _ in 0..num_keybundles {
204            let apke_kp = message_keygen(rng).expect("SD-APKE keygen (ephemeral) failed");
205            let metadata_kp = metadata_keygen(rng).expect("Failed to generate metadata keys");
206
207            let bundle = MessageKeyBundle::new(apke_kp, metadata_kp);
208
209            let pubkey_bytes = bundle.public().as_bytes();
210            let selfsig: Signature<JournalistEphemeralKey> = signing_key.sign(&pubkey_bytes);
211
212            key_bundles.push(SignedMessageKeyBundle { bundle, selfsig });
213        }
214        assert_eq!(key_bundles.len(), num_keybundles);
215
216        let session_storage = SessionStorage {
217            fpf_key: None,
218            nr_key: None,
219            fpf_signature: None,
220        };
221
222        Self {
223            signing_key: KeyPair {
224                sk: signing_key,
225                pk: verifying_key,
226            },
227            fetch_key: KeyPair {
228                sk: sk_fetch,
229                pk: pk_fetch,
230            },
231            reply_apke,
232            message_keys: key_bundles,
233            self_signature,
234            signed_longterm_key_bytes: selfsigned_pubkeys,
235            session_storage,
236        }
237    }
238
239    pub fn public(&self, idx: usize) -> JournalistPublicView {
240        let kb = self.message_keys.get(idx).expect("Bad index");
241        JournalistPublicView::new(
242            self.signing_key.pk,
243            self.fetch_key.pk.clone(),
244            self.reply_apke.public_key().clone(),
245            self.self_signature,
246            self.signed_longterm_key_bytes.clone(),
247            (kb.bundle.public(), kb.selfsig),
248        )
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255    use crate::Enrollable;
256    use rand_chacha::ChaCha20Rng;
257    use rand_core::SeedableRng;
258
259    #[test]
260    fn test_journalist_setup() {
261        let mut rng = ChaCha20Rng::seed_from_u64(666);
262
263        let journalist = Journalist::new(&mut rng, 5);
264        assert_eq!(journalist.message_keys.len(), 5);
265        let skb: Vec<SignedKeyBundlePublic> = journalist.signed_keybundles();
266        assert_eq!(journalist.message_keys.len(), skb.len());
267
268        let kbs: Vec<&MessageKeyBundle> = journalist.keybundles();
269        assert_eq!(kbs.len(), journalist.message_keys.len());
270
271        for i in 0..kbs.len() {
272            assert_eq!(
273                journalist.message_keys[i]
274                    .bundle
275                    .apke
276                    .public_key()
277                    .as_bytes(),
278                kbs[i].apke.public_key().as_bytes()
279            );
280            assert_eq!(
281                journalist.message_keys[i]
282                    .bundle
283                    .metadata_kp
284                    .private_key()
285                    .as_bytes(),
286                kbs[i].metadata_kp.private_key().as_bytes()
287            );
288            assert_eq!(
289                journalist.message_keys[i]
290                    .bundle
291                    .metadata_kp
292                    .public_key()
293                    .as_bytes(),
294                kbs[i].metadata_kp.public_key().as_bytes()
295            );
296        }
297    }
298
299    #[test]
300    fn test_journalist_enroll_selfsig() {
301        let mut rng = ChaCha20Rng::seed_from_u64(666);
302
303        let journalist = Journalist::new(&mut rng, 5);
304        let e = journalist.enroll();
305
306        journalist
307            .signing_key()
308            .verify(e.bundle.as_bytes(), &e.selfsig)
309            .expect("Need correct enrollment sig");
310    }
311}