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

Add GroupSendCredential

This credential is issued by the group server and presented to the
chat server to prove that the holder is a member of *some* group with
a known list of people. This can be used to replace the access key
requirement for multi-recipient sealed sender sends.
This commit is contained in:
Jordan Rose 2023-12-11 13:45:12 -08:00 committed by GitHub
parent bc18bb0ecf
commit 0d09a8352c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1521 additions and 6 deletions

1
Cargo.lock generated
View File

@ -3811,6 +3811,7 @@ dependencies = [
"bincode",
"criterion",
"curve25519-dalek",
"derive-where",
"displaydoc",
"hex",
"hex-literal",

View File

@ -0,0 +1,181 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.zkgroup.integrationtests;
import static org.junit.Assert.assertThrows;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.Test;
import org.signal.libsignal.protocol.ServiceId;
import org.signal.libsignal.protocol.util.Hex;
import org.signal.libsignal.zkgroup.SecureRandomTest;
import org.signal.libsignal.zkgroup.ServerPublicParams;
import org.signal.libsignal.zkgroup.ServerSecretParams;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.groups.ClientZkGroupCipher;
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
import org.signal.libsignal.zkgroup.groups.UuidCiphertext;
import org.signal.libsignal.zkgroup.groupsend.GroupSendCredential;
import org.signal.libsignal.zkgroup.groupsend.GroupSendCredentialPresentation;
import org.signal.libsignal.zkgroup.groupsend.GroupSendCredentialResponse;
public final class GroupSendCredentialTest extends SecureRandomTest {
private static final byte[] TEST_ARRAY_32 =
Hex.fromStringCondensedAssert(
"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f");
private static final byte[] TEST_ARRAY_32_1 =
Hex.fromStringCondensedAssert(
"6465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f80818283");
private static final byte[] TEST_ARRAY_32_2 =
Hex.fromStringCondensedAssert(
"c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7");
@Test
public void testGroupSendIntegration() throws Exception {
ServiceId.Aci aliceServiceId =
ServiceId.Aci.parseFromString("38381c3b-2606-4ca7-9310-7cb927f2ab4a");
ServiceId.Aci bobServiceId =
ServiceId.Aci.parseFromString("e80f7bbe-5b94-471e-bd8c-2173654ea3d1");
ServiceId.Aci eveServiceId =
ServiceId.Aci.parseFromString("3f0f4734-e331-4434-bd4f-6d8f6ea6dcc7");
ServiceId.Aci malloryServiceId =
ServiceId.Aci.parseFromString("5d088142-6fd7-4dbd-af00-fdda1b3ce988");
// SERVER
// Generate keys
ServerSecretParams serverSecretParams =
ServerSecretParams.generate(createSecureRandom(TEST_ARRAY_32));
ServerPublicParams serverPublicParams = serverSecretParams.getPublicParams();
// CLIENT
// Generate keys
GroupMasterKey masterKey = new GroupMasterKey(TEST_ARRAY_32_1);
GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(masterKey);
// Set up group state
UuidCiphertext aliceCiphertext =
new ClientZkGroupCipher(groupSecretParams).encrypt(aliceServiceId);
List<UuidCiphertext> groupCiphertexts =
Stream.of(aliceServiceId, bobServiceId, eveServiceId, malloryServiceId)
.map((next) -> new ClientZkGroupCipher(groupSecretParams).encrypt(next))
.collect(Collectors.toList());
// SERVER
// Issue credential
GroupSendCredentialResponse response =
GroupSendCredentialResponse.issueCredential(
groupCiphertexts, aliceCiphertext, serverSecretParams);
// CLIENT
// Gets stored credential
GroupSendCredential credential =
response.receive(
Arrays.asList(aliceServiceId, bobServiceId, eveServiceId, malloryServiceId),
aliceServiceId,
serverPublicParams,
groupSecretParams);
assertThrows(
VerificationFailedException.class,
() ->
response.receive(
Arrays.asList(aliceServiceId, bobServiceId, eveServiceId, malloryServiceId),
bobServiceId,
serverPublicParams,
groupSecretParams));
assertThrows(
VerificationFailedException.class,
() ->
response.receive(
Arrays.asList(bobServiceId, eveServiceId, malloryServiceId),
aliceServiceId,
serverPublicParams,
groupSecretParams));
assertThrows(
VerificationFailedException.class,
() ->
response.receive(
Arrays.asList(aliceServiceId, eveServiceId, malloryServiceId),
aliceServiceId,
serverPublicParams,
groupSecretParams));
GroupSendCredentialPresentation presentation =
credential.present(serverPublicParams, createSecureRandom(TEST_ARRAY_32_2));
// SERVER
// Verify presentation
presentation.verify(
Arrays.asList(bobServiceId, eveServiceId, malloryServiceId), serverSecretParams);
presentation.verify(
Arrays.asList(bobServiceId, eveServiceId, malloryServiceId),
Instant.now().plus(1, ChronoUnit.HOURS),
serverSecretParams);
assertThrows(
VerificationFailedException.class,
() ->
presentation.verify(
Arrays.asList(aliceServiceId, bobServiceId, eveServiceId, malloryServiceId),
serverSecretParams));
assertThrows(
VerificationFailedException.class,
() ->
presentation.verify(Arrays.asList(eveServiceId, malloryServiceId), serverSecretParams));
// Credential should definitely be expired after two full days.
assertThrows(
VerificationFailedException.class,
() ->
presentation.verify(
Arrays.asList(bobServiceId, eveServiceId, malloryServiceId),
Instant.now()
.truncatedTo(ChronoUnit.DAYS)
.plus(2, ChronoUnit.DAYS)
.plus(1, ChronoUnit.SECONDS),
serverSecretParams));
}
@Test
public void testEmptyCredential() throws Exception {
ServiceId.Aci aliceServiceId =
ServiceId.Aci.parseFromString("38381c3b-2606-4ca7-9310-7cb927f2ab4a");
// SERVER
// Generate keys
ServerSecretParams serverSecretParams =
ServerSecretParams.generate(createSecureRandom(TEST_ARRAY_32));
ServerPublicParams serverPublicParams = serverSecretParams.getPublicParams();
// CLIENT
// Generate keys
GroupMasterKey masterKey = new GroupMasterKey(TEST_ARRAY_32_1);
GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(masterKey);
// Set up group state
UuidCiphertext aliceCiphertext =
new ClientZkGroupCipher(groupSecretParams).encrypt(aliceServiceId);
// SERVER
// Issue credential
GroupSendCredentialResponse response =
GroupSendCredentialResponse.issueCredential(
Arrays.asList(aliceCiphertext), aliceCiphertext, serverSecretParams);
// CLIENT
// Gets stored credential
response.receive(
Arrays.asList(aliceServiceId), aliceServiceId, serverPublicParams, groupSecretParams);
}
}

View File

@ -253,6 +253,17 @@ public final class Native {
public static native byte[] GroupSecretParams_GetMasterKey(byte[] params);
public static native byte[] GroupSecretParams_GetPublicParams(byte[] params);
public static native void GroupSendCredentialPresentation_CheckValidContents(byte[] presentationBytes);
public static native void GroupSendCredentialPresentation_Verify(byte[] presentationBytes, byte[] groupMembers, long now, byte[] serverParams);
public static native void GroupSendCredentialResponse_CheckValidContents(byte[] responseBytes);
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 void GroupSendCredential_CheckValidContents(byte[] paramsBytes);
public static native byte[] GroupSendCredential_PresentDeterministic(byte[] credentialBytes, byte[] serverParams, byte[] randomness);
public static native long GroupSessionBuilder_CreateSenderKeyDistributionMessage(long sender, UUID distributionId, SenderKeyStore store);
public static native void GroupSessionBuilder_ProcessSenderKeyDistributionMessage(long sender, long senderKeyDistributionMessage, SenderKeyStore store);

View File

@ -0,0 +1,42 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.zkgroup.groupsend;
import static org.signal.libsignal.zkgroup.internal.Constants.RANDOM_LENGTH;
import java.security.SecureRandom;
import org.signal.libsignal.internal.Native;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.ServerPublicParams;
import org.signal.libsignal.zkgroup.internal.ByteArray;
public final class GroupSendCredential extends ByteArray {
public GroupSendCredential(byte[] contents) throws InvalidInputException {
super(contents);
Native.GroupSendCredential_CheckValidContents(contents);
}
public GroupSendCredentialPresentation present(ServerPublicParams serverParams) {
return present(serverParams, new SecureRandom());
}
public GroupSendCredentialPresentation present(
ServerPublicParams serverParams, SecureRandom secureRandom) {
byte[] random = new byte[RANDOM_LENGTH];
secureRandom.nextBytes(random);
byte[] newContents =
Native.GroupSendCredential_PresentDeterministic(
getInternalContentsForJNI(), serverParams.getInternalContentsForJNI(), random);
try {
return new GroupSendCredentialPresentation(newContents);
} catch (InvalidInputException e) {
throw new AssertionError(e);
}
}
}

View File

@ -0,0 +1,38 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.zkgroup.groupsend;
import java.time.Instant;
import java.util.List;
import org.signal.libsignal.internal.Native;
import org.signal.libsignal.protocol.ServiceId;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.ServerSecretParams;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.internal.ByteArray;
public final class GroupSendCredentialPresentation extends ByteArray {
public GroupSendCredentialPresentation(byte[] contents) throws InvalidInputException {
super(contents);
Native.GroupSendCredentialPresentation_CheckValidContents(contents);
}
public void verify(List<ServiceId> groupMembers, ServerSecretParams serverParams)
throws VerificationFailedException {
verify(groupMembers, Instant.now(), serverParams);
}
public void verify(
List<ServiceId> groupMembers, Instant currentTime, ServerSecretParams serverParams)
throws VerificationFailedException {
Native.GroupSendCredentialPresentation_Verify(
getInternalContentsForJNI(),
ServiceId.toConcatenatedFixedWidthBinary(groupMembers),
currentTime.getEpochSecond(),
serverParams.getInternalContentsForJNI());
}
}

View File

@ -0,0 +1,108 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
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;
import org.signal.libsignal.internal.Native;
import org.signal.libsignal.protocol.ServiceId;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.ServerPublicParams;
import org.signal.libsignal.zkgroup.ServerSecretParams;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
import org.signal.libsignal.zkgroup.groups.UuidCiphertext;
import org.signal.libsignal.zkgroup.internal.ByteArray;
public final class GroupSendCredentialResponse extends ByteArray {
public GroupSendCredentialResponse(byte[] contents) throws InvalidInputException {
super(contents);
Native.GroupSendCredentialResponse_CheckValidContents(contents);
}
private static Instant defaultExpiration() {
long expirationEpochSecond =
Native.GroupSendCredentialResponse_DefaultExpirationBasedOnCurrentTime();
return Instant.ofEpochSecond(expirationEpochSecond);
}
public static GroupSendCredentialResponse issueCredential(
List<UuidCiphertext> groupMembers, UuidCiphertext requestingUser, ServerSecretParams params) {
return issueCredential(
groupMembers, requestingUser, defaultExpiration(), params, new SecureRandom());
}
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(),
requestingUser.getInternalContentsForJNI(),
expiration.getEpochSecond(),
params.getInternalContentsForJNI(),
random);
try {
return new GroupSendCredentialResponse(newContents);
} catch (InvalidInputException e) {
throw new AssertionError(e);
}
}
public GroupSendCredential receive(
List<ServiceId> groupMembers,
ServiceId.Aci localUser,
ServerPublicParams serverParams,
GroupSecretParams groupParams)
throws VerificationFailedException {
return receive(groupMembers, localUser, Instant.now(), serverParams, groupParams);
}
public GroupSendCredential receive(
List<ServiceId> groupMembers,
ServiceId.Aci localUser,
Instant now,
ServerPublicParams serverParams,
GroupSecretParams groupParams)
throws VerificationFailedException {
byte[] newContents =
Native.GroupSendCredentialResponse_Receive(
getInternalContentsForJNI(),
ServiceId.toConcatenatedFixedWidthBinary(groupMembers),
localUser.toServiceIdFixedWidthBinary(),
now.getEpochSecond(),
serverParams.getInternalContentsForJNI(),
groupParams.getInternalContentsForJNI());
try {
return new GroupSendCredential(newContents);
} catch (InvalidInputException e) {
throw new AssertionError(e);
}
}
}

