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