Skip to main content

securedrop_protocol_minimal/
journalist.rs

1use alloc::vec::Vec;
2use rand_core::{CryptoRng, RngCore};
3
4use crate::VerifyingKey;
5use crate::api::Client;
6use crate::ciphertext::Plaintext;
7use crate::keys::*;
8use crate::message::{
9    MessageKeyPair, MessagePrivateKey, MessagePublicKey, keygen as message_keygen,
10};
11use crate::metadata::{MetadataKeyPair, MetadataPublicKey, keygen as metadata_keygen};
12use crate::primitives::dh_akem::{DhAkemPrivateKey, DhAkemPublicKey};
13use crate::primitives::mlkem::{MLKEM768PrivateKey, MLKEM768PublicKey};
14use crate::primitives::provider;
15use crate::primitives::x25519::DHPrivateKey;
16use crate::primitives::x25519::DHPublicKey;
17use crate::primitives::x25519::generate_dh_keypair;
18use crate::sign::{JournalistEphemeralKey, JournalistLongTermKey, Signature, SigningKey};
19use crate::traits::{Enrollable, JournalistPublic, RestrictedApi, UserPublic, UserSecret};
20
21// caution: do not re-export!
22use crate::sealed;
23
24#[cfg(not(hax))]
25impl sealed::Sealed for Journalist {}
26
27#[cfg(not(hax))]
28impl RestrictedApi for Journalist {}
29
30/// Journalists: ingredients.
31/// Journalists have a signing/verifying key, a reply key,
32/// a fetch key, and a collection of one-time signed key bundles
33pub struct Journalist {
34    signing_key: SigningKeyPair,
35    fetch_key: DhFetchKeyPair,
36    message_keys: Vec<SignedMessageKeyBundle>,
37    /// Long-term SD-APKE key tuple `(sk_J^APKE, pk_J^APKE)`
38    reply_apke: MessageKeyPair,
39    self_signature: Signature<JournalistLongTermKey>,
40    signed_longterm_key_bytes: SignedLongtermPubKeyBytes,
41    session_storage: SessionStorage,
42}
43
44// Public-facing representation of a journalist
45// used to send them a message
46#[cfg_attr(not(hax), derive(serde::Serialize, serde::Deserialize))]
47pub struct JournalistPublicView {
48    vk: VerifyingKey,
49    fetch_pk: DHPublicKey,
50    reply_apke_pk: MessagePublicKey,
51    signed_longterm_key_bytes: SignedLongtermPubKeyBytes,
52    selfsig: Signature<JournalistLongTermKey>,
53    kb: SignedKeyBundlePublic,
54}
55
56impl JournalistPublicView {
57    pub fn new(
58        vk: VerifyingKey,
59        fetch: DHPublicKey,
60        reply_apke: MessagePublicKey,
61        selfsig: Signature<JournalistLongTermKey>,
62        signed_longterm_key_bytes: SignedLongtermPubKeyBytes,
63        kb: SignedKeyBundlePublic,
64    ) -> Self {
65        Self {
66            vk,
67            fetch_pk: fetch,
68            reply_apke_pk: reply_apke,
69            selfsig,
70            signed_longterm_key_bytes,
71            kb,
72        }
73    }
74}
75
76impl UserPublic for JournalistPublicView {
77    fn fetch_pk(&self) -> &DHPublicKey {
78        &self.fetch_pk
79    }
80
81    fn message_auth_pk(&self) -> &MessagePublicKey {
82        &self.reply_apke_pk
83    }
84
85    fn message_metadata_pk(&self) -> &MetadataPublicKey {
86        &self.kb.0.metadata_pk
87    }
88
89    fn message_enc_pk(&self) -> &MessagePublicKey {
90        &self.kb.0.apke_pk
91    }
92}
93
94impl JournalistPublic for JournalistPublicView {
95    fn verifying_key(&self) -> &VerifyingKey {
96        &self.vk
97    }
98
99    fn self_signature(&self) -> &Signature<JournalistLongTermKey> {
100        &self.selfsig
101    }
102
103    fn signed_keybytes(&self) -> &SignedLongtermPubKeyBytes {
104        &self.signed_longterm_key_bytes
105    }
106
107    fn ephemeral_bundle(&self) -> &KeyBundlePublic {
108        &self.kb.0
109    }
110
111    fn ephemeral_signature(&self) -> &Signature<JournalistEphemeralKey> {
112        &self.kb.1
113    }
114}
115
116impl Client for Journalist {
117    fn newsroom_verifying_key(&self) -> Option<&VerifyingKey> {
118        self.session_storage.nr_key.as_ref()
119    }
120
121    fn set_newsroom_verifying_key(&mut self, key: VerifyingKey) {
122        self.session_storage.nr_key = Some(key);
123    }
124}
125
126#[cfg_attr(hax, hax_lib::fstar::verification_status(lax))]
127fn keybundle_refs(message_keys: &[SignedMessageKeyBundle]) -> Vec<&MessageKeyBundle> {
128    let mut out = Vec::new();
129    for signed in message_keys.iter() {
130        out.push(&signed.bundle);
131    }
132    out
133}
134
135#[cfg_attr(hax, hax_lib::fstar::verification_status(lax))]
136fn signed_keybundle_publics(message_keys: &[SignedMessageKeyBundle]) -> Vec<SignedKeyBundlePublic> {
137    let mut out = Vec::new();
138    for signed in message_keys.iter() {
139        out.push((signed.bundle.public(), signed.selfsig));
140    }
141    out
142}
143
144/// Private, common to all users, implemented for Journalists
145impl UserSecret for Journalist {
146    fn num_bundles(&self) -> usize {
147        self.message_keys.len()
148    }
149
150    fn fetch_keypair(&self) -> (&DHPrivateKey, &DHPublicKey) {
151        (&self.fetch_key.sk, &self.fetch_key.pk)
152    }
153
154    fn message_auth_key(&self) -> &crate::message::MessagePrivateKey {
155        self.reply_apke.private_key()
156    }
157
158    fn own_message_auth_pk(&self) -> &MessagePublicKey {
159        self.reply_apke.public_key()
160    }
161
162    fn build_message(&self, message: Vec<u8>) -> Plaintext {
163        // TODO: the journalist doesn't attach their own keys,
164        // because the source pulls a fresh set of keys and verifies them
165        // in order to reply. either fill with random bytes or use
166        // another scheme (fixme)
167        Plaintext {
168            sender_fetch_key: [0u8; crate::primitives::x25519::DH_PUBLIC_KEY_LEN],
169            sender_reply_pubkey_hybrid: [0u8; crate::primitives::xwing::XWING_PUBLIC_KEY_LEN],
170            msg: message,
171        }
172    }
173
174    fn keybundles(&self) -> Vec<&MessageKeyBundle> {
175        keybundle_refs(&self.message_keys)
176    }
177}
178
179impl Enrollable for Journalist {
180    fn enroll(&self) -> Enrollment {
181        Enrollment {
182            bundle: self.signed_longterm_key_bytes.clone(),
183            selfsig: self.self_signature,
184            keys: (
185                self.signing_key.pk,
186                self.fetch_key.pk.clone(),
187                self.reply_apke.public_key().clone(),
188            ),
189        }
190    }
191
192    fn signed_keybundles(&self) -> Vec<SignedKeyBundlePublic> {
193        signed_keybundle_publics(&self.message_keys)
194    }
195
196    fn signing_key(&self) -> &VerifyingKey {
197        &self.signing_key.pk
198    }
199}
200
201/// Generate one ephemeral key bundle and sign its pubkeys.
202#[cfg_attr(hax, hax_lib::fstar::verification_status(lax))]
203fn make_signed_bundle<R: RngCore + CryptoRng>(
204    rng: &mut R,
205    signing_key: &SigningKey,
206) -> SignedMessageKeyBundle {
207    let apke_kp = message_keygen(rng).expect("SD-APKE ephemeral keygen failed");
208    let metadata_kp = metadata_keygen(rng).expect("Failed to generate metadata keys");
209
210    let bundle = MessageKeyBundle::new(apke_kp, metadata_kp);
211
212    let pubkey_bytes = bundle.public().as_bytes();
213    let selfsig: Signature<JournalistEphemeralKey> = signing_key.sign(&pubkey_bytes);
214
215    SignedMessageKeyBundle { bundle, selfsig }
216}
217
218impl Journalist {
219    #[cfg_attr(hax, hax_lib::opaque)]
220    pub fn new<R: RngCore + CryptoRng>(rng: &mut R, num_keybundles: usize) -> Self {
221        let mut key_bundles: Vec<SignedMessageKeyBundle> = Vec::with_capacity(num_keybundles);
222
223        let signing_key = SigningKey::new(rng).expect("Signing keygen failed");
224        let verifying_key = signing_key.vk;
225
226        let (sk_fetch, pk_fetch) = generate_dh_keypair(rng).expect("DH Keygen (Fetch) failed");
227
228        let reply_apke = message_keygen(rng).expect("SD-APKE Keygen (Reply) failed");
229
230        // Self-sign long-term pubkeys (for enrollment).
231        // Covers pk_J^APKE = (pk_J^AKEM, pk_J^PQ) and pk_J^fetch
232        let selfsigned_pubkeys =
233            SignedLongtermPubKeyBytes::from_keys(reply_apke.public_key(), &pk_fetch);
234        let self_signature: Signature<JournalistLongTermKey> =
235            signing_key.sign(selfsigned_pubkeys.as_bytes());
236
237        // Generate one-time/short-lived keybundles.
238        for _ in 0..num_keybundles {
239            key_bundles.push(make_signed_bundle(rng, &signing_key));
240        }
241        assert_eq!(key_bundles.len(), num_keybundles);
242
243        let session_storage = SessionStorage {
244            fpf_key: None,
245            nr_key: None,
246            fpf_signature: None,
247        };
248
249        Self {
250            signing_key: KeyPair {
251                sk: signing_key,
252                pk: verifying_key,
253            },
254            fetch_key: KeyPair {
255                sk: sk_fetch,
256                pk: pk_fetch,
257            },
258            reply_apke,
259            message_keys: key_bundles,
260            self_signature,
261            signed_longterm_key_bytes: selfsigned_pubkeys,
262            session_storage,
263        }
264    }
265
266    #[cfg_attr(hax, hax_lib::opaque)]
267    pub fn public(&self, idx: usize) -> JournalistPublicView {
268        let kb = self.message_keys.get(idx).expect("Bad index");
269        JournalistPublicView::new(
270            self.signing_key.pk,
271            self.fetch_key.pk.clone(),
272            self.reply_apke.public_key().clone(),
273            self.self_signature,
274            self.signed_longterm_key_bytes.clone(),
275            (kb.bundle.public(), kb.selfsig),
276        )
277    }
278
279    /// Extract the long-term keypairs as raw bytes, sufficient to
280    /// reconstruct the long-term Journalist state via
281    /// [`Journalist::from_long_term_bytes`].
282    pub fn long_term_bytes(&self) -> JournalistLongTermBytes {
283        JournalistLongTermBytes {
284            sig_seed: self.signing_key.sk.as_bytes(),
285            fetch_sk: *self.fetch_key.sk.as_bytes(),
286            apke_dhakem_sk: *self.reply_apke.private_key().dhakem.as_bytes(),
287            apke_mlkem_sk: *self.reply_apke.private_key().mlkem.as_bytes(),
288            apke_mlkem_pk: *self.reply_apke.public_key().mlkem.as_bytes(),
289        }
290    }
291
292    /// Reconstruct the long-term Journalist state from raw key bytes.
293    #[cfg_attr(hax, hax_lib::opaque)]
294    pub fn from_long_term_bytes(parts: JournalistLongTermBytes) -> Self {
295        use crate::message::{MessagePrivateKey, MessagePublicKey};
296        use crate::primitives::dh_akem::{DhAkemPrivateKey, DhAkemPublicKey};
297        use crate::primitives::mlkem::{MLKEM768PrivateKey, MLKEM768PublicKey};
298        use crate::primitives::provider;
299        use crate::primitives::x25519::{DHPrivateKey, dh_public_key_from_scalar};
300
301        let signing_key = SigningKey::from_seed(parts.sig_seed);
302        let verifying_key = signing_key.vk;
303
304        let sk_fetch = DHPrivateKey::from_bytes(parts.fetch_sk);
305        let pk_fetch = dh_public_key_from_scalar(parts.fetch_sk);
306
307        let mut apke_dhakem_pk_bytes = [0u8; 32];
308        provider::curve25519::secret_to_public(&mut apke_dhakem_pk_bytes, &parts.apke_dhakem_sk);
309        let apke_dhakem_sk = DhAkemPrivateKey::from_bytes(parts.apke_dhakem_sk);
310        let apke_dhakem_pk = DhAkemPublicKey::from_bytes(apke_dhakem_pk_bytes);
311        let apke_mlkem_sk = MLKEM768PrivateKey::from_bytes(parts.apke_mlkem_sk);
312        let apke_mlkem_pk = MLKEM768PublicKey::from_bytes(parts.apke_mlkem_pk);
313
314        let reply_apke = MessageKeyPair::new(
315            MessagePrivateKey {
316                dhakem: apke_dhakem_sk,
317                mlkem: apke_mlkem_sk,
318            },
319            MessagePublicKey {
320                dhakem: apke_dhakem_pk,
321                mlkem: apke_mlkem_pk,
322            },
323        );
324
325        let signed_longterm_key_bytes =
326            SignedLongtermPubKeyBytes::from_keys(reply_apke.public_key(), &pk_fetch);
327        let self_signature: Signature<JournalistLongTermKey> =
328            signing_key.sign(signed_longterm_key_bytes.as_bytes());
329
330        Self {
331            signing_key: KeyPair {
332                sk: signing_key,
333                pk: verifying_key,
334            },
335            fetch_key: KeyPair {
336                sk: sk_fetch,
337                pk: pk_fetch,
338            },
339            reply_apke,
340            message_keys: Vec::new(),
341            self_signature,
342            signed_longterm_key_bytes,
343            session_storage: SessionStorage {
344                fpf_key: None,
345                nr_key: None,
346                fpf_signature: None,
347            },
348        }
349    }
350
351    /// Generate `n` fresh signed ephemeral key bundles and retain them in memory.
352    ///
353    /// The public halves are uploaded to the server via
354    /// [`create_ephemeral_key_request`](crate::api::JournalistApi::create_ephemeral_key_request).
355    ///
356    /// The secret halves should be persisted via [`Journalist::ephemeral_bundle_bytes`].
357    #[cfg_attr(hax, hax_lib::opaque)]
358    pub fn generate_ephemeral_bundles<R: RngCore + CryptoRng>(&mut self, rng: &mut R, n: usize) {
359        for _ in 0..n {
360            let signed = make_signed_bundle(rng, &self.signing_key.sk);
361            self.message_keys.push(signed);
362        }
363    }
364
365    /// Extract the secret halves of the retained ephemeral key bundles so we can
366    /// reconstruct them via [`Journalist::load_ephemeral_bundles`].
367    ///
368    /// Used by the demo.
369    #[cfg_attr(hax, hax_lib::opaque)]
370    pub fn ephemeral_bundle_bytes(&self) -> Vec<EphemeralBundleBytes> {
371        self.message_keys
372            .iter()
373            .map(|signed| EphemeralBundleBytes::from_bundle(&signed.bundle))
374            .collect()
375    }
376
377    /// Reconstruct ephemeral key bundles from persisted secret bytes.
378    ///
379    /// Used by the demo
380    #[cfg_attr(hax, hax_lib::opaque)]
381    pub fn load_ephemeral_bundles(&mut self, bundles: Vec<EphemeralBundleBytes>) {
382        for bytes in bundles {
383            let bundle = bytes.into_bundle();
384            let pubkey_bytes = bundle.public().as_bytes();
385            // Temp: doing this just because we are generating SignedMessageKeyBundle here
386            // and we didnt persist the signature
387            let selfsig: Signature<JournalistEphemeralKey> =
388                self.signing_key.sk.sign(&pubkey_bytes);
389            self.message_keys
390                .push(SignedMessageKeyBundle { bundle, selfsig });
391        }
392    }
393}
394
395/// Byte representation of a [`Journalist`]'s long-term keypairs, sufficient
396/// to reconstruct the long-term state via
397/// [`Journalist::from_long_term_bytes`].
398pub struct JournalistLongTermBytes {
399    pub sig_seed: [u8; 32],
400    pub fetch_sk: [u8; 32],
401    pub apke_dhakem_sk: [u8; 32],
402    pub apke_mlkem_sk: [u8; crate::primitives::mlkem::MLKEM768_PRIVATE_KEY_LEN],
403    pub apke_mlkem_pk: [u8; crate::primitives::mlkem::MLKEM768_PUBLIC_KEY_LEN],
404}
405
406impl JournalistLongTermBytes {
407    /// Serialized length of `sig_seed || fetch_sk || apke_dhakem_sk || apke_mlkem_sk || apke_mlkem_pk`.
408    pub const LEN: usize = 32
409        + 32
410        + 32
411        + crate::primitives::mlkem::MLKEM768_PRIVATE_KEY_LEN
412        + crate::primitives::mlkem::MLKEM768_PUBLIC_KEY_LEN;
413
414    /// Serialize as `sig_seed || fetch_sk || apke_dhakem_sk || apke_mlkem_sk || apke_mlkem_pk`.
415    pub fn as_bytes(&self) -> Vec<u8> {
416        let mut out = Vec::with_capacity(Self::LEN);
417        out.extend_from_slice(&self.sig_seed);
418        out.extend_from_slice(&self.fetch_sk);
419        out.extend_from_slice(&self.apke_dhakem_sk);
420        out.extend_from_slice(&self.apke_mlkem_sk);
421        out.extend_from_slice(&self.apke_mlkem_pk);
422        out
423    }
424
425    /// Deserialize from `sig_seed || fetch_sk || apke_dhakem_sk || apke_mlkem_sk || apke_mlkem_pk` bytes.
426    ///
427    /// # Errors
428    ///
429    /// Returns an error if the byte slice has the incorrect length.
430    pub fn from_bytes(bytes: &[u8]) -> Result<Self, anyhow::Error> {
431        if bytes.len() != Self::LEN {
432            return Err(anyhow::anyhow!(
433                "Invalid JournalistLongTermBytes length: expected {}, got {}",
434                Self::LEN,
435                bytes.len()
436            ));
437        }
438
439        let (sig_seed, rest) = bytes.split_at(32);
440        let (fetch_sk, rest) = rest.split_at(32);
441        let (apke_dhakem_sk, rest) = rest.split_at(32);
442        let (apke_mlkem_sk, apke_mlkem_pk) =
443            rest.split_at(crate::primitives::mlkem::MLKEM768_PRIVATE_KEY_LEN);
444
445        // the expects here are fine because the length check above ensures we have the correct length
446        Ok(Self {
447            sig_seed: sig_seed.try_into().expect("wrong checked length"),
448            fetch_sk: fetch_sk.try_into().expect("wrong checked length"),
449            apke_dhakem_sk: apke_dhakem_sk.try_into().expect("wrong checked length"),
450            apke_mlkem_sk: apke_mlkem_sk.try_into().expect("wrong checked length"),
451            apke_mlkem_pk: apke_mlkem_pk.try_into().expect("wrong checked length"),
452        })
453    }
454}
455
456/// Byte representation of one ephemeral key bundle's secret halves
457pub struct EphemeralBundleBytes {
458    pub apke_dhakem_sk: [u8; 32],
459    pub apke_mlkem_sk: [u8; crate::primitives::mlkem::MLKEM768_PRIVATE_KEY_LEN],
460    pub apke_mlkem_pk: [u8; crate::primitives::mlkem::MLKEM768_PUBLIC_KEY_LEN],
461    pub metadata_sk: [u8; crate::primitives::xwing::XWING_PRIVATE_KEY_LEN],
462    pub metadata_pk: [u8; crate::primitives::xwing::XWING_PUBLIC_KEY_LEN],
463}
464
465impl EphemeralBundleBytes {
466    /// Serialized length of
467    /// `apke_dhakem_sk || apke_mlkem_sk || apke_mlkem_pk || metadata_sk || metadata_pk`.
468    pub const LEN: usize = 32
469        + crate::primitives::mlkem::MLKEM768_PRIVATE_KEY_LEN
470        + crate::primitives::mlkem::MLKEM768_PUBLIC_KEY_LEN
471        + crate::primitives::xwing::XWING_PRIVATE_KEY_LEN
472        + crate::primitives::xwing::XWING_PUBLIC_KEY_LEN;
473
474    fn from_bundle(bundle: &MessageKeyBundle) -> Self {
475        Self {
476            apke_dhakem_sk: *bundle.apke.private_key().dhakem.as_bytes(),
477            apke_mlkem_sk: *bundle.apke.private_key().mlkem.as_bytes(),
478            apke_mlkem_pk: *bundle.apke.public_key().mlkem.as_bytes(),
479            metadata_sk: *bundle.metadata_kp.secret_bytes(),
480            metadata_pk: *bundle.metadata_kp.public_bytes(),
481        }
482    }
483
484    #[cfg_attr(hax, hax_lib::opaque)]
485    fn into_bundle(self) -> MessageKeyBundle {
486        let mut apke_dhakem_pk_bytes = [0u8; 32];
487        provider::curve25519::secret_to_public(&mut apke_dhakem_pk_bytes, &self.apke_dhakem_sk);
488
489        let apke = MessageKeyPair::new(
490            MessagePrivateKey {
491                dhakem: DhAkemPrivateKey::from_bytes(self.apke_dhakem_sk),
492                mlkem: MLKEM768PrivateKey::from_bytes(self.apke_mlkem_sk),
493            },
494            MessagePublicKey {
495                dhakem: DhAkemPublicKey::from_bytes(apke_dhakem_pk_bytes),
496                mlkem: MLKEM768PublicKey::from_bytes(self.apke_mlkem_pk),
497            },
498        );
499        let metadata_kp = MetadataKeyPair::from_key_bytes(self.metadata_sk, self.metadata_pk);
500
501        MessageKeyBundle::new(apke, metadata_kp)
502    }
503
504    /// Serialize as
505    /// `apke_dhakem_sk || apke_mlkem_sk || apke_mlkem_pk || metadata_sk || metadata_pk`.
506    pub fn as_bytes(&self) -> Vec<u8> {
507        let mut out = Vec::with_capacity(Self::LEN);
508        out.extend_from_slice(&self.apke_dhakem_sk);
509        out.extend_from_slice(&self.apke_mlkem_sk);
510        out.extend_from_slice(&self.apke_mlkem_pk);
511        out.extend_from_slice(&self.metadata_sk);
512        out.extend_from_slice(&self.metadata_pk);
513        out
514    }
515
516    /// Deserialize from
517    /// `apke_dhakem_sk || apke_mlkem_sk || apke_mlkem_pk || metadata_sk || metadata_pk` bytes.
518    ///
519    /// # Errors
520    ///
521    /// Returns an error if the byte slice has the incorrect length.
522    pub fn from_bytes(bytes: &[u8]) -> Result<Self, anyhow::Error> {
523        if bytes.len() != Self::LEN {
524            return Err(anyhow::anyhow!(
525                "Invalid EphemeralBundleBytes length: expected {}, got {}",
526                Self::LEN,
527                bytes.len()
528            ));
529        }
530
531        let (apke_dhakem_sk, rest) = bytes.split_at(32);
532        let (apke_mlkem_sk, rest) =
533            rest.split_at(crate::primitives::mlkem::MLKEM768_PRIVATE_KEY_LEN);
534        let (apke_mlkem_pk, rest) =
535            rest.split_at(crate::primitives::mlkem::MLKEM768_PUBLIC_KEY_LEN);
536        let (metadata_sk, metadata_pk) =
537            rest.split_at(crate::primitives::xwing::XWING_PRIVATE_KEY_LEN);
538
539        // the expects here are fine bc the length check above ensures we have the correct length
540        Ok(Self {
541            apke_dhakem_sk: apke_dhakem_sk.try_into().expect("wrong checked length"),
542            apke_mlkem_sk: apke_mlkem_sk.try_into().expect("wrong checked length"),
543            apke_mlkem_pk: apke_mlkem_pk.try_into().expect("wrong checked length"),
544            metadata_sk: metadata_sk.try_into().expect("wrong checked length"),
545            metadata_pk: metadata_pk.try_into().expect("wrong checked length"),
546        })
547    }
548}
549
550#[cfg(test)]
551mod tests {
552    use super::*;
553    use crate::Enrollable;
554    use crate::api::JournalistApi;
555    use crate::wire::setup::{JournalistEphemeralKeyRequest, JournalistSetupRequest};
556    use rand_chacha::ChaCha20Rng;
557    use rand_core::SeedableRng;
558
559    #[test]
560    fn test_journalist_setup_request_serde_roundtrip() {
561        let mut rng = ChaCha20Rng::seed_from_u64(7);
562        let journalist = Journalist::new(&mut rng, 0);
563        let req = JournalistSetupRequest {
564            enrollment: journalist.enroll(),
565        };
566        let json = serde_json::to_string(&req).expect("serialize");
567        let restored: JournalistSetupRequest = serde_json::from_str(&json).expect("deserialize");
568        assert_eq!(
569            req.enrollment.bundle.as_bytes(),
570            restored.enrollment.bundle.as_bytes()
571        );
572        assert_eq!(
573            req.enrollment.selfsig.as_bytes(),
574            restored.enrollment.selfsig.as_bytes()
575        );
576        assert_eq!(
577            req.enrollment.keys.0.into_bytes(),
578            restored.enrollment.keys.0.into_bytes()
579        );
580        assert_eq!(
581            req.enrollment.keys.1.into_bytes(),
582            restored.enrollment.keys.1.into_bytes()
583        );
584        assert_eq!(
585            req.enrollment.keys.2.as_bytes(),
586            restored.enrollment.keys.2.as_bytes()
587        );
588    }
589
590    #[test]
591    fn test_journalist_setup() {
592        let mut rng = ChaCha20Rng::seed_from_u64(666);
593
594        let journalist = Journalist::new(&mut rng, 5);
595        assert_eq!(journalist.message_keys.len(), 5);
596        let skb: Vec<SignedKeyBundlePublic> = journalist.signed_keybundles();
597        assert_eq!(journalist.message_keys.len(), skb.len());
598
599        let kbs: Vec<&MessageKeyBundle> = journalist.keybundles();
600        assert_eq!(kbs.len(), journalist.message_keys.len());
601
602        for i in 0..kbs.len() {
603            assert_eq!(
604                journalist.message_keys[i]
605                    .bundle
606                    .apke
607                    .public_key()
608                    .as_bytes(),
609                kbs[i].apke.public_key().as_bytes()
610            );
611            assert_eq!(
612                journalist.message_keys[i]
613                    .bundle
614                    .metadata_kp
615                    .private_key()
616                    .as_bytes(),
617                kbs[i].metadata_kp.private_key().as_bytes()
618            );
619            assert_eq!(
620                journalist.message_keys[i]
621                    .bundle
622                    .metadata_kp
623                    .public_key()
624                    .as_bytes(),
625                kbs[i].metadata_kp.public_key().as_bytes()
626            );
627        }
628    }
629
630    #[test]
631    fn test_journalist_enroll_selfsig() {
632        let mut rng = ChaCha20Rng::seed_from_u64(666);
633
634        let journalist = Journalist::new(&mut rng, 5);
635        let e = journalist.enroll();
636
637        journalist
638            .signing_key()
639            .verify(e.bundle.as_bytes(), &e.selfsig)
640            .expect("Need correct enrollment sig");
641    }
642
643    use proptest::prelude::*;
644
645    proptest! {
646        #[test]
647        fn test_journalist_long_term_bytes_roundtrip(rng_seed: u64) {
648            let mut rng = ChaCha20Rng::seed_from_u64(rng_seed);
649            let original = Journalist::new(&mut rng, 0);
650            let parts = original.long_term_bytes();
651            let restored = Journalist::from_long_term_bytes(parts);
652
653            // Long-term verifying key and self-signature must match.
654            prop_assert_eq!(
655                original.signing_key.pk.into_bytes(),
656                restored.signing_key.pk.into_bytes()
657            );
658            prop_assert_eq!(
659                original.signed_longterm_key_bytes.as_bytes(),
660                restored.signed_longterm_key_bytes.as_bytes()
661            );
662            prop_assert_eq!(
663                original.self_signature.as_bytes(),
664                restored.self_signature.as_bytes()
665            );
666            prop_assert!(restored.message_keys.is_empty());
667        }
668
669        #[test]
670        fn test_journalist_long_term_bytes_serde_roundtrip(rng_seed: u64) {
671            let mut rng = ChaCha20Rng::seed_from_u64(rng_seed);
672            let parts = Journalist::new(&mut rng, 0).long_term_bytes();
673
674            let bytes = parts.as_bytes();
675            prop_assert_eq!(bytes.len(), JournalistLongTermBytes::LEN);
676
677            let restored = JournalistLongTermBytes::from_bytes(&bytes).expect("valid length");
678            prop_assert_eq!(restored.sig_seed, parts.sig_seed);
679            prop_assert_eq!(restored.fetch_sk, parts.fetch_sk);
680            prop_assert_eq!(restored.apke_dhakem_sk, parts.apke_dhakem_sk);
681            prop_assert_eq!(restored.apke_mlkem_sk, parts.apke_mlkem_sk);
682            prop_assert_eq!(restored.apke_mlkem_pk, parts.apke_mlkem_pk);
683        }
684
685        #[test]
686        fn test_ephemeral_bundle_bytes_roundtrip(rng_seed: u64, n in 0usize..4) {
687            let mut rng = ChaCha20Rng::seed_from_u64(rng_seed);
688            let mut original = Journalist::new(&mut rng, 0);
689            original.generate_ephemeral_bundles(&mut rng, n);
690
691            let persisted: Vec<EphemeralBundleBytes> = original
692                .ephemeral_bundle_bytes()
693                .into_iter()
694                .map(|b| {
695                    let bytes = b.as_bytes();
696                    prop_assert_eq!(bytes.len(), EphemeralBundleBytes::LEN);
697                    Ok(EphemeralBundleBytes::from_bytes(&bytes).expect("valid length"))
698                })
699                .collect::<Result<_, TestCaseError>>()?;
700
701            let mut restored = Journalist::from_long_term_bytes(original.long_term_bytes());
702            restored.load_ephemeral_bundles(persisted);
703
704            let orig_pub = original.signed_keybundles();
705            let restored_pub = restored.signed_keybundles();
706            prop_assert_eq!(orig_pub.len(), n);
707            prop_assert_eq!(restored_pub.len(), n);
708            for (a, b) in orig_pub.iter().zip(restored_pub.iter()) {
709                prop_assert_eq!(a.0.as_bytes(), b.0.as_bytes());
710                prop_assert_eq!(a.1.as_bytes(), b.1.as_bytes());
711            }
712        }
713
714        #[test]
715        fn test_journalist_ephemeral_key_request_serde_roundtrip(rng_seed: u64, n in 1usize..4) {
716            let mut rng = ChaCha20Rng::seed_from_u64(rng_seed);
717            let mut journalist = Journalist::new(&mut rng, 0);
718            journalist.generate_ephemeral_bundles(&mut rng, n);
719
720            let req = journalist.create_ephemeral_key_request();
721            let json = serde_json::to_string(&req).expect("serialize");
722            let restored: JournalistEphemeralKeyRequest =
723                serde_json::from_str(&json).expect("deserialize");
724
725            prop_assert_eq!(
726                req.verifying_key.into_bytes(),
727                restored.verifying_key.into_bytes()
728            );
729            prop_assert_eq!(req.bundles.len(), n);
730            prop_assert_eq!(restored.bundles.len(), n);
731            for (a, b) in req.bundles.iter().zip(restored.bundles.iter()) {
732                prop_assert_eq!(a.0.as_bytes(), b.0.as_bytes());
733                prop_assert_eq!(a.1.as_bytes(), b.1.as_bytes());
734            }
735        }
736    }
737
738    #[test]
739    fn test_journalist_long_term_bytes_from_bytes_rejects_wrong_length() {
740        assert!(JournalistLongTermBytes::from_bytes(&[]).is_err());
741        assert!(
742            JournalistLongTermBytes::from_bytes(&[0u8; JournalistLongTermBytes::LEN - 1]).is_err()
743        );
744        assert!(
745            JournalistLongTermBytes::from_bytes(&[0u8; JournalistLongTermBytes::LEN + 1]).is_err()
746        );
747    }
748}