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