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:
parent
bc18bb0ecf
commit
0d09a8352c
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -3811,6 +3811,7 @@ dependencies = [
|
||||
"bincode",
|
||||
"criterion",
|
||||
"curve25519-dalek",
|
||||
"derive-where",
|
||||
"displaydoc",
|
||||
"hex",
|
||||
"hex-literal",
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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
8
node/Native.d.ts
vendored
@ -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;
|
||||
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
39
node/ts/zkgroup/groupsend/GroupSendCredential.ts
Normal file
39
node/ts/zkgroup/groupsend/GroupSendCredential.ts
Normal 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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
31
node/ts/zkgroup/groupsend/GroupSendCredentialPresentation.ts
Normal file
31
node/ts/zkgroup/groupsend/GroupSendCredentialPresentation.ts
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
94
node/ts/zkgroup/groupsend/GroupSendCredentialResponse.ts
Normal file
94
node/ts/zkgroup/groupsend/GroupSendCredentialResponse.ts
Normal 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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
|
296
rust/zkgroup/src/api/groups/group_send_credential.rs
Normal file
296
rust/zkgroup/src/api/groups/group_send_credential.rs
Normal 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(¶ms.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(¶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,
|
||||
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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
171
rust/zkgroup/tests/group_send_flow.rs
Normal file
171
rust/zkgroup/tests/group_send_flow.rs
Normal 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,
|
||||
);
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user