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

java: Implement GroupEndorsement APIs

This commit is contained in:
Jordan Rose 2024-03-04 17:43:54 -08:00
parent cdef8228a2
commit 2aa3c34088
5 changed files with 710 additions and 0 deletions

View File

@ -0,0 +1,228 @@
//
// Copyright 2024 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.zkgroup.integrationtests;
import static org.junit.Assert.assertEquals;
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.GroupSendDerivedKeyPair;
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsement;
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsementsResponse;
import org.signal.libsignal.zkgroup.groupsend.GroupSendFullToken;
public final class GroupSendEndorsementTest 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 endorsements
Instant expiration = Instant.now().truncatedTo(ChronoUnit.DAYS).plus(2, ChronoUnit.DAYS);
GroupSendDerivedKeyPair keyPair =
GroupSendDerivedKeyPair.forExpiration(expiration, serverSecretParams);
GroupSendEndorsementsResponse response =
GroupSendEndorsementsResponse.issue(groupCiphertexts, keyPair);
// CLIENT
// Gets stored endorsements
GroupSendEndorsementsResponse.ReceivedEndorsements receivedEndorsements =
response.receive(
Arrays.asList(aliceServiceId, bobServiceId, eveServiceId, malloryServiceId),
aliceServiceId,
groupSecretParams,
serverPublicParams);
assertThrows(
VerificationFailedException.class,
() ->
response.receive(
Arrays.asList(bobServiceId, eveServiceId, malloryServiceId),
aliceServiceId,
groupSecretParams,
serverPublicParams));
assertThrows(
VerificationFailedException.class,
() ->
response.receive(
Arrays.asList(aliceServiceId, eveServiceId, malloryServiceId),
aliceServiceId,
groupSecretParams,
serverPublicParams));
// Try receive with ciphertexts instead.
{
GroupSendEndorsementsResponse.ReceivedEndorsements repeatReceivedEndorsements =
response.receive(groupCiphertexts, aliceCiphertext, serverPublicParams);
assertEquals(receivedEndorsements.endorsements, repeatReceivedEndorsements.endorsements);
assertEquals(
receivedEndorsements.combinedEndorsement, repeatReceivedEndorsements.combinedEndorsement);
assertThrows(
"missing local user",
VerificationFailedException.class,
() ->
response.receive(
groupCiphertexts.stream().skip(1).collect(Collectors.toList()),
aliceCiphertext,
serverPublicParams));
assertThrows(
"missing another user",
VerificationFailedException.class,
() ->
response.receive(
groupCiphertexts.stream().limit(3).collect(Collectors.toList()),
aliceCiphertext,
serverPublicParams));
}
GroupSendEndorsement.Token combinedToken =
receivedEndorsements.combinedEndorsement.toToken(groupSecretParams);
GroupSendFullToken fullCombinedToken = combinedToken.toFullToken(response.getExpiration());
// SERVER
// Verify token
GroupSendDerivedKeyPair verifyKey =
GroupSendDerivedKeyPair.forExpiration(
fullCombinedToken.getExpiration(), serverSecretParams);
fullCombinedToken.verify(
Arrays.asList(bobServiceId, eveServiceId, malloryServiceId), verifyKey);
fullCombinedToken.verify(
Arrays.asList(bobServiceId, eveServiceId, malloryServiceId),
Instant.now().plus(1, ChronoUnit.HOURS),
verifyKey);
assertThrows(
"included extra user",
VerificationFailedException.class,
() ->
fullCombinedToken.verify(
Arrays.asList(aliceServiceId, bobServiceId, eveServiceId, malloryServiceId),
verifyKey));
assertThrows(
"missing user",
VerificationFailedException.class,
() -> fullCombinedToken.verify(Arrays.asList(eveServiceId, malloryServiceId), verifyKey));
assertThrows(
"expired",
VerificationFailedException.class,
() ->
fullCombinedToken.verify(
Arrays.asList(bobServiceId, eveServiceId, malloryServiceId),
Instant.now()
.truncatedTo(ChronoUnit.DAYS)
.plus(2, ChronoUnit.DAYS)
.plus(1, ChronoUnit.SECONDS),
verifyKey));
// Excluding a user
{
// CLIENT
GroupSendEndorsement everybodyButMallory =
receivedEndorsements.combinedEndorsement.byRemoving(
receivedEndorsements.endorsements.get(3));
GroupSendFullToken fullEverybodyButMalloryToken =
everybodyButMallory.toToken(groupSecretParams).toFullToken(response.getExpiration());
// SERVER
GroupSendDerivedKeyPair everybodyButMalloryKey =
GroupSendDerivedKeyPair.forExpiration(
fullEverybodyButMalloryToken.getExpiration(), serverSecretParams);
fullEverybodyButMalloryToken.verify(
Arrays.asList(bobServiceId, eveServiceId), everybodyButMalloryKey);
}
// Custom combine
{
// CLIENT
GroupSendEndorsement bobAndEve =
GroupSendEndorsement.combine(
Arrays.asList(
receivedEndorsements.endorsements.get(1),
receivedEndorsements.endorsements.get(2)));
GroupSendFullToken fullBobAndEveToken =
bobAndEve.toToken(groupSecretParams).toFullToken(response.getExpiration());
// SERVER
GroupSendDerivedKeyPair bobAndEveKey =
GroupSendDerivedKeyPair.forExpiration(
fullBobAndEveToken.getExpiration(), serverSecretParams);
fullBobAndEveToken.verify(Arrays.asList(bobServiceId, eveServiceId), bobAndEveKey);
}
// Single-user
{
// CLIENT
GroupSendEndorsement bobEndorsement = receivedEndorsements.endorsements.get(1);
GroupSendFullToken fullBobToken =
bobEndorsement.toToken(groupSecretParams).toFullToken(response.getExpiration());
// SERVER
GroupSendDerivedKeyPair bobKey =
GroupSendDerivedKeyPair.forExpiration(fullBobToken.getExpiration(), serverSecretParams);
fullBobToken.verify(Arrays.asList(bobServiceId), bobKey);
}
}
}

