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