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
19use crate::sealed;
21
22#[cfg(not(hax))]
23impl sealed::Sealed for Journalist {}
24
25#[cfg(not(hax))]
26impl RestrictedApi for Journalist {}
27
28pub struct Journalist {
32 signing_key: SigningKeyPair,
33 fetch_key: DhFetchKeyPair,
34 message_keys: Vec<SignedMessageKeyBundle>,
35 reply_apke: MessageKeyPair,
37 self_signature: Signature<JournalistLongTermKey>,
38 signed_longterm_key_bytes: SignedLongtermPubKeyBytes,
39 session_storage: SessionStorage,
40}
41
42#[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
142impl 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 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#[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 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 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 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 #[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 #[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 #[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 #[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 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
387pub 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 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 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 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 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
448pub 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 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 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 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 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 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}