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