8
node/Native.d.ts vendored
View File

@ -159,6 +159,14 @@ export function GroupSecretParams_EncryptServiceId(params: Serialized<GroupSecre
export function GroupSecretParams_GenerateDeterministic(randomness: Buffer): Serialized<GroupSecretParams>;
export function GroupSecretParams_GetMasterKey(params: Serialized<GroupSecretParams>): Serialized<GroupMasterKey>;
export function GroupSecretParams_GetPublicParams(params: Serialized<GroupSecretParams>): Serialized<GroupPublicParams>;
export function GroupSendCredentialPresentation_CheckValidContents(presentationBytes: Buffer): void;
export function GroupSendCredentialPresentation_Verify(presentationBytes: Buffer, groupMembers: Buffer, now: Timestamp, serverParams: Serialized<ServerSecretParams>): void;
export function GroupSendCredentialResponse_CheckValidContents(responseBytes: Buffer): void;
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 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;
export function HsmEnclaveClient_CompleteHandshake(cli: Wrapper<HsmEnclaveClient>, handshakeReceived: Buffer): void;
export function HsmEnclaveClient_EstablishedRecv(cli: Wrapper<HsmEnclaveClient>, receivedCiphertext: Buffer): Buffer;

View File

@ -23,6 +23,7 @@ import {
CallLinkSecretParams,
CallLinkAuthCredentialResponse,
BackupAuthCredentialRequestContext,
GroupSendCredentialResponse,
} from '../zkgroup/';
import { Aci, Pni } from '../Address';
import { Uuid } from '..';
@ -794,4 +795,143 @@ describe('ZKGroup', () => {
);
});
});
describe('GroupSendCredential', () => {
it('works in normal usage', () => {
const serverSecretParams =
ServerSecretParams.generateWithRandom(TEST_ARRAY_32);
const serverPublicParams = serverSecretParams.getPublicParams();
const aliceAci = Aci.parseFromServiceIdString(
'9d0652a3-dcc3-4d11-975f-74d61598733f'
);
const bobAci = Aci.parseFromServiceIdString(
'6838237d-02f6-4098-b110-698253d15961'
);
const eveAci = Aci.parseFromServiceIdString(
'3f0f4734-e331-4434-bd4f-6d8f6ea6dcc7'
);
const malloryAci = Aci.parseFromServiceIdString(
'5d088142-6fd7-4dbd-af00-fdda1b3ce988'
);
const masterKey = new GroupMasterKey(TEST_ARRAY_32_1);
const groupSecretParams =
GroupSecretParams.deriveFromMasterKey(masterKey);
const aliceCiphertext = new ClientZkGroupCipher(
groupSecretParams
).encryptServiceId(aliceAci);
const groupCiphertexts = [aliceAci, bobAci, eveAci, malloryAci].map(
(next) =>
new ClientZkGroupCipher(groupSecretParams).encryptServiceId(next)
);
// Server
const response = GroupSendCredentialResponse.issueCredential(
groupCiphertexts,
aliceCiphertext,
serverSecretParams
);
// Client
const credential = response.receive(
[aliceAci, bobAci, eveAci, malloryAci],
aliceAci,
serverPublicParams,
groupSecretParams
);
assert.throws(() =>
response.receive(
[aliceAci, bobAci, eveAci, malloryAci],
bobAci,
serverPublicParams,
groupSecretParams
)
);
assert.throws(() =>
response.receive(
[bobAci, eveAci, malloryAci],
aliceAci,
serverPublicParams,
groupSecretParams
)
);
assert.throws(() =>
response.receive(
[aliceAci, eveAci, malloryAci],
aliceAci,
serverPublicParams,
groupSecretParams
)
);
const presentation = credential.presentWithRandom(
serverPublicParams,
TEST_ARRAY_32_2
);
// Server
presentation.verify([bobAci, eveAci, malloryAci], serverSecretParams);
presentation.verify(
[bobAci, eveAci, malloryAci],
serverSecretParams,
new Date(Date.now() + 60 * 60 * 1000)
);
assert.throws(() =>
presentation.verify(
[aliceAci, bobAci, eveAci, malloryAci],
serverSecretParams
)
);
assert.throws(() =>
presentation.verify([eveAci, malloryAci], serverSecretParams)
);
// credential should definitely be expired after 2 days
const now = Math.floor(Date.now() / 1000);
const startOfDay = now - (now % SECONDS_PER_DAY);
assert.throws(() =>
presentation.verify(
[bobAci, eveAci, malloryAci],
serverSecretParams,
new Date(1000 * (startOfDay + 2 * SECONDS_PER_DAY + 1))
)
);
});
it('works with empty credentials', () => {
const serverSecretParams =
ServerSecretParams.generateWithRandom(TEST_ARRAY_32);
const serverPublicParams = serverSecretParams.getPublicParams();
const aliceAci = Aci.parseFromServiceIdString(
'9d0652a3-dcc3-4d11-975f-74d61598733f'
);
const masterKey = new GroupMasterKey(TEST_ARRAY_32_1);
const groupSecretParams =
GroupSecretParams.deriveFromMasterKey(masterKey);
const aliceCiphertext = new ClientZkGroupCipher(
groupSecretParams
).encryptServiceId(aliceAci);
// Server
const response = GroupSendCredentialResponse.issueCredential(
[aliceCiphertext],
aliceCiphertext,
serverSecretParams
);
// Client
const _credential = response.receive(
[aliceAci],
aliceAci,
serverPublicParams,
groupSecretParams
);
});
});
});

