Group Send Endorsement support for pre-key fetch endpoint

This commit is contained in:
Jonathan Klabunde Tomer 2024-04-19 15:40:46 -07:00 committed by GitHub
parent ab64828661
commit b8f64fe3d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 468 additions and 123 deletions

View File

@ -795,7 +795,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
.intercept(requestAttributesInterceptor)
.intercept(new ProhibitAuthenticationInterceptor(clientConnectionManager))
.addService(new AccountsAnonymousGrpcService(accountsManager, rateLimiters))
.addService(new KeysAnonymousGrpcService(accountsManager, keysManager))
.addService(new KeysAnonymousGrpcService(accountsManager, keysManager, zkSecretParams, Clock.systemUTC()))
.addService(new PaymentsGrpcService(currencyManager))
.addService(ExternalServiceCredentialsAnonymousGrpcService.create(accountsManager, config))
.addService(new ProfileAnonymousGrpcService(accountsManager, profilesManager, profileBadgeConverter, zkProfileOperations));
@ -961,7 +961,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
new DirectoryV2Controller(directoryV2CredentialsGenerator),
new DonationController(clock, zkReceiptOperations, redeemedReceiptsManager, accountsManager, config.getBadges(),
ReceiptCredentialPresentation::new),
new KeysController(rateLimiters, keysManager, accountsManager),
new KeysController(rateLimiters, keysManager, accountsManager, zkSecretParams, Clock.systemUTC()),
new MessageController(rateLimiters, messageByteLimitCardinalityEstimator, messageSender, receiptSender,
accountsManager, messagesManager, pushNotificationManager, reportMessageManager,
multiRecipientMessageExecutor, messageDeliveryScheduler, reportSpamTokenProvider, clientReleaseManager,

View File

@ -18,6 +18,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Clock;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@ -30,18 +31,26 @@ import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.zkgroup.ServerSecretParams;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.groupsend.GroupSendDerivedKeyPair;
import org.signal.libsignal.zkgroup.groupsend.GroupSendFullToken;
import org.whispersystems.textsecuregcm.auth.Anonymous;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.GroupSendTokenHeader;
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
import org.whispersystems.textsecuregcm.entities.CheckKeysRequest;
import org.whispersystems.textsecuregcm.entities.ECPreKey;
@ -74,6 +83,8 @@ public class KeysController {
private final RateLimiters rateLimiters;
private final KeysManager keysManager;
private final AccountsManager accounts;
private final ServerSecretParams serverSecretParams;
private final Clock clock;
private static final String GET_KEYS_COUNTER_NAME = MetricsUtil.name(KeysController.class, "getKeys");
private static final String STORE_KEYS_COUNTER_NAME = MetricsUtil.name(KeysController.class, "storeKeys");
@ -83,10 +94,12 @@ public class KeysController {
private static final CompletableFuture<?>[] EMPTY_FUTURE_ARRAY = new CompletableFuture[0];
public KeysController(RateLimiters rateLimiters, KeysManager keysManager, AccountsManager accounts) {
public KeysController(RateLimiters rateLimiters, KeysManager keysManager, AccountsManager accounts, ServerSecretParams serverSecretParams, Clock clock) {
this.rateLimiters = rateLimiters;
this.keysManager = keysManager;
this.accounts = accounts;
this.serverSecretParams = serverSecretParams;
this.clock = clock;
}
@GET
@ -298,7 +311,8 @@ public class KeysController {
@Operation(summary = "Fetch public keys for another user",
description = "Retrieves the public identity key and available device prekeys for a specified account or phone-number identity")
@ApiResponse(responseCode = "200", description = "Indicates at least one prekey was available for at least one requested device.", useReturnTypeSchema = true)
@ApiResponse(responseCode = "401", description = "Account authentication check failed and unidentified-access key was not supplied or invalid.")
@ApiResponse(responseCode = "400", description = "A group send endorsement and other authorization (account authentication or unidentified-access key) were both provided.")
@ApiResponse(responseCode = "401", description = "Account authentication check failed and unidentified-access key or group send endorsement token was not supplied or invalid.")
@ApiResponse(responseCode = "404", description = "Requested identity or device does not exist, is not active, or has no available prekeys.")
@ApiResponse(responseCode = "429", description = "Rate limit exceeded.", headers = @Header(
name = "Retry-After",
@ -306,6 +320,7 @@ public class KeysController {
public PreKeyResponse getDeviceKeys(
@ReadOnly @Auth Optional<AuthenticatedAccount> auth,
@HeaderParam(HeaderUtils.UNIDENTIFIED_ACCESS_KEY) Optional<Anonymous> accessKey,
@HeaderParam(HeaderUtils.GROUP_SEND_TOKEN) Optional<GroupSendTokenHeader> groupSendToken,
@Parameter(description="the account or phone-number identifier to retrieve keys for")
@PathParam("identifier") ServiceIdentifier targetIdentifier,
@ -316,20 +331,27 @@ public class KeysController {
@HeaderParam(HttpHeaders.USER_AGENT) String userAgent)
throws RateLimitExceededException {
if (auth.isEmpty() && accessKey.isEmpty()) {
if (auth.isEmpty() && accessKey.isEmpty() && groupSendToken.isEmpty()) {
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}
final Optional<Account> account = auth.map(AuthenticatedAccount::getAccount);
final Optional<Account> maybeTarget = accounts.getByServiceIdentifier(targetIdentifier);
final Account target;
{
final Optional<Account> maybeTarget = accounts.getByServiceIdentifier(targetIdentifier);
if (groupSendToken.isPresent()) {
if (auth.isPresent() || accessKey.isPresent()) {
throw new BadRequestException();
}
try {
final GroupSendFullToken token = groupSendToken.get().token();
token.verify(List.of(targetIdentifier.toLibsignal()), clock.instant(), GroupSendDerivedKeyPair.forExpiration(token.getExpiration(), serverSecretParams));
} catch (VerificationFailedException e) {
throw new NotAuthorizedException(e);
}
} else {
OptionalAccess.verify(account, accessKey, maybeTarget, deviceId);
target = maybeTarget.orElseThrow();
}
final Account target = maybeTarget.orElseThrow(NotFoundException::new);
if (account.isPresent()) {
rateLimiters.getPreKeysLimiter().validate(

View File

@ -0,0 +1,45 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.grpc;
import com.google.protobuf.ByteString;
import io.grpc.Status;
import java.time.Clock;
import java.util.List;
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.groupsend.GroupSendDerivedKeyPair;
import org.signal.libsignal.zkgroup.groupsend.GroupSendFullToken;
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
import reactor.core.publisher.Mono;
public class GroupSendTokenUtil {
private final ServerSecretParams serverSecretParams;
private final Clock clock;
public GroupSendTokenUtil(final ServerSecretParams serverSecretParams, final Clock clock) {
this.serverSecretParams = serverSecretParams;
this.clock = clock;
}
public Mono<Void> checkGroupSendToken(final ByteString serializedGroupSendToken, List<ServiceIdentifier> serviceIdentifiers) {
try {
final GroupSendFullToken token = new GroupSendFullToken(serializedGroupSendToken.toByteArray());
final List<ServiceId> serviceIds = serviceIdentifiers.stream().map(ServiceIdentifier::toLibsignal).toList();
token.verify(serviceIds, clock.instant(), GroupSendDerivedKeyPair.forExpiration(token.getExpiration(), serverSecretParams));
return Mono.empty();
} catch (InvalidInputException e) {
return Mono.error(Status.INVALID_ARGUMENT.asException());
} catch (VerificationFailedException e) {
return Mono.error(Status.UNAUTHENTICATED.asException());
}
}
}

View File

@ -9,15 +9,20 @@ import com.google.protobuf.ByteString;
import io.grpc.Status;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Clock;
import java.util.Arrays;
import java.util.List;
import org.signal.chat.keys.CheckIdentityKeyRequest;
import org.signal.chat.keys.CheckIdentityKeyResponse;
import org.signal.chat.keys.GetPreKeysAnonymousRequest;
import org.signal.chat.keys.GetPreKeysResponse;
import org.signal.chat.keys.ReactorKeysAnonymousGrpc;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.zkgroup.ServerSecretParams;
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil;
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.KeysManager;
import reactor.core.publisher.Flux;
@ -28,11 +33,14 @@ public class KeysAnonymousGrpcService extends ReactorKeysAnonymousGrpc.KeysAnony
private final AccountsManager accountsManager;
private final KeysManager keysManager;
private final GroupSendTokenUtil groupSendTokenUtil;
public KeysAnonymousGrpcService(final AccountsManager accountsManager, final KeysManager keysManager) {
public KeysAnonymousGrpcService(
final AccountsManager accountsManager, final KeysManager keysManager, final ServerSecretParams serverSecretParams, final Clock clock) {
this.accountsManager = accountsManager;
this.keysManager = keysManager;
}
this.groupSendTokenUtil = new GroupSendTokenUtil(serverSecretParams, clock);
}
@Override
public Mono<GetPreKeysResponse> getPreKeys(final GetPreKeysAnonymousRequest request) {
@ -41,13 +49,19 @@ public class KeysAnonymousGrpcService extends ReactorKeysAnonymousGrpc.KeysAnony
final byte deviceId = DeviceIdUtil.validate(request.getRequest().getDeviceId());
return Mono.fromFuture(() -> accountsManager.getByServiceIdentifierAsync(serviceIdentifier))
.flatMap(Mono::justOrEmpty)
.switchIfEmpty(Mono.error(Status.UNAUTHENTICATED.asException()))
.flatMap(targetAccount ->
UnidentifiedAccessUtil.checkUnidentifiedAccess(targetAccount, request.getUnidentifiedAccessKey().toByteArray())
? KeysGrpcHelper.getPreKeys(targetAccount, serviceIdentifier.identityType(), deviceId, keysManager)
: Mono.error(Status.UNAUTHENTICATED.asException()));
return switch (request.getAuthorizationCase()) {
case GROUP_SEND_TOKEN ->
groupSendTokenUtil.checkGroupSendToken(request.getGroupSendToken(), List.of(serviceIdentifier))
.then(lookUpAccount(serviceIdentifier, Status.NOT_FOUND))
.flatMap(targetAccount -> KeysGrpcHelper.getPreKeys(targetAccount, serviceIdentifier.identityType(), deviceId, keysManager));
case UNIDENTIFIED_ACCESS_KEY ->
lookUpAccount(serviceIdentifier, Status.UNAUTHENTICATED)
.flatMap(targetAccount ->
UnidentifiedAccessUtil.checkUnidentifiedAccess(targetAccount, request.getUnidentifiedAccessKey().toByteArray())
? KeysGrpcHelper.getPreKeys(targetAccount, serviceIdentifier.identityType(), deviceId, keysManager)
: Mono.error(Status.UNAUTHENTICATED.asException()));
default -> Mono.error(Status.INVALID_ARGUMENT.asException());
};
}
@Override
@ -69,6 +83,12 @@ public class KeysAnonymousGrpcService extends ReactorKeysAnonymousGrpc.KeysAnony
);
}
private Mono<Account> lookUpAccount(final ServiceIdentifier serviceIdentifier, final Status onNotFound) {
return Mono.fromFuture(() -> accountsManager.getByServiceIdentifierAsync(serviceIdentifier))
.flatMap(Mono::justOrEmpty)
.switchIfEmpty(Mono.error(onNotFound.asException()));
}
private static boolean fingerprintMatches(final IdentityKey identityKey, final byte[] fingerprint) {
final byte[] digest;
try {

View File

@ -164,9 +164,19 @@ message GetPreKeysAnonymousRequest {
GetPreKeysRequest request = 1;
/**
* The unidentified access key (UAK) for the targeted account.
* A means to authorize the request.
*/
bytes unidentified_access_key = 2;
oneof authorization {
/**
* The unidentified access key (UAK) for the targeted account.
*/
bytes unidentified_access_key = 2;
/**
* A group send endorsement token for the targeted account.
*/
bytes group_send_token = 3;
}
}
message GetPreKeysResponse {

View File

@ -29,13 +29,18 @@ import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.glassfish.jersey.server.ServerProperties;
@ -52,6 +57,7 @@ import org.mockito.ArgumentCaptor;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECKeyPair;
import org.signal.libsignal.zkgroup.ServerSecretParams;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.entities.CheckKeysRequest;
import org.whispersystems.textsecuregcm.entities.ECPreKey;
@ -64,6 +70,7 @@ import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
import org.whispersystems.textsecuregcm.identity.IdentityType;
import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier;
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.mappers.CompletionExceptionMapper;
@ -79,6 +86,7 @@ import org.whispersystems.textsecuregcm.tests.util.KeysHelper;
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
import org.whispersystems.textsecuregcm.util.CompletableFutureTestUtil;
import org.whispersystems.textsecuregcm.util.HeaderUtils;
import org.whispersystems.textsecuregcm.util.TestClock;
@ExtendWith(DropwizardExtensionsSupport.class)
class KeysControllerTest {
@ -86,8 +94,13 @@ class KeysControllerTest {
private static final String EXISTS_NUMBER = "+14152222222";
private static final UUID EXISTS_UUID = UUID.randomUUID();
private static final UUID EXISTS_PNI = UUID.randomUUID();
private static final AciServiceIdentifier EXISTS_ACI = new AciServiceIdentifier(EXISTS_UUID);
private static final UUID OTHER_UUID = UUID.randomUUID();
private static final AciServiceIdentifier OTHER_ACI = new AciServiceIdentifier(OTHER_UUID);
private static final UUID NOT_EXISTS_UUID = UUID.randomUUID();
private static final AciServiceIdentifier NOT_EXISTS_ACI = new AciServiceIdentifier(NOT_EXISTS_UUID);
private static final byte SAMPLE_DEVICE_ID = 1;
private static final byte SAMPLE_DEVICE_ID2 = 2;
@ -136,6 +149,10 @@ class KeysControllerTest {
private static final RateLimiters rateLimiters = mock(RateLimiters.class);
private static final RateLimiter rateLimiter = mock(RateLimiter.class );
private static final ServerSecretParams serverSecretParams = ServerSecretParams.generate();
private static final TestClock clock = TestClock.now();
private static final ResourceExtension resources = ResourceExtension.builder()
.addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE)
.addProvider(AuthHelper.getAuthFilter())
@ -143,7 +160,7 @@ class KeysControllerTest {
.addProvider(new AuthValueFactoryProvider.Binder<>(AuthenticatedAccount.class))
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new ServerRejectedExceptionMapper())
.addResource(new KeysController(rateLimiters, KEYS, accounts))
.addResource(new KeysController(rateLimiters, KEYS, accounts, serverSecretParams, clock))
.addResource(new RateLimitExceededExceptionMapper())
.build();
@ -183,6 +200,8 @@ class KeysControllerTest {
@BeforeEach
void setup() {
clock.unpin();
sampleDevice = mock(Device.class);
final Device sampleDevice2 = mock(Device.class);
final Device sampleDevice3 = mock(Device.class);
@ -529,6 +548,68 @@ class KeysControllerTest {
verifyNoMoreInteractions(KEYS);
}
@ParameterizedTest
@MethodSource
void testGetKeysWithGroupSendEndorsement(
ServiceIdentifier target, ServiceIdentifier authorizedTarget, Duration timeLeft, boolean includeUak, int expectedResponse) throws Exception {
final Instant expiration = Instant.now().truncatedTo(ChronoUnit.DAYS);
clock.pin(expiration.minus(timeLeft));
Invocation.Builder builder = resources.getJerseyTest()
.target(String.format("/v2/keys/%s/1", target.toServiceIdentifierString()))
.queryParam("pq", "true")
.request()
.header(HeaderUtils.GROUP_SEND_TOKEN, AuthHelper.validGroupSendTokenHeader(serverSecretParams, List.of(authorizedTarget), expiration));
if (includeUak) {
builder = builder.header(HeaderUtils.UNIDENTIFIED_ACCESS_KEY, AuthHelper.getUnidentifiedAccessHeader("1337".getBytes()));
}
Response response = builder.get();
assertThat(response.getStatus()).isEqualTo(expectedResponse);
if (expectedResponse == 200) {
PreKeyResponse result = response.readEntity(PreKeyResponse.class);
assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey(IdentityType.ACI));
assertThat(result.getDevicesCount()).isEqualTo(1);
assertEquals(SAMPLE_KEY, result.getDevice(SAMPLE_DEVICE_ID).getPreKey());
assertEquals(SAMPLE_PQ_KEY, result.getDevice(SAMPLE_DEVICE_ID).getPqPreKey());
assertEquals(SAMPLE_SIGNED_KEY, result.getDevice(SAMPLE_DEVICE_ID).getSignedPreKey());
verify(KEYS).takeEC(EXISTS_UUID, SAMPLE_DEVICE_ID);
verify(KEYS).takePQ(EXISTS_UUID, SAMPLE_DEVICE_ID);
verify(KEYS).getEcSignedPreKey(EXISTS_UUID, SAMPLE_DEVICE_ID);
}
verifyNoMoreInteractions(KEYS);
}
private static Stream<Arguments> testGetKeysWithGroupSendEndorsement() {
return Stream.of(
// valid endorsement
Arguments.of(EXISTS_ACI, EXISTS_ACI, Duration.ofHours(1), false, 200),
// expired endorsement, not authorized
Arguments.of(EXISTS_ACI, EXISTS_ACI, Duration.ofHours(-1), false, 401),
// endorsement for the wrong recipient, not authorized
Arguments.of(EXISTS_ACI, OTHER_ACI, Duration.ofHours(1), false, 401),
// expired endorsement for the wrong recipient, not authorized
Arguments.of(EXISTS_ACI, OTHER_ACI, Duration.ofHours(-1), false, 401),
// valid endorsement for the right recipient but they aren't registered, not found
Arguments.of(NOT_EXISTS_ACI, NOT_EXISTS_ACI, Duration.ofHours(1), false, 404),
// expired endorsement for the right recipient but they aren't registered, not authorized (NOT not found)
Arguments.of(NOT_EXISTS_ACI, NOT_EXISTS_ACI, Duration.ofHours(-1), false, 401),
// valid endorsement but also a UAK, bad request
Arguments.of(EXISTS_ACI, EXISTS_ACI, Duration.ofHours(1), true, 400));
}
@Test
void testNoDevices() {

View File

@ -433,7 +433,7 @@ class MessageControllerTest {
.queryParam("story", story)
.request()
.header(HeaderUtils.GROUP_SEND_TOKEN,
validGroupSendTokenHeader(List.of(authorizedRecipient), expiration));
AuthHelper.validGroupSendTokenHeader(serverSecretParams, List.of(authorizedRecipient), expiration));
if (includeUak) {
builder = builder.header(HeaderUtils.UNIDENTIFIED_ACCESS_KEY, Base64.getEncoder().encodeToString(UNIDENTIFIED_ACCESS_BYTES));
@ -1351,8 +1351,8 @@ class MessageControllerTest {
.queryParam("urgent", false)
.request()
.header(HttpHeaders.USER_AGENT, "FIXME")
.header(HeaderUtils.GROUP_SEND_TOKEN, validGroupSendTokenHeader(
List.of(SINGLE_DEVICE_ACI_ID, MULTI_DEVICE_ACI_ID), Instant.parse("2024-04-10T00:00:00.00Z")))
.header(HeaderUtils.GROUP_SEND_TOKEN, AuthHelper.validGroupSendTokenHeader(
serverSecretParams, List.of(SINGLE_DEVICE_ACI_ID, MULTI_DEVICE_ACI_ID), Instant.parse("2024-04-10T00:00:00.00Z")))
.put(Entity.entity(stream, MultiRecipientMessageProvider.MEDIA_TYPE));
assertThat("Unexpected response", response.getStatus(), is(equalTo(200)));
@ -1389,8 +1389,8 @@ class MessageControllerTest {
.queryParam("urgent", false)
.request()
.header(HttpHeaders.USER_AGENT, "FIXME")
.header(HeaderUtils.GROUP_SEND_TOKEN, validGroupSendTokenHeader(
List.of(MULTI_DEVICE_ACI_ID), Instant.parse("2024-04-10T00:00:00.00Z")))
.header(HeaderUtils.GROUP_SEND_TOKEN, AuthHelper.validGroupSendTokenHeader(
serverSecretParams, List.of(MULTI_DEVICE_ACI_ID), Instant.parse("2024-04-10T00:00:00.00Z")))
.put(Entity.entity(stream, MultiRecipientMessageProvider.MEDIA_TYPE));
assertThat("Unexpected response", response.getStatus(), is(equalTo(401)));
@ -1419,39 +1419,14 @@ class MessageControllerTest {
.queryParam("urgent", false)
.request()
.header(HttpHeaders.USER_AGENT, "FIXME")
.header(HeaderUtils.GROUP_SEND_TOKEN, validGroupSendTokenHeader(
List.of(SINGLE_DEVICE_ACI_ID, MULTI_DEVICE_ACI_ID), Instant.parse("2024-04-10T00:00:00.00Z")))
.header(HeaderUtils.GROUP_SEND_TOKEN, AuthHelper.validGroupSendTokenHeader(
serverSecretParams, List.of(SINGLE_DEVICE_ACI_ID, MULTI_DEVICE_ACI_ID), Instant.parse("2024-04-10T00:00:00.00Z")))
.put(Entity.entity(stream, MultiRecipientMessageProvider.MEDIA_TYPE));
assertThat("Unexpected response", response.getStatus(), is(equalTo(401)));
verifyNoMoreInteractions(messageSender);
}
private String validGroupSendTokenHeader(List<ServiceIdentifier> recipients, Instant expiration) throws Exception {
final ServerPublicParams serverPublicParams = serverSecretParams.getPublicParams();
final GroupMasterKey groupMasterKey = new GroupMasterKey(new byte[32]);
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
final ClientZkGroupCipher clientZkGroupCipher = new ClientZkGroupCipher(groupSecretParams);
final ServiceId.Aci sender = new ServiceId.Aci(UUID.randomUUID());
List<ServiceId> groupPlaintexts = Stream.concat(Stream.of(sender), recipients.stream().map(ServiceIdentifier::toLibsignal)).toList();
List<UuidCiphertext> groupCiphertexts = groupPlaintexts.stream()
.map(clientZkGroupCipher::encrypt)
.toList();
GroupSendDerivedKeyPair keyPair = GroupSendDerivedKeyPair.forExpiration(expiration, serverSecretParams);
GroupSendEndorsementsResponse endorsementsResponse =
GroupSendEndorsementsResponse.issue(groupCiphertexts, keyPair);
ReceivedEndorsements endorsements =
endorsementsResponse.receive(
groupPlaintexts,
sender,
expiration.minus(Duration.ofDays(1)),
groupSecretParams,
serverPublicParams);
GroupSendFullToken token = endorsements.combinedEndorsement().toFullToken(groupSecretParams, expiration);
return Base64.getEncoder().encodeToString(token.serialize());
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
void testMultiRecipientRedisBombProtection(final boolean useExplicitIdentifier) throws Exception {

View File

@ -6,23 +6,27 @@
package org.whispersystems.textsecuregcm.grpc;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyByte;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertStatusException;
import com.google.protobuf.ByteString;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
@ -41,6 +45,7 @@ import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECKeyPair;
import org.signal.libsignal.zkgroup.ServerSecretParams;
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil;
import org.whispersystems.textsecuregcm.entities.ECPreKey;
import org.whispersystems.textsecuregcm.entities.ECSignedPreKey;
@ -52,7 +57,10 @@ import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.KeysManager;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.tests.util.DevicesHelper;
import org.whispersystems.textsecuregcm.tests.util.KeysHelper;
import org.whispersystems.textsecuregcm.util.TestClock;
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
import org.whispersystems.textsecuregcm.util.UUIDUtil;
import org.whispersystems.textsecuregcm.util.Util;
@ -60,6 +68,9 @@ import reactor.core.publisher.Flux;
class KeysAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<KeysAnonymousGrpcService, KeysAnonymousGrpc.KeysAnonymousBlockingStub> {
private static final ServerSecretParams SERVER_SECRET_PARAMS = ServerSecretParams.generate();
private static final TestClock CLOCK = TestClock.now();
@Mock
private AccountsManager accountsManager;
@ -68,71 +79,54 @@ class KeysAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<KeysAnonymousGrpcS
@Override
protected KeysAnonymousGrpcService createServiceBeforeEachTest() {
return new KeysAnonymousGrpcService(accountsManager, keysManager);
return new KeysAnonymousGrpcService(accountsManager, keysManager, SERVER_SECRET_PARAMS, CLOCK);
}
@Test
void getPreKeys() {
void getPreKeysUnidentifiedAccessKey() {
final Account targetAccount = mock(Account.class);
final Device targetDevice = mock(Device.class);
final Device targetDevice = DevicesHelper.createDevice(Device.PRIMARY_ID);
when(targetAccount.getDevice(Device.PRIMARY_ID)).thenReturn(Optional.of(targetDevice));
final ECKeyPair identityKeyPair = Curve.generateKeyPair();
final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey());
final UUID identifier = UUID.randomUUID();
final UUID uuid = UUID.randomUUID();
final AciServiceIdentifier identifier = new AciServiceIdentifier(uuid);
final byte[] unidentifiedAccessKey = TestRandomUtil.nextBytes(UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH);
when(targetDevice.getId()).thenReturn(Device.PRIMARY_ID);
when(targetDevice.isEnabled()).thenReturn(true);
when(targetAccount.getDevice(Device.PRIMARY_ID)).thenReturn(Optional.of(targetDevice));
when(targetAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey));
when(targetAccount.getIdentifier(IdentityType.ACI)).thenReturn(identifier);
when(targetAccount.getIdentifier(IdentityType.ACI)).thenReturn(uuid);
when(targetAccount.getIdentityKey(IdentityType.ACI)).thenReturn(identityKey);
when(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(identifier)))
when(accountsManager.getByServiceIdentifierAsync(identifier))
.thenReturn(CompletableFuture.completedFuture(Optional.of(targetAccount)));
final ECPreKey ecPreKey = new ECPreKey(1, Curve.generateKeyPair().getPublicKey());
final ECSignedPreKey ecSignedPreKey = KeysHelper.signedECPreKey(2, identityKeyPair);
final KEMSignedPreKey kemSignedPreKey = KeysHelper.signedKEMPreKey(3, identityKeyPair);
when(keysManager.takeEC(identifier, Device.PRIMARY_ID))
when(keysManager.takeEC(uuid, Device.PRIMARY_ID))
.thenReturn(CompletableFuture.completedFuture(Optional.of(ecPreKey)));
when(keysManager.takePQ(identifier, Device.PRIMARY_ID))
when(keysManager.takePQ(uuid, Device.PRIMARY_ID))
.thenReturn(CompletableFuture.completedFuture(Optional.of(kemSignedPreKey)));
when(keysManager.getEcSignedPreKey(identifier, Device.PRIMARY_ID))
when(keysManager.getEcSignedPreKey(uuid, Device.PRIMARY_ID))
.thenReturn(CompletableFuture.completedFuture(Optional.of(ecSignedPreKey)));
final GetPreKeysResponse response = unauthenticatedServiceStub().getPreKeys(GetPreKeysAnonymousRequest.newBuilder()
.setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey))
.setRequest(GetPreKeysRequest.newBuilder()
.setTargetIdentifier(ServiceIdentifier.newBuilder()
.setIdentityType(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI)
.setUuid(UUIDUtil.toByteString(identifier))
.build())
.setDeviceId(Device.PRIMARY_ID)
.build())
.setTargetIdentifier(ServiceIdentifierUtil.toGrpcServiceIdentifier(identifier))
.setDeviceId(Device.PRIMARY_ID))
.build());
final GetPreKeysResponse expectedResponse = GetPreKeysResponse.newBuilder()
.setIdentityKey(ByteString.copyFrom(identityKey.serialize()))
.putPreKeys(Device.PRIMARY_ID, GetPreKeysResponse.PreKeyBundle.newBuilder()
.setEcOneTimePreKey(EcPreKey.newBuilder()
.setKeyId(ecPreKey.keyId())
.setPublicKey(ByteString.copyFrom(ecPreKey.serializedPublicKey()))
.build())
.setEcSignedPreKey(EcSignedPreKey.newBuilder()
.setKeyId(ecSignedPreKey.keyId())
.setPublicKey(ByteString.copyFrom(ecSignedPreKey.serializedPublicKey()))
.setSignature(ByteString.copyFrom(ecSignedPreKey.signature()))
.build())
.setKemOneTimePreKey(KemSignedPreKey.newBuilder()
.setKeyId(kemSignedPreKey.keyId())
.setPublicKey(ByteString.copyFrom(kemSignedPreKey.serializedPublicKey()))
.setSignature(ByteString.copyFrom(kemSignedPreKey.signature()))
.build())
.setEcOneTimePreKey(toGrpcEcPreKey(ecPreKey))
.setEcSignedPreKey(toGrpcEcSignedPreKey(ecSignedPreKey))
.setKemOneTimePreKey(toGrpcKemSignedPreKey(kemSignedPreKey))
.build())
.build();
@ -140,50 +134,177 @@ class KeysAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<KeysAnonymousGrpcS
}
@Test
void getPreKeysIncorrectUnidentifiedAccessKey() {
void getPreKeysGroupSendEndorsement() throws Exception {
final Account targetAccount = mock(Account.class);
final Device targetDevice = DevicesHelper.createDevice(Device.PRIMARY_ID);
when(targetAccount.getDevice(Device.PRIMARY_ID)).thenReturn(Optional.of(targetDevice));
final ECKeyPair identityKeyPair = Curve.generateKeyPair();
final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey());
final UUID identifier = UUID.randomUUID();
final UUID uuid = UUID.randomUUID();
final AciServiceIdentifier identifier = new AciServiceIdentifier(uuid);
final byte[] unidentifiedAccessKey = TestRandomUtil.nextBytes(UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH);
when(targetAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey));
when(targetAccount.getUuid()).thenReturn(identifier);
when(targetAccount.getIdentifier(IdentityType.ACI)).thenReturn(uuid);
when(targetAccount.getIdentityKey(IdentityType.ACI)).thenReturn(identityKey);
when(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(identifier)))
when(accountsManager.getByServiceIdentifierAsync(identifier))
.thenReturn(CompletableFuture.completedFuture(Optional.of(targetAccount)));
assertStatusException(Status.UNAUTHENTICATED, () -> unauthenticatedServiceStub().getPreKeys(GetPreKeysAnonymousRequest.newBuilder()
final ECPreKey ecPreKey = new ECPreKey(1, Curve.generateKeyPair().getPublicKey());
final ECSignedPreKey ecSignedPreKey = KeysHelper.signedECPreKey(2, identityKeyPair);
final KEMSignedPreKey kemSignedPreKey = KeysHelper.signedKEMPreKey(3, identityKeyPair);
when(keysManager.takeEC(uuid, Device.PRIMARY_ID))
.thenReturn(CompletableFuture.completedFuture(Optional.of(ecPreKey)));
when(keysManager.takePQ(uuid, Device.PRIMARY_ID))
.thenReturn(CompletableFuture.completedFuture(Optional.of(kemSignedPreKey)));
when(keysManager.getEcSignedPreKey(uuid, Device.PRIMARY_ID))
.thenReturn(CompletableFuture.completedFuture(Optional.of(ecSignedPreKey)));
// Expirations must be on day boundaries or libsignal will refuse to create or verify the token
final Instant expiration = Instant.now().truncatedTo(ChronoUnit.DAYS);
CLOCK.pin(expiration.minus(Duration.ofHours(1))); // set time so the credential isn't expired yet
final byte[] token = AuthHelper.validGroupSendToken(SERVER_SECRET_PARAMS, List.of(identifier), expiration);
final GetPreKeysResponse response = unauthenticatedServiceStub().getPreKeys(GetPreKeysAnonymousRequest.newBuilder()
.setGroupSendToken(ByteString.copyFrom(token))
.setRequest(GetPreKeysRequest.newBuilder()
.setTargetIdentifier(ServiceIdentifier.newBuilder()
.setIdentityType(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI)
.setUuid(UUIDUtil.toByteString(identifier))
.build())
.setDeviceId(Device.PRIMARY_ID)
.setTargetIdentifier(ServiceIdentifierUtil.toGrpcServiceIdentifier(identifier))
.setDeviceId(Device.PRIMARY_ID))
.build());
final GetPreKeysResponse expectedResponse = GetPreKeysResponse.newBuilder()
.setIdentityKey(ByteString.copyFrom(identityKey.serialize()))
.putPreKeys(Device.PRIMARY_ID, GetPreKeysResponse.PreKeyBundle.newBuilder()
.setEcOneTimePreKey(toGrpcEcPreKey(ecPreKey))
.setEcSignedPreKey(toGrpcEcSignedPreKey(ecSignedPreKey))
.setKemOneTimePreKey(toGrpcKemSignedPreKey(kemSignedPreKey))
.build())
.build()));
.build();
assertEquals(expectedResponse, response);
}
@Test
void getPreKeysAccountNotFound() {
when(accountsManager.getByServiceIdentifierAsync(any()))
void getPreKeysNoAuth() {
assertGetKeysFailure(Status.INVALID_ARGUMENT, GetPreKeysAnonymousRequest.newBuilder()
.setRequest(GetPreKeysRequest.newBuilder()
.setTargetIdentifier(ServiceIdentifierUtil.toGrpcServiceIdentifier(new AciServiceIdentifier(UUID.randomUUID())))
.setDeviceId(Device.PRIMARY_ID))
.build());
verifyNoInteractions(accountsManager);
verifyNoInteractions(keysManager);
}
@Test
void getPreKeysIncorrectUnidentifiedAccessKey() {
final Account targetAccount = mock(Account.class);
final UUID uuid = UUID.randomUUID();
final AciServiceIdentifier identifier = new AciServiceIdentifier(uuid);
final byte[] unidentifiedAccessKey = TestRandomUtil.nextBytes(UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH);
when(targetAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey));
when(accountsManager.getByServiceIdentifierAsync(identifier))
.thenReturn(CompletableFuture.completedFuture(Optional.of(targetAccount)));
assertGetKeysFailure(Status.UNAUTHENTICATED, GetPreKeysAnonymousRequest.newBuilder()
.setUnidentifiedAccessKey(UUIDUtil.toByteString(UUID.randomUUID()))
.setRequest(GetPreKeysRequest.newBuilder()
.setTargetIdentifier(ServiceIdentifierUtil.toGrpcServiceIdentifier(identifier))
.setDeviceId(Device.PRIMARY_ID))
.build());
verifyNoInteractions(keysManager);
}
@Test
void getPreKeysExpiredGroupSendEndorsement() throws Exception {
final UUID uuid = UUID.randomUUID();
final AciServiceIdentifier identifier = new AciServiceIdentifier(uuid);
// Expirations must be on day boundaries or libsignal will refuse to create or verify the token
final Instant expiration = Instant.now().truncatedTo(ChronoUnit.DAYS);
CLOCK.pin(expiration.plus(Duration.ofHours(1))); // set time so our token is already expired
final byte[] token = AuthHelper.validGroupSendToken(SERVER_SECRET_PARAMS, List.of(identifier), expiration);
assertGetKeysFailure(Status.UNAUTHENTICATED, GetPreKeysAnonymousRequest.newBuilder()
.setGroupSendToken(ByteString.copyFrom(token))
.setRequest(GetPreKeysRequest.newBuilder()
.setTargetIdentifier(ServiceIdentifierUtil.toGrpcServiceIdentifier(identifier))
.setDeviceId(Device.PRIMARY_ID))
.build());
verifyNoInteractions(accountsManager);
verifyNoInteractions(keysManager);
}
@Test
void getPreKeysIncorrectGroupSendEndorsement() throws Exception {
final AciServiceIdentifier authorizedIdentifier = new AciServiceIdentifier(UUID.randomUUID());
final AciServiceIdentifier targetIdentifier = new AciServiceIdentifier(UUID.randomUUID());
// Expirations must be on day boundaries or libsignal will refuse to create or verify the token
final Instant expiration = Instant.now().truncatedTo(ChronoUnit.DAYS);
CLOCK.pin(expiration.minus(Duration.ofHours(1))); // set time so the credential isn't expired yet
final AciServiceIdentifier wrongAci = new AciServiceIdentifier(UUID.randomUUID());
final byte[] token = AuthHelper.validGroupSendToken(SERVER_SECRET_PARAMS, List.of(authorizedIdentifier), expiration);
assertGetKeysFailure(Status.UNAUTHENTICATED, GetPreKeysAnonymousRequest.newBuilder()
.setGroupSendToken(ByteString.copyFrom(token))
.setRequest(GetPreKeysRequest.newBuilder()
.setTargetIdentifier(ServiceIdentifierUtil.toGrpcServiceIdentifier(targetIdentifier))
.setDeviceId(Device.PRIMARY_ID))
.build());
verifyNoInteractions(accountsManager);
verifyNoInteractions(keysManager);
}
@Test
void getPreKeysAccountNotFoundUnidentifiedAccessKey() {
final AciServiceIdentifier nonexistentAci = new AciServiceIdentifier(UUID.randomUUID());
when(accountsManager.getByServiceIdentifierAsync(nonexistentAci))
.thenReturn(CompletableFuture.completedFuture(Optional.empty()));
final StatusRuntimeException exception =
assertThrows(StatusRuntimeException.class,
() -> unauthenticatedServiceStub().getPreKeys(GetPreKeysAnonymousRequest.newBuilder()
.setUnidentifiedAccessKey(UUIDUtil.toByteString(UUID.randomUUID()))
.setRequest(GetPreKeysRequest.newBuilder()
.setTargetIdentifier(ServiceIdentifier.newBuilder()
.setIdentityType(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI)
.setUuid(UUIDUtil.toByteString(UUID.randomUUID()))
.build())
.build())
.build()));
assertGetKeysFailure(Status.UNAUTHENTICATED,
GetPreKeysAnonymousRequest.newBuilder()
.setUnidentifiedAccessKey(UUIDUtil.toByteString(UUID.randomUUID()))
.setRequest(GetPreKeysRequest.newBuilder()
.setTargetIdentifier(ServiceIdentifierUtil.toGrpcServiceIdentifier(nonexistentAci)))
.build());
assertEquals(Status.Code.UNAUTHENTICATED, exception.getStatus().getCode());
verifyNoInteractions(keysManager);
}
@Test
void getPreKeysAccountNotFoundGroupSendEndorsement() throws Exception {
final AciServiceIdentifier nonexistentAci = new AciServiceIdentifier(UUID.randomUUID());
// Expirations must be on day boundaries or libsignal will refuse to create or verify the token
final Instant expiration = Instant.now().truncatedTo(ChronoUnit.DAYS);
CLOCK.pin(expiration.minus(Duration.ofHours(1))); // set time so the credential isn't expired yet
final byte[] token = AuthHelper.validGroupSendToken(SERVER_SECRET_PARAMS, List.of(nonexistentAci), expiration);
when(accountsManager.getByServiceIdentifierAsync(nonexistentAci))
.thenReturn(CompletableFuture.completedFuture(Optional.empty()));
assertGetKeysFailure(Status.NOT_FOUND,
GetPreKeysAnonymousRequest.newBuilder()
.setGroupSendToken(ByteString.copyFrom(token))
.setRequest(GetPreKeysRequest.newBuilder()
.setTargetIdentifier(ServiceIdentifierUtil.toGrpcServiceIdentifier(nonexistentAci)))
.build());
verifyNoInteractions(keysManager);
}
@ParameterizedTest
@ -203,16 +324,14 @@ class KeysAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<KeysAnonymousGrpcS
when(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(accountIdentifier)))
.thenReturn(CompletableFuture.completedFuture(Optional.of(targetAccount)));
assertStatusException(Status.NOT_FOUND, () -> unauthenticatedServiceStub().getPreKeys(GetPreKeysAnonymousRequest.newBuilder()
assertGetKeysFailure(Status.NOT_FOUND, GetPreKeysAnonymousRequest.newBuilder()
.setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey))
.setRequest(GetPreKeysRequest.newBuilder()
.setTargetIdentifier(ServiceIdentifier.newBuilder()
.setIdentityType(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI)
.setUuid(UUIDUtil.toByteString(accountIdentifier))
.build())
.setDeviceId(deviceId)
.build())
.build()));
.setUuid(UUIDUtil.toByteString(accountIdentifier)))
.setDeviceId(deviceId))
.build());
}
@Test
@ -291,4 +410,32 @@ class KeysAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<KeysAnonymousGrpcS
throw new AssertionError("All Java implementations must support SHA-256 MessageDigest algorithm", e);
}
}
private void assertGetKeysFailure(Status code, GetPreKeysAnonymousRequest request) {
assertStatusException(code, () -> unauthenticatedServiceStub().getPreKeys(request));
}
private static EcPreKey toGrpcEcPreKey(final ECPreKey preKey) {
return EcPreKey.newBuilder()
.setKeyId(preKey.keyId())
.setPublicKey(ByteString.copyFrom(preKey.publicKey().serialize()))
.build();
}
private static EcSignedPreKey toGrpcEcSignedPreKey(final ECSignedPreKey preKey) {
return EcSignedPreKey.newBuilder()
.setKeyId(preKey.keyId())
.setPublicKey(ByteString.copyFrom(preKey.publicKey().serialize()))
.setSignature(ByteString.copyFrom(preKey.signature()))
.build();
}
private static KemSignedPreKey toGrpcKemSignedPreKey(final KEMSignedPreKey preKey) {
return KemSignedPreKey.newBuilder()
.setKeyId(preKey.keyId())
.setPublicKey(ByteString.copyFrom(preKey.publicKey().serialize()))
.setSignature(ByteString.copyFrom(preKey.signature()))
.build();
}
}

View File

@ -18,6 +18,8 @@ import io.dropwizard.auth.PolymorphicAuthDynamicFeature;
import io.dropwizard.auth.basic.BasicCredentialAuthFilter;
import io.dropwizard.auth.basic.BasicCredentials;
import java.security.Principal;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
@ -26,17 +28,31 @@ import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.UUID;
import java.util.stream.Stream;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.ServiceId;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECKeyPair;
import org.signal.libsignal.zkgroup.ServerPublicParams;
import org.signal.libsignal.zkgroup.ServerSecretParams;
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.GroupSendEndorsementsResponse;
import org.signal.libsignal.zkgroup.groupsend.GroupSendFullToken;
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsementsResponse.ReceivedEndorsements;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
import org.whispersystems.textsecuregcm.identity.IdentityType;
import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier;
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
@ -317,4 +333,33 @@ public class AuthHelper {
EXTENSION_TEST_ACCOUNTS.clear();
}
}
public static byte[] validGroupSendToken(ServerSecretParams serverSecretParams, List<ServiceIdentifier> recipients, Instant expiration) throws Exception {
final ServerPublicParams serverPublicParams = serverSecretParams.getPublicParams();
final GroupMasterKey groupMasterKey = new GroupMasterKey(new byte[32]);
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
final ClientZkGroupCipher clientZkGroupCipher = new ClientZkGroupCipher(groupSecretParams);
final ServiceId.Aci sender = new ServiceId.Aci(UUID.randomUUID());
List<ServiceId> groupPlaintexts = Stream.concat(Stream.of(sender), recipients.stream().map(ServiceIdentifier::toLibsignal)).toList();
List<UuidCiphertext> groupCiphertexts = groupPlaintexts.stream()
.map(clientZkGroupCipher::encrypt)
.toList();
GroupSendDerivedKeyPair keyPair = GroupSendDerivedKeyPair.forExpiration(expiration, serverSecretParams);
GroupSendEndorsementsResponse endorsementsResponse =
GroupSendEndorsementsResponse.issue(groupCiphertexts, keyPair);
ReceivedEndorsements endorsements =
endorsementsResponse.receive(
groupPlaintexts,
sender,
expiration.minus(Duration.ofDays(1)),
groupSecretParams,
serverPublicParams);
GroupSendFullToken token = endorsements.combinedEndorsement().toFullToken(groupSecretParams, expiration);
return token.serialize();
}
public static String validGroupSendTokenHeader(ServerSecretParams serverSecretParams, List<ServiceIdentifier> recipients, Instant expiration) throws Exception {
return Base64.getEncoder().encodeToString(validGroupSendToken(serverSecretParams, recipients, expiration));
}
}