1use std::fmt::Debug;
15
16use derive_where::derive_where;
17use partial_default::PartialDefault;
18use poksho::ShoApi;
19use rayon::iter::{IndexedParallelIterator as _, ParallelIterator as _};
20use serde::{Deserialize, Serialize};
21use zkcredential::attributes::Attribute as _;
22
23use crate::common::array_utils;
24use crate::common::serialization::ReservedByte;
25use crate::crypto::uid_encryption;
26use crate::groups::{GroupSecretParams, UuidCiphertext};
27use crate::{
28 RandomnessBytes, SECONDS_PER_DAY, Timestamp, ZkGroupDeserializationFailure,
29 ZkGroupVerificationFailure, crypto,
30};
31
32const SECONDS_PER_HOUR: u64 = 60 * 60;
33
34#[derive(Clone, Serialize, Deserialize, PartialDefault)]
40pub struct GroupSendDerivedKeyPair {
41 reserved: ReservedByte,
42 key_pair: zkcredential::endorsements::ServerDerivedKeyPair,
43 expiration: Timestamp,
44}
45
46impl GroupSendDerivedKeyPair {
47 fn tag_info(expiration: Timestamp) -> impl poksho::ShoApi + Clone {
50 let mut sho = poksho::ShoHmacSha256::new(b"20240215_Signal_GroupSendEndorsement");
51 sho.absorb_and_ratchet(&expiration.to_be_bytes());
52 sho
53 }
54
55 pub fn for_expiration(
57 expiration: Timestamp,
58 root: impl AsRef<zkcredential::endorsements::ServerRootKeyPair>,
59 ) -> Self {
60 Self {
61 reserved: ReservedByte::default(),
62 key_pair: root.as_ref().derive_key(Self::tag_info(expiration)),
63 expiration,
64 }
65 }
66}
67
68#[derive(Clone, Serialize, Deserialize, PartialDefault, Debug)]
73pub struct GroupSendEndorsementsResponse {
74 reserved: ReservedByte,
75 endorsements: zkcredential::endorsements::EndorsementResponse,
76 expiration: Timestamp,
77}
78
79impl GroupSendEndorsementsResponse {
80 pub fn default_expiration(current_time: Timestamp) -> Timestamp {
81 let current_time_in_seconds = current_time.epoch_seconds();
84 let start_of_day = current_time_in_seconds - (current_time_in_seconds % SECONDS_PER_DAY);
85 let mut expiration = start_of_day + 2 * SECONDS_PER_DAY;
86 if (expiration - current_time_in_seconds) < SECONDS_PER_DAY + SECONDS_PER_HOUR {
87 expiration += SECONDS_PER_DAY;
88 }
89 Timestamp::from_epoch_seconds(expiration)
90 }
91
92 fn sort_points(points: &mut [(usize, curve25519_dalek_signal::RistrettoPoint)]) {
99 debug_assert!(points.iter().enumerate().all(|(i, (j, _))| i == *j));
100 let sort_keys = curve25519_dalek_signal::RistrettoPoint::double_and_compress_batch(
101 points.iter().map(|(_i, point)| point),
102 );
103 points.sort_unstable_by_key(|(i, _point)| sort_keys[*i].as_bytes());
104 }
105
106 pub fn issue(
110 member_ciphertexts: impl IntoIterator<Item = UuidCiphertext>,
111 key_pair: &GroupSendDerivedKeyPair,
112 randomness: RandomnessBytes,
113 ) -> Self {
114 let mut points_to_sign: Vec<(usize, curve25519_dalek_signal::RistrettoPoint)> =
118 member_ciphertexts
119 .into_iter()
120 .map(|ciphertext| ciphertext.ciphertext.as_points()[0])
121 .enumerate()
122 .collect();
123 Self::sort_points(&mut points_to_sign);
124
125 let endorsements = zkcredential::endorsements::EndorsementResponse::issue(
126 points_to_sign.iter().map(|(_i, point)| *point),
127 &key_pair.key_pair,
128 randomness,
129 );
130
131 Self {
136 reserved: ReservedByte::default(),
137 endorsements,
138 expiration: key_pair.expiration,
139 }
140 }
141
142 pub fn expiration(&self) -> Timestamp {
144 self.expiration
145 }
146
147 fn derive_public_signing_key_from_expiration(
154 &self,
155 now: Timestamp,
156 root_public_key: impl AsRef<zkcredential::endorsements::ServerRootPublicKey>,
157 ) -> Result<zkcredential::endorsements::ServerDerivedPublicKey, ZkGroupVerificationFailure>
158 {
159 if !self.expiration.is_day_aligned() {
160 return Err(ZkGroupVerificationFailure);
163 }
164 let time_remaining_in_seconds = self.expiration.saturating_seconds_since(now);
165 if time_remaining_in_seconds < 2 * SECONDS_PER_HOUR {
166 return Err(ZkGroupVerificationFailure);
170 }
171 if time_remaining_in_seconds > 7 * SECONDS_PER_DAY {
172 return Err(ZkGroupVerificationFailure);
175 }
176
177 Ok(root_public_key
178 .as_ref()
179 .derive_key(GroupSendDerivedKeyPair::tag_info(self.expiration)))
180 }
181
182 pub fn receive_with_service_ids_single_threaded(
188 self,
189 user_ids: impl IntoIterator<Item = libsignal_core::ServiceId>,
190 now: Timestamp,
191 group_params: &GroupSecretParams,
192 root_public_key: impl AsRef<zkcredential::endorsements::ServerRootPublicKey>,
193 ) -> Result<Vec<ReceivedEndorsement>, ZkGroupVerificationFailure> {
194 let derived_key = self.derive_public_signing_key_from_expiration(now, root_public_key)?;
195
196 let uid_sho_seed = crypto::uid_struct::UidStruct::seed_M1();
201 let mut member_points: Vec<(usize, curve25519_dalek_signal::RistrettoPoint)> = user_ids
202 .into_iter()
203 .map(|user_id| {
204 group_params.uid_enc_key_pair.a1
205 * crypto::uid_struct::UidStruct::calc_M1(uid_sho_seed.clone(), user_id)
206 })
207 .enumerate()
208 .collect();
209 Self::sort_points(&mut member_points);
210
211 let endorsements = self
212 .endorsements
213 .receive(member_points.iter().map(|(_i, point)| *point), &derived_key)
214 .map_err(|_| ZkGroupVerificationFailure)?;
215
216 Ok(array_utils::collect_permutation(
217 endorsements
218 .compressed
219 .into_iter()
220 .zip(endorsements.decompressed)
221 .map(|(compressed, decompressed)| ReceivedEndorsement {
222 compressed: GroupSendEndorsement {
223 reserved: ReservedByte::default(),
224 endorsement: compressed,
225 },
226 decompressed: GroupSendEndorsement {
227 reserved: ReservedByte::default(),
228 endorsement: decompressed,
229 },
230 })
231 .zip(member_points.iter().map(|(i, _)| *i)),
232 ))
233 }
234
235 pub fn receive_with_service_ids<T>(
243 self,
244 user_ids: T,
245 now: Timestamp,
246 group_params: &GroupSecretParams,
247 root_public_key: impl AsRef<zkcredential::endorsements::ServerRootPublicKey>,
248 ) -> Result<Vec<ReceivedEndorsement>, ZkGroupVerificationFailure>
249 where
250 T: rayon::iter::IntoParallelIterator<
251 Item = libsignal_core::ServiceId,
252 Iter: rayon::iter::IndexedParallelIterator,
253 >,
254 {
255 let derived_key = self.derive_public_signing_key_from_expiration(now, root_public_key)?;
256
257 let uid_sho_seed = crypto::uid_struct::UidStruct::seed_M1();
262 let mut member_points: Vec<(usize, curve25519_dalek_signal::RistrettoPoint)> = user_ids
263 .into_par_iter()
264 .map(|user_id| {
265 group_params.uid_enc_key_pair.a1
266 * crypto::uid_struct::UidStruct::calc_M1(uid_sho_seed.clone(), user_id)
267 })
268 .enumerate()
269 .collect();
270 Self::sort_points(&mut member_points);
271
272 let endorsements = self
273 .endorsements
274 .receive(member_points.iter().map(|(_i, point)| *point), &derived_key)
275 .map_err(|_| ZkGroupVerificationFailure)?;
276
277 Ok(array_utils::collect_permutation(
278 endorsements
279 .compressed
280 .into_iter()
281 .zip(endorsements.decompressed)
282 .map(|(compressed, decompressed)| ReceivedEndorsement {
283 compressed: GroupSendEndorsement {
284 reserved: ReservedByte::default(),
285 endorsement: compressed,
286 },
287 decompressed: GroupSendEndorsement {
288 reserved: ReservedByte::default(),
289 endorsement: decompressed,
290 },
291 })
292 .zip(member_points.iter().map(|(i, _)| *i)),
293 ))
294 }
295
296 pub fn receive_with_ciphertexts(
305 self,
306 member_ciphertexts: impl IntoIterator<Item = UuidCiphertext>,
307 now: Timestamp,
308 root_public_key: impl AsRef<zkcredential::endorsements::ServerRootPublicKey>,
309 ) -> Result<Vec<ReceivedEndorsement>, ZkGroupVerificationFailure> {
310 let derived_key = self.derive_public_signing_key_from_expiration(now, root_public_key)?;
311
312 let mut points_to_check: Vec<_> = member_ciphertexts
316 .into_iter()
317 .map(|ciphertext| ciphertext.ciphertext.as_points()[0])
318 .enumerate()
319 .collect();
320 Self::sort_points(&mut points_to_check);
321
322 let endorsements = self
323 .endorsements
324 .receive(
325 points_to_check.iter().map(|(_i, point)| *point),
326 &derived_key,
327 )
328 .map_err(|_| ZkGroupVerificationFailure)?;
329
330 Ok(array_utils::collect_permutation(
331 endorsements
332 .compressed
333 .into_iter()
334 .zip(endorsements.decompressed)
335 .map(|(compressed, decompressed)| ReceivedEndorsement {
336 compressed: GroupSendEndorsement {
337 reserved: ReservedByte::default(),
338 endorsement: compressed,
339 },
340 decompressed: GroupSendEndorsement {
341 reserved: ReservedByte::default(),
342 endorsement: decompressed,
343 },
344 })
345 .zip(points_to_check.iter().map(|(i, _)| *i)),
346 ))
347 }
348}
349
350#[derive(Serialize, Deserialize, PartialDefault, Clone, Copy)]
356#[partial_default(bound = "Storage: curve25519_dalek_signal::traits::Identity")]
357#[derive_where(PartialEq; Storage: subtle::ConstantTimeEq)]
358pub struct GroupSendEndorsement<Storage = curve25519_dalek_signal::RistrettoPoint> {
359 reserved: ReservedByte,
360 endorsement: zkcredential::endorsements::Endorsement<Storage>,
361}
362
363impl Debug for GroupSendEndorsement<curve25519_dalek_signal::RistrettoPoint> {
364 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
365 f.debug_struct("GroupSendEndorsement")
366 .field("reserved", &self.reserved)
367 .field("endorsement", &self.endorsement)
368 .finish()
369 }
370}
371
372impl Debug for GroupSendEndorsement<curve25519_dalek_signal::ristretto::CompressedRistretto> {
373 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
374 f.debug_struct("GroupSendEndorsement")
375 .field("reserved", &self.reserved)
376 .field("endorsement", &self.endorsement)
377 .finish()
378 }
379}
380
381#[allow(missing_docs)]
391#[derive(Clone, Copy, PartialDefault)]
392pub struct ReceivedEndorsement {
393 pub compressed: GroupSendEndorsement<curve25519_dalek_signal::ristretto::CompressedRistretto>,
401 pub decompressed: GroupSendEndorsement,
402}
403
404impl GroupSendEndorsement<curve25519_dalek_signal::ristretto::CompressedRistretto> {
405 pub fn decompress(
413 self,
414 ) -> Result<
415 GroupSendEndorsement<curve25519_dalek_signal::RistrettoPoint>,
416 ZkGroupDeserializationFailure,
417 > {
418 Ok(GroupSendEndorsement {
419 reserved: self.reserved,
420 endorsement: self
421 .endorsement
422 .decompress()
423 .map_err(|_| ZkGroupDeserializationFailure::new::<Self>())?,
424 })
425 }
426}
427
428impl GroupSendEndorsement<curve25519_dalek_signal::RistrettoPoint> {
429 pub fn compress(
434 self,
435 ) -> GroupSendEndorsement<curve25519_dalek_signal::ristretto::CompressedRistretto> {
436 GroupSendEndorsement {
437 reserved: self.reserved,
438 endorsement: self.endorsement.compress(),
439 }
440 }
441}
442
443impl GroupSendEndorsement {
444 pub fn combine(
451 endorsements: impl IntoIterator<Item = GroupSendEndorsement>,
452 ) -> GroupSendEndorsement {
453 let mut endorsements = endorsements.into_iter();
454 let Some(mut result) = endorsements.next() else {
455 return GroupSendEndorsement {
459 reserved: ReservedByte::default(),
460 endorsement: Default::default(),
461 };
462 };
463 for next in endorsements {
464 assert_eq!(
465 result.reserved, next.reserved,
466 "endorsements must all have the same version"
467 );
468 result.endorsement = result.endorsement.combine_with(&next.endorsement);
469 }
470 result
471 }
472
473 pub fn remove(&self, unwanted_endorsements: &GroupSendEndorsement) -> GroupSendEndorsement {
481 assert_eq!(
482 self.reserved, unwanted_endorsements.reserved,
483 "endorsements must have the same version"
484 );
485 GroupSendEndorsement {
486 reserved: self.reserved,
487 endorsement: self.endorsement.remove(&unwanted_endorsements.endorsement),
488 }
489 }
490
491 pub fn to_token<T: AsRef<uid_encryption::KeyPair>>(&self, key_pair: T) -> GroupSendToken {
496 let client_key =
497 zkcredential::endorsements::ClientDecryptionKey::for_first_point_of_attribute(
498 key_pair.as_ref(),
499 );
500 let raw_token = self.endorsement.to_token(&client_key);
501 GroupSendToken {
502 reserved: ReservedByte::default(),
503 raw_token,
504 }
505 }
506}
507
508#[derive(Clone, Serialize, Deserialize, PartialDefault)]
513pub struct GroupSendToken {
514 reserved: ReservedByte,
515 raw_token: Box<[u8]>,
516}
517
518impl Debug for GroupSendToken {
519 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
520 f.debug_struct("GroupSendToken")
521 .field("reserved", &self.reserved)
522 .field("raw_token", &zkcredential::PrintAsHex(&*self.raw_token))
523 .finish()
524 }
525}
526
527impl GroupSendToken {
528 pub fn into_full_token(self, expiration: Timestamp) -> GroupSendFullToken {
532 GroupSendFullToken {
533 reserved: self.reserved,
534 raw_token: self.raw_token,
535 expiration,
536 }
537 }
538}
539
540#[derive(Clone, Serialize, Deserialize, PartialDefault)]
544pub struct GroupSendFullToken {
545 reserved: ReservedByte,
546 raw_token: Box<[u8]>,
547 expiration: Timestamp,
548}
549
550impl Debug for GroupSendFullToken {
551 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
552 f.debug_struct("GroupSendFullToken")
553 .field("reserved", &self.reserved)
554 .field("raw_token", &zkcredential::PrintAsHex(&*self.raw_token))
555 .field("expiration", &self.expiration)
556 .finish()
557 }
558}
559
560impl GroupSendFullToken {
561 pub fn expiration(&self) -> Timestamp {
562 self.expiration
563 }
564
565 pub fn verify(
568 &self,
569 user_ids: impl IntoIterator<Item = libsignal_core::ServiceId>,
570 now: Timestamp,
571 key_pair: &GroupSendDerivedKeyPair,
572 ) -> Result<(), ZkGroupVerificationFailure> {
573 if now > self.expiration {
574 return Err(ZkGroupVerificationFailure);
575 }
576 assert_eq!(
577 self.expiration, key_pair.expiration,
578 "wrong key pair used for this token"
579 );
580
581 let uid_sho_seed = crypto::uid_struct::UidStruct::seed_M1();
582 let user_id_sum: curve25519_dalek_signal::RistrettoPoint = user_ids
583 .into_iter()
584 .map(|user_id| crypto::uid_struct::UidStruct::calc_M1(uid_sho_seed.clone(), user_id))
585 .sum();
586
587 key_pair
588 .key_pair
589 .verify(&user_id_sum, &self.raw_token)
590 .map_err(|_| ZkGroupVerificationFailure)
591 }
592}