View File

@ -0,0 +1,39 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
import { randomBytes } from 'crypto';
import ByteArray from '../internal/ByteArray';
import { RANDOM_LENGTH } from '../internal/Constants';
import * as Native from '../../../Native';
import GroupSendCredentialPresentation from './GroupSendCredentialPresentation';
import ServerPublicParams from '../ServerPublicParams';
export default class GroupSendCredential extends ByteArray {
private readonly __type?: never;
constructor(contents: Buffer) {
super(contents, Native.GroupSendCredential_CheckValidContents);
}
present(serverParams: ServerPublicParams): GroupSendCredentialPresentation {
const random = randomBytes(RANDOM_LENGTH);
return this.presentWithRandom(serverParams, random);
}
presentWithRandom(
serverParams: ServerPublicParams,
random: Buffer
): GroupSendCredentialPresentation {
return new GroupSendCredentialPresentation(
Native.GroupSendCredential_PresentDeterministic(
this.contents,
serverParams.contents,
random
)
);
}
}

View File

@ -0,0 +1,31 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
import ByteArray from '../internal/ByteArray';
import * as Native from '../../../Native';
import ServerSecretParams from '../ServerSecretParams';
import { ServiceId } from '../../Address';
export default class GroupSendCredentialPresentation extends ByteArray {
private readonly __type?: never;
constructor(contents: Buffer) {
super(contents, Native.GroupSendCredentialPresentation_CheckValidContents);
}
verify(
groupMembers: ServiceId[],
serverParams: ServerSecretParams,
now: Date = new Date()
): void {
Native.GroupSendCredentialPresentation_Verify(
this.contents,
ServiceId.toConcatenatedFixedWidthBinary(groupMembers),
Math.floor(now.getTime() / 1000),
serverParams.contents
);
}
}

View File