View File

@ -0,0 +1,48 @@
//
// Copyright 2024 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.zkgroup.groupsend;
import static org.signal.libsignal.internal.FilterExceptions.filterExceptions;
import java.time.Instant;
import org.signal.libsignal.internal.Native;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.ServerSecretParams;
import org.signal.libsignal.zkgroup.internal.ByteArray;
/**
* The key pair used to issue and verify group send endorsements.
*
* <p>Group send endorsements use a different key pair depending on the endorsement's expiration
* (but not the user ID being endorsed). The server may cache these keys to avoid the (small) cost
* of deriving them from the root key in {@link ServerSecretParams}. The key object stores the
* expiration so that it doesn't need to be provided again when issuing endorsements.
*
* @see GroupSendEndorsementsResponse#issue
* @see GroupSendFullToken#verify
*/
public final class GroupSendDerivedKeyPair extends ByteArray {
public GroupSendDerivedKeyPair(byte[] contents) throws InvalidInputException {
super(contents);
filterExceptions(
InvalidInputException.class,
() -> Native.GroupSendDerivedKeyPair_CheckValidContents(contents));
}
/**
* Derives a new key for group send endorsements that expire at {@code expiration}.
*
* <p>{@code expiration} must be day-aligned as a protection against fingerprinting by the issuing
* server.
*/
public static GroupSendDerivedKeyPair forExpiration(
Instant expiration, ServerSecretParams params) {
byte[] newContents =
Native.GroupSendDerivedKeyPair_ForExpiration(
expiration.getEpochSecond(), params.getInternalContentsForJNI());
return filterExceptions(() -> new GroupSendDerivedKeyPair(newContents));
}
}

View File

