Compare commits

...

2 Commits
main ... 0.41.2

Author SHA1 Message Date
Sergey Skrobotov
e465b77cb6 Bump to version v0.41.2 2024-03-08 16:48:36 -08:00
Sergey Skrobotov
4388fac6a7 Revert "zkgroup: Implement GroupSendEndorsements"
This reverts commit 1c8fd06486.
2024-03-08 15:56:46 -08:00
21 changed files with 54 additions and 967 deletions

7
Cargo.lock generated
View File

@ -1853,7 +1853,7 @@ dependencies = [
[[package]]
name = "libsignal-ffi"
version = "0.41.1"
version = "0.41.2"
dependencies = [
"async-trait",
"attest",
@ -1875,7 +1875,7 @@ dependencies = [
[[package]]
name = "libsignal-jni"
version = "0.41.1"
version = "0.41.2"
dependencies = [
"async-trait",
"cfg-if",
@ -2001,7 +2001,7 @@ dependencies = [
[[package]]
name = "libsignal-node"
version = "0.41.1"
version = "0.41.2"
dependencies = [
"async-trait",
"cmake",
@ -4521,7 +4521,6 @@ dependencies = [
"partial-default",
"poksho",
"rand",
"rayon",
"serde",
"sha2",
"signal-crypto",

View File

@ -5,7 +5,7 @@
Pod::Spec.new do |s|
s.name = 'LibSignalClient'
s.version = '0.41.1'
s.version = '0.41.2'
s.summary = 'A Swift wrapper library for communicating with the Signal messaging service.'
s.homepage = 'https://github.com/signalapp/libsignal'

View File

@ -738,12 +738,12 @@ For more information on this, and how to apply and follow the GNU AGPL, see
<li><a href="https://crates.io/crates/libsignal-bridge">libsignal-bridge 0.1.0</a></li>
<li><a href="https://crates.io/crates/libsignal-bridge-macros">libsignal-bridge-macros 0.1.0</a></li>
<li><a href="https://crates.io/crates/libsignal-core">libsignal-core 0.1.0</a></li>
<li><a href="https://crates.io/crates/libsignal-ffi">libsignal-ffi 0.41.1</a></li>
<li><a href="https://crates.io/crates/libsignal-jni">libsignal-jni 0.41.1</a></li>
<li><a href="https://crates.io/crates/libsignal-ffi">libsignal-ffi 0.41.2</a></li>
<li><a href="https://crates.io/crates/libsignal-jni">libsignal-jni 0.41.2</a></li>
<li><a href="https://crates.io/crates/libsignal-message-backup">libsignal-message-backup 0.1.0</a></li>
<li><a href="https://crates.io/crates/libsignal-message-backup-macros">libsignal-message-backup-macros 0.1.0</a></li>
<li><a href="https://crates.io/crates/libsignal-net">libsignal-net 0.1.0</a></li>
<li><a href="https://crates.io/crates/libsignal-node">libsignal-node 0.41.1</a></li>
<li><a href="https://crates.io/crates/libsignal-node">libsignal-node 0.41.2</a></li>
<li><a href="https://crates.io/crates/libsignal-protocol">libsignal-protocol 0.1.0</a></li>
<li><a href="https://crates.io/crates/libsignal-svr3">libsignal-svr3 0.1.0</a></li>
<li><a href="https://crates.io/crates/poksho">poksho 0.7.0</a></li>

View File

@ -669,7 +669,7 @@ For more information on this, and how to apply and follow the GNU AGPL, see
```
## attest 0.1.0, device-transfer 0.1.0, libsignal-bridge 0.1.0, libsignal-bridge-macros 0.1.0, libsignal-core 0.1.0, libsignal-ffi 0.41.1, libsignal-jni 0.41.1, libsignal-message-backup 0.1.0, libsignal-message-backup-macros 0.1.0, libsignal-net 0.1.0, libsignal-node 0.41.1, libsignal-protocol 0.1.0, libsignal-svr3 0.1.0, poksho 0.7.0, signal-crypto 0.1.0, signal-media 0.1.0, signal-neon-futures 0.1.0, signal-neon-futures-tests 0.1.0, signal-pin 0.1.0, usernames 0.1.0, zkcredential 0.1.0, zkgroup 0.9.0
## attest 0.1.0, device-transfer 0.1.0, libsignal-bridge 0.1.0, libsignal-bridge-macros 0.1.0, libsignal-core 0.1.0, libsignal-ffi 0.41.2, libsignal-jni 0.41.2, libsignal-message-backup 0.1.0, libsignal-message-backup-macros 0.1.0, libsignal-net 0.1.0, libsignal-node 0.41.2, libsignal-protocol 0.1.0, libsignal-svr3 0.1.0, poksho 0.7.0, signal-crypto 0.1.0, signal-media 0.1.0, signal-neon-futures 0.1.0, signal-neon-futures-tests 0.1.0, signal-pin 0.1.0, usernames 0.1.0, zkcredential 0.1.0, zkgroup 0.9.0
```
GNU AFFERO GENERAL PUBLIC LICENSE

View File

@ -924,7 +924,7 @@ You should also get your employer (if you work as a programmer) or school, if an
<key>License</key>
<string>GNU Affero General Public License v3.0</string>
<key>Title</key>
<string>attest 0.1.0, device-transfer 0.1.0, libsignal-bridge 0.1.0, libsignal-bridge-macros 0.1.0, libsignal-core 0.1.0, libsignal-ffi 0.41.1, libsignal-jni 0.41.1, libsignal-message-backup 0.1.0, libsignal-message-backup-macros 0.1.0, libsignal-net 0.1.0, libsignal-node 0.41.1, libsignal-protocol 0.1.0, libsignal-svr3 0.1.0, poksho 0.7.0, signal-crypto 0.1.0, signal-media 0.1.0, signal-neon-futures 0.1.0, signal-neon-futures-tests 0.1.0, signal-pin 0.1.0, usernames 0.1.0, zkcredential 0.1.0, zkgroup 0.9.0</string>
<string>attest 0.1.0, device-transfer 0.1.0, libsignal-bridge 0.1.0, libsignal-bridge-macros 0.1.0, libsignal-core 0.1.0, libsignal-ffi 0.41.2, libsignal-jni 0.41.2, libsignal-message-backup 0.1.0, libsignal-message-backup-macros 0.1.0, libsignal-net 0.1.0, libsignal-node 0.41.2, libsignal-protocol 0.1.0, libsignal-svr3 0.1.0, poksho 0.7.0, signal-crypto 0.1.0, signal-media 0.1.0, signal-neon-futures 0.1.0, signal-neon-futures-tests 0.1.0, signal-pin 0.1.0, usernames 0.1.0, zkcredential 0.1.0, zkgroup 0.9.0</string>
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>

View File

@ -6,7 +6,7 @@ plugins {
}
allprojects {
version = "0.41.1"
version = "0.41.2"
group = "org.signal"
}

View File

@ -75,5 +75,6 @@
{ "version": "v0.40.0", "size": 3667720 },
{ "version": "v0.40.1", "size": 3676408 },
{ "version": "v0.41.0", "size": 3880136 },
{ "version": "v0.41.1", "size": 3885624 }
{ "version": "v0.41.1", "size": 3885624 },
{ "version": "v0.41.2", "size": 3888200 }
]

View File

@ -1,6 +1,6 @@
{
"name": "@signalapp/libsignal-client",
"version": "0.41.1",
"version": "0.41.2",
"license": "AGPL-3.0-only",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@ -5,7 +5,7 @@
[package]
name = "libsignal-ffi"
version = "0.41.1"
version = "0.41.2"
authors = ["Signal Messenger LLC"]
edition = "2021"
license = "AGPL-3.0-only"

View File

@ -5,7 +5,7 @@
[package]
name = "libsignal-jni"
version = "0.41.1"
version = "0.41.2"
authors = ["Signal Messenger LLC"]
edition = "2021"
license = "AGPL-3.0-only"

View File

@ -5,7 +5,7 @@
[package]
name = "libsignal-node"
version = "0.41.1"
version = "0.41.2"
authors = ["Signal Messenger LLC"]
license = "AGPL-3.0-only"
edition = "2021"

View File

@ -42,12 +42,6 @@
//! that covers all endorsements issued together. Tokens can then be lazily generated on an
//! individual basis from the validated endorsements.
//!
//! Note that the "combine" operation (and its reverse, "remove") imply that the client has a
//! limited ability to synthesize endorsements that the issuing server never sees---for instance,
//! given an endorsement for attribute point P, the client can synthesize an endorsement for 2P, 3P,
//! -P, etc. Because of this, it's critical that the points used for the hidden attributes not be
//! algebraically related (a hash is recommended).
//!
//! This model can be extended to endorsements over *tuples* of attribute points as long as the
//! client uses only a single blinding key, but that has not been implemented here.
//!
@ -175,7 +169,7 @@ pub struct EndorsementResponse {
///
/// Endorsements may be persisted on the client, or may be eagerly converted to tokens using
/// [`to_token`][Self::to_token].
#[derive(Clone, Copy, Serialize, Deserialize, PartialDefault)]
#[derive(Clone, Serialize, Deserialize, PartialDefault)]
pub struct Endorsement {
R: RistrettoPoint,
}
@ -429,67 +423,18 @@ impl Endorsement {
/// Combines several endorsements into one.
///
/// All endorsements must have been signed with the same server key, and they must be for points
/// hidden with the same client key, or the resulting endorsement will not produce a valid
/// token.
/// hidden with the same client key, or the resulting endorsement will not produce a valid token.
///
/// This is a set-like operation: order does not matter, and the result is equivalent to the
/// server issuing an endorsement of a sum of hidden attribute points. It is still an
/// all-or-nothing endorsement; it does not allow one endorsement to be used for *any* point in
/// the set, nor arbitrary subsets.
///
/// This is equivalent to calling [`Self::combine_with`] repeatedly.
pub fn combine(endorsements: impl IntoIterator<Item = Endorsement>) -> Endorsement {
Endorsement {
R: endorsements.into_iter().map(|each| each.R).sum(),
}
}
/// Combines this endorsement with another.
///
/// Both endorsements must have been signed with the same server key, and they must be for
/// points hidden with the same client key, or the resulting endorsement will not produce a
/// valid token.
///
/// This is a set-like operation: order does not matter, and the result is equivalent to the
/// server issuing an endorsement of a sum of hidden attribute points. It is still an
/// all-or-nothing endorsement; it does not allow one endorsement to be used for *either* point
/// in the set.
///
/// This is equivalent to [`Self::combine`].
pub fn combine_with(&self, other: &Endorsement) -> Endorsement {
Endorsement {
R: self.R + other.R,
}
}
/// Creates an endorsement with `other` removed from `self`.
///
/// This is useful when `self` represents a [combined](Self::combine) endorsement, but you want
/// to remove some of the attributes from the original combined set.
///
/// ```
/// # use zkcredential::endorsements::Endorsement;
/// # fn example(a: Endorsement, b: Endorsement, c: Endorsement) {
/// let abc = Endorsement::combine([a, b, c]);
/// let a_and_c = abc.remove(&b); // Equivalent to a.combine_with(c).
/// # }
/// ```
///
/// Both endorsements must have been signed with the same server key, and they must be for
/// points hidden with the same client key, or the resulting endorsement will not produce a
/// valid token. Removing endorsements not present in `self` will also result in an endorsement
/// that won't produce valid tokens.
///
/// This is a set-like operation: order does not matter, and the result is equivalent to the
/// server issuing an endorsement of a difference of hidden attribute points. Multiple
/// endorsements can be removed by calling this method repeatedly, or by removing a single
/// combined endorsement.
pub fn remove(&self, other: &Endorsement) -> Endorsement {
Endorsement {
R: self.R - other.R,
}
}
/// Generates a token from this endorsement, for sending to the verifying server.
pub fn to_token(&self, client_key: &ClientDecryptionKey) -> Box<[u8]> {
let P = self.R * client_key.a_inv;
@ -644,10 +589,11 @@ mod tests {
let decrypt_key = ClientDecryptionKey::from_blinding_scalar(client_raw_key);
let todays_public_key = root_key.public.derive_key(info_sho.clone());
let endorsements = issued_endorsements
let mut endorsements = issued_endorsements
.receive(encrypted_points, &todays_public_key)
.unwrap();
let combined = Endorsement::combine(endorsements.iter().copied()).remove(&endorsements[1]);
endorsements.remove(1);
let combined = Endorsement::combine(endorsements);
let token = combined.to_token(&decrypt_key);
todays_key
@ -656,10 +602,6 @@ mod tests {
&token,
)
.unwrap();
let manually_combined = endorsements[0].combine_with(&endorsements[2]);
let manual_token = manually_combined.to_token(&decrypt_key);
assert_eq!(&token, &manual_token);
}
#[test]

View File

@ -15,7 +15,7 @@ license = "AGPL-3.0-only"
libsignal-core = { path = "../core" }
poksho = { path = "../poksho" }
signal-crypto = { path = "../crypto" }
zkcredential = { path = "../zkcredential", features = ["rayon"] }
zkcredential = { path = "../zkcredential" }
aes-gcm-siv = "0.11.1"
bincode = "1.2.1"
@ -26,7 +26,6 @@ hex-literal = "0.4.1"
lazy_static = "1.4.0"
num_enum = "0.6.1"
partial-default = { version = "0.1.0", features = ["derive"] }
rayon = "1.8.0"
serde = { version = "1.0.106", features = ["derive"] }
sha2 = "0.10.0"
subtle = "2.3"
@ -44,7 +43,6 @@ features = ["serde"]
version = "4.1.1"
[dev-dependencies]
rand = "0.8"
uuid = { version = "1", features = ["v5"] }
# For benchmarking

View File

@ -4,8 +4,8 @@
//
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use rayon::iter::{IntoParallelRefIterator, ParallelIterator as _};
use zkgroup::SECONDS_PER_DAY;
extern crate zkgroup;
fn benchmark_integration_auth(c: &mut Criterion) {
let server_secret_params = zkgroup::ServerSecretParams::generate(zkgroup::TEST_ARRAY_32);
@ -240,7 +240,7 @@ pub fn benchmark_integration_profile(c: &mut Criterion) {
);
}
pub fn benchmark_group_send_credential(c: &mut Criterion) {
pub fn benchmark_group_send(c: &mut Criterion) {
const DAY_ALIGNED_TIMESTAMP: zkgroup::Timestamp = 1681344000; // 2023-04-13 00:00:00 UTC
// SERVER
@ -374,143 +374,10 @@ pub fn benchmark_group_send_credential(c: &mut Criterion) {
}
}
pub fn benchmark_group_send_endorsements(c: &mut Criterion) {
const DAY_ALIGNED_TIMESTAMP: zkgroup::Timestamp = 1681344000; // 2023-04-13 00:00:00 UTC
let now = DAY_ALIGNED_TIMESTAMP;
// SERVER
let server_secret_params = zkgroup::ServerSecretParams::generate(zkgroup::TEST_ARRAY_32);
let server_public_params = server_secret_params.get_public_params();
let todays_key = zkgroup::groups::GroupSendDerivedKeyPair::for_expiration(
now + SECONDS_PER_DAY,
&server_secret_params,
);
// CLIENT
let master_key = zkgroup::groups::GroupMasterKey::new(zkgroup::TEST_ARRAY_32_1);
let group_secret_params =
zkgroup::groups::GroupSecretParams::derive_from_master_key(master_key);
let aci = libsignal_core::Aci::from_uuid_bytes(zkgroup::TEST_ARRAY_16);
let all_members: Vec<libsignal_core::ServiceId> = std::iter::once(aci)
.chain((1u16..).map(|i| {
// Generate arbitrary v5 (hash-based) UUIDs for the rest of the group.
libsignal_core::Aci::from(uuid::Uuid::new_v5(
&uuid::Uuid::from_bytes(zkgroup::TEST_ARRAY_16_1),
&i.to_be_bytes(),
))
}))
.map(libsignal_core::ServiceId::from)
.take(1000)
.collect();
let all_member_ciphertexts: Vec<_> = all_members
.iter()
.map(|member| group_secret_params.encrypt_service_id(*member))
.collect();
let mut benchmark_group = c.benchmark_group("group_send_endorsements");
for group_size in [2, 5, 10, 100, 1000] {
let group = &all_members[..group_size];
let group_ciphertexts = &all_member_ciphertexts[..group_size];
let endorsement_response = zkgroup::groups::GroupSendEndorsementsResponse::issue(
group_ciphertexts.iter().copied(),
&todays_key,
zkgroup::TEST_ARRAY_32_2,
);
benchmark_group.bench_function(BenchmarkId::new("issue", group_size), |b| {
b.iter(|| {
zkgroup::groups::GroupSendEndorsementsResponse::issue(
group_ciphertexts.iter().copied(),
&todays_key,
zkgroup::TEST_ARRAY_32_2,
)
})
});
let serialized_response = zkgroup::serialize(&endorsement_response);
let endorsements = endorsement_response
.receive_with_service_ids_single_threaded(
group.iter().copied(),
now,
&group_secret_params,
&server_public_params,
)
.expect("issued endorsements should be valid");
benchmark_group.bench_function(
BenchmarkId::new("deserialize_and_receive_with_service_ids", group_size),
|b| {
b.iter(|| {
let endorsement_response: zkgroup::groups::GroupSendEndorsementsResponse =
zkgroup::deserialize(&serialized_response).expect("valid");
endorsement_response
.receive_with_service_ids_single_threaded(
group.iter().copied(),
now,
&group_secret_params,
&server_public_params,
)
.expect("issued endorsements should be valid")
})
},
);
benchmark_group.bench_function(
BenchmarkId::new(
"deserialize_and_receive_with_service_ids_parallel",
group_size,
),
|b| {
b.iter(|| {
let endorsement_response: zkgroup::groups::GroupSendEndorsementsResponse =
zkgroup::deserialize(&serialized_response).expect("valid");
endorsement_response
.receive_with_service_ids(
group.par_iter().copied(),
now,
&group_secret_params,
&server_public_params,
)
.expect("issued endorsements should be valid")
})
},
);
benchmark_group.bench_function(
BenchmarkId::new("deserialize_and_receive_with_ciphertexts", group_size),
|b| {
b.iter(|| {
let endorsement_response: zkgroup::groups::GroupSendEndorsementsResponse =
zkgroup::deserialize(&serialized_response).expect("valid");
endorsement_response
.receive_with_ciphertexts(
group_ciphertexts.iter().copied(),
now,
&server_public_params,
)
.expect("issued credential should be valid")
})
},
);
benchmark_group.bench_function(BenchmarkId::new("combine", group_size), |b| {
b.iter(|| zkgroup::groups::GroupSendEndorsement::combine(endorsements.iter().cloned()))
});
// We're not going to measure to_token or verify, since they aren't usually done in bulk.
// zkcredential does have a benchmark for them and zkgroup wouldn't add much overhead.
}
}
criterion_group!(
benches,
benchmark_integration_profile,
benchmark_integration_auth,
benchmark_group_send_credential,
benchmark_group_send_endorsements,
benchmark_group_send,
);
criterion_main!(benches);

View File

@ -5,7 +5,6 @@
pub mod group_params;
mod group_send_credential;
mod group_send_endorsement;
pub mod profile_key_ciphertext;
pub mod uuid_ciphertext;
@ -13,9 +12,5 @@ pub use group_params::{GroupMasterKey, GroupPublicParams, GroupSecretParams};
pub use group_send_credential::{
GroupSendCredential, GroupSendCredentialPresentation, GroupSendCredentialResponse,
};
pub use group_send_endorsement::{
GroupSendDerivedKeyPair, GroupSendEndorsement, GroupSendEndorsementsResponse,
GroupSendFullToken, GroupSendToken,
};
pub use profile_key_ciphertext::ProfileKeyCiphertext;
pub use uuid_ciphertext::UuidCiphertext;

View File

@ -1,443 +0,0 @@
//
// Copyright 2024 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
//! Provides GroupSendEndorsement and related types.
//!
//! GroupSendEndorsement is a MAC over:
//! - a ServiceId (computed from the ciphertexts on the group server at issuance, passed decrypted
//! to the chat server for verification)
//! - an expiration timestamp, truncated to day granularity (chosen by the group server at issuance,
//! passed publicly to the chat server for verification)
use partial_default::PartialDefault;
use poksho::ShoApi;
use rayon::iter::{IndexedParallelIterator as _, ParallelIterator as _};
use serde::{Deserialize, Serialize};
use zkcredential::attributes::Attribute as _;
use crate::common::array_utils;
use crate::common::serialization::ReservedByte;
use crate::groups::{GroupSecretParams, UuidCiphertext};
use crate::{
crypto, RandomnessBytes, ServerPublicParams, ServerSecretParams, Timestamp,
ZkGroupVerificationFailure, SECONDS_PER_DAY,
};
const SECONDS_PER_HOUR: u64 = 60 * 60;
/// A key pair used to sign endorsements for a particular expiration.
///
/// These are intended to be cheaply cached -- it's not a problem to regenerate them, but they're
/// expected to be reused frequently enough that they're *worth* caching, given that they're only
/// rotated every 24 hours.
#[derive(Serialize, Deserialize, PartialDefault)]
pub struct GroupSendDerivedKeyPair {
reserved: ReservedByte,
key_pair: zkcredential::endorsements::ServerDerivedKeyPair,
expiration: Timestamp,
}
impl GroupSendDerivedKeyPair {
/// Encapsulates the "tag info", or public attributes, of an endorsement, which is used to derive
/// the appropriate signing key.
fn tag_info(expiration: Timestamp) -> impl poksho::ShoApi + Clone {
let mut sho = poksho::ShoHmacSha256::new(b"20240215_Signal_GroupSendEndorsement");
sho.absorb_and_ratchet(&expiration.to_be_bytes());
sho
}
/// Derives the appropriate key pair for the given expiration.
pub fn for_expiration(expiration: Timestamp, params: &ServerSecretParams) -> Self {
Self {
reserved: ReservedByte::default(),
key_pair: params
.endorsement_key_pair
.derive_key(Self::tag_info(expiration)),
expiration,
}
}
}
/// The response issued from the group server, containing endorsements for all of a group's members.
///
/// The group server may cache this for a particular group as long as the group membership does not
/// change (being careful of expiration, of course). It is the same for every requesting member.
#[derive(Serialize, Deserialize, PartialDefault)]
pub struct GroupSendEndorsementsResponse {
reserved: ReservedByte,
endorsements: zkcredential::endorsements::EndorsementResponse,
expiration: Timestamp,
}
impl GroupSendEndorsementsResponse {
pub fn default_expiration(current_time_in_seconds: Timestamp) -> Timestamp {
// Return the end of the next day, unless that's less than 25 hours away.
// In that case, return the end of the following day.
let start_of_day = current_time_in_seconds - (current_time_in_seconds % SECONDS_PER_DAY);
let mut expiration = start_of_day + 2 * SECONDS_PER_DAY;
if (expiration - current_time_in_seconds) < SECONDS_PER_DAY + SECONDS_PER_HOUR {
expiration += SECONDS_PER_DAY;
}
expiration
}
/// Sorts `points` in *some* deterministic order based on the contents of each `RistrettoPoint`.
///
/// Changing this order is a breaking change, since the issuing server and client must agree on
/// it.
///
/// The `usize` in each pair must be the original index of the point.
fn sort_points(points: &mut [(usize, curve25519_dalek::RistrettoPoint)]) {
debug_assert!(points.iter().enumerate().all(|(i, (j, _))| i == *j));
let sort_keys = curve25519_dalek::RistrettoPoint::double_and_compress_batch(
points.iter().map(|(_i, point)| point),
);
points.sort_unstable_by_key(|(i, _point)| sort_keys[*i].as_bytes());
}
/// Issues new endorsements, one for each of `member_ciphertexts`.
///
/// `expiration` must match the expiration used to derive `key_pair`;
pub fn issue(
member_ciphertexts: impl IntoIterator<Item = UuidCiphertext>,
key_pair: &GroupSendDerivedKeyPair,
randomness: RandomnessBytes,
) -> Self {
// Note: we could save some work here by pulling the single point we need out of the
// serialized bytes, and operating directly on that. However, we'd have to remember to
// update that if the serialization format ever changes.
let mut points_to_sign: Vec<(usize, curve25519_dalek::RistrettoPoint)> = member_ciphertexts
.into_iter()
.map(|ciphertext| ciphertext.ciphertext.as_points()[0])
.enumerate()
.collect();
Self::sort_points(&mut points_to_sign);
let endorsements = zkcredential::endorsements::EndorsementResponse::issue(
points_to_sign.iter().map(|(_i, point)| *point),
&key_pair.key_pair,
randomness,
);
// We don't bother to "un-sort" the endorsements back to the original order of the points,
// because clients don't keep track of that order anyway. Instead, we return the
// endorsements in the sorted order we computed above.
Self {
reserved: ReservedByte::default(),
endorsements,
expiration: key_pair.expiration,
}
}
/// Returns the expiration for all endorsements in the response.
pub fn expiration(&self) -> Timestamp {
self.expiration
}
/// Validates `self.expiration` against `now` and derives the appropriate signing key (using
/// [`GroupSendDerivedKeyPair::tag_info`]).
///
/// Note that if a client expects to receive endorsements from many different groups in one day
/// it *could* be worth caching this, but the operation is pretty cheap compared to the rest of
/// verifying responses, so we don't think it would make that much of a difference.
fn derive_public_signing_key_from_expiration(
&self,
now: Timestamp,
server_params: &ServerPublicParams,
) -> Result<zkcredential::endorsements::ServerDerivedPublicKey, ZkGroupVerificationFailure>
{
if self.expiration % SECONDS_PER_DAY != 0 {
// Reject credentials that don't expire on a day boundary,
// because the server might be trying to fingerprint us.
return Err(ZkGroupVerificationFailure);
}
let time_remaining_in_seconds = self.expiration.saturating_sub(now);
if time_remaining_in_seconds < 2 * SECONDS_PER_HOUR {
// Reject credentials that expire in less than two hours,
// including those that might expire in the past.
// Two hours allows for clock skew plus incorrect summer time settings (+/- 1 hour).
return Err(ZkGroupVerificationFailure);
}
if time_remaining_in_seconds > 7 * SECONDS_PER_DAY {
// Reject credentials with expirations more than 7 days from now,
// because the server might be trying to fingerprint us.
return Err(ZkGroupVerificationFailure);
}
Ok(server_params
.endorsement_public_key
.derive_key(GroupSendDerivedKeyPair::tag_info(self.expiration)))
}
/// Same as [`receive_with_service_ids`], but without parallelizing the zkgroup-specific parts
/// of the operation.
///
/// Only interesting for benchmarking. The zkcredential part of the operation may still be
/// parallelized.
pub fn receive_with_service_ids_single_threaded(
self,
user_ids: impl IntoIterator<Item = libsignal_core::ServiceId>,
now: Timestamp,
group_params: &GroupSecretParams,
server_params: &ServerPublicParams,
) -> Result<Vec<GroupSendEndorsement>, ZkGroupVerificationFailure> {
let derived_key = self.derive_public_signing_key_from_expiration(now, server_params)?;
// The endorsements are sorted by the serialized *ciphertext* representations.
// We have to compute the ciphertexts (expensive), but we can skip the second point (which
// would be much more expensive).
// We zip the results together with a set of indexes so we can un-sort the results later.
let mut member_points: Vec<(usize, curve25519_dalek::RistrettoPoint)> = user_ids
.into_iter()
.map(|user_id| {
group_params.uid_enc_key_pair.a1 * crypto::uid_struct::UidStruct::calc_M1(user_id)
})
.enumerate()
.collect();
Self::sort_points(&mut member_points);
let endorsements = self
.endorsements
.receive(member_points.iter().map(|(_i, point)| *point), &derived_key)
.map_err(|_| ZkGroupVerificationFailure)?;
Ok(array_utils::collect_permutation(
endorsements
.into_iter()
.map(|endorsement| GroupSendEndorsement {
reserved: ReservedByte::default(),
endorsement,
})
.zip(member_points.iter().map(|(i, _)| *i)),
))
}
/// Validates and returns the endorsements issued by the server.
///
/// The result will be in the same order as `user_ids`. `user_ids` should contain the current
/// user as well.
///
/// If you already have the member ciphertexts for the group available,
/// [`receive_with_ciphertexts`] will be faster than this method.
pub fn receive_with_service_ids<T>(
self,
user_ids: T,
now: Timestamp,
group_params: &GroupSecretParams,
server_params: &ServerPublicParams,
) -> Result<Vec<GroupSendEndorsement>, ZkGroupVerificationFailure>
where
T: rayon::iter::IntoParallelIterator<Item = libsignal_core::ServiceId>,
T::Iter: rayon::iter::IndexedParallelIterator,
{
let derived_key = self.derive_public_signing_key_from_expiration(now, server_params)?;
// The endorsements are sorted based on the *ciphertext* representations.
// We have to compute the ciphertexts (expensive), but we can skip the second point (which
// would be much more expensive).
// We zip the results together with a set of indexes so we can un-sort the results later.
let mut member_points: Vec<(usize, curve25519_dalek::RistrettoPoint)> = user_ids
.into_par_iter()
.map(|user_id| {
group_params.uid_enc_key_pair.a1 * crypto::uid_struct::UidStruct::calc_M1(user_id)
})
.enumerate()
.collect();
Self::sort_points(&mut member_points);
let endorsements = self
.endorsements
.receive(member_points.iter().map(|(_i, point)| *point), &derived_key)
.map_err(|_| ZkGroupVerificationFailure)?;
Ok(array_utils::collect_permutation(
endorsements
.into_iter()
.map(|endorsement| GroupSendEndorsement {
reserved: ReservedByte::default(),
endorsement,
})
.zip(member_points.iter().map(|(i, _)| *i)),
))
}
/// Validates and returns the endorsements issued by the server.
///
/// The result will be in the same order as `member_ciphertexts`. `member_ciphertexts` should
/// contain the current user as well.
///
/// If you don't already have the member ciphertexts for the group available,
/// [`receive_with_service_ids`] will be faster than computing them separately, using this
/// method, and then throwing the ciphertexts away.
pub fn receive_with_ciphertexts(
self,
member_ciphertexts: impl IntoIterator<Item = UuidCiphertext>,
now: Timestamp,
server_params: &ServerPublicParams,
) -> Result<Vec<GroupSendEndorsement>, ZkGroupVerificationFailure> {
let derived_key = self.derive_public_signing_key_from_expiration(now, server_params)?;
// Note: we could save some work here by pulling the single point we need out of the
// serialized form of UuidCiphertext, and operating directly on that. However, we'd have to
// remember to update that if the serialization format ever changes.
let mut points_to_check: Vec<_> = member_ciphertexts
.into_iter()
.map(|ciphertext| ciphertext.ciphertext.as_points()[0])
.enumerate()
.collect();
Self::sort_points(&mut points_to_check);
let endorsements = self
.endorsements
.receive(
points_to_check.iter().map(|(_i, point)| *point),
&derived_key,
)
.map_err(|_| ZkGroupVerificationFailure)?;
Ok(array_utils::collect_permutation(
endorsements
.into_iter()
.map(|endorsement| GroupSendEndorsement {
reserved: ReservedByte::default(),
endorsement,
})
.zip(points_to_check.iter().map(|(i, _)| *i)),
))
}
}
/// A single endorsement, for one or multiple group members.
#[derive(Serialize, Deserialize, PartialDefault, Clone, Copy)]
pub struct GroupSendEndorsement {
reserved: ReservedByte,
endorsement: zkcredential::endorsements::Endorsement,
}
impl GroupSendEndorsement {
/// Combines several endorsements into one.
///
/// All endorsements must have been generated from the same issuance, or the resulting
/// endorsement will not produce a valid token.
///
/// This is a set-like operation: order does not matter.
pub fn combine(
endorsements: impl IntoIterator<Item = GroupSendEndorsement>,
) -> GroupSendEndorsement {
let mut endorsements = endorsements.into_iter();
let mut result = endorsements
.next()
.expect("must pass at least one endorsement");
for next in endorsements {
assert_eq!(
result.reserved, next.reserved,
"endorsements must all have the same version"
);
result.endorsement = result.endorsement.combine_with(&next.endorsement);
}
result
}
/// Removes endorsements from a previously-combined endorsement.
///
/// Removing endorsements not present in `self` will result in an endorsement that will not
/// produce a valid token.
///
/// This is a set-like operation: order does not matter. Multiple endorsements can be removed by
/// calling this method repeatedly, or by removing a single combined endorsement.
pub fn remove(&self, unwanted_endorsements: &GroupSendEndorsement) -> GroupSendEndorsement {
assert_eq!(
self.reserved, unwanted_endorsements.reserved,
"endorsements must have the same version"
);
GroupSendEndorsement {
reserved: self.reserved,
endorsement: self.endorsement.remove(&unwanted_endorsements.endorsement),
}
}
/// Generates a bearer token from the endorsement.
///
/// This can be cached by the client for repeatedly sending to the same recipient,
/// but must be converted to a GroupSendFullToken before sending it to the server.
pub fn to_token(&self, group_params: &GroupSecretParams) -> GroupSendToken {
let client_key =
zkcredential::endorsements::ClientDecryptionKey::for_first_point_of_attribute(
&group_params.uid_enc_key_pair,
);
let raw_token = self.endorsement.to_token(&client_key);
GroupSendToken {
reserved: ReservedByte::default(),
raw_token,
}
}
}
/// A token representing an endorsement.
///
/// This can be cached by the client for repeatedly sending to the same recipient,
/// but must be converted to a GroupSendFullToken before sending it to the server.
#[derive(Serialize, Deserialize, PartialDefault)]
pub struct GroupSendToken {
reserved: ReservedByte,
raw_token: Box<[u8]>,
}
impl GroupSendToken {
/// Attaches the expiration to this token to create a GroupSendFullToken.
///
/// If the incorrect expiration is used, the token will fail verification.
pub fn into_full_token(self, expiration: Timestamp) -> GroupSendFullToken {
GroupSendFullToken {
reserved: self.reserved,
raw_token: self.raw_token,
expiration,
}
}
}
/// A token representing an endorsement, along with its expiration.
///
/// This will be serialized and sent to the chat server for verification.
#[derive(Serialize, Deserialize, PartialDefault)]
pub struct GroupSendFullToken {
reserved: ReservedByte,
raw_token: Box<[u8]>,
expiration: Timestamp,
}
impl GroupSendFullToken {
pub fn expiration(&self) -> Timestamp {
self.expiration
}
/// Checks whether the token is (still) valid for sending to `user_ids` at `now` according to
/// `key_pair`.
pub fn verify(
&self,
user_ids: impl IntoIterator<Item = libsignal_core::ServiceId>,
now: Timestamp,
key_pair: &GroupSendDerivedKeyPair,
) -> Result<(), ZkGroupVerificationFailure> {
if now > self.expiration {
return Err(ZkGroupVerificationFailure);
}
assert_eq!(
self.expiration, key_pair.expiration,
"wrong key pair used for this token"
);
let user_id_sum: curve25519_dalek::RistrettoPoint = user_ids
.into_iter()
.map(crypto::uid_struct::UidStruct::calc_M1)
.sum();
key_pair
.key_pair
.verify(&user_id_sum, &self.raw_token)
.map_err(|_| ZkGroupVerificationFailure)
}
}

View File

@ -36,7 +36,6 @@ pub struct ServerSecretParams {
crypto::credentials::KeyPair<crypto::credentials::AuthCredentialWithPni>,
pub(crate) generic_credential_key_pair: zkcredential::credentials::CredentialKeyPair,
pub(crate) endorsement_key_pair: zkcredential::endorsements::ServerRootKeyPair,
}
#[derive(Clone, Serialize, Deserialize, PartialDefault)]
@ -57,7 +56,6 @@ pub struct ServerPublicParams {
auth_credentials_with_pni_public_key: crypto::credentials::PublicKey,
pub(crate) generic_credential_public_key: zkcredential::credentials::CredentialPublicKey,
pub(crate) endorsement_public_key: zkcredential::endorsements::ServerRootPublicKey,
}
impl ServerSecretParams {
@ -77,8 +75,6 @@ impl ServerSecretParams {
let auth_credentials_with_pni_key_pair = crypto::credentials::KeyPair::generate(&mut sho);
let generic_credential_key_pair =
zkcredential::credentials::CredentialKeyPair::generate(randomness);
let endorsement_key_pair =
zkcredential::endorsements::ServerRootKeyPair::generate(randomness);
Self {
reserved: Default::default(),
@ -90,7 +86,6 @@ impl ServerSecretParams {
expiring_profile_key_credentials_key_pair,
auth_credentials_with_pni_key_pair,
generic_credential_key_pair,
endorsement_key_pair,
}
}
@ -111,7 +106,6 @@ impl ServerSecretParams {
.auth_credentials_with_pni_key_pair
.get_public_key(),
generic_credential_public_key: self.generic_credential_key_pair.public_key().clone(),
endorsement_public_key: self.endorsement_key_pair.public_key().clone(),
}
}

View File

@ -5,7 +5,6 @@
use std::ops::Index;
use partial_default::PartialDefault;
use serde::{Deserialize, Serialize};
/// Abstracts over fixed-length arrays (and similar types) with an element type `T`.
@ -58,71 +57,30 @@ where
}
}
pub(crate) fn collect_permutation<T: PartialDefault + Clone>(
iter: impl ExactSizeIterator<Item = (T, usize)>,
) -> Vec<T> {
let mut result = vec![T::partial_default(); iter.len()];
for (value, position) in iter {
result[position] = value
}
result
#[test]
fn test_one_based_indexing() {
let array = OneBased([10, 20, 30]);
assert_eq!(10, array[1]);
assert_eq!(20, array[2]);
assert_eq!(30, array[3]);
}
#[cfg(test)]
mod tests {
use rand::Rng as _;
use super::*;
#[test]
fn test_one_based_indexing() {
let array = OneBased([10, 20, 30]);
assert_eq!(10, array[1]);
assert_eq!(20, array[2]);
assert_eq!(30, array[3]);
}
#[test]
#[should_panic]
fn test_one_based_indexing_with_zero() {
let array = OneBased([10, 20, 30]);
let _ = array[0];
}
#[test]
#[should_panic]
fn test_one_based_indexing_past_end() {
let array = OneBased([10, 20, 30]);
let _ = array[4];
}
#[test]
fn test_one_based_iter() {
let array = OneBased([10, 20, 30]);
assert_eq!(vec![10, 20, 30], array.iter().copied().collect::<Vec<_>>());
}
#[test]
fn test_permute_simple() {
let elements = [5, 6, 7, 8];
let permutation = [3, 2, 1, 0];
let result = collect_permutation(elements.into_iter().zip(permutation));
assert_eq!([8, 7, 6, 5].as_slice(), result.as_slice());
}
#[test]
fn test_permute_scramble_and_unscramble() {
for _ in 0..100 {
let mut elements = [0u32; 512];
rand::thread_rng().fill(&mut elements);
let mut elements_with_indexes: Vec<_> = elements.into_iter().zip(0..).collect();
elements_with_indexes.sort_unstable();
let result = collect_permutation(elements_with_indexes.into_iter());
assert_eq!(elements.as_slice(), result.as_slice());
}
}
#[test]
#[should_panic]
fn test_one_based_indexing_with_zero() {
let array = OneBased([10, 20, 30]);
let _ = array[0];
}
#[test]
#[should_panic]
fn test_one_based_indexing_past_end() {
let array = OneBased([10, 20, 30]);
let _ = array[4];
}
#[test]
fn test_one_based_iter() {
let array = OneBased([10, 20, 30]);
assert_eq!(vec![10, 20, 30], array.iter().copied().collect::<Vec<_>>());
}

View File

@ -42,8 +42,8 @@ pub const RECEIPT_CREDENTIAL_REQUEST_CONTEXT_LEN: usize = 177;
pub const RECEIPT_CREDENTIAL_RESPONSE_LEN: usize = 409;
pub const RECEIPT_SERIAL_LEN: usize = 16;
pub const RESERVED_LEN: usize = 1;
pub const SERVER_SECRET_PARAMS_LEN: usize = 2721;
pub const SERVER_PUBLIC_PARAMS_LEN: usize = 673;
pub const SERVER_SECRET_PARAMS_LEN: usize = 2689;
pub const SERVER_PUBLIC_PARAMS_LEN: usize = 641;
pub const UUID_CIPHERTEXT_LEN: usize = 65;
pub const RANDOMNESS_LEN: usize = 32;
pub const SIGNATURE_LEN: usize = 64;

View File

@ -1,224 +0,0 @@
//
// Copyright 2024 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use zkgroup::{RandomnessBytes, Timestamp, RANDOMNESS_LEN, SECONDS_PER_DAY, UUID_LEN};
const DAY_ALIGNED_TIMESTAMP: Timestamp = 1681344000; // 2023-04-13 00:00:00 UTC
#[test]
fn test_endorsement() {
let randomness1: RandomnessBytes = [0x43u8; RANDOMNESS_LEN];
let randomness2: RandomnessBytes = [0x44u8; RANDOMNESS_LEN];
let randomness3: RandomnessBytes = [0x45u8; RANDOMNESS_LEN];
// first set up a group
let client_user_id = libsignal_core::Aci::from_uuid_bytes([0x04u8; UUID_LEN]);
let moxie_user_id =
libsignal_core::Aci::from(uuid::uuid!("e36fdce7-36da-4c6f-a21b-9afe2b754650"));
let brian_user_id =
libsignal_core::Aci::from(uuid::uuid!("8c78cd2a-16ff-427d-83dc-1a5e36ce713d"));
let group_members = [
client_user_id.into(),
moxie_user_id.into(),
brian_user_id.into(),
];
let group_members_without_requester = [brian_user_id.into(), moxie_user_id.into()];
let group_secret_params = zkgroup::groups::GroupSecretParams::generate(randomness1);
let ciphertexts: Vec<_> = group_members
.iter()
.map(|member| group_secret_params.encrypt_service_id(*member))
.collect();
// server generated materials; issuance request -> issuance response
let server_secret_params = zkgroup::ServerSecretParams::generate(randomness2);
let todays_key = zkgroup::groups::GroupSendDerivedKeyPair::for_expiration(
DAY_ALIGNED_TIMESTAMP + SECONDS_PER_DAY,
&server_secret_params,
);
// Generate a response to test receive_with_service_ids:
{
let response = zkgroup::groups::GroupSendEndorsementsResponse::issue(
ciphertexts.iter().copied(),
&todays_key,
randomness3,
);
// client generated materials; issuance response -> redemption request
let server_public_params = server_secret_params.get_public_params();
let expiration = response.expiration();
let endorsements = response
.receive_with_service_ids(
group_members,
DAY_ALIGNED_TIMESTAMP,
&group_secret_params,
&server_public_params,
)
.expect("issued endorsements should be valid");
let combined_endorsements =
zkgroup::groups::GroupSendEndorsement::combine(endorsements.clone())
.remove(&endorsements[0]);
let token = combined_endorsements
.to_token(&group_secret_params)
.into_full_token(expiration);
// server verification of the credential presentation
assert_eq!(token.expiration(), expiration);
token
.verify(
group_members_without_requester,
DAY_ALIGNED_TIMESTAMP,
&todays_key,
)
.expect("credential should be valid for the timestamp given");
}
// Try again for receive_with_ciphertexts:
{
let response = zkgroup::groups::GroupSendEndorsementsResponse::issue(
ciphertexts.iter().copied(),
&todays_key,
randomness3,
);
// client generated materials; issuance response -> redemption request
let server_public_params = server_secret_params.get_public_params();
let expiration = response.expiration();
let endorsements = response
.receive_with_ciphertexts(
ciphertexts.iter().copied(),
DAY_ALIGNED_TIMESTAMP,
&server_public_params,
)
.expect("issued endorsements should be valid");
let combined_endorsements =
zkgroup::groups::GroupSendEndorsement::combine(endorsements.clone())
.remove(&endorsements[0]);
let token = combined_endorsements
.to_token(&group_secret_params)
.into_full_token(expiration);
// server verification of the credential presentation
assert_eq!(token.expiration(), expiration);
token
.verify(
group_members_without_requester,
DAY_ALIGNED_TIMESTAMP,
&todays_key,
)
.expect("credential should be valid for the timestamp given");
}
}
#[test]
fn test_single_member_group() {
let randomness1: RandomnessBytes = [0x43u8; RANDOMNESS_LEN];
let randomness2: RandomnessBytes = [0x44u8; RANDOMNESS_LEN];
let randomness3: RandomnessBytes = [0x45u8; RANDOMNESS_LEN];
// first set up a group
let client_user_id = libsignal_core::Aci::from_uuid_bytes([0x04u8; UUID_LEN]);
let group_secret_params = zkgroup::groups::GroupSecretParams::generate(randomness1);
let client_user_id_ciphertext = group_secret_params.encrypt_service_id(client_user_id.into());
// server generated materials; issuance request -> issuance response
let server_secret_params = zkgroup::ServerSecretParams::generate(randomness2);
let todays_key = zkgroup::groups::GroupSendDerivedKeyPair::for_expiration(
DAY_ALIGNED_TIMESTAMP + SECONDS_PER_DAY,
&server_secret_params,
);
let response = zkgroup::groups::GroupSendEndorsementsResponse::issue(
[client_user_id_ciphertext],
&todays_key,
randomness3,
);
// client generated materials; issuance response -> redemption request
let server_public_params = server_secret_params.get_public_params();
let _endorsements = response
.receive_with_service_ids(
[client_user_id.into()],
DAY_ALIGNED_TIMESTAMP,
&group_secret_params,
&server_public_params,
)
.expect("issued endorsements should be valid");
}
#[test]
fn test_client_rejects_bad_expirations() {
let randomness1: RandomnessBytes = [0x43u8; RANDOMNESS_LEN];
let randomness2: RandomnessBytes = [0x44u8; RANDOMNESS_LEN];
let randomness3: RandomnessBytes = [0x45u8; RANDOMNESS_LEN];
// first set up a group
let client_user_id = libsignal_core::Aci::from_uuid_bytes([0x04u8; UUID_LEN]);
let moxie_user_id =
libsignal_core::Aci::from(uuid::uuid!("e36fdce7-36da-4c6f-a21b-9afe2b754650"));
let brian_user_id =
libsignal_core::Aci::from(uuid::uuid!("8c78cd2a-16ff-427d-83dc-1a5e36ce713d"));
let group_members = [
client_user_id.into(),
moxie_user_id.into(),
brian_user_id.into(),
];
let group_secret_params = zkgroup::groups::GroupSecretParams::generate(randomness1);
let ciphertexts: Vec<_> = group_members
.iter()
.map(|member| group_secret_params.encrypt_service_id(*member))
.collect();
let server_secret_params = zkgroup::ServerSecretParams::generate(randomness2);
let server_public_params = server_secret_params.get_public_params();
let expect_credential_rejected = |now: zkgroup::Timestamp, expiration: zkgroup::Timestamp| {
let key = zkgroup::groups::GroupSendDerivedKeyPair::for_expiration(
expiration,
&server_secret_params,
);
let response = zkgroup::groups::GroupSendEndorsementsResponse::issue(
ciphertexts.iter().cloned(),
&key,
randomness3,
);
assert!(
response
.receive_with_service_ids(
[client_user_id.into()],
now,
&group_secret_params,
&server_public_params,
)
.is_err(),
"now: {now}, expiration: {expiration}"
);
};
expect_credential_rejected(DAY_ALIGNED_TIMESTAMP, DAY_ALIGNED_TIMESTAMP);
expect_credential_rejected(
DAY_ALIGNED_TIMESTAMP,
DAY_ALIGNED_TIMESTAMP - SECONDS_PER_DAY,
);
expect_credential_rejected(DAY_ALIGNED_TIMESTAMP, DAY_ALIGNED_TIMESTAMP + 1);
expect_credential_rejected(
DAY_ALIGNED_TIMESTAMP,
DAY_ALIGNED_TIMESTAMP + 8 * SECONDS_PER_DAY,
);
expect_credential_rejected(
DAY_ALIGNED_TIMESTAMP,
DAY_ALIGNED_TIMESTAMP + 1000 * SECONDS_PER_DAY,
);
}

View File

@ -94,9 +94,9 @@ SPDX-License-Identifier: AGPL-3.0-only
#define SignalRESERVED_LEN 1
#define SignalSERVER_SECRET_PARAMS_LEN 2721
#define SignalSERVER_SECRET_PARAMS_LEN 2689
#define SignalSERVER_PUBLIC_PARAMS_LEN 673
#define SignalSERVER_PUBLIC_PARAMS_LEN 641
#define SignalUUID_CIPHERTEXT_LEN 65