@ -0,0 +1,94 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
import { randomBytes } from 'crypto';
import ByteArray from '../internal/ByteArray';
import * as Native from '../../../Native';
import { RANDOM_LENGTH } from '../internal/Constants';
import GroupSendCredential from './GroupSendCredential';
import GroupSecretParams from '../groups/GroupSecretParams';
import ServerSecretParams from '../ServerSecretParams';
import ServerPublicParams from '../ServerPublicParams';
import UuidCiphertext from '../groups/UuidCiphertext';
import { Aci, ServiceId } from '../../Address';
export default class GroupSendCredentialResponse extends ByteArray {
private readonly __type?: never;
constructor(contents: Buffer) {
super(contents, Native.GroupSendCredentialResponse_CheckValidContents);
}
private static defaultExpiration(): Date {
const expirationInSeconds =
Native.GroupSendCredentialResponse_DefaultExpirationBasedOnCurrentTime();
return new Date(expirationInSeconds * 1000);
}
static issueCredential(
groupMembers: UuidCiphertext[],
requestingMember: UuidCiphertext,
params: ServerSecretParams
): GroupSendCredentialResponse {
const random = randomBytes(RANDOM_LENGTH);
return this.issueCredentialWithExpirationAndRandom(
groupMembers,
requestingMember,
this.defaultExpiration(),
params,
random
);
}
static issueCredentialWithExpirationAndRandom(
groupMembers: UuidCiphertext[],
requestingMember: UuidCiphertext,
expiration: Date,
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,
requestingMember.contents,
Math.floor(expiration.getTime() / 1000),
params.contents,
random
)
);
}
receive(
groupMembers: ServiceId[],
localUser: Aci,
serverParams: ServerPublicParams,
groupParams: GroupSecretParams,
now: Date = new Date()
): GroupSendCredential {
return new GroupSendCredential(
Native.GroupSendCredentialResponse_Receive(
this.contents,
ServiceId.toConcatenatedFixedWidthBinary(groupMembers),
localUser.getServiceIdFixedWidthBinary(),
Math.floor(now.getTime() / 1000),
serverParams.contents,
groupParams.contents
)
);
}
}

View File

@ -76,3 +76,9 @@ export { default as BackupAuthCredentialPresentation } from './backups/BackupAut
export { default as BackupAuthCredentialRequest } from './backups/BackupAuthCredentialRequest';
export { default as BackupAuthCredentialRequestContext } from './backups/BackupAuthCredentialRequestContext';
export { default as BackupAuthCredentialResponse } from './backups/BackupAuthCredentialResponse';
// Group Send
export { default as GroupSendCredential } from './groupsend/GroupSendCredential';
export { default as GroupSendCredentialPresentation } from './groupsend/GroupSendCredentialPresentation';
export { default as GroupSendCredentialResponse } from './groupsend/GroupSendCredentialResponse';

View File

@ -546,6 +546,23 @@ impl<'a> AsyncArgTypeInfo<'a> for &'a [u8] {
}
}
/// See [`AssumedImmutableBuffer`].
impl<'storage, 'context: 'storage> ArgTypeInfo<'storage, 'context>
for crate::protocol::ServiceIdSequence<'storage>
{
type ArgType = JsBuffer;
type StoredType = AssumedImmutableBuffer<'context>;
fn borrow(
cx: &mut FunctionContext,
foreign: Handle<'context, Self::ArgType>,
) -> NeonResult<Self::StoredType> {
Ok(AssumedImmutableBuffer::new(cx, foreign))
}
fn load_from(stored: &'storage mut Self::StoredType) -> Self {
Self::parse(&*stored)
}
}
/// See [`PersistentAssumedImmutableBuffer`].
impl<'a> AsyncArgTypeInfo<'a> for crate::protocol::ServiceIdSequence<'a> {
type ArgType = JsBuffer;

View File

@ -3,6 +3,8 @@
// SPDX-License-Identifier: AGPL-3.0-only
//
use std::time::SystemTime;
use ::zkgroup;
use libsignal_bridge_macros::*;
use libsignal_protocol::{Aci, Pni, ServiceId};
@ -23,6 +25,7 @@ use zkgroup::backups::{
BackupAuthCredentialRequestContext, BackupAuthCredentialResponse,
};
use crate::protocol::ServiceIdSequence;
use crate::support::*;
use crate::*;
@ -1120,3 +1123,108 @@ fn BackupAuthCredentialPresentation_GetReceiptLevel(presentation_bytes: &[u8]) -
.expect("should have been parsed previously");
presentation.receipt_level()
}
#[bridge_fn]
fn GroupSendCredentialResponse_DefaultExpirationBasedOnCurrentTime() -> Timestamp {
GroupSendCredentialResponse::default_expiration(
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
)
.into()
}
#[bridge_fn]
fn GroupSendCredentialResponse_IssueDeterministic(
concatenated_group_member_ciphertexts: &[u8],
requester: Serialized<UuidCiphertext>,
expiration: Timestamp,
server_params: Serialized<ServerSecretParams>,
randomness: &[u8; RANDOMNESS_LEN],
) -> Result<Vec<u8>, ZkGroupVerificationFailure> {
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 response = GroupSendCredentialResponse::issue_credential(
user_id_ciphertexts,
&requester,
expiration.as_seconds(),
&server_params,
*randomness,
)?;
Ok(zkgroup::serialize(&response))
}
#[bridge_fn_void]
fn GroupSendCredentialResponse_CheckValidContents(
response_bytes: &[u8],
) -> Result<(), ZkGroupDeserializationFailure> {
validate_serialization::<GroupSendCredentialResponse>(response_bytes)
}
#[bridge_fn]
fn GroupSendCredentialResponse_Receive(
response_bytes: &[u8],
group_members: ServiceIdSequence<'_>,
local_aci: Aci,
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");
let credential = response.receive(
&server_params,
&group_params,
group_members,
local_aci.into(),
now.as_seconds(),
)?;
Ok(zkgroup::serialize(&credential))
}
#[bridge_fn_void]
fn GroupSendCredential_CheckValidContents(
params_bytes: &[u8],
) -> Result<(), ZkGroupDeserializationFailure> {
validate_serialization::<GroupSendCredential>(params_bytes)
}
#[bridge_fn]
fn GroupSendCredential_PresentDeterministic(
credential_bytes: &[u8],
server_params: Serialized<ServerPublicParams>,
randomness: &[u8; RANDOMNESS_LEN],
) -> Result<Vec<u8>, ZkGroupVerificationFailure> {
let credential = zkgroup::deserialize::<GroupSendCredential>(credential_bytes)
.expect("should have been parsed previously");
let presentation = credential.present(&server_params, *randomness);
Ok(zkgroup::serialize(&presentation))
}
#[bridge_fn_void]
fn GroupSendCredentialPresentation_CheckValidContents(
presentation_bytes: &[u8],
) -> Result<(), ZkGroupDeserializationFailure> {
validate_serialization::<GroupSendCredentialPresentation>(presentation_bytes)
}
#[bridge_fn_void]
fn GroupSendCredentialPresentation_Verify(
presentation_bytes: &[u8],
group_members: ServiceIdSequence<'_>,
now: Timestamp,
server_params: Serialized<ServerSecretParams>,
) -> Result<(), ZkGroupVerificationFailure> {
let presentation = zkgroup::deserialize::<GroupSendCredentialPresentation>(presentation_bytes)
.expect("should have been parsed previously");
presentation.verify(group_members, now.as_seconds(), &server_params)
}

