zkgroup/api/call_links/
create_credential.rs

1//
2// Copyright 2023 Signal Messenger, LLC.
3// SPDX-License-Identifier: AGPL-3.0-only
4//
5
6//! Provides CreateCallLinkCredential and related types.
7//!
8//! CreateCallLinkCredential is a MAC over:
9//! - a call link room ID (chosen by the client, blinded at issuance, revealed for verification)
10//! - the user's ACI (provided by the chat server at issuance, passed encrypted to the calling server for verification)
11//! - a timestamp, truncated to day granularity (chosen by the chat server at issuance, passed publicly to the calling server for verification)
12
13use curve25519_dalek_signal::ristretto::RistrettoPoint;
14use partial_default::PartialDefault;
15use poksho::ShoApi;
16use serde::{Deserialize, Serialize};
17
18use super::{CallLinkPublicParams, CallLinkSecretParams};
19use crate::common::serialization::ReservedByte;
20use crate::common::sho::Sho;
21use crate::common::simple_types::*;
22use crate::crypto::uid_encryption;
23use crate::crypto::uid_struct::UidStruct;
24use crate::generic_server_params::{GenericServerPublicParams, GenericServerSecretParams};
25use crate::groups::UuidCiphertext;
26use crate::ZkGroupVerificationFailure;
27
28#[derive(Serialize, Deserialize, Clone, Copy)]
29struct CallLinkRoomIdPoint(RistrettoPoint);
30
31impl CallLinkRoomIdPoint {
32    fn new(room_id: &[u8]) -> Self {
33        Self(Sho::new(b"20230413_Signal_CallLinkRoomId", room_id).get_point())
34    }
35}
36
37impl zkcredential::attributes::RevealedAttribute for CallLinkRoomIdPoint {
38    fn as_point(&self) -> RistrettoPoint {
39        self.0
40    }
41}
42
43const CREDENTIAL_LABEL: &[u8] = b"20230413_Signal_CreateCallLinkCredential";
44
45#[derive(Serialize, Deserialize, PartialDefault)]
46pub struct CreateCallLinkCredentialRequestContext {
47    reserved: ReservedByte,
48    blinded_room_id: zkcredential::issuance::blind::BlindedPoint,
49    key_pair: zkcredential::issuance::blind::BlindingKeyPair,
50}
51
52impl CreateCallLinkCredentialRequestContext {
53    pub fn new(room_id: &[u8], randomness: RandomnessBytes) -> Self {
54        let mut sho =
55            poksho::ShoHmacSha256::new(b"20230413_Signal_CreateCallLinkCredentialRequest");
56        sho.absorb_and_ratchet(&randomness);
57
58        let key_pair = zkcredential::issuance::blind::BlindingKeyPair::generate(&mut sho);
59        let blinded_room_id = key_pair
60            .blind(&CallLinkRoomIdPoint::new(room_id), &mut sho)
61            .into();
62
63        Self {
64            reserved: Default::default(),
65            blinded_room_id,
66            key_pair,
67        }
68    }
69
70    pub fn get_request(&self) -> CreateCallLinkCredentialRequest {
71        CreateCallLinkCredentialRequest {
72            reserved: Default::default(),
73            blinded_room_id: self.blinded_room_id,
74            public_key: *self.key_pair.public_key(),
75        }
76    }
77}
78
79#[derive(Serialize, Deserialize, PartialDefault)]
80pub struct CreateCallLinkCredentialRequest {
81    reserved: ReservedByte,
82    blinded_room_id: zkcredential::issuance::blind::BlindedPoint,
83    public_key: zkcredential::issuance::blind::BlindingPublicKey,
84    // Note that unlike ProfileKeyCredentialRequest, we don't have a proof. This is because our only
85    // "blinded" attribute is derived from the room ID, making it effectively random as far as the
86    // issuing server is concerned. Whether or not the server is willing to issue a
87    // CreateCallLinkCredential doesn't depend on what that room ID is; in the very unlikely case of
88    // a collision, the client will get a failure when they use the credential presentation to
89    // actually attempt to create the link.
90    //
91    // (RingRTC will only generate room IDs of a certain form, but we don't need to enforce that
92    // using zkproofs; we can do so more directly in RingRTC and on the calling server.)
93}
94
95impl CreateCallLinkCredentialRequest {
96    pub fn issue(
97        &self,
98        user_id: libsignal_core::Aci,
99        timestamp: Timestamp,
100        params: &GenericServerSecretParams,
101        randomness: RandomnessBytes,
102    ) -> CreateCallLinkCredentialResponse {
103        CreateCallLinkCredentialResponse {
104            reserved: Default::default(),
105            timestamp,
106            blinded_credential: zkcredential::issuance::IssuanceProofBuilder::new(CREDENTIAL_LABEL)
107                .add_public_attribute(&timestamp)
108                .add_attribute(&UidStruct::from_service_id(user_id.into()))
109                .add_blinded_revealed_attribute(&self.blinded_room_id)
110                .issue(&params.credential_key, &self.public_key, randomness),
111        }
112    }
113}
114
115#[derive(Serialize, Deserialize, PartialDefault)]
116pub struct CreateCallLinkCredentialResponse {
117    reserved: ReservedByte,
118    // Does not include the room ID or the user ID, because the client already knows those.
119    timestamp: Timestamp,
120    blinded_credential: zkcredential::issuance::blind::BlindedIssuanceProof,
121}
122
123impl CreateCallLinkCredentialRequestContext {
124    pub fn receive(
125        self,
126        response: CreateCallLinkCredentialResponse,
127        user_id: libsignal_core::Aci,
128        params: &GenericServerPublicParams,
129    ) -> Result<CreateCallLinkCredential, ZkGroupVerificationFailure> {
130        if !response.timestamp.is_day_aligned() {
131            return Err(ZkGroupVerificationFailure);
132        }
133
134        Ok(CreateCallLinkCredential {
135            reserved: Default::default(),
136            timestamp: response.timestamp,
137            credential: zkcredential::issuance::IssuanceProofBuilder::new(CREDENTIAL_LABEL)
138                .add_public_attribute(&response.timestamp)
139                .add_attribute(&UidStruct::from_service_id(user_id.into()))
140                .add_blinded_revealed_attribute(&self.blinded_room_id)
141                .verify(
142                    &params.credential_key,
143                    &self.key_pair,
144                    response.blinded_credential,
145                )
146                .map_err(|_| ZkGroupVerificationFailure)?,
147        })
148    }
149}
150
151#[derive(Serialize, Deserialize, PartialDefault)]
152pub struct CreateCallLinkCredential {
153    reserved: ReservedByte,
154    // We could avoid having to pass in the room ID or user ID again if we saved them here, but
155    // that's readily available information in the apps, so we may as well keep the credential
156    // small.
157    timestamp: Timestamp,
158    credential: zkcredential::credentials::Credential,
159}
160
161impl CreateCallLinkCredential {
162    pub fn present(
163        &self,
164        room_id: &[u8],
165        user_id: libsignal_core::Aci,
166        server_params: &GenericServerPublicParams,
167        call_link_params: &CallLinkSecretParams,
168        randomness: RandomnessBytes,
169    ) -> CreateCallLinkCredentialPresentation {
170        let user_id = UidStruct::from_service_id(user_id.into());
171        let encrypted_user_id = call_link_params.uid_enc_key_pair.encrypt(&user_id);
172        CreateCallLinkCredentialPresentation {
173            reserved: Default::default(),
174            timestamp: self.timestamp,
175            user_id: encrypted_user_id,
176            proof: zkcredential::presentation::PresentationProofBuilder::new(CREDENTIAL_LABEL)
177                .add_attribute(&user_id, &call_link_params.uid_enc_key_pair)
178                .add_revealed_attribute(&CallLinkRoomIdPoint::new(room_id))
179                .present(&server_params.credential_key, &self.credential, randomness),
180        }
181    }
182}
183
184#[derive(Serialize, Deserialize, PartialDefault)]
185pub struct CreateCallLinkCredentialPresentation {
186    reserved: ReservedByte,
187    // The room ID is provided externally as part of the request.
188    user_id: zkcredential::attributes::Ciphertext<uid_encryption::UidEncryptionDomain>,
189    timestamp: Timestamp,
190    proof: zkcredential::presentation::PresentationProof,
191}
192
193impl CreateCallLinkCredentialPresentation {
194    pub fn verify(
195        &self,
196        room_id: &[u8],
197        current_time: Timestamp,
198        server_params: &GenericServerSecretParams,
199        call_link_params: &CallLinkPublicParams,
200    ) -> Result<(), ZkGroupVerificationFailure> {
201        let expiration = self
202            .timestamp
203            .checked_add_seconds(30 * 60 * 60) // 30 hours, to account for clock skew
204            .ok_or(ZkGroupVerificationFailure)?;
205
206        if !(self.timestamp..expiration).contains(&current_time) {
207            return Err(ZkGroupVerificationFailure);
208        }
209
210        zkcredential::presentation::PresentationProofVerifier::new(CREDENTIAL_LABEL)
211            .add_public_attribute(&self.timestamp)
212            .add_attribute(&self.user_id, &call_link_params.uid_enc_public_key)
213            .add_revealed_attribute(&CallLinkRoomIdPoint::new(room_id))
214            .verify(&server_params.credential_key, &self.proof)
215            .map_err(|_| ZkGroupVerificationFailure)
216    }
217
218    pub fn get_user_id(&self) -> UuidCiphertext {
219        UuidCiphertext {
220            reserved: Default::default(),
221            ciphertext: self.user_id,
222        }
223    }
224}