Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e465b77cb6 | ||
|
|
4388fac6a7 |
7
Cargo.lock
generated
7
Cargo.lock
generated
@ -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",
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -6,7 +6,7 @@ plugins {
|
||||
}
|
||||
|
||||
allprojects {
|
||||
version = "0.41.1"
|
||||
version = "0.41.2"
|
||||
group = "org.signal"
|
||||
}
|
||||
|
||||
|
||||
@ -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 }
|
||||
]
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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<_>>());
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
@ -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
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user