View File

@ -285,6 +285,16 @@ impl<D: Domain> KeyPair<D> {
/// Encrypts `attr` according to Chase-Perrin-Zaverucha section 4.1.
#[inline]
pub fn encrypt(&self, attr: &D::Attribute) -> Ciphertext<D> {
self.encrypt_arbitrary_attribute(attr)
}
/// Encrypts `attr` according to Chase-Perrin-Zaverucha section 4.1, even if the attribute is
/// not normally associated with this key.
///
/// Allows controlling the domain of the resulting ciphertext, to not get confused with the
/// usual ciphertexts produced by [`Self::encrypt`].
#[inline]
pub fn encrypt_arbitrary_attribute<D2>(&self, attr: &dyn Attribute) -> Ciphertext<D2> {
let [M1, M2] = attr.as_points();
let E_A1 = self.a1 * M1;
let E_A2 = (self.a2 * E_A1) + M2;

View File

@ -19,6 +19,7 @@ zkcredential = { path = "../zkcredential" }
aes-gcm-siv = "0.11.1"
bincode = "1.2.1"
derive-where = "1.2.5"
displaydoc = "0.2"
hex = "0.4.0"
hex-literal = "0.4.1"

View File

@ -4,9 +4,13 @@
//
pub mod group_params;
mod group_send_credential;
pub mod profile_key_ciphertext;
pub mod uuid_ciphertext;
pub use group_params::{GroupMasterKey, GroupPublicParams, GroupSecretParams};
pub use group_send_credential::{
GroupSendCredential, GroupSendCredentialPresentation, GroupSendCredentialResponse,
};
pub use profile_key_ciphertext::ProfileKeyCiphertext;
pub use uuid_ciphertext::UuidCiphertext;

View File

@ -0,0 +1,296 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
//! Provides GroupSendCredential and related types.
//!
//! GroupSendCredential is a MAC over:
//! - a set of ACIs (computed from the ciphertexts on the group server at issuance, passed decrypted to the chat server for verification)
//! - a timestamp, truncated to day granularity (chosen by the group server at issuance, passed publicly to the chat server for verification)
use std::marker::PhantomData;
use derive_where::derive_where;
use partial_default::PartialDefault;
use serde::{Deserialize, Serialize};
use zkcredential::attributes::{Attribute, Domain};
use crate::common::simple_types::*;
use crate::crypto::uid_encryption;
use crate::crypto::uid_struct::UidStruct;
use crate::groups::{GroupSecretParams, UuidCiphertext};
use crate::{ServerPublicParams, ServerSecretParams, ZkGroupVerificationFailure, SECONDS_PER_DAY};
const CREDENTIAL_LABEL: &[u8] = b"20231011_Signal_GroupSendCredential";
const SECONDS_PER_HOUR: u64 = 60 * 60;
#[derive(PartialEq, Eq, Serialize, Deserialize)]
#[derive_where(Default)]
struct UserIdSet<T> {
points: [curve25519_dalek::RistrettoPoint; 2],
kind: PhantomData<T>,
}
impl<T: Attribute + Eq> UserIdSet<T> {
fn from_user_ids_omitting_requester(
user_ids: impl IntoIterator<Item = T>,
requester: &T,
) -> Result<Self, ZkGroupVerificationFailure> {
let mut user_id_set = UserIdSet::default();
let mut has_seen_requester = false;
for ciphertext in user_ids {
if &ciphertext == requester {
if has_seen_requester {
// Requester is present multiple times?
return Err(ZkGroupVerificationFailure);
}
has_seen_requester = true;
continue;
}
let points = ciphertext.as_points();
user_id_set.points[0] += points[0];
user_id_set.points[1] += points[1];
}
if !has_seen_requester {
// Requester is not in group.
return Err(ZkGroupVerificationFailure);
}
Ok(user_id_set)
}
fn from_user_ids(user_ids: impl IntoIterator<Item = T>) -> Self {
user_ids
.into_iter()
.fold(Default::default(), |mut acc, next| {
let points = next.as_points();
acc.points[0] += points[0];
acc.points[1] += points[1];
acc
})
}
}
impl<T> zkcredential::attributes::Attribute for UserIdSet<T> {
fn as_points(&self) -> [curve25519_dalek::RistrettoPoint; 2] {
self.points
}
}
#[derive(Serialize, Deserialize, PartialDefault)]
pub struct GroupSendCredentialResponse {
reserved: ReservedBytes,
proof: zkcredential::issuance::IssuanceProof,
user_id_set: UserIdSet<uid_encryption::Ciphertext>,
expiration: Timestamp,
}
impl GroupSendCredentialResponse {
pub fn default_expiration(current_time_in_seconds: Timestamp) -> Timestamp {
// Return the end of the current day, unless that's less than two 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 + SECONDS_PER_DAY;
if (expiration - current_time_in_seconds) < 2 * SECONDS_PER_HOUR {
expiration += SECONDS_PER_DAY;
}
expiration
}
pub fn issue_credential(
user_id_ciphertexts: impl IntoIterator<Item = UuidCiphertext>,
requester: &UuidCiphertext,
expiration: Timestamp,
params: &ServerSecretParams,
randomness: RandomnessBytes,
) -> Result<GroupSendCredentialResponse, ZkGroupVerificationFailure> {
let user_id_set = UserIdSet::from_user_ids_omitting_requester(
user_id_ciphertexts.into_iter().map(|c| c.ciphertext),
&requester.ciphertext,
)?;
let proof = zkcredential::issuance::IssuanceProofBuilder::new(CREDENTIAL_LABEL)
.add_attribute(&user_id_set)
.add_public_attribute(&expiration)
.issue(&params.generic_credential_key_pair, randomness);
Ok(Self {
reserved: [0],
proof,
user_id_set,
expiration,
})
}
pub fn receive(
self,
params: &ServerPublicParams,
group_params: &GroupSecretParams,
user_ids: impl IntoIterator<Item = libsignal_protocol::ServiceId>,
requester: libsignal_protocol::ServiceId,
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 = UserIdSet::from_user_ids_omitting_requester(
user_ids.into_iter().map(UidStruct::from_service_id),
&UidStruct::from_service_id(requester),
)?;
let user_id_set_ciphertext = group_params
.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,
expiration: self.expiration,
encryption_key_pair,
})
}
}
#[derive(Serialize, Deserialize, PartialDefault)]
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>>,
expiration: Timestamp,
// Additionally includes this because we'd need to recompute it with every message otherwise.
encryption_key_pair: zkcredential::attributes::KeyPair<InverseUidEncryptionDomain>,
}
impl GroupSendCredential {
pub fn present(
&self,
server_params: &ServerPublicParams,
randomness: RandomnessBytes,
) -> GroupSendCredentialPresentation {
let proof = zkcredential::presentation::PresentationProofBuilder::new(CREDENTIAL_LABEL)
.add_attribute_without_verified_key(
&self.user_id_set_ciphertext,
&self.encryption_key_pair,
)
.present(
&server_params.generic_credential_public_key,
&self.credential,
randomness,
);
GroupSendCredentialPresentation {
reserved: [0],
proof,
expiration: self.expiration,
}
}
}
#[derive(Serialize, Deserialize, PartialDefault)]
pub struct GroupSendCredentialPresentation {
reserved: ReservedBytes,
proof: zkcredential::presentation::PresentationProof,
// Does not include the set of user IDs because that's in the message payload
expiration: Timestamp,
}
impl GroupSendCredentialPresentation {
pub fn verify(
&self,
user_ids: impl IntoIterator<Item = libsignal_protocol::ServiceId>,
current_time_in_seconds: Timestamp,
server_params: &ServerSecretParams,
) -> Result<(), ZkGroupVerificationFailure> {
if current_time_in_seconds > self.expiration {
return Err(ZkGroupVerificationFailure);
}
let user_id_set =
UserIdSet::from_user_ids(user_ids.into_iter().map(UidStruct::from_service_id));
zkcredential::presentation::PresentationProofVerifier::new(CREDENTIAL_LABEL)
.add_attribute_without_verified_key(&user_id_set, InverseUidEncryptionDomain::ID)
.add_public_attribute(&self.expiration)
.verify(&server_params.generic_credential_key_pair, &self.proof)
.map_err(|_| ZkGroupVerificationFailure)
}
}
struct InverseUidEncryptionDomain;
impl zkcredential::attributes::Domain for InverseUidEncryptionDomain {
type Attribute = UserIdSet<uid_encryption::Ciphertext>;
const ID: &'static str = "Signal_GroupSendCredential_InverseUidEncryptionDomain_20231011";
fn G_a() -> [curve25519_dalek::RistrettoPoint; 2] {
static STORAGE: std::sync::OnceLock<[curve25519_dalek::RistrettoPoint; 2]> =
std::sync::OnceLock::new();
*zkcredential::attributes::derive_default_generator_points::<Self>(&STORAGE)
}
}
#[cfg(test)]
mod tests {
use super::*;
const DAY_ALIGNED_TIMESTAMP: Timestamp = 1681344000; // 2023-04-13 00:00:00 UTC
#[test]
fn test_default_expiration() {
assert_eq!(
DAY_ALIGNED_TIMESTAMP + SECONDS_PER_DAY,
GroupSendCredentialResponse::default_expiration(DAY_ALIGNED_TIMESTAMP)
);
assert_eq!(
DAY_ALIGNED_TIMESTAMP + SECONDS_PER_DAY,
GroupSendCredentialResponse::default_expiration(DAY_ALIGNED_TIMESTAMP + 1)
);
assert_eq!(
DAY_ALIGNED_TIMESTAMP + SECONDS_PER_DAY,
GroupSendCredentialResponse::default_expiration(
DAY_ALIGNED_TIMESTAMP + SECONDS_PER_HOUR
)
);
assert_eq!(
DAY_ALIGNED_TIMESTAMP + SECONDS_PER_DAY,
GroupSendCredentialResponse::default_expiration(
DAY_ALIGNED_TIMESTAMP + 22 * SECONDS_PER_HOUR
)
);
assert_eq!(
DAY_ALIGNED_TIMESTAMP + 2 * SECONDS_PER_DAY,
GroupSendCredentialResponse::default_expiration(
DAY_ALIGNED_TIMESTAMP + 22 * SECONDS_PER_HOUR + 1
)
);
assert_eq!(
DAY_ALIGNED_TIMESTAMP + 2 * SECONDS_PER_DAY,
GroupSendCredentialResponse::default_expiration(
DAY_ALIGNED_TIMESTAMP + 23 * SECONDS_PER_HOUR
)
);
assert_eq!(
DAY_ALIGNED_TIMESTAMP + 2 * SECONDS_PER_DAY,
GroupSendCredentialResponse::default_expiration(
DAY_ALIGNED_TIMESTAMP + SECONDS_PER_DAY - 1
)
);
}
}

