0
0
mirror of https://github.com/signalapp/libsignal.git synced 2024-09-20 03:52:17 +02:00

zkgroup: Add GroupSendCredentialResponse::receive_with_ciphertexts

If a client already has the members of a group as ciphertexts, it's
more efficient to receive a GroupSendCredential that way, because then
they get to skip the conversion from ServiceId to UidStruct. If they
don't, however, the existing entry point is going to be both more
convenient and faster.

For Swift and Java, this is an overload of the existing receive()
method; for TypeScript, it's receiveWithCiphertexts.
This commit is contained in:
Jordan Rose 2024-01-22 12:34:34 -08:00 committed by GitHub
parent c38fcf6ccc
commit 1f8701213b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 592 additions and 29 deletions

View File

@ -111,6 +111,31 @@ public final class GroupSendCredentialTest extends SecureRandomTest {
serverPublicParams,
groupSecretParams));
// Try receive with ciphertexts instead.
response.receive(groupCiphertexts, aliceCiphertext, serverPublicParams, groupSecretParams);
assertThrows(
VerificationFailedException.class,
() ->
response.receive(
groupCiphertexts, groupCiphertexts.get(1), serverPublicParams, groupSecretParams));
assertThrows(
VerificationFailedException.class,
() ->
response.receive(
groupCiphertexts.stream().skip(1).collect(Collectors.toList()),
aliceCiphertext,
serverPublicParams,
groupSecretParams));
assertThrows(
VerificationFailedException.class,
() ->
response.receive(
groupCiphertexts.stream().limit(3).collect(Collectors.toList()),
aliceCiphertext,
serverPublicParams,
groupSecretParams));
GroupSendCredentialPresentation presentation =
credential.present(serverPublicParams, createSecureRandom(TEST_ARRAY_32_2));

View File

@ -260,6 +260,7 @@ public final class Native {
public static native long GroupSendCredentialResponse_DefaultExpirationBasedOnCurrentTime();
public static native byte[] GroupSendCredentialResponse_IssueDeterministic(byte[] concatenatedGroupMemberCiphertexts, byte[] requester, long expiration, byte[] serverParams, byte[] randomness);
public static native byte[] GroupSendCredentialResponse_Receive(byte[] responseBytes, byte[] groupMembers, byte[] localAci, long now, byte[] serverParams, byte[] groupParams);
public static native byte[] GroupSendCredentialResponse_ReceiveWithCiphertexts(byte[] responseBytes, byte[] concatenatedGroupMemberCiphertexts, byte[] requester, long now, byte[] serverParams, byte[] groupParams);
public static native void GroupSendCredential_CheckValidContents(byte[] paramsBytes);
public static native byte[] GroupSendCredential_PresentDeterministic(byte[] credentialBytes, byte[] serverParams, byte[] randomness);

View File

@ -5,6 +5,9 @@
package org.signal.libsignal.zkgroup.groups;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import org.signal.libsignal.internal.Native;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.internal.ByteArray;
@ -14,4 +17,17 @@ public final class UuidCiphertext extends ByteArray {
super(contents);
Native.UuidCiphertext_CheckValidContents(contents);
}
public static byte[] serializeAndConcatenate(List<UuidCiphertext> ciphertexts) {
ByteArrayOutputStream concatenated = new ByteArrayOutputStream();
for (UuidCiphertext member : ciphertexts) {
try {
concatenated.write(member.getInternalContentsForJNI());
} catch (IOException e) {
// ByteArrayOutputStream should never fail.
throw new AssertionError(e);
}
}
return concatenated.toByteArray();
}
}

View File

