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(Clone, 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(Clone, 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(Clone, 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(Clone, 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(Clone, 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 crate::ServerSecretParams::check_auth_credential_redemption_time(
288 self.redemption_time,
289 current_time,
290 )?;
291
292 zkcredential::presentation::PresentationProofVerifier::new(CREDENTIAL_LABEL)
293 .add_public_attribute(&self.redemption_time)
294 .add_public_attribute(&u64::from(self.backup_level))
295 .add_public_attribute(&u64::from(self.credential_type))
296 .add_revealed_attribute(&BackupIdPoint::new(&self.backup_id))
297 .verify(&server_params.credential_key, &self.proof)
298 .map_err(|_| ZkGroupVerificationFailure)
299 }
300
301 pub fn backup_level(&self) -> BackupLevel {
302 self.backup_level
303 }
304
305 pub fn credential_type(&self) -> BackupCredentialType {
306 self.credential_type
307 }
308
309 pub fn backup_id(&self) -> libsignal_account_keys::BackupId {
310 self.backup_id
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use assert_matches::assert_matches;
317
318 use super::*;
319 use crate::{RANDOMNESS_LEN, RandomnessBytes, SECONDS_PER_DAY, Timestamp, common};
320
321 const DAY_ALIGNED_TIMESTAMP: Timestamp = Timestamp::from_epoch_seconds(1681344000); const KEY: libsignal_account_keys::BackupKey = libsignal_account_keys::BackupKey([0x42u8; 32]);
323 const ACI: uuid::Uuid = uuid::uuid!("c0fc16e4-bae5-4343-9f0d-e7ecf4251343");
324 const SERVER_SECRET_RAND: RandomnessBytes = [0xA0; RANDOMNESS_LEN];
325 const ISSUE_RAND: RandomnessBytes = [0xA1; RANDOMNESS_LEN];
326 const PRESENT_RAND: RandomnessBytes = [0xA2; RANDOMNESS_LEN];
327
328 fn server_secret_params() -> GenericServerSecretParams {
329 GenericServerSecretParams::generate(SERVER_SECRET_RAND)
330 }
331
332 fn generate_credential(redemption_time: Timestamp) -> BackupAuthCredential {
333 let request_context = BackupAuthCredentialRequestContext::new(&KEY, ACI.into());
335 let request = request_context.get_request();
336
337 let blinded_credential = request.issue(
339 redemption_time,
340 BackupLevel::Free,
341 BackupCredentialType::Messages,
342 &server_secret_params(),
343 ISSUE_RAND,
344 );
345
346 let server_public_params = server_secret_params().get_public_params();
348 request_context
349 .receive(blinded_credential, &server_public_params, redemption_time)
350 .expect("credential should be valid")
351 }
352
353 #[test]
354 fn test_server_verify_expiration() {
355 let credential = generate_credential(DAY_ALIGNED_TIMESTAMP);
356 let presentation =
357 credential.present(&server_secret_params().get_public_params(), PRESENT_RAND);
358
359 presentation
360 .verify(DAY_ALIGNED_TIMESTAMP, &server_secret_params())
361 .expect("presentation should be valid");
362
363 presentation
364 .verify(
365 DAY_ALIGNED_TIMESTAMP.sub_seconds(SECONDS_PER_DAY + 1),
366 &server_secret_params(),
367 )
368 .expect_err("credential should not be valid 24h before redemption time");
369 presentation
370 .verify(
371 DAY_ALIGNED_TIMESTAMP.add_seconds(2 * SECONDS_PER_DAY + 1),
372 &server_secret_params(),
373 )
374 .expect_err("credential should not be valid after expiration (2 days later)");
375 }
376
377 #[test]
378 fn test_server_verify_wrong_backup_id() {
379 let credential = generate_credential(DAY_ALIGNED_TIMESTAMP);
380 let valid_presentation =
381 credential.present(&server_secret_params().get_public_params(), PRESENT_RAND);
382 let invalid_presentation = BackupAuthCredentialPresentation {
383 backup_id: libsignal_account_keys::BackupId(*b"a fake backup-id"),
384 ..valid_presentation
385 };
386 invalid_presentation
387 .verify(DAY_ALIGNED_TIMESTAMP, &server_secret_params())
388 .expect_err("credential should not be valid with different backup-id");
389 }
390
391 #[test]
392 fn test_server_verify_wrong_redemption() {
393 let credential = generate_credential(DAY_ALIGNED_TIMESTAMP);
394 let valid_presentation =
395 credential.present(&server_secret_params().get_public_params(), PRESENT_RAND);
396 let invalid_presentation = BackupAuthCredentialPresentation {
397 redemption_time: DAY_ALIGNED_TIMESTAMP.add_seconds(1),
398 ..valid_presentation
399 };
400 invalid_presentation
401 .verify(DAY_ALIGNED_TIMESTAMP, &server_secret_params())
402 .expect_err("credential should not be valid with altered redemption_time");
403 }
404
405 #[test]
406 fn test_server_verify_wrong_receipt_level() {
407 let credential = generate_credential(DAY_ALIGNED_TIMESTAMP);
408 let valid_presentation =
409 credential.present(&server_secret_params().get_public_params(), PRESENT_RAND);
410 let invalid_presentation = BackupAuthCredentialPresentation {
411 backup_level: BackupLevel::Paid,
413 ..valid_presentation
414 };
415 invalid_presentation
416 .verify(DAY_ALIGNED_TIMESTAMP, &server_secret_params())
417 .expect_err("credential should not be valid with wrong receipt");
418 }
419
420 #[test]
421 fn test_client_enforces_timestamp() {
422 let redemption_time: Timestamp = DAY_ALIGNED_TIMESTAMP;
423
424 let request_context = BackupAuthCredentialRequestContext::new(&KEY, ACI.into());
425 let request = request_context.get_request();
426 let blinded_credential = request.issue(
427 redemption_time,
428 BackupLevel::Free,
429 BackupCredentialType::Messages,
430 &server_secret_params(),
431 ISSUE_RAND,
432 );
433 assert!(
434 request_context
435 .receive(
436 blinded_credential,
437 &server_secret_params().get_public_params(),
438 redemption_time.add_seconds(SECONDS_PER_DAY),
439 )
440 .is_err(),
441 "client should require that timestamp matches its expectation"
442 );
443 }
444
445 #[test]
446 fn test_client_enforces_timestamp_granularity() {
447 let redemption_time: Timestamp = DAY_ALIGNED_TIMESTAMP.add_seconds(60 * 60); let request_context = BackupAuthCredentialRequestContext::new(&KEY, ACI.into());
450 let request = request_context.get_request();
451 let blinded_credential = request.issue(
452 redemption_time,
453 BackupLevel::Free,
454 BackupCredentialType::Messages,
455 &server_secret_params(),
456 ISSUE_RAND,
457 );
458 assert!(
459 request_context
460 .receive(
461 blinded_credential,
462 &server_secret_params().get_public_params(),
463 redemption_time,
464 )
465 .is_err(),
466 "client should require that timestamp is on a day boundary"
467 );
468 }
469
470 #[test]
471 fn test_backup_level_serialization() {
472 let free_bytes = common::serialization::serialize(&BackupLevel::Free);
473 let paid_bytes = common::serialization::serialize(&BackupLevel::Paid);
474 assert_eq!(free_bytes.len(), 8);
475 assert_eq!(paid_bytes.len(), 8);
476
477 let free_num: u64 = common::serialization::deserialize(&free_bytes).expect("valid u64");
478 let paid_num: u64 = common::serialization::deserialize(&paid_bytes).expect("valid u64");
479 assert_eq!(free_num, 200);
480 assert_eq!(paid_num, 201);
481
482 let free: BackupLevel =
483 common::serialization::deserialize(&free_bytes).expect("valid level");
484 let paid: BackupLevel =
485 common::serialization::deserialize(&paid_bytes).expect("valid level");
486 assert_eq!(free, BackupLevel::Free);
487 assert_eq!(paid, BackupLevel::Paid);
488 }
489
490 #[test]
491 fn test_backup_level_validation() {
492 assert_matches!(
494 BackupLevel::try_from(0x100000000000u64 + u64::from(BackupLevel::Free)),
495 Err(_)
496 );
497 }
498}