View File

@ -12,7 +12,7 @@ use crate::common::sho::*;
use crate::common::simple_types::*;
use crate::{api, crypto};
#[derive(Copy, Clone, Serialize, Deserialize, PartialDefault)]
#[derive(Clone, Serialize, Deserialize, PartialDefault)]
pub struct ServerSecretParams {
pub(crate) reserved: ReservedBytes,
pub(crate) auth_credentials_key_pair:
@ -33,9 +33,11 @@ pub struct ServerSecretParams {
crypto::credentials::KeyPair<crypto::credentials::ExpiringProfileKeyCredential>,
auth_credentials_with_pni_key_pair:
crypto::credentials::KeyPair<crypto::credentials::AuthCredentialWithPni>,
pub(crate) generic_credential_key_pair: zkcredential::credentials::CredentialKeyPair,
}
#[derive(Copy, Clone, Serialize, Deserialize, PartialDefault)]
#[derive(Clone, Serialize, Deserialize, PartialDefault)]
pub struct ServerPublicParams {
pub(crate) reserved: ReservedBytes,
pub(crate) auth_credentials_public_key: crypto::credentials::PublicKey,
@ -51,6 +53,8 @@ pub struct ServerPublicParams {
expiring_profile_key_credentials_public_key: crypto::credentials::PublicKey,
auth_credentials_with_pni_public_key: crypto::credentials::PublicKey,
pub(crate) generic_credential_public_key: zkcredential::credentials::CredentialPublicKey,
}
impl ServerSecretParams {
@ -68,6 +72,8 @@ impl ServerSecretParams {
let expiring_profile_key_credentials_key_pair =
crypto::credentials::KeyPair::generate(&mut sho);
let auth_credentials_with_pni_key_pair = crypto::credentials::KeyPair::generate(&mut sho);
let generic_credential_key_pair =
zkcredential::credentials::CredentialKeyPair::generate(randomness);
Self {
reserved: Default::default(),
@ -78,6 +84,7 @@ impl ServerSecretParams {
pni_credentials_key_pair,
expiring_profile_key_credentials_key_pair,
auth_credentials_with_pni_key_pair,
generic_credential_key_pair,
}
}
@ -97,6 +104,7 @@ impl ServerSecretParams {
auth_credentials_with_pni_public_key: self
.auth_credentials_with_pni_key_pair
.get_public_key(),
generic_credential_public_key: self.generic_credential_key_pair.public_key().clone(),
}
}

View File

@ -41,8 +41,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 = 2305;
pub const SERVER_PUBLIC_PARAMS_LEN: usize = 417;
pub const SERVER_SECRET_PARAMS_LEN: usize = 2689;
pub const SERVER_PUBLIC_PARAMS_LEN: usize = 641;
pub const UUID_CIPHERTEXT_LEN: usize = 65;
pub const RANDOMNESS_LEN: usize = 32;
pub const SIGNATURE_LEN: usize = 64;

View File

@ -0,0 +1,171 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use zkgroup::groups::GroupSendCredentialResponse;
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_credential() {
let randomness1: RandomnessBytes = [0x43u8; RANDOMNESS_LEN];
let randomness2: RandomnessBytes = [0x44u8; RANDOMNESS_LEN];
let randomness3: RandomnessBytes = [0x45u8; RANDOMNESS_LEN];
let randomness4: RandomnessBytes = [0x46u8; RANDOMNESS_LEN];
// first set up a group
let client_user_id = libsignal_protocol::Aci::from_uuid_bytes([0x04u8; UUID_LEN]);
let moxie_user_id =
libsignal_protocol::Aci::from(uuid::uuid!("e36fdce7-36da-4c6f-a21b-9afe2b754650"));
let brian_user_id =
libsignal_protocol::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();
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 credential_response = GroupSendCredentialResponse::issue_credential(
ciphertexts,
&client_user_id_ciphertext,
DAY_ALIGNED_TIMESTAMP,
&server_secret_params,
randomness3,
)
.expect("valid request");
// client generated materials; issuance response -> redemption request
let server_public_params = server_secret_params.get_public_params();
let credential = credential_response
.receive(
&server_public_params,
&group_secret_params,
group_members,
client_user_id.into(),
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(
group_members_without_requester,
DAY_ALIGNED_TIMESTAMP,
&server_secret_params,
)
.expect("credential should be valid for the timestamp given");
}
#[test]
fn test_empty_credential_can_be_issued_and_received() {
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_protocol::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 credential_response = GroupSendCredentialResponse::issue_credential(
[client_user_id_ciphertext],
&client_user_id_ciphertext,
DAY_ALIGNED_TIMESTAMP,
&server_secret_params,
randomness3,
)
.expect("valid request");
let server_public_params = server_secret_params.get_public_params();
let _credential = credential_response
.receive(
&server_public_params,
&group_secret_params,
[client_user_id.into()],
client_user_id.into(),
DAY_ALIGNED_TIMESTAMP,
)
.expect("issued credential 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_protocol::Aci::from_uuid_bytes([0x04u8; UUID_LEN]);
let moxie_user_id =
libsignal_protocol::Aci::from(uuid::uuid!("e36fdce7-36da-4c6f-a21b-9afe2b754650"));
let brian_user_id =
libsignal_protocol::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 client_user_id_ciphertext = group_secret_params.encrypt_service_id(client_user_id.into());
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 credential_response = GroupSendCredentialResponse::issue_credential(
ciphertexts.clone(),
&client_user_id_ciphertext,
expiration,
&server_secret_params,
randomness3,
)
.expect("valid request");
assert!(
credential_response
.receive(
&server_public_params,
&group_secret_params,
group_members,
client_user_id.into(),
now,
)
.is_err(),
"now: {now}, expiration: {expiration}"
);
};
expect_credential_rejected(DAY_ALIGNED_TIMESTAMP, DAY_ALIGNED_TIMESTAMP + 1);
expect_credential_rejected(
DAY_ALIGNED_TIMESTAMP,
DAY_ALIGNED_TIMESTAMP + 8 * SECONDS_PER_DAY,
);
expect_credential_rejected(
DAY_ALIGNED_TIMESTAMP,
DAY_ALIGNED_TIMESTAMP + 1000 * SECONDS_PER_DAY,
);
}

View File

@ -0,0 +1,34 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import SignalFfi
public class GroupSendCredential: ByteArray {
public required init(contents: [UInt8]) throws {
try super.init(contents, checkValid: signal_group_send_credential_check_valid_contents)
}
public func present(serverParams: ServerPublicParams) -> GroupSendCredentialPresentation {
return failOnError {
present(serverParams: serverParams, randomness: try .generate())
}
}
public func present(serverParams: ServerPublicParams, randomness: Randomness) -> GroupSendCredentialPresentation {
return failOnError {
try withUnsafeBorrowedBuffer { contents in
try serverParams.withUnsafePointerToSerialized { serverParams in
try randomness.withUnsafePointerToBytes { randomness in
try invokeFnReturningVariableLengthSerialized {
signal_group_send_credential_present_deterministic($0, contents, serverParams, randomness)
}
}
}
}
}
}
}

View File

@ -0,0 +1,24 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import SignalFfi
public class GroupSendCredentialPresentation: ByteArray {
public required init(contents: [UInt8]) throws {
try super.init(contents, checkValid: signal_group_send_credential_presentation_check_valid_contents)
}
public func verify(groupMembers: [ServiceId], now: Date = Date(), serverParams: ServerSecretParams) throws {
try withUnsafeBorrowedBuffer { contents in
try ServiceId.concatenatedFixedWidthBinary(groupMembers).withUnsafeBorrowedBuffer { groupMembers in
try serverParams.withUnsafePointerToSerialized { serverParams in
try checkError(signal_group_send_credential_presentation_verify(contents, groupMembers, UInt64(now.timeIntervalSince1970), serverParams))
}
}
}
}
}

View File

@ -0,0 +1,68 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import SignalFfi
public class GroupSendCredentialResponse: ByteArray {
public required init(contents: [UInt8]) throws {
try super.init(contents, checkValid: signal_group_send_credential_response_check_valid_contents)
}
public static func defaultExpiration() -> Date {
let expiration = failOnError {
try invokeFnReturningInteger {
signal_group_send_credential_response_default_expiration_based_on_current_time($0)
}
}
return Date(timeIntervalSince1970: TimeInterval(expiration))
}
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())
}
}
public static func issueCredential(groupMembers: [UuidCiphertext], requestingMember: UuidCiphertext, expiration: Date = GroupSendCredentialResponse.defaultExpiration(), params: ServerSecretParams, randomness: Randomness) -> GroupSendCredentialResponse {
let concatenated = groupMembers.flatMap { $0.serialize() }
return failOnError {
return try concatenated.withUnsafeBorrowedBuffer { concatenated in
try requestingMember.withUnsafePointerToSerialized { requestingMember in
try params.withUnsafePointerToSerialized { params in
try randomness.withUnsafePointerToBytes { randomness in
try invokeFnReturningVariableLengthSerialized {
signal_group_send_credential_response_issue_deterministic(
$0,
concatenated,
requestingMember,
UInt64(expiration.timeIntervalSince1970),
params,
randomness)
}
}
}
}
}
}
}
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
try localUser.withPointerToFixedWidthBinary { localUser in
try serverParams.withUnsafePointerToSerialized { serverParams in
try groupParams.withUnsafePointerToSerialized { groupParams in
try invokeFnReturningVariableLengthSerialized {
signal_group_send_credential_response_receive($0, response, groupMembers, localUser, UInt64(now.timeIntervalSince1970), serverParams, groupParams)
}
}
}
}
}
}
}
}