@ -0,0 +1,134 @@
//
// Copyright 2024 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.zkgroup.groupsend;
import static org.signal.libsignal.internal.FilterExceptions.filterExceptions;
import java.nio.ByteBuffer;
import java.time.Instant;
import java.util.List;
import org.signal.libsignal.internal.Native;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
import org.signal.libsignal.zkgroup.internal.ByteArray;
/**
* An endorsement for a user or set of users in a group.
*
* <p>GroupSendEndorsements provide a form of authorization by demonstrating that the holder of the
* endorsement is in a group with a particular user or set of users. They can be {@link #combine
* combined} and {@link #byRemoving removed} in a set-like fashion.
*
* <p>The endorsement "flow" starts with receiving a {@link GroupSendEndorsementsResponse} from the
* group server, which contains endorsements for all members in a group (including the local user).
* The response object provides the single expiration for all the endorsements. From there, the
* {@code receive} method produces a {@link GroupSendEndorsementsResponse.ReceivedEndorsements},
* which exposes the individual endorsements as well as a combined endorsement for everyone but the
* local user. Clients should save these endorsements and the expiration with the group state.
*
* <p>When it comes time to send a message to an individual user, clients should check to see if
* they have a {@link GroupSendEndorsement.Token} for that user, and generate and cache one using
* {@link #toToken} if not. The token should then be converted to a full token using {@link
* GroupSendEndorsement.Token#toFullToken}, providing the expiration saved previously. Finally, the
* serialized full token can be used as authorization in a request to the chat server.
*
* <p>Similarly, when it comes time to send a message to the group, clients should start by {@link
* #byRemoving removing} the endorsements of any users they are excluding (say, because they need a
* Sender Key Distribution Message first), and then converting the resulting endorsement to a token.
* From there, the token can be converted to a full token and serialized as for an individual send.
* (Saving the repeated work of converting to a token is left to the clients here; worst case, it's
* still cheaper than a usual zkgroup presentation.)
*/
public final class GroupSendEndorsement extends ByteArray {
public GroupSendEndorsement(byte[] contents) throws InvalidInputException {
super(contents);
filterExceptions(
InvalidInputException.class,
() -> Native.GroupSendEndorsement_CheckValidContents(contents));
}
/**
* Combines several endorsements into one.
*
* <p>For example, if you have endorsements to send to Meredith and Aruna individually, then you
* can combine them to produce an endorsement to send a multi-recipient message to the two of
* them.
*/
public static GroupSendEndorsement combine(List<GroupSendEndorsement> endorsements) {
ByteBuffer[] buffers = new ByteBuffer[endorsements.size()];
int nextOffset = 0;
for (GroupSendEndorsement next : endorsements) {
byte[] nextEndorsementRaw = next.getInternalContentsForJNI();
buffers[nextOffset] = ByteBuffer.allocateDirect(nextEndorsementRaw.length);
buffers[nextOffset].put(nextEndorsementRaw);
++nextOffset;
}
byte[] rawCombinedEndorsement = Native.GroupSendEndorsement_Combine(buffers);
return filterExceptions(() -> new GroupSendEndorsement(rawCombinedEndorsement));
}
/**
* Removes an endorsement (individual or combined) from this combined endorsement.
*
* <p>If {@code this} is <em>not</em> a combined endorsement, or {@code toRemove} includes
* endorsements that were not combined into {@code this}, the result will not generate valid
* tokens.
*/
public GroupSendEndorsement byRemoving(GroupSendEndorsement toRemove) {
byte[] rawResult =
Native.GroupSendEndorsement_Remove(
getInternalContentsForJNI(), toRemove.getInternalContentsForJNI());
return filterExceptions(() -> new GroupSendEndorsement(rawResult));
}
/**
* A minimal cacheable representation of an endorsement.
*
* <p>This contains the minimal information needed to represent this specific endorsement; it must
* be converted to a {@link GroupSendFullToken} before sending to the chat server. (It is valid to
* do this immediately; it just uses up extra space.)
*
* <p>Generated by {@link GroupSendEndorsement#toToken}.
*/
public static class Token extends ByteArray {
public Token(byte[] contents) throws InvalidInputException {
super(contents);
filterExceptions(
InvalidInputException.class, () -> Native.GroupSendToken_CheckValidContents(contents));
}
/**
* Converts this token to a "full token", which can be sent to the chat server as
* authentication.
*
* <p>{@code expiration} must be the same expiration that was in the original {@link
* GroupSendEndorsementsResponse}, or the resulting token will fail to verify.
*/
public GroupSendFullToken toFullToken(Instant expiration) {
byte[] rawResult =
Native.GroupSendToken_ToFullToken(
getInternalContentsForJNI(), expiration.getEpochSecond());
return filterExceptions(() -> new GroupSendFullToken(rawResult));
}
}
/**
* Generates a cacheable token used to authenticate sends.
*
* <p>The token is no longer associated with the group; it merely identifies the user or set of
* users referenced by this endorsement. (Of course, a set of users is a pretty good stand-in for
* a group.)
*
* @see Token
*/
public Token toToken(GroupSecretParams groupParams) {
byte[] rawResult =
Native.GroupSendEndorsement_ToToken(
getInternalContentsForJNI(), groupParams.getInternalContentsForJNI());
return filterExceptions(() -> new Token(rawResult));
}
}