@ -13,6 +13,16 @@ import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.ServerPublicParams;
import org.signal.libsignal.zkgroup.internal.ByteArray;
/**
* A credential indicating membership in a group, based on the set of <em>other</em> users in the
* group with you.
*
* <p>Follows the usual zkgroup pattern of "issue response -> receive response -> present credential
* -> verify presentation".
*
* @see GroupSendCredentialResponse
* @see GroupSendCredentialPresentation
*/
public final class GroupSendCredential extends ByteArray {
public GroupSendCredential(byte[] contents) throws InvalidInputException {
@ -20,10 +30,18 @@ public final class GroupSendCredential extends ByteArray {
Native.GroupSendCredential_CheckValidContents(contents);
}
/** Generates a new presentation, so that multiple uses of this credential are harder to link. */
public GroupSendCredentialPresentation present(ServerPublicParams serverParams) {
return present(serverParams, new SecureRandom());
}
/**
* Generates a new presentation with a dedicated source of randomness.
*
* <p>Should only be used for testing purposes.
*
* @see #present(ServerPublicParams)
*/
public GroupSendCredentialPresentation present(
ServerPublicParams serverParams, SecureRandom secureRandom) {
byte[] random = new byte[RANDOM_LENGTH];

View File

@ -14,6 +14,16 @@ import org.signal.libsignal.zkgroup.ServerSecretParams;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.internal.ByteArray;
/**
* A credential presentation indicating membership in a group, based on the set of <em>other</em>
* users in the group with you.
*
* <p>Follows the usual zkgroup pattern of "issue response -> receive response -> present credential
* -> verify presentation".
*
* @see GroupSendCredentialResponse
* @see GroupSendCredential
*/
public final class GroupSendCredentialPresentation extends ByteArray {
public GroupSendCredentialPresentation(byte[] contents) throws InvalidInputException {
@ -21,11 +31,26 @@ public final class GroupSendCredentialPresentation extends ByteArray {
Native.GroupSendCredentialPresentation_CheckValidContents(contents);
}
/**
* Verifies that the credential is valid for a group containing the holder and {@code
* groupMembers}.
*
* @throws VerificationFailedException if the credential is not valid for any reason
*/
public void verify(List<ServiceId> groupMembers, ServerSecretParams serverParams)
throws VerificationFailedException {
verify(groupMembers, Instant.now(), serverParams);
}
/**
* Verifies that the credential would be valid for a group containing the holder and {@code
* groupMembers} at a given time.
*
* <p>Should only be used for testing purposes.
*
* @throws VerificationFailedException if the credential is not valid for any reason
* @see #verify(List, ServerSecretParams)
*/
public void verify(
List<ServiceId> groupMembers, Instant currentTime, ServerSecretParams serverParams)
throws VerificationFailedException {

View File

@ -7,8 +7,6 @@ package org.signal.libsignal.zkgroup.groupsend;
import static org.signal.libsignal.zkgroup.internal.Constants.RANDOM_LENGTH;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.List;
@ -22,6 +20,16 @@ import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
import org.signal.libsignal.zkgroup.groups.UuidCiphertext;
import org.signal.libsignal.zkgroup.internal.ByteArray;
/**
* The issuance of a credential indicating membership in a group, based on the set of <em>other</em>
* users in the group with you.
*
* <p>Follows the usual zkgroup pattern of "issue response -> receive response -> present credential
* -> verify presentation".
*
* @see GroupSendCredential
* @see GroupSendCredentialPresentation
*/
public final class GroupSendCredentialResponse extends ByteArray {
public GroupSendCredentialResponse(byte[] contents) throws InvalidInputException {
super(contents);
@ -34,34 +42,37 @@ public final class GroupSendCredentialResponse extends ByteArray {
return Instant.ofEpochSecond(expirationEpochSecond);
}
/**
* Issues a new credential stating that {@code requestingUser} is a member of a group containing
* {@code groupMembers}.
*
* <p>{@code groupMembers} should include {@code requestingUser} as well.
*/
public static GroupSendCredentialResponse issueCredential(
List<UuidCiphertext> groupMembers, UuidCiphertext requestingUser, ServerSecretParams params) {
return issueCredential(
groupMembers, requestingUser, defaultExpiration(), params, new SecureRandom());
}
/**
* Issues a new credential stating that {@code requestingUser} is a member of a group containing
* {@code groupMembers}, with an explicitly-chosen expiration.
*
* <p>{@code groupMembers} should include {@code requestingUser} as well. {@code expiration} must
* be day-aligned as a protection against fingerprinting by the issuing server.
*/
public static GroupSendCredentialResponse issueCredential(
List<UuidCiphertext> groupMembers,
UuidCiphertext requestingUser,
Instant expiration,
ServerSecretParams params,
SecureRandom secureRandom) {
ByteArrayOutputStream concatenated = new ByteArrayOutputStream();
for (UuidCiphertext member : groupMembers) {
try {
concatenated.write(member.getInternalContentsForJNI());
} catch (IOException e) {
// ByteArrayOutputStream should never fail.
throw new AssertionError(e);
}
}
byte[] random = new byte[RANDOM_LENGTH];
secureRandom.nextBytes(random);
byte[] newContents =
Native.GroupSendCredentialResponse_IssueDeterministic(
concatenated.toByteArray(),
UuidCiphertext.serializeAndConcatenate(groupMembers),
requestingUser.getInternalContentsForJNI(),
expiration.getEpochSecond(),
params.getInternalContentsForJNI(),
@ -74,6 +85,19 @@ public final class GroupSendCredentialResponse extends ByteArray {
}
}
/**
* Receives, validates, and extracts the credential from a response.
*
* <p>Note that the {@code receive} operation is provided for both {@link ServiceId}s and {@link
* UuidCiphertext}s. If you already have the ciphertexts for the group members available, {@link
* #receive(List, UuidCiphertext, ServerPublicParams, GroupSecretParams)} will be
* <em>significantly</em> faster; if you don't, this method is faster than generating the
* ciphertexts and throwing them away afterwards.
*
* <p>{@code localUser} should be included in {@code groupMembers}.
*
* @throws VerificationFailedException if the credential is not valid for any reason
*/
public GroupSendCredential receive(
List<ServiceId> groupMembers,
ServiceId.Aci localUser,
@ -83,6 +107,14 @@ public final class GroupSendCredentialResponse extends ByteArray {
return receive(groupMembers, localUser, Instant.now(), serverParams, groupParams);
}
/**
* Receives, validates, and extracts the credential from a response, assuming a specific current
* time.
*
* <p>This should only be used for testing purposes.
*
* @see #receive(List, ServiceId.Aci, ServerPublicParams, GroupSecretParams)
*/
public GroupSendCredential receive(
List<ServiceId> groupMembers,
ServiceId.Aci localUser,
@ -105,4 +137,57 @@ public final class GroupSendCredentialResponse extends ByteArray {
throw new AssertionError(e);
}
}
/**
* Receives, validates, and extracts the credential from a response.
*
* <p>Note that the {@code receive} operation is provided for both {@link ServiceId}s and {@link
* UuidCiphertext}s. If you already have the ciphertexts for the group members available, this
* method will be <em>significantly</em> faster; if you don't, {@link #receive(List,
* ServiceId.Aci, ServerPublicParams, GroupSecretParams)} is faster than generating the
* ciphertexts and throwing them away afterwards.
*
* <p>{@code localUser} should be included in {@code groupMembers}.
*
* @throws VerificationFailedException if the credential is not valid for any reason
*/
public GroupSendCredential receive(
List<UuidCiphertext> groupMembers,
UuidCiphertext localUser,
ServerPublicParams serverParams,
GroupSecretParams groupParams)
throws VerificationFailedException {
return receive(groupMembers, localUser, Instant.now(), serverParams, groupParams);
}
/**
* Receives, validates, and extracts the credential from a response, assuming a specific current
* time.
*
* <p>This should only be used for testing purposes.
*
* @see #receive(List, UuidCiphertext, ServerPublicParams, GroupSecretParams)
*/
public GroupSendCredential receive(
List<UuidCiphertext> groupMembers,
UuidCiphertext localUser,
Instant now,
ServerPublicParams serverParams,
GroupSecretParams groupParams)
throws VerificationFailedException {
byte[] newContents =
Native.GroupSendCredentialResponse_ReceiveWithCiphertexts(
getInternalContentsForJNI(),
UuidCiphertext.serializeAndConcatenate(groupMembers),
localUser.getInternalContentsForJNI(),
now.getEpochSecond(),
serverParams.getInternalContentsForJNI(),
groupParams.getInternalContentsForJNI());
try {
return new GroupSendCredential(newContents);
} catch (InvalidInputException e) {
throw new AssertionError(e);
}
}
}

1
node/Native.d.ts vendored
View File

@ -196,6 +196,7 @@ export function GroupSendCredentialResponse_CheckValidContents(responseBytes: Bu
export function GroupSendCredentialResponse_DefaultExpirationBasedOnCurrentTime(): Timestamp;
export function GroupSendCredentialResponse_IssueDeterministic(concatenatedGroupMemberCiphertexts: Buffer, requester: Serialized<UuidCiphertext>, expiration: Timestamp, serverParams: Serialized<ServerSecretParams>, randomness: Buffer): Buffer;
export function GroupSendCredentialResponse_Receive(responseBytes: Buffer, groupMembers: Buffer, localAci: Buffer, now: Timestamp, serverParams: Serialized<ServerPublicParams>, groupParams: Serialized<GroupSecretParams>): Buffer;
export function GroupSendCredentialResponse_ReceiveWithCiphertexts(responseBytes: Buffer, concatenatedGroupMemberCiphertexts: Buffer, requester: Serialized<UuidCiphertext>, now: Timestamp, serverParams: Serialized<ServerPublicParams>, groupParams: Serialized<GroupSecretParams>): Buffer;
export function GroupSendCredential_CheckValidContents(paramsBytes: Buffer): void;
export function GroupSendCredential_PresentDeterministic(credentialBytes: Buffer, serverParams: Serialized<ServerPublicParams>, randomness: Buffer): Buffer;
export function HKDF_DeriveSecrets(outputLength: number, ikm: Buffer, label: Buffer | null, salt: Buffer | null): Buffer;

View File

@ -866,6 +866,38 @@ describe('ZKGroup', () => {
)
);
// Try the other receive too
void response.receiveWithCiphertexts(
groupCiphertexts,
aliceCiphertext,
serverPublicParams,
groupSecretParams
);
assert.throws(() =>
response.receiveWithCiphertexts(
groupCiphertexts,
groupCiphertexts[1],
serverPublicParams,
groupSecretParams
)
);
assert.throws(() =>
response.receiveWithCiphertexts(
groupCiphertexts.slice(1),
aliceCiphertext,
serverPublicParams,
groupSecretParams
)
);
assert.throws(() =>
response.receiveWithCiphertexts(
groupCiphertexts.slice(0, -1),
aliceCiphertext,
serverPublicParams,
groupSecretParams
)
);
const presentation = credential.presentWithRandom(
serverPublicParams,
TEST_ARRAY_32_2

View File

@ -12,4 +12,23 @@ export default class UuidCiphertext extends ByteArray {
constructor(contents: Buffer) {
super(contents, Native.UuidCiphertext_CheckValidContents);
}
static serializeAndConcatenate(ciphertexts: UuidCiphertext[]): Buffer {
if (ciphertexts.length == 0) {
return Buffer.of();
}
const uuidCiphertextLen = ciphertexts[0].contents.length;
const concatenated = Buffer.alloc(ciphertexts.length * uuidCiphertextLen);
let offset = 0;
for (const next of ciphertexts) {
if (next.contents.length !== uuidCiphertextLen) {
throw TypeError('UuidCiphertext with unexpected length');
}
concatenated.set(next.contents, offset);
offset += uuidCiphertextLen;
}
return concatenated;
}
}

View File

@ -10,8 +10,19 @@ import { RANDOM_LENGTH } from '../internal/Constants';
import * as Native from '../../../Native';
import GroupSendCredentialPresentation from './GroupSendCredentialPresentation';
import type GroupSendCredentialResponse from './GroupSendCredentialResponse'; // for docs
import ServerPublicParams from '../ServerPublicParams';
/**
* A credential indicating membership in a group, based on the set of *other* users in the
* group with you.
*
* Follows the usual zkgroup pattern of "issue response -> receive response -> present credential
* -> verify presentation".
*
* @see {@link GroupSendCredentialResponse}
* @see {@link GroupSendCredentialPresentation}
*/
export default class GroupSendCredential extends ByteArray {
private readonly __type?: never;
@ -19,11 +30,21 @@ export default class GroupSendCredential extends ByteArray {
super(contents, Native.GroupSendCredential_CheckValidContents);
}
/**
* Generates a new presentation, so that multiple uses of this credential are harder to link.
*/
present(serverParams: ServerPublicParams): GroupSendCredentialPresentation {
const random = randomBytes(RANDOM_LENGTH);
return this.presentWithRandom(serverParams, random);
}
/**
* Generates a new presentation with a dedicated source of randomness.
*
* Should only be used for testing purposes.
*
* @see {@link GroupSendCredential#present}
*/
presentWithRandom(
serverParams: ServerPublicParams,
random: Buffer

View File

@ -9,6 +9,21 @@ import * as Native from '../../../Native';
import ServerSecretParams from '../ServerSecretParams';
import { ServiceId } from '../../Address';
// For docs:
import type GroupSendCredential from './GroupSendCredential';
import type GroupSendCredentialResponse from './GroupSendCredentialResponse';
import type { VerificationFailedError } from '../../Errors';
/**
* A credential presentation indicating membership in a group, based on the set of *other* users in
* the group with you.
*
* Follows the usual zkgroup pattern of "issue response -> receive response -> present credential ->
* verify presentation".
*
* @see {@link GroupSendCredentialResponse}
* @see {@link GroupSendCredential}
*/
export default class GroupSendCredentialPresentation extends ByteArray {
private readonly __type?: never;
@ -16,6 +31,11 @@ export default class GroupSendCredentialPresentation extends ByteArray {
super(contents, Native.GroupSendCredentialPresentation_CheckValidContents);
}
/**
* Verifies that the credential is valid for a group containing the holder and `groupMembers`.
*
* @throws {VerificationFailedError} if the credential is not valid for any reason
*/
verify(
groupMembers: ServiceId[],
serverParams: ServerSecretParams,

View File

@ -16,6 +16,20 @@ import ServerPublicParams from '../ServerPublicParams';
import UuidCiphertext from '../groups/UuidCiphertext';
import { Aci, ServiceId } from '../../Address';
// For docs
import type GroupSendCredentialPresentation from './GroupSendCredentialPresentation';
import type { VerificationFailedError } from '../../Errors';
/**
* The issuance of a credential indicating membership in a group, based on the set of *other* users
* in the group with you.
*
* Follows the usual zkgroup pattern of "issue response -> receive response -> present credential ->
* verify presentation".
*
* @see {@link GroupSendCredential}
* @see {@link GroupSendCredentialPresentation}
*/
export default class GroupSendCredentialResponse extends ByteArray {
private readonly __type?: never;
@ -29,6 +43,12 @@ export default class GroupSendCredentialResponse extends ByteArray {
return new Date(expirationInSeconds * 1000);
}
/**
* Issues a new credential stating that `requestingMember` is a member of a group containing
* `groupMembers`.
*
* `groupMembers` should include `requestingMember` as well.
*/
static issueCredential(
groupMembers: UuidCiphertext[],
requestingMember: UuidCiphertext,
@ -44,6 +64,14 @@ export default class GroupSendCredentialResponse extends ByteArray {
);
}
/**
* Issues a new credential stating that `requestingMember` is a member of a group containing
* `groupMembers`, with an explicity-chosen expiration and source of randomness.
*
* Should only be used for testing purposes.
*
* @see {@link GroupSendCredentialResponse#issueCredential}
*/
static issueCredentialWithExpirationAndRandom(
groupMembers: UuidCiphertext[],
requestingMember: UuidCiphertext,
@ -51,20 +79,9 @@ export default class GroupSendCredentialResponse extends ByteArray {
params: ServerSecretParams,
random: Buffer
): GroupSendCredentialResponse {
const uuidCiphertextLen = requestingMember.contents.length;
const concatenated = Buffer.alloc(groupMembers.length * uuidCiphertextLen);
let offset = 0;
for (const member of groupMembers) {
if (member.contents.length !== uuidCiphertextLen) {
throw TypeError('UuidCiphertext with unexpected length');
}
concatenated.set(member.contents, offset);
offset += uuidCiphertextLen;
}
return new GroupSendCredentialResponse(
Native.GroupSendCredentialResponse_IssueDeterministic(
concatenated,
UuidCiphertext.serializeAndConcatenate(groupMembers),
requestingMember.contents,
Math.floor(expiration.getTime() / 1000),
params.contents,
@ -73,6 +90,19 @@ export default class GroupSendCredentialResponse extends ByteArray {
);
}
/**
* Receives, validates, and extracts the credential from a response.
*
* Note that the `receive` operation is provided for both {@link ServiceId}s and
* {@link UuidCiphertext}s. If you already have the ciphertexts for the group members available,
* {@link GroupSendCredentialResponse#receiveWithCiphertexts} will be *significantly* faster; if
* you don't, this method is faster than generating the ciphertexts and throwing them away
* afterwards.
*
* `localUser` should be included in `groupMembers`.
*
* @throws {VerificationFailedError} if the credential is not valid for any reason
*/
receive(
groupMembers: ServiceId[],
localUser: Aci,
@ -91,4 +121,36 @@ export default class GroupSendCredentialResponse extends ByteArray {
)
);
}
/**
* Receives, validates, and extracts the credential from a response.
*
* Note that the `receive` operation is provided for both {@link ServiceId}s and
* {@link UuidCiphertext}s. If you already have the ciphertexts for the group members available,
* this method will be *significantly* faster; if you don't,
* {@link GroupSendCredentialResponse#receive} is faster than generating the ciphertexts and
* throwing them away afterwards.
*
* `localUser` should be included in `groupMembers`.
*
* @throws {VerificationFailedError} if the credential is not valid for any reason
*/
receiveWithCiphertexts(
groupMembers: UuidCiphertext[],
localUser: UuidCiphertext,
serverParams: ServerPublicParams,
groupParams: GroupSecretParams,
now: Date = new Date()
): GroupSendCredential {
return new GroupSendCredential(
Native.GroupSendCredentialResponse_ReceiveWithCiphertexts(
this.contents,
UuidCiphertext.serializeAndConcatenate(groupMembers),
localUser.contents,
Math.floor(now.getTime() / 1000),
serverParams.contents,
groupParams.contents
)
);
}
}

View File

@ -1189,6 +1189,36 @@ fn GroupSendCredentialResponse_Receive(
Ok(zkgroup::serialize(&credential))
}
#[bridge_fn]
fn GroupSendCredentialResponse_ReceiveWithCiphertexts(
response_bytes: &[u8],
concatenated_group_member_ciphertexts: &[u8],
requester: Serialized<UuidCiphertext>,
now: Timestamp,
server_params: Serialized<ServerPublicParams>,
group_params: Serialized<GroupSecretParams>,
) -> Result<Vec<u8>, ZkGroupVerificationFailure> {
let response = zkgroup::deserialize::<GroupSendCredentialResponse>(response_bytes)
.expect("should have been parsed previously");
assert!(concatenated_group_member_ciphertexts.len() % UUID_CIPHERTEXT_LEN == 0);
let user_id_ciphertexts = concatenated_group_member_ciphertexts
.chunks_exact(UUID_CIPHERTEXT_LEN)
.map(|serialized| {
zkgroup::deserialize::<UuidCiphertext>(serialized)
.expect("should have been parsed previously")
});
let credential = response.receive_with_ciphertexts(
&server_params,
&group_params,
user_id_ciphertexts,
&requester,
now.as_seconds(),
)?;
Ok(zkgroup::serialize(&credential))
}
#[bridge_fn_void]
fn GroupSendCredential_CheckValidContents(
params_bytes: &[u8],

View File

@ -327,6 +327,25 @@ pub fn benchmark_group_send(c: &mut Criterion) {
},
);
benchmark_group.bench_function(
BenchmarkId::new("deserialize_and_receive_with_ciphertexts", group_size),
|b| {
b.iter(|| {
let credential_response: zkgroup::groups::GroupSendCredentialResponse =
zkgroup::deserialize(&serialized_credential_response).expect("valid");
credential_response
.receive_with_ciphertexts(
&server_public_params,
&group_secret_params,
group_ciphertexts.clone().copied(),
&all_member_ciphertexts[0],
DAY_ALIGNED_TIMESTAMP,
)
.expect("issued credential should be valid")
})
},
);
let presentation = credential.present(&server_public_params, zkgroup::TEST_ARRAY_32_3);
benchmark_group.bench_function(BenchmarkId::new("present", group_size), |b| {

View File

@ -80,6 +80,19 @@ impl<T> zkcredential::attributes::Attribute for UserIdSet<T> {
}
}
impl<T> From<zkcredential::attributes::Ciphertext<UserIdSet<T::Attribute>>>
for UserIdSet<zkcredential::attributes::Ciphertext<T>>
where
T: zkcredential::attributes::Domain,
{
fn from(value: zkcredential::attributes::Ciphertext<UserIdSet<T::Attribute>>) -> Self {
Self {
points: value.as_points(),
kind: PhantomData,
}
}
}
#[derive(Serialize, Deserialize, PartialDefault)]
pub struct GroupSendCredentialResponse {
reserved: ReservedBytes,
@ -149,6 +162,46 @@ impl GroupSendCredentialResponse {
.uid_enc_key_pair
.encrypt_arbitrary_attribute(&user_id_set);
let raw_credential = zkcredential::issuance::IssuanceProofBuilder::new(CREDENTIAL_LABEL)
.add_attribute(&user_id_set_ciphertext)
.add_public_attribute(&self.expiration)
.verify(&params.generic_credential_public_key, self.proof)
.map_err(|_| ZkGroupVerificationFailure)?;
let encryption_key_pair =
zkcredential::attributes::KeyPair::inverse_of(&group_params.uid_enc_key_pair);
Ok(GroupSendCredential {
reserved: [0],
credential: raw_credential,
user_id_set_ciphertext: user_id_set_ciphertext.into(),
expiration: self.expiration,
encryption_key_pair,
})
}
pub fn receive_with_ciphertexts(
self,
params: &ServerPublicParams,
group_params: &GroupSecretParams,
user_id_ciphertexts: impl IntoIterator<Item = UuidCiphertext>,
requester: &UuidCiphertext,
now: Timestamp,
) -> Result<GroupSendCredential, ZkGroupVerificationFailure> {
if self.expiration % SECONDS_PER_DAY != 0 {
return Err(ZkGroupVerificationFailure);
}
if self.expiration.saturating_sub(now) > 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);
}
let user_id_set_ciphertext = UserIdSet::from_user_ids_omitting_requester(
user_id_ciphertexts.into_iter().map(|c| c.ciphertext),
&requester.ciphertext,
)?;
let raw_credential = zkcredential::issuance::IssuanceProofBuilder::new(CREDENTIAL_LABEL)
.add_attribute(&user_id_set_ciphertext)
.add_public_attribute(&self.expiration)
@ -172,9 +225,7 @@ impl GroupSendCredentialResponse {
pub struct GroupSendCredential {
reserved: ReservedBytes,
credential: zkcredential::credentials::Credential,
// UserIdSet is *not* an encryption domain, but we just need a marker type to distinguish this
// from a normal Ciphertext<UidEncryptionDomain>.
user_id_set_ciphertext: zkcredential::attributes::Ciphertext<UserIdSet<UidStruct>>,
user_id_set_ciphertext: UserIdSet<crate::crypto::uid_encryption::Ciphertext>,
expiration: Timestamp,
// Additionally includes this because we'd need to recompute it with every message otherwise.
encryption_key_pair: zkcredential::attributes::KeyPair<InverseUidEncryptionDomain>,

View File

@ -40,7 +40,7 @@ fn test_credential() {
// server generated materials; issuance request -> issuance response
let server_secret_params = zkgroup::ServerSecretParams::generate(randomness2);
let credential_response = GroupSendCredentialResponse::issue_credential(
ciphertexts,
ciphertexts.iter().copied(),
&client_user_id_ciphertext,
DAY_ALIGNED_TIMESTAMP,
&server_secret_params,
@ -62,6 +62,37 @@ fn test_credential() {
let presentation = credential.present(&server_public_params, randomness4);
// server verification of the credential presentation
presentation
.verify(
group_members_without_requester,
DAY_ALIGNED_TIMESTAMP,
&server_secret_params,
)
.expect("credential should be valid for the timestamp given");
// Try again with the alternate receive implementation
let credential_response = GroupSendCredentialResponse::issue_credential(
ciphertexts.iter().copied(),
&client_user_id_ciphertext,
DAY_ALIGNED_TIMESTAMP,
&server_secret_params,
randomness3,
)
.expect("valid request");
let credential = credential_response
.receive_with_ciphertexts(
&server_public_params,
&group_secret_params,
ciphertexts.iter().copied(),
&client_user_id_ciphertext,
DAY_ALIGNED_TIMESTAMP,
)
.expect("issued credential should be valid");
let presentation = credential.present(&server_public_params, randomness4);
// server verification of the credential presentation
presentation
.verify(

View File

@ -6,18 +6,37 @@
import Foundation
import SignalFfi
/**
* A credential indicating membership in a group, based on the set of *other* users in the group
* with you.
*
* Follows the usual zkgroup pattern of "issue response -> receive response -> present credential ->
* verify presentation".
*
* - SeeAlso: ``GroupSendCredentialResponse``, ``GroupSendCredentialPresentation``
*/
public class GroupSendCredential: ByteArray {
public required init(contents: [UInt8]) throws {
try super.init(contents, checkValid: signal_group_send_credential_check_valid_contents)
}
/**
* Generates a new presentation, so that multiple uses of this credential are harder to link.
*/
public func present(serverParams: ServerPublicParams) -> GroupSendCredentialPresentation {
return failOnError {
present(serverParams: serverParams, randomness: try .generate())
}
}
/**
* Generates a new presentation with a dedicated source of randomness.
*
* Should only be used for testing purposes.
*
* - SeeAlso: ``present(serverParams:)``
*/
public func present(serverParams: ServerPublicParams, randomness: Randomness) -> GroupSendCredentialPresentation {
return failOnError {
try withUnsafeBorrowedBuffer { contents in

View File

@ -6,12 +6,26 @@
import Foundation
import SignalFfi
/**
* A credential indicating membership in a group, based on the set of *other* users in the
* group with you.
*
* Follows the usual zkgroup pattern of "issue response -> receive response -> present credential
* -> verify presentation".
*
* - SeeAlso: ``GroupSendCredentialResponse``, ``GroupSendCredential``
*/
public class GroupSendCredentialPresentation: ByteArray {
public required init(contents: [UInt8]) throws {
try super.init(contents, checkValid: signal_group_send_credential_presentation_check_valid_contents)
}
/**
* Verifies that the credential is valid for a group containing the holder and `groupMembers`.
*
* - Throws: ``SignalError/verificationFailed(_:)`` if the credential is not valid for any reason
*/
public func verify(groupMembers: [ServiceId], now: Date = Date(), serverParams: ServerSecretParams) throws {
try withUnsafeBorrowedBuffer { contents in
try ServiceId.concatenatedFixedWidthBinary(groupMembers).withUnsafeBorrowedBuffer { groupMembers in

View File

@ -6,6 +6,15 @@
import Foundation
import SignalFfi
/**
* The issuance of a credential indicating membership in a group, based on the set of *other* users
* in the group with you.
*
* Follows the usual zkgroup pattern of "issue response -> receive response -> present credential ->
* verify presentation".
*
* - SeeAlso: ``GroupSendCredential``, ``GroupSendCredentialPresentation``
*/
public class GroupSendCredentialResponse: ByteArray {
public required init(contents: [UInt8]) throws {
try super.init(contents, checkValid: signal_group_send_credential_response_check_valid_contents)
@ -20,12 +29,26 @@ public class GroupSendCredentialResponse: ByteArray {
return Date(timeIntervalSince1970: TimeInterval(expiration))
}
/**
* Issues a new credential stating that `requestingMember` is a member of a group containing
* `groupMembers`.
*
* `groupMembers` should include `requestingMember` as well.
*/
public static func issueCredential(groupMembers: [UuidCiphertext], requestingMember: UuidCiphertext, expiration: Date = GroupSendCredentialResponse.defaultExpiration(), params: ServerSecretParams) -> GroupSendCredentialResponse {
return failOnError {
issueCredential(groupMembers: groupMembers, requestingMember: requestingMember, expiration: expiration, params: params, randomness: try .generate())
}
}
/**
* Issues a new credential stating that `requestingMember` is a member of a group containing
* `groupMembers`, with an explictly-chosen source of randomness.
*
* Should only be used for testing purposes.
*
* - SeeAlso: ``issueCredential(groupMembers:requestingMember:expiration:params:)``
*/
public static func issueCredential(groupMembers: [UuidCiphertext], requestingMember: UuidCiphertext, expiration: Date = GroupSendCredentialResponse.defaultExpiration(), params: ServerSecretParams, randomness: Randomness) -> GroupSendCredentialResponse {
let concatenated = groupMembers.flatMap { $0.serialize() }
@ -50,6 +73,19 @@ public class GroupSendCredentialResponse: ByteArray {
}
}
/**
* Receives, validates, and extracts the credential from a response.
*
* Note that the `receive` operation is provided for both ``ServiceId``s and ``UuidCiphertext``s.
* If you already have the ciphertexts for the group members available,
* ``receive(groupMembers:localUser:now:serverParams:groupParams:)-5ipwi`` will be *significantly*
* faster; if you don't, this method is faster than generating the ciphertexts and throwing them
* away afterwards.
*
* `localUser` should be included in `groupMembers`.
*
* - Throws: ``SignalError/verificationFailed(_:)`` if the credential is not valid for any reason
*/
public func receive(groupMembers: [ServiceId], localUser: Aci, now: Date = Date(), serverParams: ServerPublicParams, groupParams: GroupSecretParams) throws -> GroupSendCredential {
return try withUnsafeBorrowedBuffer { response in
try ServiceId.concatenatedFixedWidthBinary(groupMembers).withUnsafeBorrowedBuffer { groupMembers in
@ -65,4 +101,34 @@ public class GroupSendCredentialResponse: ByteArray {
}
}
}
/**
* Receives, validates, and extracts the credential from a response.
*
* Note that the `receive` operation is provided for both ``ServiceId``s and ``UuidCiphertext``s.
* If you already have the ciphertexts for the group members available, this method will be
* *significantly* faster; if you don't,
* ``receive(groupMembers:localUser:now:serverParams:groupParams:)-4eco5`` is faster than
* generating the ciphertexts and
* throwing them away afterwards.
*
* `localUser` should be included in `groupMembers`.
*
* - Throws: ``SignalError/verificationFailed(_:)`` if the credential is not valid for any reason
*/
public func receive(groupMembers: [UuidCiphertext], localUser: UuidCiphertext, now: Date = Date(), serverParams: ServerPublicParams, groupParams: GroupSecretParams) throws -> GroupSendCredential {
return try withUnsafeBorrowedBuffer { response in
try groupMembers.flatMap { $0.serialize() }.withUnsafeBorrowedBuffer { groupMembers in
try localUser.withUnsafePointerToSerialized { localUser in
try serverParams.withUnsafePointerToSerialized { serverParams in
try groupParams.withUnsafePointerToSerialized { groupParams in
try invokeFnReturningVariableLengthSerialized {
signal_group_send_credential_response_receive_with_ciphertexts($0, response, groupMembers, localUser, UInt64(now.timeIntervalSince1970), serverParams, groupParams)
}
}
}
}
}
}
}
}

View File

@ -1190,6 +1190,8 @@ SignalFfiError *signal_group_send_credential_response_check_valid_contents(Signa
SignalFfiError *signal_group_send_credential_response_receive(SignalOwnedBuffer *out, SignalBorrowedBuffer response_bytes, SignalBorrowedBuffer group_members, const SignalServiceIdFixedWidthBinaryBytes *local_aci, uint64_t now, const unsigned char (*server_params)[SignalSERVER_PUBLIC_PARAMS_LEN], const unsigned char (*group_params)[SignalGROUP_SECRET_PARAMS_LEN]);
SignalFfiError *signal_group_send_credential_response_receive_with_ciphertexts(SignalOwnedBuffer *out, SignalBorrowedBuffer response_bytes, SignalBorrowedBuffer concatenated_group_member_ciphertexts, const unsigned char (*requester)[SignalUUID_CIPHERTEXT_LEN], uint64_t now, const unsigned char (*server_params)[SignalSERVER_PUBLIC_PARAMS_LEN], const unsigned char (*group_params)[SignalGROUP_SECRET_PARAMS_LEN]);
SignalFfiError *signal_group_send_credential_check_valid_contents(SignalBorrowedBuffer params_bytes);
SignalFfiError *signal_group_send_credential_present_deterministic(SignalOwnedBuffer *out, SignalBorrowedBuffer credential_bytes, const unsigned char (*server_params)[SignalSERVER_PUBLIC_PARAMS_LEN], const uint8_t (*randomness)[SignalRANDOMNESS_LEN]);

View File

@ -545,6 +545,12 @@ class ZKGroupTests: TestCaseBase {
XCTAssertThrowsError(try response.receive(groupMembers: [bobAci, eveAci, malloryAci], localUser: aliceAci, serverParams: serverPublicParams, groupParams: groupSecretParams))
XCTAssertThrowsError(try response.receive(groupMembers: [aliceAci, eveAci, malloryAci], localUser: aliceAci, serverParams: serverPublicParams, groupParams: groupSecretParams))
// Try again with the alternate receive.
_ = try! response.receive(groupMembers: groupCiphertexts, localUser: aliceCiphertext, serverParams: serverPublicParams, groupParams: groupSecretParams)
XCTAssertThrowsError(try response.receive(groupMembers: groupCiphertexts, localUser: groupCiphertexts[1], serverParams: serverPublicParams, groupParams: groupSecretParams))
XCTAssertThrowsError(try response.receive(groupMembers: Array(groupCiphertexts.dropFirst()), localUser: aliceCiphertext, serverParams: serverPublicParams, groupParams: groupSecretParams))
XCTAssertThrowsError(try response.receive(groupMembers: Array(groupCiphertexts.dropLast()), localUser: aliceCiphertext, serverParams: serverPublicParams, groupParams: groupSecretParams))
let presentation = credential.present(serverParams: serverPublicParams, randomness: TEST_ARRAY_32_3)
// Server