View File

@ -92,9 +92,9 @@ SPDX-License-Identifier: AGPL-3.0-only
#define SignalRESERVED_LEN 1
#define SignalSERVER_SECRET_PARAMS_LEN 2305
#define SignalSERVER_SECRET_PARAMS_LEN 2689
#define SignalSERVER_PUBLIC_PARAMS_LEN 417
#define SignalSERVER_PUBLIC_PARAMS_LEN 641
#define SignalUUID_CIPHERTEXT_LEN 65
@ -1177,6 +1177,22 @@ SignalFfiError *signal_backup_auth_credential_presentation_check_valid_contents(
SignalFfiError *signal_backup_auth_credential_presentation_verify(SignalBorrowedBuffer presentation_bytes, uint64_t now, SignalBorrowedBuffer server_params_bytes);
SignalFfiError *signal_group_send_credential_response_default_expiration_based_on_current_time(uint64_t *out);
SignalFfiError *signal_group_send_credential_response_issue_deterministic(SignalOwnedBuffer *out, SignalBorrowedBuffer concatenated_group_member_ciphertexts, const unsigned char (*requester)[SignalUUID_CIPHERTEXT_LEN], uint64_t expiration, const unsigned char (*server_params)[SignalSERVER_SECRET_PARAMS_LEN], const uint8_t (*randomness)[SignalRANDOMNESS_LEN]);
SignalFfiError *signal_group_send_credential_response_check_valid_contents(SignalBorrowedBuffer response_bytes);
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_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]);
SignalFfiError *signal_group_send_credential_presentation_check_valid_contents(SignalBorrowedBuffer presentation_bytes);
SignalFfiError *signal_group_send_credential_presentation_verify(SignalBorrowedBuffer presentation_bytes, SignalBorrowedBuffer group_members, uint64_t now, const unsigned char (*server_params)[SignalSERVER_SECRET_PARAMS_LEN]);
SignalFfiError *signal_verify_signature(bool *out, SignalBorrowedBuffer cert_pem, SignalBorrowedBuffer body, SignalBorrowedBuffer signature, uint64_t current_timestamp);
SignalFfiError *signal_pin_hash_destroy(SignalPinHash *p);