View File

@ -0,0 +1,231 @@
//
// Copyright 2024 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.zkgroup.groupsend;
import static org.signal.libsignal.internal.FilterExceptions.filterExceptions;
import static org.signal.libsignal.zkgroup.internal.Constants.RANDOM_LENGTH;
import java.nio.ByteBuffer;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.ArrayList;
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.VerificationFailedException;
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
import org.signal.libsignal.zkgroup.groups.UuidCiphertext;
import org.signal.libsignal.zkgroup.internal.ByteArray;
/**
* A set of endorsements of the members in a group, along with a proof of their validity.
*
* <p>Issued by the group server based on the group's member ciphertexts. The endorsements will
* eventually be verified by the chat server in the form of {@link GroupSendFullToken}s. See {@link
* GroupSendEndorsement} for a full description of the endorsement flow from the client's
* perspective.
*/
public final class GroupSendEndorsementsResponse extends ByteArray {
public GroupSendEndorsementsResponse(byte[] contents) throws InvalidInputException {
super(contents);
filterExceptions(
InvalidInputException.class,
() -> Native.GroupSendEndorsementsResponse_CheckValidContents(contents));
}
/**
* Issues a new set of endorsements for {@code groupMembers}.
*
* <p>{@code groupMembers} should include {@code requestingUser} as well.
*/
public static GroupSendEndorsementsResponse issue(
List<UuidCiphertext> groupMembers, GroupSendDerivedKeyPair keyPair) {
return issue(groupMembers, keyPair, new SecureRandom());
}
/**
* Issues a new set of endorsements for {@code groupMembers}.
*
* <p>{@code groupMembers} should include {@code requestingUser} as well.
*/
public static GroupSendEndorsementsResponse issue(
List<UuidCiphertext> groupMembers,
GroupSendDerivedKeyPair keyPair,
SecureRandom secureRandom) {
byte[] random = new byte[RANDOM_LENGTH];
secureRandom.nextBytes(random);
byte[] newContents =
filterExceptions(
() ->
Native.GroupSendEndorsementsResponse_IssueDeterministic(
UuidCiphertext.serializeAndConcatenate(groupMembers),
keyPair.getInternalContentsForJNI(),
random));
return filterExceptions(() -> new GroupSendEndorsementsResponse(newContents));
}
/** Returns the expiration for the contained endorsements. */
public Instant getExpiration() {
return Instant.ofEpochSecond(
Native.GroupSendEndorsementsResponse_GetExpiration(getInternalContentsForJNI()));
}
/**
* A collection of endorsements known to be valid.
*
* <p>The result of the {@code receive} operations on {@link GroupSendEndorsementsResponse}.
* Contains an endorsement for each member of the group, in the same order they were originally
* provided, plus a combined endorsement for "everyone but me", intended for multi-recipient
* sends.
*/
public class ReceivedEndorsements {
/**
* One endorsement per member of the group, in the same order the members were originally
* provided.
*/
public List<GroupSendEndorsement> endorsements;
/** An endorsement for everyone in the group but the local user, for multi-recipient sends. */
public GroupSendEndorsement combinedEndorsement;
<T> ReceivedEndorsements(
List<GroupSendEndorsement> endorsements, List<T> members, T localMember) {
this.endorsements = endorsements;
int memberCount = members.size();
assert endorsements.size() == memberCount;
ByteBuffer[] buffers = new ByteBuffer[memberCount - 1];
int nextOffset = 0;
for (int i = 0; i < memberCount; ++i) {
if (members.get(i).equals(localMember)) {
continue;
}
if (nextOffset == memberCount - 1) {
throw new IllegalArgumentException("member list did not contain the local user");
}
byte[] nextEndorsementRaw = endorsements.get(i).getInternalContentsForJNI();
buffers[nextOffset] = ByteBuffer.allocateDirect(nextEndorsementRaw.length);
buffers[nextOffset].put(nextEndorsementRaw);
++nextOffset;
}
byte[] rawCombinedEndorsement = Native.GroupSendEndorsement_Combine(buffers);
this.combinedEndorsement =
filterExceptions(() -> new GroupSendEndorsement(rawCombinedEndorsement));
}
}
/**
* Receives, validates, and extracts the endorsements from a response.
*
* <p>Note that the {@code receive} operation is provided for both {@link ServiceId}s and {@link
* UuidCiphertext}s. If you already have the ciphertexts for the group members available, {@link
* #receive(List, UuidCiphertext, ServerPublicParams)} will be <em>significantly</em> faster; if
* you don't, this method is faster than generating the ciphertexts and throwing them away
* afterwards.
*
* <p>{@code localUser} should be included in {@code groupMembers}.
*
* @throws VerificationFailedException if the endorsements are not valid for any reason
*/
public ReceivedEndorsements receive(
List<ServiceId> groupMembers,
ServiceId.Aci localUser,
GroupSecretParams groupParams,
ServerPublicParams serverParams)
throws VerificationFailedException {
return receive(groupMembers, localUser, Instant.now(), groupParams, serverParams);
}
/**
* Receives, validates, and extracts the endorsements from a response, assuming a specific current
* time.
*
* <p>This should only be used for testing purposes.
*
* @see #receive(List, ServiceId.Aci, GroupSecretParams, ServerPublicParams)
*/
public ReceivedEndorsements receive(
List<ServiceId> groupMembers,
ServiceId.Aci localUser,
Instant now,
GroupSecretParams groupParams,
ServerPublicParams serverParams)
throws VerificationFailedException {
byte[][] endorsementContents =
filterExceptions(
VerificationFailedException.class,
() ->
(byte[][])
Native.GroupSendEndorsementsResponse_ReceiveWithServiceIds(
getInternalContentsForJNI(),
ServiceId.toConcatenatedFixedWidthBinary(groupMembers),
now.getEpochSecond(),
groupParams.getInternalContentsForJNI(),
serverParams.getInternalContentsForJNI()));
List<GroupSendEndorsement> endorsements = new ArrayList<>(endorsementContents.length);
for (byte[] contents : endorsementContents) {
endorsements.add(filterExceptions(() -> new GroupSendEndorsement(contents)));
}
return new ReceivedEndorsements(endorsements, groupMembers, localUser);
}
/**
* Receives, validates, and extracts the endorsements from a response.
*
* <p>Note that the {@code receive} operation is provided for both {@link ServiceId}s and {@link
* UuidCiphertext}s. If you already have the ciphertexts for the group members available, this
* method will be <em>significantly</em> faster; if you don't, {@link #receive(List,
* ServiceId.Aci, GroupSecretParams, ServerPublicParams)} is faster than generating the
* ciphertexts and throwing them away afterwards.
*
* <p>{@code localUser} should be included in {@code groupMembers}.
*
* @throws VerificationFailedException if the endorsements are not valid for any reason
*/
public ReceivedEndorsements receive(
List<UuidCiphertext> groupMembers, UuidCiphertext localUser, ServerPublicParams serverParams)
throws VerificationFailedException {
return receive(groupMembers, localUser, Instant.now(), serverParams);
}
/**
* Receives, validates, and extracts the endorsements from a response, assuming a specific current
* time.
*
* <p>This should only be used for testing purposes.
*
* @see #receive(List, UuidCiphertext, ServerPublicParams)
*/
public ReceivedEndorsements receive(
List<UuidCiphertext> groupMembers,
UuidCiphertext localUser,
Instant now,
ServerPublicParams serverParams)
throws VerificationFailedException {
byte[][] endorsementContents =
filterExceptions(
VerificationFailedException.class,
() ->
(byte[][])
Native.GroupSendEndorsementsResponse_ReceiveWithCiphertexts(
getInternalContentsForJNI(),
UuidCiphertext.serializeAndConcatenate(groupMembers),
now.getEpochSecond(),
serverParams.getInternalContentsForJNI()));
List<GroupSendEndorsement> endorsements = new ArrayList<>(endorsementContents.length);
for (byte[] contents : endorsementContents) {
endorsements.add(filterExceptions(() -> new GroupSendEndorsement(contents)));
}
return new ReceivedEndorsements(endorsements, groupMembers, localUser);
}
}

