1use curve25519_dalek_signal::ristretto::RistrettoPoint;
19use partial_default::PartialDefault;
20use poksho::ShoApi;
21use serde::{Deserialize, Serialize};
22
23use crate::common::serialization::ReservedByte;
24use crate::common::sho::Sho;
25use crate::common::simple_types::*;
26use crate::generic_server_params::{GenericServerPublicParams, GenericServerSecretParams};
27use crate::{ZkGroupDeserializationFailure, ZkGroupVerificationFailure, SECONDS_PER_DAY};
28
29#[derive(Serialize, Deserialize, Clone, Copy)]
30struct BackupIdPoint(RistrettoPoint);
31
32impl BackupIdPoint {
33 fn new(backup_id: &libsignal_account_keys::BackupId) -> Self {
34 Self(Sho::new(b"20231003_Signal_BackupId", &backup_id.0).get_point())
35 }
36}
37
38impl zkcredential::attributes::RevealedAttribute for BackupIdPoint {
39 fn as_point(&self) -> RistrettoPoint {
40 self.0
41 }
42}
43
44const CREDENTIAL_LABEL: &[u8] = b"20231003_Signal_BackupAuthCredential";
45
46#[derive(
51 Copy, Clone, Serialize, Deserialize, PartialEq, Eq, PartialDefault, Debug, derive_more::TryFrom,
52)]
53#[serde(into = "u64", try_from = "u64")]
54#[repr(u8)]
55#[try_from(repr)]
56pub enum BackupLevel {
57 #[partial_default]
58 Free = 200,
59 Paid = 201,
60}
61
62impl From<BackupLevel> for u64 {
63 fn from(backup_level: BackupLevel) -> Self {
64 backup_level as u64
65 }
66}
67
68impl TryFrom<u64> for BackupLevel {
69 type Error = ZkGroupDeserializationFailure;
72 fn try_from(value: u64) -> Result<Self, Self::Error> {
73 u8::try_from(value)
74 .ok()
75 .and_then(|v| BackupLevel::try_from(v).ok())
76 .ok_or(ZkGroupDeserializationFailure::new::<Self>())
77 }
78}
79
80#[derive(
81 Copy, Clone, Serialize, Deserialize, PartialEq, Eq, PartialDefault, Debug, derive_more::TryFrom,
82)]
83#[serde(into = "u64", try_from = "u64")]
84#[repr(u8)]
85#[try_from(repr)]
86pub enum BackupCredentialType {
87 #[partial_default]
88 Messages = 1,
89 Media = 2,
90}
91
92impl From<BackupCredentialType> for u64 {
93 fn from(credential_type: BackupCredentialType) -> Self {
94 credential_type as u64
95 }
96}
97
98impl TryFrom<u64> for BackupCredentialType {
99 type Error = ZkGroupDeserializationFailure;
102 fn try_from(value: u64) -> Result<Self, Self::Error> {
103 u8::try_from(value)
104 .ok()
105 .and_then(|v| BackupCredentialType::try_from(v).ok())
106 .ok_or(ZkGroupDeserializationFailure::new::<Self>())
107 }
108}
109
110#[derive(Serialize, Deserialize, PartialDefault)]
111pub struct BackupAuthCredentialRequestContext {
112 reserved: ReservedByte,
113 blinded_backup_id: zkcredential::issuance::blind::BlindedPoint,
114 backup_id: libsignal_account_keys::BackupId,
115 key_pair: zkcredential::issuance::blind::BlindingKeyPair,
116}
117
118impl BackupAuthCredentialRequestContext {
119 pub fn new<const VERSION: u8>(
120 backup_key: &libsignal_account_keys::BackupKey<VERSION>,
121 aci: libsignal_core::Aci,
122 ) -> Self {
123 let backup_id = backup_key.derive_backup_id(&aci);
125
126 let mut sho = poksho::ShoHmacSha256::new(b"20231003_Signal_BackupAuthCredentialRequest");
127 sho.absorb_and_ratchet(uuid::Uuid::from(aci).as_bytes());
128 sho.absorb_and_ratchet(&backup_key.0);
129
130 let key_pair = zkcredential::issuance::blind::BlindingKeyPair::generate(&mut sho);
131
132 let blinded_backup_id = key_pair
133 .blind(&BackupIdPoint::new(&backup_id), &mut sho)
134 .into();
135
136 Self {
137 reserved: Default::default(),
138 blinded_backup_id,
139 backup_id,
140 key_pair,
141 }
142 }
143
144 pub fn get_request(&self) -> BackupAuthCredentialRequest {
145 BackupAuthCredentialRequest {
146 reserved: Default::default(),
147 blinded_backup_id: self.blinded_backup_id,
148 public_key: *self.key_pair.public_key(),
149 }
150 }
151}
152
153#[derive(Serialize, Deserialize, PartialDefault)]
154pub struct BackupAuthCredentialRequest {
155 reserved: ReservedByte,
156 blinded_backup_id: zkcredential::issuance::blind::BlindedPoint,
157 public_key: zkcredential::issuance::blind::BlindingPublicKey,
158}
159
160impl BackupAuthCredentialRequest {
161 pub fn issue(
162 &self,
163 redemption_time: Timestamp,
164 backup_level: BackupLevel,
165 credential_type: BackupCredentialType,
166 params: &GenericServerSecretParams,
167 randomness: RandomnessBytes,
168 ) -> BackupAuthCredentialResponse {
169 BackupAuthCredentialResponse {
170 reserved: Default::default(),
171 redemption_time,
172 backup_level,
173 credential_type,
174 blinded_credential: zkcredential::issuance::IssuanceProofBuilder::new(CREDENTIAL_LABEL)
175 .add_public_attribute(&redemption_time)
176 .add_public_attribute(&u64::from(backup_level))
177 .add_public_attribute(&u64::from(credential_type))
178 .add_blinded_revealed_attribute(&self.blinded_backup_id)
179 .issue(¶ms.credential_key, &self.public_key, randomness),
180 }
181 }
182}
183
184#[derive(Serialize, Deserialize, PartialDefault)]
185pub struct BackupAuthCredentialResponse {
186 reserved: ReservedByte,
187 redemption_time: Timestamp,
191 backup_level: BackupLevel,
192 credential_type: BackupCredentialType,
193 blinded_credential: zkcredential::issuance::blind::BlindedIssuanceProof,
194}
195
196impl BackupAuthCredentialRequestContext {
197 pub fn receive(
198 self,
199 response: BackupAuthCredentialResponse,
200 params: &GenericServerPublicParams,
201 expected_redemption_time: Timestamp,
202 ) -> Result<BackupAuthCredential, ZkGroupVerificationFailure> {
203 if response.redemption_time != expected_redemption_time
204 || !response.redemption_time.is_day_aligned()
205 {
206 return Err(ZkGroupVerificationFailure);
207 }
208
209 Ok(BackupAuthCredential {
210 reserved: Default::default(),
211 redemption_time: response.redemption_time,
212 backup_level: response.backup_level,
213 credential_type: response.credential_type,
214 credential: zkcredential::issuance::IssuanceProofBuilder::new(CREDENTIAL_LABEL)
215 .add_public_attribute(&response.redemption_time)
216 .add_public_attribute(&u64::from(response.backup_level))
217 .add_public_attribute(&u64::from(response.credential_type))
218 .add_blinded_revealed_attribute(&self.blinded_backup_id)
219 .verify(
220 ¶ms.credential_key,
221 &self.key_pair,
222 response.blinded_credential,
223 )
224 .map_err(|_| ZkGroupVerificationFailure)?,
225 backup_id: self.backup_id,
226 })
227 }
228}
229
230#[derive(Serialize, Deserialize, PartialDefault)]
231pub struct BackupAuthCredential {
232 reserved: ReservedByte,
233 redemption_time: Timestamp,
234 backup_level: BackupLevel,
235 credential_type: BackupCredentialType,
236 credential: zkcredential::credentials::Credential,
237 backup_id: libsignal_account_keys::BackupId,
238}
239
240impl BackupAuthCredential {
241 pub fn present(
242 &self,
243 server_params: &GenericServerPublicParams,
244 randomness: RandomnessBytes,
245 ) -> BackupAuthCredentialPresentation {
246 BackupAuthCredentialPresentation {
247 version: Default::default(),
248 redemption_time: self.redemption_time,
249 backup_level: self.backup_level,
250 credential_type: self.credential_type,
251 backup_id: self.backup_id,
252 proof: zkcredential::presentation::PresentationProofBuilder::new(CREDENTIAL_LABEL)
253 .add_revealed_attribute(&BackupIdPoint::new(&self.backup_id))
254 .present(&server_params.credential_key, &self.credential, randomness),
255 }
256 }
257
258 pub fn backup_id(&self) -> libsignal_account_keys::BackupId {
259 self.backup_id
260 }
261
262 pub fn backup_level(&self) -> BackupLevel {
263 self.backup_level
264 }
265
266 pub fn credential_type(&self) -> BackupCredentialType {
267 self.credential_type
268 }
269}
270
271#[derive(Serialize, Deserialize, PartialDefault)]
272pub struct BackupAuthCredentialPresentation {
273 version: ReservedByte,
274 backup_level: BackupLevel,
275 credential_type: BackupCredentialType,
276 redemption_time: Timestamp,
277 proof: zkcredential::presentation::PresentationProof,
278 backup_id: libsignal_account_keys::BackupId,
279}
280
281impl BackupAuthCredentialPresentation {
282 pub fn verify(
283 &self,
284 current_time: Timestamp,
285 server_params: &GenericServerSecretParams,
286 ) -> Result<(), ZkGroupVerificationFailure> {
287 let acceptable_start_time = self
288 .redemption_time
289 .checked_sub_seconds(SECONDS_PER_DAY)
290 .ok_or(ZkGroupVerificationFailure)?;
291 let acceptable_end_time = self
292 .redemption_time
293 .checked_add_seconds(2 * SECONDS_PER_DAY)
294 .ok_or(ZkGroupVerificationFailure)?;
295
296 if !(acceptable_start_time..=acceptable_end_time).contains(¤t_time) {
297 return Err(ZkGroupVerificationFailure);
298 }
299
300 zkcredential::presentation::PresentationProofVerifier::new(CREDENTIAL_LABEL)
301 .add_public_attribute(&self.redemption_time)
302 .add_public_attribute(&u64::from(self.backup_level))
303 .add_public_attribute(&u64::from(self.credential_type))
304 .add_revealed_attribute(&BackupIdPoint::new(&self.backup_id))
305 .verify(&server_params.credential_key, &self.proof)
306 .map_err(|_| ZkGroupVerificationFailure)
307 }
308
309 pub fn backup_level(&self) -> BackupLevel {
310 self.backup_level
311 }
312
313 pub fn credential_type(&self) -> BackupCredentialType {
314 self.credential_type
315 }
316
317 pub fn backup_id(&self) -> libsignal_account_keys::BackupId {
318 self.backup_id
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use assert_matches::assert_matches;
325
326 use super::*;
327 use crate::{common, RandomnessBytes, Timestamp, RANDOMNESS_LEN, SECONDS_PER_DAY};
328
329 const DAY_ALIGNED_TIMESTAMP: Timestamp = Timestamp::from_epoch_seconds(1681344000); const KEY: libsignal_account_keys::BackupKey = libsignal_account_keys::BackupKey([0x42u8; 32]);
331 const ACI: uuid::Uuid = uuid::uuid!("c0fc16e4-bae5-4343-9f0d-e7ecf4251343");
332 const SERVER_SECRET_RAND: RandomnessBytes = [0xA0; RANDOMNESS_LEN];
333 const ISSUE_RAND: RandomnessBytes = [0xA1; RANDOMNESS_LEN];
334 const PRESENT_RAND: RandomnessBytes = [0xA2; RANDOMNESS_LEN];
335
336 fn server_secret_params() -> GenericServerSecretParams {
337 GenericServerSecretParams::generate(SERVER_SECRET_RAND)
338 }
339
340 fn generate_credential(redemption_time: Timestamp) -> BackupAuthCredential {
341 let request_context = BackupAuthCredentialRequestContext::new(&KEY, ACI.into());
343 let request = request_context.get_request();
344
345 let blinded_credential = request.issue(
347 redemption_time,
348 BackupLevel::Free,
349 BackupCredentialType::Messages,
350 &server_secret_params(),
351 ISSUE_RAND,
352 );
353
354 let server_public_params = server_secret_params().get_public_params();
356 request_context
357 .receive(blinded_credential, &server_public_params, redemption_time)
358 .expect("credential should be valid")
359 }
360
361 #[test]
362 fn test_server_verify_expiration() {
363 let credential = generate_credential(DAY_ALIGNED_TIMESTAMP);
364 let presentation =
365 credential.present(&server_secret_params().get_public_params(), PRESENT_RAND);
366
367 presentation
368 .verify(DAY_ALIGNED_TIMESTAMP, &server_secret_params())
369 .expect("presentation should be valid");
370
371 presentation
372 .verify(
373 DAY_ALIGNED_TIMESTAMP.sub_seconds(SECONDS_PER_DAY + 1),
374 &server_secret_params(),
375 )
376 .expect_err("credential should not be valid 24h before redemption time");
377 presentation
378 .verify(
379 DAY_ALIGNED_TIMESTAMP.add_seconds(2 * SECONDS_PER_DAY + 1),
380 &server_secret_params(),
381 )
382 .expect_err("credential should not be valid after expiration (2 days later)");
383 }
384
385 #[test]
386 fn test_server_verify_wrong_backup_id() {
387 let credential = generate_credential(DAY_ALIGNED_TIMESTAMP);
388 let valid_presentation =
389 credential.present(&server_secret_params().get_public_params(), PRESENT_RAND);
390 let invalid_presentation = BackupAuthCredentialPresentation {
391 backup_id: libsignal_account_keys::BackupId(*b"a fake backup-id"),
392 ..valid_presentation
393 };
394 invalid_presentation
395 .verify(DAY_ALIGNED_TIMESTAMP, &server_secret_params())
396 .expect_err("credential should not be valid with different backup-id");
397 }
398
399 #[test]
400 fn test_server_verify_wrong_redemption() {
401 let credential = generate_credential(DAY_ALIGNED_TIMESTAMP);
402 let valid_presentation =
403 credential.present(&server_secret_params().get_public_params(), PRESENT_RAND);
404 let invalid_presentation = BackupAuthCredentialPresentation {
405 redemption_time: DAY_ALIGNED_TIMESTAMP.add_seconds(1),
406 ..valid_presentation
407 };
408 invalid_presentation
409 .verify(DAY_ALIGNED_TIMESTAMP, &server_secret_params())
410 .expect_err("credential should not be valid with altered redemption_time");
411 }
412
413 #[test]
414 fn test_server_verify_wrong_receipt_level() {
415 let credential = generate_credential(DAY_ALIGNED_TIMESTAMP);
416 let valid_presentation =
417 credential.present(&server_secret_params().get_public_params(), PRESENT_RAND);
418 let invalid_presentation = BackupAuthCredentialPresentation {
419 backup_level: BackupLevel::Paid,
421 ..valid_presentation
422 };
423 invalid_presentation
424 .verify(DAY_ALIGNED_TIMESTAMP, &server_secret_params())
425 .expect_err("credential should not be valid with wrong receipt");
426 }
427
428 #[test]
429 fn test_client_enforces_timestamp() {
430 let redemption_time: Timestamp = DAY_ALIGNED_TIMESTAMP;
431
432 let request_context = BackupAuthCredentialRequestContext::new(&KEY, ACI.into());
433 let request = request_context.get_request();
434 let blinded_credential = request.issue(
435 redemption_time,
436 BackupLevel::Free,
437 BackupCredentialType::Messages,
438 &server_secret_params(),
439 ISSUE_RAND,
440 );
441 assert!(
442 request_context
443 .receive(
444 blinded_credential,
445 &server_secret_params().get_public_params(),
446 redemption_time.add_seconds(SECONDS_PER_DAY),
447 )
448 .is_err(),
449 "client should require that timestamp matches its expectation"
450 );
451 }
452
453 #[test]
454 fn test_client_enforces_timestamp_granularity() {
455 let redemption_time: Timestamp = DAY_ALIGNED_TIMESTAMP.add_seconds(60 * 60); let request_context = BackupAuthCredentialRequestContext::new(&KEY, ACI.into());
458 let request = request_context.get_request();
459 let blinded_credential = request.issue(
460 redemption_time,
461 BackupLevel::Free,
462 BackupCredentialType::Messages,
463 &server_secret_params(),
464 ISSUE_RAND,
465 );
466 assert!(
467 request_context
468 .receive(
469 blinded_credential,
470 &server_secret_params().get_public_params(),
471 redemption_time,
472 )
473 .is_err(),
474 "client should require that timestamp is on a day boundary"
475 );
476 }
477
478 #[test]
479 fn test_backup_level_serialization() {
480 let free_bytes = common::serialization::serialize(&BackupLevel::Free);
481 let paid_bytes = common::serialization::serialize(&BackupLevel::Paid);
482 assert_eq!(free_bytes.len(), 8);
483 assert_eq!(paid_bytes.len(), 8);
484
485 let free_num: u64 = common::serialization::deserialize(&free_bytes).expect("valid u64");
486 let paid_num: u64 = common::serialization::deserialize(&paid_bytes).expect("valid u64");
487 assert_eq!(free_num, 200);
488 assert_eq!(paid_num, 201);
489
490 let free: BackupLevel =
491 common::serialization::deserialize(&free_bytes).expect("valid level");
492 let paid: BackupLevel =
493 common::serialization::deserialize(&paid_bytes).expect("valid level");
494 assert_eq!(free, BackupLevel::Free);
495 assert_eq!(paid, BackupLevel::Paid);
496 }
497
498 #[test]
499 fn test_backup_level_validation() {
500 assert_matches!(
502 BackupLevel::try_from(0x100000000000u64 + u64::from(BackupLevel::Free)),
503 Err(_)
504 );
505 }
506}