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};
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 log::warn!(
205 "redemption_time mismatch: {} != {}",
206 response.redemption_time.epoch_seconds(),
207 expected_redemption_time.epoch_seconds()
208 );
209 return Err(ZkGroupVerificationFailure);
210 }
211
212 if !response.redemption_time.is_day_aligned() {
213 log::warn!(
214 "redemption_time is not day-aligned: {}",
215 response.redemption_time.epoch_seconds()
216 );
217 return Err(ZkGroupVerificationFailure);
218 }
219
220 let credential = zkcredential::issuance::IssuanceProofBuilder::new(CREDENTIAL_LABEL)
221 .add_public_attribute(&response.redemption_time)
222 .add_public_attribute(&u64::from(response.backup_level))
223 .add_public_attribute(&u64::from(response.credential_type))
224 .add_blinded_revealed_attribute(&self.blinded_backup_id)
225 .verify(
226 ¶ms.credential_key,
227 &self.key_pair,
228 response.blinded_credential,
229 )
230 .map_err(|_| ZkGroupVerificationFailure)?;
231
232 Ok(BackupAuthCredential {
233 reserved: Default::default(),
234 redemption_time: response.redemption_time,
235 backup_level: response.backup_level,
236 credential_type: response.credential_type,
237 credential,
238 backup_id: self.backup_id,
239 })
240 }
241}
242
243#[derive(Serialize, Deserialize, PartialDefault)]
244pub struct BackupAuthCredential {
245 reserved: ReservedByte,
246 redemption_time: Timestamp,
247 backup_level: BackupLevel,
248 credential_type: BackupCredentialType,
249 credential: zkcredential::credentials::Credential,
250 backup_id: libsignal_account_keys::BackupId,
251}
252
253impl BackupAuthCredential {
254 pub fn present(
255 &self,
256 server_params: &GenericServerPublicParams,
257 randomness: RandomnessBytes,
258 ) -> BackupAuthCredentialPresentation {
259 BackupAuthCredentialPresentation {
260 version: Default::default(),
261 redemption_time: self.redemption_time,
262 backup_level: self.backup_level,
263 credential_type: self.credential_type,
264 backup_id: self.backup_id,
265 proof: zkcredential::presentation::PresentationProofBuilder::new(CREDENTIAL_LABEL)
266 .add_revealed_attribute(&BackupIdPoint::new(&self.backup_id))
267 .present(&server_params.credential_key, &self.credential, randomness),
268 }
269 }
270
271 pub fn backup_id(&self) -> libsignal_account_keys::BackupId {
272 self.backup_id
273 }
274
275 pub fn backup_level(&self) -> BackupLevel {
276 self.backup_level
277 }
278
279 pub fn credential_type(&self) -> BackupCredentialType {
280 self.credential_type
281 }
282}
283
284#[derive(Serialize, Deserialize, PartialDefault)]
285pub struct BackupAuthCredentialPresentation {
286 version: ReservedByte,
287 backup_level: BackupLevel,
288 credential_type: BackupCredentialType,
289 redemption_time: Timestamp,
290 proof: zkcredential::presentation::PresentationProof,
291 backup_id: libsignal_account_keys::BackupId,
292}
293
294impl BackupAuthCredentialPresentation {
295 pub fn verify(
296 &self,
297 current_time: Timestamp,
298 server_params: &GenericServerSecretParams,
299 ) -> Result<(), ZkGroupVerificationFailure> {
300 crate::ServerSecretParams::check_auth_credential_redemption_time(
301 self.redemption_time,
302 current_time,
303 )?;
304
305 zkcredential::presentation::PresentationProofVerifier::new(CREDENTIAL_LABEL)
306 .add_public_attribute(&self.redemption_time)
307 .add_public_attribute(&u64::from(self.backup_level))
308 .add_public_attribute(&u64::from(self.credential_type))
309 .add_revealed_attribute(&BackupIdPoint::new(&self.backup_id))
310 .verify(&server_params.credential_key, &self.proof)
311 .map_err(|_| ZkGroupVerificationFailure)
312 }
313
314 pub fn backup_level(&self) -> BackupLevel {
315 self.backup_level
316 }
317
318 pub fn credential_type(&self) -> BackupCredentialType {
319 self.credential_type
320 }
321
322 pub fn backup_id(&self) -> libsignal_account_keys::BackupId {
323 self.backup_id
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use assert_matches::assert_matches;
330
331 use super::*;
332 use crate::{RANDOMNESS_LEN, RandomnessBytes, SECONDS_PER_DAY, Timestamp, common};
333
334 const DAY_ALIGNED_TIMESTAMP: Timestamp = Timestamp::from_epoch_seconds(1681344000); const KEY: libsignal_account_keys::BackupKey = libsignal_account_keys::BackupKey([0x42u8; 32]);
336 const ACI: uuid::Uuid = uuid::uuid!("c0fc16e4-bae5-4343-9f0d-e7ecf4251343");
337 const SERVER_SECRET_RAND: RandomnessBytes = [0xA0; RANDOMNESS_LEN];
338 const ISSUE_RAND: RandomnessBytes = [0xA1; RANDOMNESS_LEN];
339 const PRESENT_RAND: RandomnessBytes = [0xA2; RANDOMNESS_LEN];
340
341 fn server_secret_params() -> GenericServerSecretParams {
342 GenericServerSecretParams::generate(SERVER_SECRET_RAND)
343 }
344
345 fn generate_credential(redemption_time: Timestamp) -> BackupAuthCredential {
346 let request_context = BackupAuthCredentialRequestContext::new(&KEY, ACI.into());
348 let request = request_context.get_request();
349
350 let blinded_credential = request.issue(
352 redemption_time,
353 BackupLevel::Free,
354 BackupCredentialType::Messages,
355 &server_secret_params(),
356 ISSUE_RAND,
357 );
358
359 let server_public_params = server_secret_params().get_public_params();
361 request_context
362 .receive(blinded_credential, &server_public_params, redemption_time)
363 .expect("credential should be valid")
364 }
365
366 #[test]
367 fn test_server_verify_expiration() {
368 let credential = generate_credential(DAY_ALIGNED_TIMESTAMP);
369 let presentation =
370 credential.present(&server_secret_params().get_public_params(), PRESENT_RAND);
371
372 presentation
373 .verify(DAY_ALIGNED_TIMESTAMP, &server_secret_params())
374 .expect("presentation should be valid");
375
376 presentation
377 .verify(
378 DAY_ALIGNED_TIMESTAMP.sub_seconds(SECONDS_PER_DAY + 1),
379 &server_secret_params(),
380 )
381 .expect_err("credential should not be valid 24h before redemption time");
382 presentation
383 .verify(
384 DAY_ALIGNED_TIMESTAMP.add_seconds(2 * SECONDS_PER_DAY + 1),
385 &server_secret_params(),
386 )
387 .expect_err("credential should not be valid after expiration (2 days later)");
388 }
389
390 #[test]
391 fn test_server_verify_wrong_backup_id() {
392 let credential = generate_credential(DAY_ALIGNED_TIMESTAMP);
393 let valid_presentation =
394 credential.present(&server_secret_params().get_public_params(), PRESENT_RAND);
395 let invalid_presentation = BackupAuthCredentialPresentation {
396 backup_id: libsignal_account_keys::BackupId(*b"a fake backup-id"),
397 ..valid_presentation
398 };
399 invalid_presentation
400 .verify(DAY_ALIGNED_TIMESTAMP, &server_secret_params())
401 .expect_err("credential should not be valid with different backup-id");
402 }
403
404 #[test]
405 fn test_server_verify_wrong_redemption() {
406 let credential = generate_credential(DAY_ALIGNED_TIMESTAMP);
407 let valid_presentation =
408 credential.present(&server_secret_params().get_public_params(), PRESENT_RAND);
409 let invalid_presentation = BackupAuthCredentialPresentation {
410 redemption_time: DAY_ALIGNED_TIMESTAMP.add_seconds(1),
411 ..valid_presentation
412 };
413 invalid_presentation
414 .verify(DAY_ALIGNED_TIMESTAMP, &server_secret_params())
415 .expect_err("credential should not be valid with altered redemption_time");
416 }
417
418 #[test]
419 fn test_server_verify_wrong_receipt_level() {
420 let credential = generate_credential(DAY_ALIGNED_TIMESTAMP);
421 let valid_presentation =
422 credential.present(&server_secret_params().get_public_params(), PRESENT_RAND);
423 let invalid_presentation = BackupAuthCredentialPresentation {
424 backup_level: BackupLevel::Paid,
426 ..valid_presentation
427 };
428 invalid_presentation
429 .verify(DAY_ALIGNED_TIMESTAMP, &server_secret_params())
430 .expect_err("credential should not be valid with wrong receipt");
431 }
432
433 #[test]
434 fn test_client_enforces_timestamp() {
435 let redemption_time: Timestamp = DAY_ALIGNED_TIMESTAMP;
436
437 let request_context = BackupAuthCredentialRequestContext::new(&KEY, ACI.into());
438 let request = request_context.get_request();
439 let blinded_credential = request.issue(
440 redemption_time,
441 BackupLevel::Free,
442 BackupCredentialType::Messages,
443 &server_secret_params(),
444 ISSUE_RAND,
445 );
446 assert!(
447 request_context
448 .receive(
449 blinded_credential,
450 &server_secret_params().get_public_params(),
451 redemption_time.add_seconds(SECONDS_PER_DAY),
452 )
453 .is_err(),
454 "client should require that timestamp matches its expectation"
455 );
456 }
457
458 #[test]
459 fn test_client_enforces_timestamp_granularity() {
460 let redemption_time: Timestamp = DAY_ALIGNED_TIMESTAMP.add_seconds(60 * 60); let request_context = BackupAuthCredentialRequestContext::new(&KEY, ACI.into());
463 let request = request_context.get_request();
464 let blinded_credential = request.issue(
465 redemption_time,
466 BackupLevel::Free,
467 BackupCredentialType::Messages,
468 &server_secret_params(),
469 ISSUE_RAND,
470 );
471 assert!(
472 request_context
473 .receive(
474 blinded_credential,
475 &server_secret_params().get_public_params(),
476 redemption_time,
477 )
478 .is_err(),
479 "client should require that timestamp is on a day boundary"
480 );
481 }
482
483 #[test]
484 fn test_backup_level_serialization() {
485 let free_bytes = common::serialization::serialize(&BackupLevel::Free);
486 let paid_bytes = common::serialization::serialize(&BackupLevel::Paid);
487 assert_eq!(free_bytes.len(), 8);
488 assert_eq!(paid_bytes.len(), 8);
489
490 let free_num: u64 = common::serialization::deserialize(&free_bytes).expect("valid u64");
491 let paid_num: u64 = common::serialization::deserialize(&paid_bytes).expect("valid u64");
492 assert_eq!(free_num, 200);
493 assert_eq!(paid_num, 201);
494
495 let free: BackupLevel =
496 common::serialization::deserialize(&free_bytes).expect("valid level");
497 let paid: BackupLevel =
498 common::serialization::deserialize(&paid_bytes).expect("valid level");
499 assert_eq!(free, BackupLevel::Free);
500 assert_eq!(paid, BackupLevel::Paid);
501 }
502
503 #[test]
504 fn test_backup_level_validation() {
505 assert_matches!(
507 BackupLevel::try_from(0x100000000000u64 + u64::from(BackupLevel::Free)),
508 Err(_)
509 );
510 }
511}