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::api::endorsement_expiration;
24use crate::common::array_utils;
25use crate::common::serialization::ReservedByte;
26use crate::crypto::uid_encryption;
27use crate::groups::{GroupSecretParams, UuidCiphertext};
28use crate::{
29 RandomnessBytes, Timestamp, ZkGroupDeserializationFailure, ZkGroupVerificationFailure, crypto,
30};
31
32#[derive(Clone, Serialize, Deserialize, PartialDefault)]
38pub struct GroupSendDerivedKeyPair {
39 reserved: ReservedByte,
40 key_pair: zkcredential::endorsements::ServerDerivedKeyPair,
41 expiration: Timestamp,
42}
43
44impl GroupSendDerivedKeyPair {
45 fn tag_info(expiration: Timestamp) -> impl poksho::ShoApi + Clone {
48 let mut sho = poksho::ShoHmacSha256::new(b"20240215_Signal_GroupSendEndorsement");
49 sho.absorb_and_ratchet(&expiration.to_be_bytes());
50 sho
51 }
52
53 pub fn for_expiration(
55 expiration: Timestamp,
56 root: impl AsRef<zkcredential::endorsements::ServerRootKeyPair>,
57 ) -> Self {
58 Self {
59 reserved: ReservedByte::default(),
60 key_pair: root.as_ref().derive_key(Self::tag_info(expiration)),
61 expiration,
62 }
63 }
64}
65
66#[derive(Clone, Serialize, Deserialize, PartialDefault, Debug)]
71pub struct GroupSendEndorsementsResponse {
72 reserved: ReservedByte,
73 endorsements: zkcredential::endorsements::EndorsementResponse,
74 expiration: Timestamp,
75}
76
77impl GroupSendEndorsementsResponse {
78 pub fn default_expiration(current_time: Timestamp) -> Timestamp {
79 endorsement_expiration::default_expiration(current_time)
80 }
81
82 fn sort_points(points: &mut [(usize, curve25519_dalek::RistrettoPoint)]) {
89 debug_assert!(points.iter().enumerate().all(|(i, (j, _))| i == *j));
90 let sort_keys = curve25519_dalek::RistrettoPoint::double_and_compress_batch(
91 points.iter().map(|(_i, point)| point),
92 );
93 points.sort_unstable_by_key(|(i, _point)| sort_keys[*i].as_bytes());
94 }
95
96 pub fn issue(
100 member_ciphertexts: impl IntoIterator<Item = UuidCiphertext>,
101 key_pair: &GroupSendDerivedKeyPair,
102 randomness: RandomnessBytes,
103 ) -> Self {
104 let mut points_to_sign: Vec<(usize, curve25519_dalek::RistrettoPoint)> = member_ciphertexts
108 .into_iter()
109 .map(|ciphertext| ciphertext.ciphertext.as_points()[0])
110 .enumerate()
111 .collect();
112 Self::sort_points(&mut points_to_sign);
113
114 let endorsements = zkcredential::endorsements::EndorsementResponse::issue(
115 points_to_sign.iter().map(|(_i, point)| *point),
116 &key_pair.key_pair,
117 randomness,
118 );
119
120 Self {
125 reserved: ReservedByte::default(),
126 endorsements,
127 expiration: key_pair.expiration,
128 }
129 }
130
131 pub fn expiration(&self) -> Timestamp {
133 self.expiration
134 }
135
136 fn derive_public_signing_key_from_expiration(
143 &self,
144 now: Timestamp,
145 root_public_key: impl AsRef<zkcredential::endorsements::ServerRootPublicKey>,
146 ) -> Result<zkcredential::endorsements::ServerDerivedPublicKey, ZkGroupVerificationFailure>
147 {
148 endorsement_expiration::validate_expiration(self.expiration, now)?;
149
150 Ok(root_public_key
151 .as_ref()
152 .derive_key(GroupSendDerivedKeyPair::tag_info(self.expiration)))
153 }
154
155 pub fn receive_with_service_ids_single_threaded(
161 self,
162 user_ids: impl IntoIterator<Item = libsignal_core::ServiceId>,
163 now: Timestamp,
164 group_params: &GroupSecretParams,
165 root_public_key: impl AsRef<zkcredential::endorsements::ServerRootPublicKey>,
166 ) -> Result<Vec<ReceivedEndorsement>, ZkGroupVerificationFailure> {
167 let derived_key = self.derive_public_signing_key_from_expiration(now, root_public_key)?;
168
169 let uid_sho_seed = crypto::uid_struct::UidStruct::seed_M1();
174 let mut member_points: Vec<(usize, curve25519_dalek::RistrettoPoint)> = user_ids
175 .into_iter()
176 .map(|user_id| {
177 group_params.uid_enc_key_pair.a1
178 * crypto::uid_struct::UidStruct::calc_M1(uid_sho_seed.clone(), user_id)
179 })
180 .enumerate()
181 .collect();
182 Self::sort_points(&mut member_points);
183
184 let endorsements = self
185 .endorsements
186 .receive(member_points.iter().map(|(_i, point)| *point), &derived_key)
187 .map_err(|_| ZkGroupVerificationFailure)?;
188
189 Ok(array_utils::collect_permutation(
190 endorsements
191 .compressed
192 .into_iter()
193 .zip(endorsements.decompressed)
194 .map(|(compressed, decompressed)| ReceivedEndorsement {
195 compressed: GroupSendEndorsement {
196 reserved: ReservedByte::default(),
197 endorsement: compressed,
198 },
199 decompressed: GroupSendEndorsement {
200 reserved: ReservedByte::default(),
201 endorsement: decompressed,
202 },
203 })
204 .zip(member_points.iter().map(|(i, _)| *i)),
205 ))
206 }
207
208 pub fn receive_with_service_ids<T>(
216 self,
217 user_ids: T,
218 now: Timestamp,
219 group_params: &GroupSecretParams,
220 root_public_key: impl AsRef<zkcredential::endorsements::ServerRootPublicKey>,
221 ) -> Result<Vec<ReceivedEndorsement>, ZkGroupVerificationFailure>
222 where
223 T: rayon::iter::IntoParallelIterator<
224 Item = libsignal_core::ServiceId,
225 Iter: rayon::iter::IndexedParallelIterator,
226 >,
227 {
228 let derived_key = self.derive_public_signing_key_from_expiration(now, root_public_key)?;
229
230 let uid_sho_seed = crypto::uid_struct::UidStruct::seed_M1();
235 let mut member_points: Vec<(usize, curve25519_dalek::RistrettoPoint)> = user_ids
236 .into_par_iter()
237 .map(|user_id| {
238 group_params.uid_enc_key_pair.a1
239 * crypto::uid_struct::UidStruct::calc_M1(uid_sho_seed.clone(), user_id)
240 })
241 .enumerate()
242 .collect();
243 Self::sort_points(&mut member_points);
244
245 let endorsements = self
246 .endorsements
247 .receive(member_points.iter().map(|(_i, point)| *point), &derived_key)
248 .map_err(|_| ZkGroupVerificationFailure)?;
249
250 Ok(array_utils::collect_permutation(
251 endorsements
252 .compressed
253 .into_iter()
254 .zip(endorsements.decompressed)
255 .map(|(compressed, decompressed)| ReceivedEndorsement {
256 compressed: GroupSendEndorsement {
257 reserved: ReservedByte::default(),
258 endorsement: compressed,
259 },
260 decompressed: GroupSendEndorsement {
261 reserved: ReservedByte::default(),
262 endorsement: decompressed,
263 },
264 })
265 .zip(member_points.iter().map(|(i, _)| *i)),
266 ))
267 }
268
269 pub fn receive_with_ciphertexts(
278 self,
279 member_ciphertexts: impl IntoIterator<Item = UuidCiphertext>,
280 now: Timestamp,
281 root_public_key: impl AsRef<zkcredential::endorsements::ServerRootPublicKey>,
282 ) -> Result<Vec<ReceivedEndorsement>, ZkGroupVerificationFailure> {
283 let derived_key = self.derive_public_signing_key_from_expiration(now, root_public_key)?;
284
285 let mut points_to_check: Vec<_> = member_ciphertexts
289 .into_iter()
290 .map(|ciphertext| ciphertext.ciphertext.as_points()[0])
291 .enumerate()
292 .collect();
293 Self::sort_points(&mut points_to_check);
294
295 let endorsements = self
296 .endorsements
297 .receive(
298 points_to_check.iter().map(|(_i, point)| *point),
299 &derived_key,
300 )
301 .map_err(|_| ZkGroupVerificationFailure)?;
302
303 Ok(array_utils::collect_permutation(
304 endorsements
305 .compressed
306 .into_iter()
307 .zip(endorsements.decompressed)
308 .map(|(compressed, decompressed)| ReceivedEndorsement {
309 compressed: GroupSendEndorsement {
310 reserved: ReservedByte::default(),
311 endorsement: compressed,
312 },
313 decompressed: GroupSendEndorsement {
314 reserved: ReservedByte::default(),
315 endorsement: decompressed,
316 },
317 })
318 .zip(points_to_check.iter().map(|(i, _)| *i)),
319 ))
320 }
321}
322
323#[derive(Serialize, Deserialize, PartialDefault, Clone, Copy)]
329#[partial_default(bound = "Storage: curve25519_dalek::traits::Identity")]
330#[derive_where(PartialEq; Storage: subtle::ConstantTimeEq)]
331pub struct GroupSendEndorsement<Storage = curve25519_dalek::RistrettoPoint> {
332 reserved: ReservedByte,
333 endorsement: zkcredential::endorsements::Endorsement<Storage>,
334}
335
336impl Debug for GroupSendEndorsement<curve25519_dalek::RistrettoPoint> {
337 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
338 f.debug_struct("GroupSendEndorsement")
339 .field("reserved", &self.reserved)
340 .field("endorsement", &self.endorsement)
341 .finish()
342 }
343}
344
345impl Debug for GroupSendEndorsement<curve25519_dalek::ristretto::CompressedRistretto> {
346 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
347 f.debug_struct("GroupSendEndorsement")
348 .field("reserved", &self.reserved)
349 .field("endorsement", &self.endorsement)
350 .finish()
351 }
352}
353
354#[allow(missing_docs)]
364#[derive(Clone, Copy, PartialDefault)]
365pub struct ReceivedEndorsement {
366 pub compressed: GroupSendEndorsement<curve25519_dalek::ristretto::CompressedRistretto>,
374 pub decompressed: GroupSendEndorsement,
375}
376
377impl GroupSendEndorsement<curve25519_dalek::ristretto::CompressedRistretto> {
378 pub fn decompress(
386 self,
387 ) -> Result<GroupSendEndorsement<curve25519_dalek::RistrettoPoint>, ZkGroupDeserializationFailure>
388 {
389 Ok(GroupSendEndorsement {
390 reserved: self.reserved,
391 endorsement: self
392 .endorsement
393 .decompress()
394 .map_err(|_| ZkGroupDeserializationFailure::new::<Self>())?,
395 })
396 }
397}
398
399impl GroupSendEndorsement<curve25519_dalek::RistrettoPoint> {
400 pub fn compress(
405 self,
406 ) -> GroupSendEndorsement<curve25519_dalek::ristretto::CompressedRistretto> {
407 GroupSendEndorsement {
408 reserved: self.reserved,
409 endorsement: self.endorsement.compress(),
410 }
411 }
412}
413
414impl GroupSendEndorsement {
415 pub fn combine(
422 endorsements: impl IntoIterator<Item = GroupSendEndorsement>,
423 ) -> GroupSendEndorsement {
424 let mut endorsements = endorsements.into_iter();
425 let Some(mut result) = endorsements.next() else {
426 return GroupSendEndorsement {
430 reserved: ReservedByte::default(),
431 endorsement: Default::default(),
432 };
433 };
434 for next in endorsements {
435 assert_eq!(
436 result.reserved, next.reserved,
437 "endorsements must all have the same version"
438 );
439 result.endorsement = result.endorsement.combine_with(&next.endorsement);
440 }
441 result
442 }
443
444 pub fn remove(&self, unwanted_endorsements: &GroupSendEndorsement) -> GroupSendEndorsement {
452 assert_eq!(
453 self.reserved, unwanted_endorsements.reserved,
454 "endorsements must have the same version"
455 );
456 GroupSendEndorsement {
457 reserved: self.reserved,
458 endorsement: self.endorsement.remove(&unwanted_endorsements.endorsement),
459 }
460 }
461
462 pub fn to_token<T: AsRef<uid_encryption::KeyPair>>(&self, key_pair: T) -> GroupSendToken {
467 let client_key =
468 zkcredential::endorsements::ClientDecryptionKey::for_first_point_of_attribute(
469 key_pair.as_ref(),
470 );
471 let raw_token = self.endorsement.to_token(&client_key);
472 GroupSendToken {
473 reserved: ReservedByte::default(),
474 raw_token,
475 }
476 }
477}
478
479#[derive(Clone, Serialize, Deserialize, PartialDefault)]
484pub struct GroupSendToken {
485 reserved: ReservedByte,
486 raw_token: Box<[u8]>,
487}
488
489impl Debug for GroupSendToken {
490 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
491 f.debug_struct("GroupSendToken")
492 .field("reserved", &self.reserved)
493 .field("raw_token", &zkcredential::PrintAsHex(&*self.raw_token))
494 .finish()
495 }
496}
497
498impl GroupSendToken {
499 pub fn into_full_token(self, expiration: Timestamp) -> GroupSendFullToken {
503 GroupSendFullToken {
504 reserved: self.reserved,
505 raw_token: self.raw_token,
506 expiration,
507 }
508 }
509}
510
511#[derive(Clone, Serialize, Deserialize, PartialDefault)]
515pub struct GroupSendFullToken {
516 reserved: ReservedByte,
517 raw_token: Box<[u8]>,
518 expiration: Timestamp,
519}
520
521impl Debug for GroupSendFullToken {
522 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
523 f.debug_struct("GroupSendFullToken")
524 .field("reserved", &self.reserved)
525 .field("raw_token", &zkcredential::PrintAsHex(&*self.raw_token))
526 .field("expiration", &self.expiration)
527 .finish()
528 }
529}
530
531impl GroupSendFullToken {
532 pub fn expiration(&self) -> Timestamp {
533 self.expiration
534 }
535
536 pub fn verify(
539 &self,
540 user_ids: impl IntoIterator<Item = libsignal_core::ServiceId>,
541 now: Timestamp,
542 key_pair: &GroupSendDerivedKeyPair,
543 ) -> Result<(), ZkGroupVerificationFailure> {
544 if now > self.expiration {
545 return Err(ZkGroupVerificationFailure);
546 }
547 assert_eq!(
548 self.expiration, key_pair.expiration,
549 "wrong key pair used for this token"
550 );
551
552 let uid_sho_seed = crypto::uid_struct::UidStruct::seed_M1();
553 let user_id_sum: curve25519_dalek::RistrettoPoint = user_ids
554 .into_iter()
555 .map(|user_id| crypto::uid_struct::UidStruct::calc_M1(uid_sho_seed.clone(), user_id))
556 .sum();
557
558 key_pair
559 .key_pair
560 .verify(&user_id_sum, &self.raw_token)
561 .map_err(|_| ZkGroupVerificationFailure)
562 }
563}