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