Add project files.

This commit is contained in:
micro
2026-06-20 09:31:59 -05:00
parent 9bb3f00d86
commit 4baa4ce8c0
346 changed files with 55751 additions and 0 deletions
+509
View File
@@ -0,0 +1,509 @@
//
// Copyright 2020-2022 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
#![allow(non_snake_case)]
use std::sync::LazyLock;
use const_str::hex;
use curve25519_dalek_signal::constants::RISTRETTO_BASEPOINT_POINT;
use curve25519_dalek_signal::ristretto::RistrettoPoint;
use curve25519_dalek_signal::scalar::Scalar;
use derive_where::derive_where;
use partial_default::PartialDefault;
use serde::{Deserialize, Serialize};
use crate::common::array_utils::{ArrayLike, OneBased};
use crate::common::sho::*;
use crate::common::simple_types::*;
use crate::crypto::receipt_struct::ReceiptStruct;
use crate::crypto::timestamp_struct::TimestampStruct;
use crate::crypto::{
profile_key_credential_request, receipt_credential_request, receipt_struct, uid_struct,
};
use crate::{
NUM_AUTH_CRED_ATTRIBUTES, NUM_PROFILE_KEY_CRED_ATTRIBUTES, NUM_RECEIPT_CRED_ATTRIBUTES,
};
static SYSTEM_PARAMS: LazyLock<SystemParams> = LazyLock::new(|| {
crate::deserialize(SystemParams::SYSTEM_HARDCODED).expect("valid hardcoded params")
});
const NUM_SUPPORTED_ATTRS: usize = 6;
#[derive(Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SystemParams {
pub(crate) G_w: RistrettoPoint,
pub(crate) G_wprime: RistrettoPoint,
pub(crate) G_x0: RistrettoPoint,
pub(crate) G_x1: RistrettoPoint,
pub(crate) G_y: OneBased<[RistrettoPoint; NUM_SUPPORTED_ATTRS]>,
pub(crate) G_m1: RistrettoPoint,
pub(crate) G_m2: RistrettoPoint,
pub(crate) G_m3: RistrettoPoint,
pub(crate) G_m4: RistrettoPoint,
pub(crate) G_m5: RistrettoPoint,
pub(crate) G_V: RistrettoPoint,
pub(crate) G_z: RistrettoPoint,
}
/// Used to specialize a [`KeyPair<S>`] to support a certain number of attributes.
///
/// The only required member is `Storage`, which should be a fixed-size array of [`Scalar`], one for
/// each attribute. However, for backwards compatibility some systems support fewer attributes than
/// are actually stored, and in this case the `NUM_ATTRS` member can be set to a custom value. Note
/// that `NUM_ATTRS` must always be less than or equal to the number of elements in `Storage`.
pub trait AttrScalars {
/// The storage (should be a fixed-size array of Scalar).
type Storage: ArrayLike<Scalar> + Copy + Eq + Serialize + for<'a> Deserialize<'a>;
/// The number of attributes supported in this system.
///
/// Defaults to the full set stored in `Self::Storage`.
const NUM_ATTRS: usize = Self::Storage::LEN;
}
impl AttrScalars for AuthCredential {
// Store four scalars for backwards compatibility.
type Storage = [Scalar; 4];
const NUM_ATTRS: usize = NUM_AUTH_CRED_ATTRIBUTES;
}
impl AttrScalars for AuthCredentialWithPni {
type Storage = [Scalar; 5];
}
impl AttrScalars for ProfileKeyCredential {
// Store four scalars for backwards compatibility.
type Storage = [Scalar; 4];
const NUM_ATTRS: usize = NUM_PROFILE_KEY_CRED_ATTRIBUTES;
}
impl AttrScalars for ExpiringProfileKeyCredential {
type Storage = [Scalar; 5];
}
impl AttrScalars for ReceiptCredential {
// Store four scalars for backwards compatibility.
type Storage = [Scalar; 4];
const NUM_ATTRS: usize = NUM_RECEIPT_CRED_ATTRIBUTES;
}
impl AttrScalars for PniCredential {
type Storage = [Scalar; 6];
}
#[derive(Serialize, Deserialize, PartialDefault)]
#[partial_default(bound = "S::Storage: Default")]
#[derive_where(Clone, Copy, PartialEq, Eq; S: AttrScalars)]
pub struct KeyPair<S: AttrScalars> {
// private
pub(crate) w: Scalar,
pub(crate) wprime: Scalar,
pub(crate) W: RistrettoPoint,
pub(crate) x0: Scalar,
pub(crate) x1: Scalar,
pub(crate) y: OneBased<S::Storage>,
// public
pub(crate) C_W: RistrettoPoint,
pub(crate) I: RistrettoPoint,
}
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, PartialDefault)]
pub struct PublicKey {
pub(crate) C_W: RistrettoPoint,
pub(crate) I: RistrettoPoint,
}
/// Unused, kept only because ServerSecretParams contains a `KeyPair<AuthCredential>`.
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, PartialDefault)]
pub(crate) struct AuthCredential {
pub(crate) t: Scalar,
pub(crate) U: RistrettoPoint,
pub(crate) V: RistrettoPoint,
}
/// Unused, kept only because ServerSecretParams contains a `KeyPair<AuthCredentialWithPni>`.
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, PartialDefault)]
pub(crate) struct AuthCredentialWithPni {
pub(crate) t: Scalar,
pub(crate) U: RistrettoPoint,
pub(crate) V: RistrettoPoint,
}
/// Unused, kept only because ServerSecretParams contains a `KeyPair<ProfileKeyCredential>`.
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProfileKeyCredential {
pub(crate) t: Scalar,
pub(crate) U: RistrettoPoint,
pub(crate) V: RistrettoPoint,
}
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, PartialDefault)]
pub struct ExpiringProfileKeyCredential {
pub(crate) t: Scalar,
pub(crate) U: RistrettoPoint,
pub(crate) V: RistrettoPoint,
}
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BlindedExpiringProfileKeyCredentialWithSecretNonce {
pub(crate) rprime: Scalar,
pub(crate) t: Scalar,
pub(crate) U: RistrettoPoint,
pub(crate) S1: RistrettoPoint,
pub(crate) S2: RistrettoPoint,
}
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, PartialDefault)]
pub struct BlindedExpiringProfileKeyCredential {
pub(crate) t: Scalar,
pub(crate) U: RistrettoPoint,
pub(crate) S1: RistrettoPoint,
pub(crate) S2: RistrettoPoint,
}
/// Unused, kept only because ServerSecretParams contains a `KeyPair<PniCredential>`.
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PniCredential {
pub(crate) t: Scalar,
pub(crate) U: RistrettoPoint,
pub(crate) V: RistrettoPoint,
}
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, PartialDefault)]
pub struct ReceiptCredential {
pub(crate) t: Scalar,
pub(crate) U: RistrettoPoint,
pub(crate) V: RistrettoPoint,
}
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BlindedReceiptCredentialWithSecretNonce {
pub(crate) rprime: Scalar,
pub(crate) t: Scalar,
pub(crate) U: RistrettoPoint,
pub(crate) S1: RistrettoPoint,
pub(crate) S2: RistrettoPoint,
}
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, PartialDefault)]
pub struct BlindedReceiptCredential {
pub(crate) t: Scalar,
pub(crate) U: RistrettoPoint,
pub(crate) S1: RistrettoPoint,
pub(crate) S2: RistrettoPoint,
}
pub(crate) fn convert_to_points_receipt_struct(
receipt: receipt_struct::ReceiptStruct,
) -> Vec<RistrettoPoint> {
let system = SystemParams::get_hardcoded();
let m1 = receipt.calc_m1();
let receipt_serial_scalar = encode_receipt_serial_bytes(receipt.receipt_serial_bytes);
vec![m1 * system.G_m1, receipt_serial_scalar * system.G_m2]
}
pub(crate) fn convert_to_point_M2_receipt_serial_bytes(
receipt_serial_bytes: ReceiptSerialBytes,
) -> RistrettoPoint {
let system = SystemParams::get_hardcoded();
let receipt_serial_scalar = encode_receipt_serial_bytes(receipt_serial_bytes);
receipt_serial_scalar * system.G_m2
}
impl SystemParams {
#[cfg(test)]
fn generate() -> Self {
let mut sho = Sho::new(
b"Signal_ZKGroup_20200424_Constant_Credentials_SystemParams_Generate",
b"",
);
let G_w = sho.get_point();
let G_wprime = sho.get_point();
let G_x0 = sho.get_point();
let G_x1 = sho.get_point();
let G_y1 = sho.get_point();
let G_y2 = sho.get_point();
let G_y3 = sho.get_point();
let G_y4 = sho.get_point();
let G_m1 = sho.get_point();
let G_m2 = sho.get_point();
let G_m3 = sho.get_point();
let G_m4 = sho.get_point();
let G_V = sho.get_point();
let G_z = sho.get_point();
// We don't ever want to use existing generator points in new ways,
// so new points have to be added at the end.
let G_y5 = sho.get_point();
let G_y6 = sho.get_point();
let G_m5 = sho.get_point();
SystemParams {
G_w,
G_wprime,
G_x0,
G_x1,
G_y: OneBased([G_y1, G_y2, G_y3, G_y4, G_y5, G_y6]),
G_m1,
G_m2,
G_m3,
G_m4,
G_m5,
G_V,
G_z,
}
}
pub fn get_hardcoded() -> SystemParams {
*SYSTEM_PARAMS
}
const SYSTEM_HARDCODED: &'static [u8] = &hex!(
"9ae7c8e5ed779b114ae7708aa2f794670adda324987b659913122c35505b105e6ca31025d2d76be7fd34944f98f7fa0e37babb2c8b98bbbdbd3dd1bf130cca2c8a9a3bdfaaa2b6b322d46b93eca7b0d51c86a3c839e11466358258a6c10c577fc2bffd34cd99164c9a6cd29fab55d91ff9269322ec3458603cc96a0d47f704058288f62ee0acedb8aa23242121d98965a9bb2991250c11758095ece0fd2b33285286fe1fcb056103b6081744b975f550d08521568dd3d8618f25c140375a0f4024c3aa23bdfffb27fbd982208d3ecd1fd3bcb7ac0c3a14b109804fc748d7fa456cffb4934f980b6e09a248a60f44a6150ae6c13d7e3c06261d7e4eed37f39f60b04dd9d607fd357012274d3c63dbb38e7378599c9e97dfbb28842694891d5f0ddc729919b798b4131503408cc57a9c532f4427632c88f54cea53861a5bc44c61cc6037dc31c2e8d4474fb519587a448693182ad9d6d86b535957858f547b9340127da75f8074caee944ac36c0ac662d38c9b3ccce03a093fcd9644047398b86b6e83372ff14fb8bb0dea65531252ac70d58a4a0810d682a0e709c9227b30ef6c8e17c5915d527221bb00da8175cd6489aa8aa492a500f9abee5690b9dfca8855dc0bd02a7f277add240f639ac16801e81574afb4683edff63b9a01e93dbd867a04b616c706c80c756c11a3016bbfb60977f4648b5f2395a4b428b7211940813e3afde2b87aa9c2c37bf716e2578f95656df12c2fb6f5d0631f6f71e2c3193f6d"
);
}
impl<S: AttrScalars> KeyPair<S> {
pub fn generate(sho: &mut Sho) -> Self {
assert!(S::NUM_ATTRS >= 1, "at least one attribute required");
assert!(
S::NUM_ATTRS <= NUM_SUPPORTED_ATTRS,
"more than {NUM_SUPPORTED_ATTRS} attributes not supported"
);
assert!(
S::NUM_ATTRS <= S::Storage::LEN,
"more attributes than storage",
);
let system = SystemParams::get_hardcoded();
let w = sho.get_scalar();
let W = w * system.G_w;
let wprime = sho.get_scalar();
let x0 = sho.get_scalar();
let x1 = sho.get_scalar();
let y = OneBased::<S::Storage>::create(|| sho.get_scalar());
let C_W = (w * system.G_w) + (wprime * system.G_wprime);
let mut I = system.G_V - (x0 * system.G_x0) - (x1 * system.G_x1);
for (yn, G_yn) in y.iter().zip(system.G_y.iter()).take(S::NUM_ATTRS) {
I -= yn * G_yn;
}
KeyPair {
w,
wprime,
W,
x0,
x1,
y,
C_W,
I,
}
}
pub fn get_public_key(&self) -> PublicKey {
PublicKey {
C_W: self.C_W,
I: self.I,
}
}
fn credential_core(
&self,
M: &[RistrettoPoint],
sho: &mut Sho,
) -> (Scalar, RistrettoPoint, RistrettoPoint) {
assert!(
M.len() <= S::NUM_ATTRS,
"more than {} attributes not supported",
S::NUM_ATTRS
);
let t = sho.get_scalar();
let U = sho.get_point();
let mut V = self.W + (self.x0 + self.x1 * t) * U;
for (yn, Mn) in self.y.iter().zip(M) {
V += yn * Mn;
}
(t, U, V)
}
}
impl KeyPair<ExpiringProfileKeyCredential> {
pub fn create_blinded_expiring_profile_key_credential(
&self,
uid: uid_struct::UidStruct,
public_key: profile_key_credential_request::PublicKey,
ciphertext: profile_key_credential_request::Ciphertext,
credential_expiration_time: Timestamp,
sho: &mut Sho,
) -> BlindedExpiringProfileKeyCredentialWithSecretNonce {
let M = [uid.M1, uid.M2];
let (t, U, Vprime) = self.credential_core(&M, sho);
let params = SystemParams::get_hardcoded();
let m5 = TimestampStruct::calc_m_from(credential_expiration_time);
let M5 = m5 * params.G_m5;
let Vprime_with_expiration = Vprime + (self.y[5] * M5);
let rprime = sho.get_scalar();
let R1 = rprime * RISTRETTO_BASEPOINT_POINT;
let R2 = rprime * public_key.Y + Vprime_with_expiration;
let S1 = R1 + (self.y[3] * ciphertext.D1) + (self.y[4] * ciphertext.E1);
let S2 = R2 + (self.y[3] * ciphertext.D2) + (self.y[4] * ciphertext.E2);
BlindedExpiringProfileKeyCredentialWithSecretNonce {
rprime,
t,
U,
S1,
S2,
}
}
}
impl KeyPair<ReceiptCredential> {
pub fn create_blinded_receipt_credential(
&self,
public_key: receipt_credential_request::PublicKey,
ciphertext: receipt_credential_request::Ciphertext,
receipt_expiration_time: Timestamp,
receipt_level: ReceiptLevel,
sho: &mut Sho,
) -> BlindedReceiptCredentialWithSecretNonce {
let params = SystemParams::get_hardcoded();
let m1 = ReceiptStruct::calc_m1_from(receipt_expiration_time, receipt_level);
let M = [m1 * params.G_m1];
let (t, U, Vprime) = self.credential_core(&M, sho);
let rprime = sho.get_scalar();
let R1 = rprime * RISTRETTO_BASEPOINT_POINT;
let R2 = rprime * public_key.Y + Vprime;
let S1 = self.y[2] * ciphertext.D1 + R1;
let S2 = self.y[2] * ciphertext.D2 + R2;
BlindedReceiptCredentialWithSecretNonce {
rprime,
t,
U,
S1,
S2,
}
}
}
impl BlindedExpiringProfileKeyCredentialWithSecretNonce {
pub fn get_blinded_expiring_profile_key_credential(
&self,
) -> BlindedExpiringProfileKeyCredential {
BlindedExpiringProfileKeyCredential {
t: self.t,
U: self.U,
S1: self.S1,
S2: self.S2,
}
}
}
impl BlindedReceiptCredentialWithSecretNonce {
pub fn get_blinded_receipt_credential(&self) -> BlindedReceiptCredential {
BlindedReceiptCredential {
t: self.t,
U: self.U,
S1: self.S1,
S2: self.S2,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::constants::*;
use crate::crypto::proofs;
#[test]
fn test_system() {
let params = SystemParams::generate();
println!("PARAMS = {:#x?}", bincode::serialize(&params));
assert!(SystemParams::generate() == SystemParams::get_hardcoded());
}
#[test]
fn test_mac() {
// It doesn't really matter *which* credential we test here, we just want to generally know
// we've set things up correctly. (Also, the credentials hardcoded here in zkgroup may
// eventually all be superseded by implementations using zkcredential, at which point this
// test can be deleted.)
let mut sho = Sho::new(b"Test_Credentials", b"");
let keypair = KeyPair::<ExpiringProfileKeyCredential>::generate(&mut sho);
let uid_bytes = TEST_ARRAY_16;
let redemption_time = Timestamp::from_epoch_seconds(37 * SECONDS_PER_DAY);
let aci = libsignal_core::Aci::from_uuid_bytes(uid_bytes);
let aci_struct = uid_struct::UidStruct::from_service_id(aci.into());
let profile_key_struct = crate::crypto::profile_key_struct::ProfileKeyStruct::new(
[1; PROFILE_KEY_LEN],
uid_bytes,
);
let request_key_pair =
crate::crypto::profile_key_credential_request::KeyPair::generate(&mut sho);
let ciphertext = request_key_pair
.encrypt(profile_key_struct, &mut sho)
.get_ciphertext();
let credential = keypair.create_blinded_expiring_profile_key_credential(
aci_struct,
request_key_pair.get_public_key(),
ciphertext,
redemption_time,
&mut sho,
);
let proof = proofs::ExpiringProfileKeyCredentialIssuanceProof::new(
keypair,
request_key_pair.get_public_key(),
ciphertext,
credential,
aci_struct,
redemption_time,
&mut sho,
);
let public_key = keypair.get_public_key();
proof
.verify(
public_key,
request_key_pair.get_public_key(),
uid_bytes,
ciphertext,
credential.get_blinded_expiring_profile_key_credential(),
redemption_time,
)
.unwrap();
let keypair_bytes = bincode::serialize(&keypair).unwrap();
let keypair2 = bincode::deserialize(&keypair_bytes).unwrap();
assert!(keypair == keypair2);
let public_key_bytes = bincode::serialize(&public_key).unwrap();
let public_key2 = bincode::deserialize(&public_key_bytes).unwrap();
assert!(public_key == public_key2);
let mac_bytes = bincode::serialize(&credential).unwrap();
println!("mac_bytes = {}", hex::encode(&mac_bytes));
assert_eq!(
mac_bytes,
hex!(
"ef47110715831160100f14d1936f4349c45b80ccaacd4edd9f949375d2d90a090888d81f8b0ed313
808b5ff7ec1957ed4e8b3d9c195b3a5abdbdd3d972c29809100a7f8dc2354be7a1d44452cbadd87e
4851ae05ebeb2586b856d35af765883a94473ad855df8583be2930e4e1d5756175a9091f2be1d8d0
21280446a7611841d6b4f2eb165267a9d1d7a800f19c2077a4ef7df721b160fe200181be3c455f1c"
)
);
}
}
@@ -0,0 +1,215 @@
//
// Copyright 2020 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
#![allow(non_snake_case)]
use std::sync::LazyLock;
use curve25519_dalek_signal::ristretto::RistrettoPoint;
use partial_default::PartialDefault;
use serde::{Deserialize, Serialize};
use subtle::{Choice, ConditionallySelectable, ConstantTimeEq};
use zkcredential::attributes::Attribute;
use crate::common::errors::*;
use crate::common::sho::*;
use crate::common::simple_types::*;
use crate::crypto::profile_key_struct;
static SYSTEM_PARAMS: LazyLock<SystemParams> = LazyLock::new(|| {
crate::deserialize(&SystemParams::SYSTEM_HARDCODED).expect("valid hardcoded params")
});
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, PartialDefault)]
pub struct SystemParams {
pub(crate) G_b1: RistrettoPoint,
pub(crate) G_b2: RistrettoPoint,
}
pub type KeyPair = zkcredential::attributes::KeyPair<ProfileKeyEncryptionDomain>;
pub type PublicKey = zkcredential::attributes::PublicKey<ProfileKeyEncryptionDomain>;
pub type Ciphertext = zkcredential::attributes::Ciphertext<ProfileKeyEncryptionDomain>;
impl SystemParams {
pub fn generate() -> Self {
let mut sho = Sho::new(
b"Signal_ZKGroup_20200424_Constant_ProfileKeyEncryption_SystemParams_Generate",
b"",
);
let G_b1 = sho.get_point();
let G_b2 = sho.get_point();
SystemParams { G_b1, G_b2 }
}
pub fn get_hardcoded() -> SystemParams {
*SYSTEM_PARAMS
}
const SYSTEM_HARDCODED: [u8; 64] = [
0xf6, 0xba, 0xa3, 0x17, 0xce, 0x18, 0x39, 0xc9, 0x3d, 0x61, 0x7e, 0xc, 0xd8, 0x37, 0xd1,
0x9d, 0xa9, 0xc8, 0xa4, 0xc5, 0x20, 0xbf, 0x7c, 0x51, 0xb1, 0xe6, 0xc2, 0xcb, 0x2a, 0x4,
0x9c, 0x61, 0x2e, 0x1, 0x75, 0x89, 0x4c, 0x87, 0x30, 0xb2, 0x3, 0xab, 0x3b, 0xd9, 0x8e,
0xcb, 0x2d, 0x81, 0xab, 0xac, 0xb6, 0x5f, 0x8a, 0x61, 0x24, 0xf4, 0x97, 0x71, 0xd1, 0x4a,
0x98, 0x52, 0x12, 0xc,
];
}
pub struct ProfileKeyEncryptionDomain;
impl zkcredential::attributes::Domain for ProfileKeyEncryptionDomain {
type Attribute = profile_key_struct::ProfileKeyStruct;
const ID: &'static str = "Signal_ZKGroup_20231011_ProfileKeyEncryption";
fn G_a() -> [RistrettoPoint; 2] {
let system = SystemParams::get_hardcoded();
[system.G_b1, system.G_b2]
}
}
impl ProfileKeyEncryptionDomain {
pub(crate) fn decrypt(
key_pair: &KeyPair,
ciphertext: &Ciphertext,
uid_bytes: UidBytes,
) -> Result<profile_key_struct::ProfileKeyStruct, ZkGroupVerificationFailure> {
let M4 = key_pair
.decrypt_to_second_point(ciphertext)
.map_err(|_| ZkGroupVerificationFailure)?;
let (mask, candidates) = M4.decode_253_bits();
let target_M3 = key_pair.a1.invert() * ciphertext.as_points()[0];
let seed_sho = profile_key_struct::ProfileKeyStruct::seed_M3();
let mut retval: profile_key_struct::ProfileKeyStruct = PartialDefault::partial_default();
let mut n_found = 0;
#[allow(clippy::needless_range_loop)]
for i in 0..8 {
let is_valid_fe = Choice::from((mask >> i) & 1);
let profile_key_bytes: ProfileKeyBytes = candidates[i];
for j in 0..8 {
let mut pk = profile_key_bytes;
if ((j >> 2) & 1) == 1 {
pk[0] |= 0x01;
}
if ((j >> 1) & 1) == 1 {
pk[31] |= 0x80;
}
if (j & 1) == 1 {
pk[31] |= 0x40;
}
let M3 =
profile_key_struct::ProfileKeyStruct::calc_M3(seed_sho.clone(), pk, uid_bytes);
let candidate_retval = profile_key_struct::ProfileKeyStruct { bytes: pk, M3, M4 };
let found = M3.ct_eq(&target_M3) & is_valid_fe;
retval.conditional_assign(&candidate_retval, found);
n_found += found.unwrap_u8();
}
}
if n_found == 1 {
Ok(retval)
} else {
Err(ZkGroupVerificationFailure)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::constants::*;
#[test]
fn test_profile_key_encryption() {
let master_key = TEST_ARRAY_32_1;
let mut sho = Sho::new(b"Test_Profile_Key_Encryption", &master_key);
//let system = SystemParams::generate();
//println!("PARAMS = {:#x?}", bincode::serialize(&system));
assert!(SystemParams::generate() == SystemParams::get_hardcoded());
let key_pair = KeyPair::derive_from(sho.as_mut());
// Test serialize of key_pair
let key_pair_bytes = bincode::serialize(&key_pair).unwrap();
match bincode::deserialize::<KeyPair>(&key_pair_bytes[0..key_pair_bytes.len() - 1]) {
Err(_) => (),
_ => unreachable!(),
};
let key_pair2: KeyPair = bincode::deserialize(&key_pair_bytes).unwrap();
assert!(key_pair == key_pair2);
let profile_key_bytes = TEST_ARRAY_32_1;
let uid_bytes = TEST_ARRAY_16_1;
let profile_key = profile_key_struct::ProfileKeyStruct::new(profile_key_bytes, uid_bytes);
let ciphertext = key_pair.encrypt(&profile_key);
// Test serialize / deserialize of Ciphertext
let ciphertext_bytes = bincode::serialize(&ciphertext).unwrap();
assert!(ciphertext_bytes.len() == 64);
let ciphertext2: Ciphertext = bincode::deserialize(&ciphertext_bytes).unwrap();
assert!(ciphertext == ciphertext2);
println!("ciphertext_bytes = {ciphertext_bytes:#x?}");
assert!(
ciphertext_bytes
== vec![
0x56, 0x18, 0xcb, 0x4c, 0x7d, 0x72, 0x1e, 0x1, 0x2b, 0x22, 0xf0, 0x77, 0xef,
0x12, 0x64, 0xf6, 0xb1, 0x43, 0xbb, 0x59, 0x7a, 0x1d, 0x66, 0x5a, 0x70, 0xaa,
0x84, 0x24, 0x5f, 0x24, 0x6d, 0x20, 0xba, 0xdb, 0x97, 0x47, 0x4a, 0x56, 0xf4,
0xb5, 0x36, 0x1a, 0xec, 0xa9, 0xd1, 0x18, 0xb7, 0x0, 0x4e, 0x14, 0x9, 0x71,
0x99, 0xa, 0xab, 0x2a, 0xf2, 0x43, 0x2d, 0x3f, 0x8f, 0x7d, 0x21, 0x3a,
]
);
let plaintext =
ProfileKeyEncryptionDomain::decrypt(&key_pair, &ciphertext2, uid_bytes).unwrap();
assert!(plaintext == profile_key);
let mut sho = Sho::new(b"Test_Repeated_ProfileKeyEnc/Dec", b"seed");
for _ in 0..100 {
let uid_bytes: UidBytes = sho.squeeze_as_array();
let profile_key_bytes: ProfileKeyBytes = sho.squeeze_as_array();
let profile_key =
profile_key_struct::ProfileKeyStruct::new(profile_key_bytes, uid_bytes);
let ciphertext = key_pair.encrypt(&profile_key);
assert!(
ProfileKeyEncryptionDomain::decrypt(&key_pair, &ciphertext, uid_bytes).unwrap()
== profile_key
);
}
let uid_bytes = TEST_ARRAY_16;
let profile_key = profile_key_struct::ProfileKeyStruct::new(TEST_ARRAY_32, TEST_ARRAY_16);
let ciphertext = key_pair.encrypt(&profile_key);
assert!(
ProfileKeyEncryptionDomain::decrypt(&key_pair, &ciphertext, uid_bytes).unwrap()
== profile_key
);
let uid_bytes = TEST_ARRAY_16;
let profile_key = profile_key_struct::ProfileKeyStruct::new(TEST_ARRAY_32_2, TEST_ARRAY_16);
let ciphertext = key_pair.encrypt(&profile_key);
assert!(
ProfileKeyEncryptionDomain::decrypt(&key_pair, &ciphertext, uid_bytes).unwrap()
== profile_key
);
let uid_bytes = TEST_ARRAY_16;
let profile_key = profile_key_struct::ProfileKeyStruct::new(TEST_ARRAY_32_3, TEST_ARRAY_16);
let ciphertext = key_pair.encrypt(&profile_key);
assert!(
ProfileKeyEncryptionDomain::decrypt(&key_pair, &ciphertext, uid_bytes).unwrap()
== profile_key
);
let uid_bytes = TEST_ARRAY_16;
let profile_key = profile_key_struct::ProfileKeyStruct::new(TEST_ARRAY_32_4, TEST_ARRAY_16);
let ciphertext = key_pair.encrypt(&profile_key);
assert!(
ProfileKeyEncryptionDomain::decrypt(&key_pair, &ciphertext, uid_bytes).unwrap()
== profile_key
);
}
}
@@ -0,0 +1,87 @@
//
// Copyright 2020 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
#![allow(non_snake_case)]
use curve25519_dalek_signal::ristretto::RistrettoPoint;
use partial_default::PartialDefault;
use serde::{Deserialize, Serialize};
use subtle::{Choice, ConditionallySelectable};
use crate::common::constants::*;
use crate::common::sho::*;
use crate::common::simple_types::*;
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, PartialDefault)]
pub struct ProfileKeyStruct {
pub(crate) bytes: ProfileKeyBytes,
pub(crate) M3: RistrettoPoint,
pub(crate) M4: RistrettoPoint,
}
impl ProfileKeyStruct {
pub fn new(profile_key_bytes: ProfileKeyBytes, uid_bytes: UidBytes) -> Self {
let mut encoded_profile_key = profile_key_bytes;
encoded_profile_key[0] &= 254;
encoded_profile_key[31] &= 63;
let M3 = Self::calc_M3(Self::seed_M3(), profile_key_bytes, uid_bytes);
let M4 = RistrettoPoint::from_uniform_bytes_single_elligator(&encoded_profile_key);
ProfileKeyStruct {
bytes: profile_key_bytes,
M3,
M4,
}
}
pub(crate) fn seed_M3() -> Sho {
Sho::new_seed(b"Signal_ZKGroup_20200424_ProfileKeyAndUid_ProfileKey_CalcM3")
}
pub(crate) fn calc_M3(
mut seed: Sho,
profile_key_bytes: ProfileKeyBytes,
uid_bytes: UidBytes,
) -> RistrettoPoint {
let mut combined_array = [0u8; PROFILE_KEY_LEN + UUID_LEN];
combined_array[..PROFILE_KEY_LEN].copy_from_slice(&profile_key_bytes);
combined_array[PROFILE_KEY_LEN..].copy_from_slice(&uid_bytes);
seed.absorb_and_ratchet(&combined_array);
seed.get_point_single_elligator()
}
pub fn to_bytes(&self) -> ProfileKeyBytes {
self.bytes
}
}
impl ConditionallySelectable for ProfileKeyStruct {
#[expect(
clippy::needless_range_loop,
reason = "an explicit loop makes it more clear that this runs in constant time"
)]
fn conditional_select(
a: &ProfileKeyStruct,
b: &ProfileKeyStruct,
choice: Choice,
) -> ProfileKeyStruct {
let mut bytes: ProfileKeyBytes = [0u8; PROFILE_KEY_LEN];
for i in 0..PROFILE_KEY_LEN {
bytes[i] = u8::conditional_select(&a.bytes[i], &b.bytes[i], choice);
}
ProfileKeyStruct {
bytes,
M3: RistrettoPoint::conditional_select(&a.M3, &b.M3, choice),
M4: RistrettoPoint::conditional_select(&a.M4, &b.M4, choice),
}
}
}
impl zkcredential::attributes::Attribute for ProfileKeyStruct {
fn as_points(&self) -> [RistrettoPoint; 2] {
[self.M3, self.M4]
}
}
+181
View File
@@ -0,0 +1,181 @@
//
// Copyright 2020 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
#![allow(non_snake_case)]
use std::sync::LazyLock;
use curve25519_dalek_signal::ristretto::RistrettoPoint;
use partial_default::PartialDefault;
use serde::{Deserialize, Serialize};
use subtle::{ConditionallySelectable, ConstantTimeEq};
use zkcredential::attributes::Attribute;
use crate::common::errors::*;
use crate::common::sho::*;
use crate::crypto::uid_struct;
static SYSTEM_PARAMS: LazyLock<SystemParams> = LazyLock::new(|| {
crate::deserialize(&SystemParams::SYSTEM_HARDCODED).expect("valid hardcoded params")
});
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, PartialDefault)]
pub struct SystemParams {
pub(crate) G_a1: RistrettoPoint,
pub(crate) G_a2: RistrettoPoint,
}
pub type KeyPair = zkcredential::attributes::KeyPair<UidEncryptionDomain>;
pub type PublicKey = zkcredential::attributes::PublicKey<UidEncryptionDomain>;
pub type Ciphertext = zkcredential::attributes::Ciphertext<UidEncryptionDomain>;
impl SystemParams {
pub fn generate() -> Self {
let mut sho = Sho::new(
b"Signal_ZKGroup_20200424_Constant_UidEncryption_SystemParams_Generate",
b"",
);
let G_a1 = sho.get_point();
let G_a2 = sho.get_point();
SystemParams { G_a1, G_a2 }
}
pub fn get_hardcoded() -> SystemParams {
*SYSTEM_PARAMS
}
const SYSTEM_HARDCODED: [u8; 64] = [
0xa6, 0x32, 0x4c, 0x36, 0x8d, 0xf7, 0x34, 0x69, 0x11, 0x47, 0x98, 0x13, 0x48, 0xb6, 0xe7,
0xeb, 0x42, 0xc3, 0x30, 0x7e, 0x71, 0x1b, 0x6c, 0x7e, 0xcc, 0xd3, 0x3, 0x2d, 0x45, 0x69,
0x3f, 0x5a, 0x4, 0x80, 0x13, 0x52, 0x5b, 0x76, 0x12, 0x4b, 0xf2, 0x64, 0xc, 0x5e, 0x93,
0x69, 0xc7, 0x6e, 0xfb, 0xe8, 0xa, 0xba, 0x2a, 0x24, 0xaa, 0x5d, 0x8e, 0x18, 0xa9, 0x8e,
0xba, 0x14, 0xf8, 0x37,
];
}
pub struct UidEncryptionDomain;
impl zkcredential::attributes::Domain for UidEncryptionDomain {
type Attribute = uid_struct::UidStruct;
const ID: &'static str = "Signal_ZKGroup_20230419_UidEncryption";
fn G_a() -> [RistrettoPoint; 2] {
let system = SystemParams::get_hardcoded();
[system.G_a1, system.G_a2]
}
}
impl UidEncryptionDomain {
pub(crate) fn decrypt(
key_pair: &KeyPair,
ciphertext: &Ciphertext,
) -> Result<libsignal_core::ServiceId, ZkGroupVerificationFailure> {
let M2 = key_pair
.decrypt_to_second_point(ciphertext)
.map_err(|_| ZkGroupVerificationFailure)?;
match M2.lizard_decode::<sha2::Sha256>() {
None => Err(ZkGroupVerificationFailure),
Some(bytes) => {
// We want to do a constant-time choice between the ACI and the PNI possibilities.
// Only at the end do we do a normal branch to see if decryption succeeded,
// and even then we don't want to expose whether we picked the ACI or the PNI.
// So we store them both in an array, and index into it at the very end.
// This isn't fully "data-oblivious"; only one service ID gets loaded from memory at
// the end, and which one is data-dependent. But it is constant-time.
let decoded_uuid = uuid::Uuid::from_bytes(bytes);
let decoded_service_ids = [
libsignal_core::Aci::from(decoded_uuid).into(),
libsignal_core::Pni::from(decoded_uuid).into(),
];
let decoded_aci = &decoded_service_ids[0];
let decoded_pni = &decoded_service_ids[1];
let sho_seed = uid_struct::UidStruct::seed_M1();
let aci_M1 = uid_struct::UidStruct::calc_M1(sho_seed.clone(), *decoded_aci);
let pni_M1 = uid_struct::UidStruct::calc_M1(sho_seed, *decoded_pni);
debug_assert!(aci_M1 != pni_M1);
let decrypted_M1 = key_pair.a1.invert() * ciphertext.as_points()[0];
let mut index = u8::MAX;
index.conditional_assign(&0, decrypted_M1.ct_eq(&aci_M1));
index.conditional_assign(&1, decrypted_M1.ct_eq(&pni_M1));
decoded_service_ids
.get(index as usize)
.copied()
.ok_or(ZkGroupVerificationFailure)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::constants::*;
#[test]
fn test_uid_encryption() {
let master_key = TEST_ARRAY_32;
let mut sho = Sho::new(b"Test_Uid_Encryption", &master_key);
//let system = SystemParams::generate();
//println!("PARAMS = {:#x?}", bincode::serialize(&system));
assert!(SystemParams::generate() == SystemParams::get_hardcoded());
let key_pair = KeyPair::derive_from(sho.as_mut());
// Test serialize of key_pair
let key_pair_bytes = bincode::serialize(&key_pair).unwrap();
match bincode::deserialize::<KeyPair>(&key_pair_bytes[0..key_pair_bytes.len() - 1]) {
Err(_) => (),
_ => unreachable!(),
};
let key_pair2: KeyPair = bincode::deserialize(&key_pair_bytes).unwrap();
assert!(key_pair == key_pair2);
let aci = libsignal_core::Aci::from_uuid_bytes(TEST_ARRAY_16);
let uid = uid_struct::UidStruct::from_service_id(aci.into());
let ciphertext = key_pair.encrypt(&uid);
// Test serialize / deserialize of Ciphertext
let ciphertext_bytes = bincode::serialize(&ciphertext).unwrap();
assert!(ciphertext_bytes.len() == 64);
let ciphertext2: Ciphertext = bincode::deserialize(&ciphertext_bytes).unwrap();
assert!(ciphertext == ciphertext2);
//println!("ciphertext_bytes = {:#x?}", ciphertext_bytes);
assert!(
ciphertext_bytes
== vec![
0xf8, 0x9e, 0xe7, 0x70, 0x5a, 0x66, 0x3, 0x6b, 0x90, 0x8d, 0xb8, 0x84, 0x21,
0x1b, 0x77, 0x3a, 0xc5, 0x43, 0xee, 0x35, 0xc4, 0xa3, 0x8, 0x62, 0x20, 0xfc,
0x3e, 0x1e, 0x35, 0xb4, 0x23, 0x4c, 0xfa, 0x1d, 0x2e, 0xea, 0x2c, 0xc2, 0xf4,
0xb4, 0xc4, 0x2c, 0xff, 0x39, 0xa9, 0xdc, 0xeb, 0x57, 0x29, 0x3b, 0x5f, 0x87,
0x70, 0xca, 0x60, 0xf9, 0xe9, 0xb7, 0x44, 0x47, 0xbf, 0xd3, 0xbd, 0x3d,
]
);
let plaintext = UidEncryptionDomain::decrypt(&key_pair, &ciphertext2).unwrap();
assert!(matches!(plaintext, libsignal_core::ServiceId::Aci(_)));
assert!(uid_struct::UidStruct::from_service_id(plaintext) == uid);
}
#[test]
fn test_pni_encryption() {
let mut sho = Sho::new(b"Test_Pni_Encryption", &[]);
let key_pair = KeyPair::derive_from(sho.as_mut());
let pni = libsignal_core::Pni::from_uuid_bytes(TEST_ARRAY_16);
let uid = uid_struct::UidStruct::from_service_id(pni.into());
let ciphertext = key_pair.encrypt(&uid);
// Test serialize / deserialize of Ciphertext
let ciphertext_bytes = bincode::serialize(&ciphertext).unwrap();
assert!(ciphertext_bytes.len() == 64);
let ciphertext2: Ciphertext = bincode::deserialize(&ciphertext_bytes).unwrap();
assert!(ciphertext == ciphertext2);
let plaintext = UidEncryptionDomain::decrypt(&key_pair, &ciphertext2).unwrap();
assert!(matches!(plaintext, libsignal_core::ServiceId::Pni(_)));
assert!(uid_struct::UidStruct::from_service_id(plaintext) == uid);
}
}
+59
View File
@@ -0,0 +1,59 @@
//
// Copyright 2020 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
#![allow(non_snake_case)]
use curve25519_dalek_signal::ristretto::RistrettoPoint;
use libsignal_core::ServiceId;
use partial_default::PartialDefault;
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use crate::common::sho::*;
use crate::common::simple_types::*;
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, PartialDefault)]
pub struct UidStruct {
// Currently unused. It would be possible to convert this back to the correct kind of ServiceId
// using the same technique as decryption: comparing possible M1 points and seeing which one
// matches. But we don't have a need for that, and therefore it's better if that operation
// remains part of decryption, so that you're guaranteed to get a valid result or an error in
// one step.
//
// At the same time, we can't just remove the field: it's serialized as part of AuthCredential
// and AuthCredentialWithPni, which clients store locally.
#[serde(rename = "bytes")]
raw_uuid_bytes: UidBytes,
pub(crate) M1: RistrettoPoint,
pub(crate) M2: RistrettoPoint,
}
impl UidStruct {
pub fn from_service_id(service_id: ServiceId) -> Self {
let M1 = Self::calc_M1(Self::seed_M1(), service_id);
let raw_uuid_bytes = service_id.raw_uuid().into_bytes();
let M2 = RistrettoPoint::lizard_encode::<Sha256>(&raw_uuid_bytes);
UidStruct {
raw_uuid_bytes,
M1,
M2,
}
}
pub(crate) fn seed_M1() -> Sho {
Sho::new_seed(b"Signal_ZKGroup_20200424_UID_CalcM1")
}
pub(crate) fn calc_M1(mut seed: Sho, service_id: ServiceId) -> RistrettoPoint {
seed.absorb_and_ratchet(&service_id.service_id_binary());
seed.get_point()
}
}
impl zkcredential::attributes::Attribute for UidStruct {
fn as_points(&self) -> [RistrettoPoint; 2] {
[self.M1, self.M2]
}
}
@@ -0,0 +1,472 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
//! Examples of using zkcredential with existing zkgroup types.
//!
//! Has to live in zkgroup because they implement zkcredential traits on zkgroup types.
use curve25519_dalek_signal::ristretto::RistrettoPoint;
use poksho::shoapi::ShoApiExt as _;
use poksho::{ShoApi, ShoSha256};
use serde::{Deserialize, Serialize};
use zkcredential::attributes::{Attribute, Domain, RevealedAttribute};
use zkcredential::credentials::CredentialKeyPair;
use zkcredential::issuance::IssuanceProofBuilder;
use zkcredential::issuance::blind::{
BlindedAttribute, BlindedPoint, BlindingKeyPair, BlindingPublicKey, WithoutNonce,
};
use zkcredential::presentation::{
PresentationProof, PresentationProofBuilder, PresentationProofVerifier,
};
use zkcredential::sho::ShoExt;
use crate::common::sho::*;
use crate::crypto::profile_key_struct::ProfileKeyStruct;
use crate::crypto::uid_struct::UidStruct;
use crate::crypto::{profile_key_encryption, uid_encryption};
use crate::{RANDOMNESS_LEN, TEST_ARRAY_16, TEST_ARRAY_32};
#[test]
fn test_mac_generic() {
let mut sho = ShoSha256::new(b"Test_Credentials");
let keypair = CredentialKeyPair::generate(sho.squeeze_and_ratchet_as_array());
let label = b"20221221_AuthCredentialLike";
let uid_bytes = TEST_ARRAY_16;
let aci = libsignal_core::Aci::from_uuid_bytes(uid_bytes);
let uid = UidStruct::from_service_id(aci.into());
let proof = IssuanceProofBuilder::new(label)
.add_attribute(&uid)
.add_public_attribute(&[1, 2, 3])
.issue(&keypair, sho.squeeze_and_ratchet_as_array());
let credential = IssuanceProofBuilder::new(label)
.add_attribute(&uid)
.add_public_attribute(&[1, 2, 3])
.verify(keypair.public_key(), proof)
.unwrap();
let uid_encryption_key = uid_encryption::KeyPair::derive_from(Sho::new(b"test", b"").as_mut());
let uid_encryption_public_key = uid_encryption_key.public_key;
let proof = PresentationProofBuilder::new(label)
.add_attribute(&uid, &uid_encryption_key)
.present(
keypair.public_key(),
&credential,
sho.squeeze_and_ratchet_as_array(),
);
PresentationProofVerifier::new(label)
.add_public_attribute(&[1, 2, 3])
.add_attribute(
&uid_encryption_key.encrypt(&uid),
&uid_encryption_public_key,
)
.verify(&keypair, &proof)
.unwrap()
}
#[test]
fn test_mac_generic_without_verifying_encryption_key() {
let mut sho = ShoSha256::new(b"Test_Credentials");
let keypair = CredentialKeyPair::generate(sho.squeeze_and_ratchet_as_array());
let label = b"20221221_AuthCredentialLike";
let uid_bytes = TEST_ARRAY_16;
let aci = libsignal_core::Aci::from_uuid_bytes(uid_bytes);
let uid = UidStruct::from_service_id(aci.into());
let proof = IssuanceProofBuilder::new(label)
.add_attribute(&uid)
.add_public_attribute(&[1, 2, 3])
.issue(&keypair, sho.squeeze_and_ratchet_as_array());
let credential = IssuanceProofBuilder::new(label)
.add_attribute(&uid)
.add_public_attribute(&[1, 2, 3])
.verify(keypair.public_key(), proof)
.unwrap();
let uid_encryption_key = uid_encryption::KeyPair::derive_from(Sho::new(b"test", b"").as_mut());
let proof = PresentationProofBuilder::new(label)
.add_attribute_without_verified_key(&uid, &uid_encryption_key)
.present(
keypair.public_key(),
&credential,
sho.squeeze_and_ratchet_as_array(),
);
PresentationProofVerifier::new(label)
.add_public_attribute(&[1, 2, 3])
.add_attribute_without_verified_key(
&uid_encryption_key.encrypt(&uid),
uid_encryption::UidEncryptionDomain::ID,
)
.verify(&keypair, &proof)
.unwrap()
}
#[test]
fn test_profile_key_credential() {
let mut sho = ShoSha256::new(b"Test_Credentials");
let keypair = CredentialKeyPair::generate(sho.squeeze_and_ratchet_as_array());
let blinding_keypair = BlindingKeyPair::generate(&mut sho);
let label = b"20221221_ProfileKeyCredentialLike";
let aci = libsignal_core::Aci::from_uuid_bytes(TEST_ARRAY_16);
let uid = UidStruct::from_service_id(aci.into());
let profile_key = ProfileKeyStruct::new(TEST_ARRAY_32, TEST_ARRAY_16);
let encrypted_profile_key = blinding_keypair.encrypt(&profile_key, &mut sho).into();
#[derive(Serialize, Deserialize)]
struct Request {
uid: UidStruct,
encrypted_profile_key: BlindedAttribute,
blinding_public_key: BlindingPublicKey,
}
// Client
let request_serialized = bincode::serialize(&Request {
uid,
encrypted_profile_key,
blinding_public_key: *blinding_keypair.public_key(),
})
.unwrap();
// Issuing server
let request: Request = bincode::deserialize(&request_serialized).unwrap();
let proof = IssuanceProofBuilder::with_authenticated_message(label, b"abc")
.add_attribute(&request.uid)
.add_blinded_attribute(&request.encrypted_profile_key)
.issue(
&keypair,
&request.blinding_public_key,
sho.squeeze_and_ratchet_as_array(),
);
let proof_serialized = bincode::serialize(&proof).unwrap();
// Client
let credential = IssuanceProofBuilder::with_authenticated_message(label, b"abc")
.add_attribute(&uid)
.add_blinded_attribute(&encrypted_profile_key)
.verify(
keypair.public_key(),
&blinding_keypair,
bincode::deserialize(&proof_serialized).unwrap(),
)
.unwrap();
let mut zkgroup_sho = Sho::new(b"test", b"");
let uid_encryption_key = uid_encryption::KeyPair::derive_from(zkgroup_sho.as_mut());
let profile_key_encryption_key =
profile_key_encryption::KeyPair::derive_from(zkgroup_sho.as_mut());
let proof = PresentationProofBuilder::with_authenticated_message(label, b"v1")
.add_attribute(&uid, &uid_encryption_key)
.add_attribute(&profile_key, &profile_key_encryption_key)
.present(
keypair.public_key(),
&credential,
sho.squeeze_and_ratchet_as_array(),
);
#[derive(Serialize, Deserialize)]
struct Presentation {
proof: PresentationProof,
encrypted_uid: uid_encryption::Ciphertext,
encrypted_profile_key: profile_key_encryption::Ciphertext,
uid_encryption_public_key: uid_encryption::PublicKey,
profile_key_encryption_public_key: profile_key_encryption::PublicKey,
}
let presentation_serialized = bincode::serialize(&Presentation {
proof,
encrypted_uid: uid_encryption_key.encrypt(&uid),
encrypted_profile_key: profile_key_encryption_key.encrypt(&profile_key),
uid_encryption_public_key: uid_encryption_key.public_key,
profile_key_encryption_public_key: profile_key_encryption_key.public_key,
})
.unwrap();
// Verifying server
let presentation: Presentation = bincode::deserialize(&presentation_serialized).unwrap();
PresentationProofVerifier::with_authenticated_message(label, b"v1")
.add_attribute(
&presentation.encrypted_uid,
&presentation.uid_encryption_public_key,
)
.add_attribute(
&presentation.encrypted_profile_key,
&presentation.profile_key_encryption_public_key,
)
.verify(&keypair, &presentation.proof)
.unwrap();
}
#[test]
fn test_profile_key_credential_only_verifying_one_encryption_key() {
let mut sho = ShoSha256::new(b"Test_Credentials");
let keypair = CredentialKeyPair::generate(sho.squeeze_and_ratchet_as_array());
let blinding_keypair = BlindingKeyPair::generate(&mut sho);
let label = b"20221221_ProfileKeyCredentialLike";
let aci = libsignal_core::Aci::from_uuid_bytes(TEST_ARRAY_16);
let uid = UidStruct::from_service_id(aci.into());
let profile_key = ProfileKeyStruct::new(TEST_ARRAY_32, TEST_ARRAY_16);
let encrypted_profile_key = blinding_keypair.encrypt(&profile_key, &mut sho).into();
#[derive(Serialize, Deserialize)]
struct Request {
uid: UidStruct,
encrypted_profile_key: BlindedAttribute,
blinding_public_key: BlindingPublicKey,
}
// Client
let request_serialized = bincode::serialize(&Request {
uid,
encrypted_profile_key,
blinding_public_key: *blinding_keypair.public_key(),
})
.unwrap();
// Issuing server
let request: Request = bincode::deserialize(&request_serialized).unwrap();
let proof = IssuanceProofBuilder::with_authenticated_message(label, b"abc")
.add_attribute(&request.uid)
.add_blinded_attribute(&request.encrypted_profile_key)
.issue(
&keypair,
&request.blinding_public_key,
sho.squeeze_and_ratchet_as_array(),
);
let proof_serialized = bincode::serialize(&proof).unwrap();
// Client
let credential = IssuanceProofBuilder::with_authenticated_message(label, b"abc")
.add_attribute(&uid)
.add_blinded_attribute(&encrypted_profile_key)
.verify(
keypair.public_key(),
&blinding_keypair,
bincode::deserialize(&proof_serialized).unwrap(),
)
.unwrap();
let mut zkgroup_sho = Sho::new(b"test", b"");
let uid_encryption_key = uid_encryption::KeyPair::derive_from(zkgroup_sho.as_mut());
let profile_key_encryption_key =
profile_key_encryption::KeyPair::derive_from(zkgroup_sho.as_mut());
let proof = PresentationProofBuilder::with_authenticated_message(label, b"v1")
.add_attribute_without_verified_key(&uid, &uid_encryption_key)
.add_attribute(&profile_key, &profile_key_encryption_key)
.present(
keypair.public_key(),
&credential,
sho.squeeze_and_ratchet_as_array(),
);
#[derive(Serialize, Deserialize)]
struct Presentation {
proof: PresentationProof,
encrypted_uid: uid_encryption::Ciphertext,
encrypted_profile_key: profile_key_encryption::Ciphertext,
uid_encryption_public_key: uid_encryption::PublicKey,
profile_key_encryption_public_key: profile_key_encryption::PublicKey,
}
let presentation_serialized = bincode::serialize(&Presentation {
proof,
encrypted_uid: uid_encryption_key.encrypt(&uid),
encrypted_profile_key: profile_key_encryption_key.encrypt(&profile_key),
uid_encryption_public_key: uid_encryption_key.public_key,
profile_key_encryption_public_key: profile_key_encryption_key.public_key,
})
.unwrap();
// Verifying server
let presentation: Presentation = bincode::deserialize(&presentation_serialized).unwrap();
PresentationProofVerifier::with_authenticated_message(label, b"v1")
.add_attribute_without_verified_key(
&presentation.encrypted_uid,
uid_encryption::UidEncryptionDomain::ID,
)
.add_attribute(
&presentation.encrypted_profile_key,
&presentation.profile_key_encryption_public_key,
)
.verify(&keypair, &presentation.proof)
.unwrap();
}
#[test]
fn test_room_credential() {
let mut sho = ShoSha256::new(b"RoomCredential");
let keypair = CredentialKeyPair::generate(sho.squeeze_and_ratchet_as_array());
let blinding_keypair = BlindingKeyPair::generate(&mut sho);
let label = b"20230330_RoomCredential";
let request_label = b"20230330_RoomCredential_Request";
#[derive(Serialize, Deserialize)]
struct RoomId {
opaque_id: RistrettoPoint,
}
impl RevealedAttribute for RoomId {
fn as_point(&self) -> RistrettoPoint {
self.opaque_id
}
}
let room_id = RoomId {
opaque_id: sho.get_point(),
};
let blinded_room_id = blinding_keypair.blind(&room_id, &mut sho);
// Generate a request proof.
let mut request_proof_statement = poksho::Statement::new();
request_proof_statement.add("Y", &[("y", "G")]);
request_proof_statement.add("D1", &[("r1", "G")]);
// For most credentials we'd want to constraint D2 as well, but room IDs are unconstrained.
// So we leave it out; if the client passes a wild D2, well, they won't get a credential back.
let mut request_scalar_args = poksho::ScalarArgs::new();
request_scalar_args.add("y", blinding_keypair.private_key().y);
request_scalar_args.add("r1", blinded_room_id.r.0);
let mut request_point_args = poksho::PointArgs::new();
request_point_args.add("Y", blinding_keypair.public_key().Y);
request_point_args.add("D1", blinded_room_id.D1);
let proof = request_proof_statement
.prove(
&request_scalar_args,
&request_point_args,
request_label,
&sho.squeeze_and_ratchet_as_array::<RANDOMNESS_LEN>(),
)
.expect("valid");
let blinded_room_id: BlindedPoint<WithoutNonce> = blinded_room_id.into();
#[derive(Serialize, Deserialize)]
struct Request {
blinded_room_id: BlindedPoint,
blinding_public_key: BlindingPublicKey,
proof: Vec<u8>,
}
// Client
let request_serialized = bincode::serialize(&Request {
blinded_room_id,
blinding_public_key: *blinding_keypair.public_key(),
proof,
})
.unwrap();
// Issuing server
let request: Request = bincode::deserialize(&request_serialized).unwrap();
let mut request_verifying_point_args = poksho::PointArgs::new();
request_verifying_point_args.add("Y", request.blinding_public_key.Y);
request_verifying_point_args.add("D1", request.blinded_room_id.D1);
request_proof_statement
.verify_proof(&request.proof, &request_verifying_point_args, request_label)
.expect("valid");
let expiration = 1680220000u32;
let proof = IssuanceProofBuilder::new(label)
.add_public_attribute(&expiration)
.add_blinded_revealed_attribute(&request.blinded_room_id)
.issue(
&keypair,
&request.blinding_public_key,
sho.squeeze_and_ratchet_as_array(),
);
let proof_serialized = bincode::serialize(&proof).unwrap();
// Client
let credential = IssuanceProofBuilder::new(label)
.add_public_attribute(&expiration)
.add_blinded_revealed_attribute(&blinded_room_id)
.verify(
keypair.public_key(),
&blinding_keypair,
bincode::deserialize(&proof_serialized).unwrap(),
)
.unwrap();
let proof = PresentationProofBuilder::new(label)
.add_revealed_attribute(&room_id)
.present(
keypair.public_key(),
&credential,
sho.squeeze_and_ratchet_as_array(),
);
#[derive(Serialize, Deserialize)]
struct Presentation {
proof: PresentationProof,
room_id: RoomId,
expiration: u32,
}
let presentation_serialized = bincode::serialize(&Presentation {
proof,
room_id,
expiration,
})
.unwrap();
// Verifying server
let presentation: Presentation = bincode::deserialize(&presentation_serialized).unwrap();
PresentationProofVerifier::new(label)
.add_public_attribute(&presentation.expiration)
.add_revealed_attribute(&presentation.room_id)
.verify(&keypair, &presentation.proof)
.unwrap();
}
struct InverseUidDecryptionKey;
impl zkcredential::attributes::Domain for InverseUidDecryptionKey {
type Attribute = uid_encryption::Ciphertext;
const ID: &'static str = "InverseUidEncryptionDomain_20231011";
fn G_a() -> [curve25519_dalek_signal::RistrettoPoint; 2] {
static STORAGE: std::sync::OnceLock<[curve25519_dalek_signal::RistrettoPoint; 2]> =
std::sync::OnceLock::new();
*zkcredential::attributes::derive_default_generator_points::<Self>(&STORAGE)
}
}
#[test]
fn test_inverse_key() {
let aci = libsignal_core::Aci::from_uuid_bytes(TEST_ARRAY_16);
let uid = UidStruct::from_service_id(aci.into());
let mut sho = Sho::new(b"test_inverse_key", b"");
let uid_encryption_key = uid_encryption::KeyPair::derive_from(sho.as_mut());
let encrypted = uid_encryption_key.encrypt(&uid);
let inverse = zkcredential::attributes::KeyPair::<InverseUidDecryptionKey>::inverse_of(
&uid_encryption_key,
);
#[allow(non_snake_case)]
let [E_A1_prime, E_A2_prime] = inverse.encrypt(&encrypted).as_points();
assert_eq!(uid.M1, E_A1_prime);
assert_eq!(uid.M2, E_A2_prime);
}