View File

@ -0,0 +1,69 @@
//
// Copyright 2024 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.zkgroup.groupsend;
import static org.signal.libsignal.internal.FilterExceptions.filterExceptions;
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.VerificationFailedException;
import org.signal.libsignal.zkgroup.internal.ByteArray;
/**
* A token representing a particular {@link GroupSendEndorsement}, along with the endorsement's
* expiration.
*
* <p>Generated by {@link GroupSendEndorsement.Token#toFullToken}, and verified by the chat server.
*/
public final class GroupSendFullToken extends ByteArray {
public GroupSendFullToken(byte[] contents) throws InvalidInputException {
super(contents);
filterExceptions(
InvalidInputException.class, () -> Native.GroupSendFullToken_CheckValidContents(contents));
}
/** Gets the expiration embedded in the token. */
public Instant getExpiration() {
return Instant.ofEpochSecond(
Native.GroupSendFullToken_GetExpiration(getInternalContentsForJNI()));
}
/**
* Verifies that this token was generated from an endorsement of {@code userIds} by {@code
* keyPair}.
*
* <p>The correct {@code keyPair} must be selected based on {@link #getExpiration}.
*
* @throws VerificationFailedException if the token is invalid.
*/
public void verify(List<ServiceId> userIds, GroupSendDerivedKeyPair keyPair)
throws VerificationFailedException {
verify(userIds, Instant.now(), keyPair);
}
/**
* Verifies that this token was generated from an endorsement of {@code userIds} by {@code
* keyPair}, assuming a specific current time.
*
* <p>This should only be used for testing purposes.
*
* @see #verify(List, GroupSendDerivedKeyPair)
*/
public void verify(List<ServiceId> userIds, Instant now, GroupSendDerivedKeyPair keyPair)
throws VerificationFailedException {
filterExceptions(
VerificationFailedException.class,
() ->
Native.GroupSendFullToken_Verify(
getInternalContentsForJNI(),
ServiceId.toConcatenatedFixedWidthBinary(userIds),
now.getEpochSecond(),
keyPair.getInternalContentsForJNI()));
}
}