View File

@ -516,4 +516,63 @@ class ZKGroupTests: TestCaseBase {
// future credential should be invalid
XCTAssertThrowsError(try presentation.verify(now: Date(timeIntervalSince1970: TimeInterval(startOfDay - 1 - SECONDS_PER_DAY)), serverParams: serverSecretParams))
}
func testGroupSendCredential() {
let serverSecretParams = try! ServerSecretParams.generate(randomness: TEST_ARRAY_32)
let serverPublicParams = try! serverSecretParams.getPublicParams()
let aliceAci = try! Aci.parseFrom(serviceIdString: "9d0652a3-dcc3-4d11-975f-74d61598733f")
let bobAci = try! Aci.parseFrom(serviceIdString: "6838237d-02f6-4098-b110-698253d15961")
let eveAci = try! Aci.parseFrom(serviceIdString: "3f0f4734-e331-4434-bd4f-6d8f6ea6dcc7")
let malloryAci = try! Aci.parseFrom(serviceIdString: "5d088142-6fd7-4dbd-af00-fdda1b3ce988")
let masterKey = try! GroupMasterKey(contents: TEST_ARRAY_32_1)
let groupSecretParams = try! GroupSecretParams.deriveFromMasterKey(groupMasterKey: masterKey)
let aliceCiphertext = try! ClientZkGroupCipher(groupSecretParams: groupSecretParams).encrypt(aliceAci)
let groupCiphertexts = [aliceAci, bobAci, eveAci, malloryAci].map {
try! ClientZkGroupCipher(groupSecretParams: groupSecretParams).encrypt($0)
}
// Server
let now = UInt64(Date().timeIntervalSince1970)
let startOfDay = now - (now % SECONDS_PER_DAY)
let response = GroupSendCredentialResponse.issueCredential(groupMembers: groupCiphertexts, requestingMember: aliceCiphertext, params: serverSecretParams, randomness: TEST_ARRAY_32_2)
// Client
let credential = try! response.receive(groupMembers: [aliceAci, bobAci, eveAci, malloryAci], localUser: aliceAci, serverParams: serverPublicParams, groupParams: groupSecretParams)
XCTAssertThrowsError(try response.receive(groupMembers: [aliceAci, bobAci, eveAci, malloryAci], localUser: bobAci, serverParams: serverPublicParams, groupParams: groupSecretParams))
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))
let presentation = credential.present(serverParams: serverPublicParams, randomness: TEST_ARRAY_32_3)
// Server
try! presentation.verify(groupMembers: [bobAci, eveAci, malloryAci], serverParams: serverSecretParams)
try! presentation.verify(groupMembers: [bobAci, eveAci, malloryAci], now: Date().addingTimeInterval(60 * 60), serverParams: serverSecretParams)
XCTAssertThrowsError(try presentation.verify(groupMembers: [aliceAci, bobAci, eveAci, malloryAci], serverParams: serverSecretParams))
XCTAssertThrowsError(try presentation.verify(groupMembers: [eveAci, malloryAci], serverParams: serverSecretParams))
// credential should definitely be expired after 2 days
XCTAssertThrowsError(try presentation.verify(groupMembers: [bobAci, eveAci, malloryAci], now: Date(timeIntervalSince1970: TimeInterval(startOfDay + SECONDS_PER_DAY * 2 + 1)), serverParams: serverSecretParams))
}
func testEmptyGroupSendCredential() {
let serverSecretParams = try! ServerSecretParams.generate(randomness: TEST_ARRAY_32)
let serverPublicParams = try! serverSecretParams.getPublicParams()
let aliceAci = try! Aci.parseFrom(serviceIdString: "9d0652a3-dcc3-4d11-975f-74d61598733f")
let masterKey = try! GroupMasterKey(contents: TEST_ARRAY_32_1)
let groupSecretParams = try! GroupSecretParams.deriveFromMasterKey(groupMasterKey: masterKey)
let aliceCiphertext = try! ClientZkGroupCipher(groupSecretParams: groupSecretParams).encrypt(aliceAci)
// Server
let response = GroupSendCredentialResponse.issueCredential(groupMembers: [aliceCiphertext], requestingMember: aliceCiphertext, params: serverSecretParams, randomness: TEST_ARRAY_32_2)
// Client
_ = try! response.receive(groupMembers: [aliceAci], localUser: aliceAci, serverParams: serverPublicParams, groupParams: groupSecretParams)
}
}