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:
parent
c38fcf6ccc
commit
1f8701213b
@ -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));
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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];
|
||||
|
@ -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 {
|
||||
|
@ -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
1
node/Native.d.ts
vendored
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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],
|
||||
|
@ -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| {
|
||||
|
@ -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(¶ms.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>,
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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]);
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user