Group Send Endorsement support for pre-key fetch endpoint
This commit is contained in:
parent
ab64828661
commit
b8f64fe3d4
|
@ -795,7 +795,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
.intercept(requestAttributesInterceptor)
|
.intercept(requestAttributesInterceptor)
|
||||||
.intercept(new ProhibitAuthenticationInterceptor(clientConnectionManager))
|
.intercept(new ProhibitAuthenticationInterceptor(clientConnectionManager))
|
||||||
.addService(new AccountsAnonymousGrpcService(accountsManager, rateLimiters))
|
.addService(new AccountsAnonymousGrpcService(accountsManager, rateLimiters))
|
||||||
.addService(new KeysAnonymousGrpcService(accountsManager, keysManager))
|
.addService(new KeysAnonymousGrpcService(accountsManager, keysManager, zkSecretParams, Clock.systemUTC()))
|
||||||
.addService(new PaymentsGrpcService(currencyManager))
|
.addService(new PaymentsGrpcService(currencyManager))
|
||||||
.addService(ExternalServiceCredentialsAnonymousGrpcService.create(accountsManager, config))
|
.addService(ExternalServiceCredentialsAnonymousGrpcService.create(accountsManager, config))
|
||||||
.addService(new ProfileAnonymousGrpcService(accountsManager, profilesManager, profileBadgeConverter, zkProfileOperations));
|
.addService(new ProfileAnonymousGrpcService(accountsManager, profilesManager, profileBadgeConverter, zkProfileOperations));
|
||||||
|
@ -961,7 +961,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
new DirectoryV2Controller(directoryV2CredentialsGenerator),
|
new DirectoryV2Controller(directoryV2CredentialsGenerator),
|
||||||
new DonationController(clock, zkReceiptOperations, redeemedReceiptsManager, accountsManager, config.getBadges(),
|
new DonationController(clock, zkReceiptOperations, redeemedReceiptsManager, accountsManager, config.getBadges(),
|
||||||
ReceiptCredentialPresentation::new),
|
ReceiptCredentialPresentation::new),
|
||||||
new KeysController(rateLimiters, keysManager, accountsManager),
|
new KeysController(rateLimiters, keysManager, accountsManager, zkSecretParams, Clock.systemUTC()),
|
||||||
new MessageController(rateLimiters, messageByteLimitCardinalityEstimator, messageSender, receiptSender,
|
new MessageController(rateLimiters, messageByteLimitCardinalityEstimator, messageSender, receiptSender,
|
||||||
accountsManager, messagesManager, pushNotificationManager, reportMessageManager,
|
accountsManager, messagesManager, pushNotificationManager, reportMessageManager,
|
||||||
multiRecipientMessageExecutor, messageDeliveryScheduler, reportSpamTokenProvider, clientReleaseManager,
|
multiRecipientMessageExecutor, messageDeliveryScheduler, reportSpamTokenProvider, clientReleaseManager,
|
||||||
|
|
|
@ -18,6 +18,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.time.Clock;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
@ -30,18 +31,26 @@ import javax.ws.rs.Consumes;
|
||||||
import javax.ws.rs.DefaultValue;
|
import javax.ws.rs.DefaultValue;
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
import javax.ws.rs.HeaderParam;
|
import javax.ws.rs.HeaderParam;
|
||||||
|
import javax.ws.rs.NotAuthorizedException;
|
||||||
import javax.ws.rs.POST;
|
import javax.ws.rs.POST;
|
||||||
import javax.ws.rs.PUT;
|
import javax.ws.rs.PUT;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
import javax.ws.rs.PathParam;
|
import javax.ws.rs.PathParam;
|
||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
import javax.ws.rs.QueryParam;
|
import javax.ws.rs.QueryParam;
|
||||||
|
import javax.ws.rs.BadRequestException;
|
||||||
|
import javax.ws.rs.NotFoundException;
|
||||||
import javax.ws.rs.WebApplicationException;
|
import javax.ws.rs.WebApplicationException;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import org.signal.libsignal.protocol.IdentityKey;
|
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.Anonymous;
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.GroupSendTokenHeader;
|
||||||
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
|
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
|
||||||
import org.whispersystems.textsecuregcm.entities.CheckKeysRequest;
|
import org.whispersystems.textsecuregcm.entities.CheckKeysRequest;
|
||||||
import org.whispersystems.textsecuregcm.entities.ECPreKey;
|
import org.whispersystems.textsecuregcm.entities.ECPreKey;
|
||||||
|
@ -74,6 +83,8 @@ public class KeysController {
|
||||||
private final RateLimiters rateLimiters;
|
private final RateLimiters rateLimiters;
|
||||||
private final KeysManager keysManager;
|
private final KeysManager keysManager;
|
||||||
private final AccountsManager accounts;
|
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 GET_KEYS_COUNTER_NAME = MetricsUtil.name(KeysController.class, "getKeys");
|
||||||
private static final String STORE_KEYS_COUNTER_NAME = MetricsUtil.name(KeysController.class, "storeKeys");
|
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];
|
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.rateLimiters = rateLimiters;
|
||||||
this.keysManager = keysManager;
|
this.keysManager = keysManager;
|
||||||
this.accounts = accounts;
|
this.accounts = accounts;
|
||||||
|
this.serverSecretParams = serverSecretParams;
|
||||||
|
this.clock = clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
|
@ -298,7 +311,8 @@ public class KeysController {
|
||||||
@Operation(summary = "Fetch public keys for another user",
|
@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")
|
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 = "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 = "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(
|
@ApiResponse(responseCode = "429", description = "Rate limit exceeded.", headers = @Header(
|
||||||
name = "Retry-After",
|
name = "Retry-After",
|
||||||
|
@ -306,6 +320,7 @@ public class KeysController {
|
||||||
public PreKeyResponse getDeviceKeys(
|
public PreKeyResponse getDeviceKeys(
|
||||||
@ReadOnly @Auth Optional<AuthenticatedAccount> auth,
|
@ReadOnly @Auth Optional<AuthenticatedAccount> auth,
|
||||||
@HeaderParam(HeaderUtils.UNIDENTIFIED_ACCESS_KEY) Optional<Anonymous> accessKey,
|
@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")
|
@Parameter(description="the account or phone-number identifier to retrieve keys for")
|
||||||
@PathParam("identifier") ServiceIdentifier targetIdentifier,
|
@PathParam("identifier") ServiceIdentifier targetIdentifier,
|
||||||
|
@ -316,20 +331,27 @@ public class KeysController {
|
||||||
@HeaderParam(HttpHeaders.USER_AGENT) String userAgent)
|
@HeaderParam(HttpHeaders.USER_AGENT) String userAgent)
|
||||||
throws RateLimitExceededException {
|
throws RateLimitExceededException {
|
||||||
|
|
||||||
if (auth.isEmpty() && accessKey.isEmpty()) {
|
if (auth.isEmpty() && accessKey.isEmpty() && groupSendToken.isEmpty()) {
|
||||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||||
}
|
}
|
||||||
|
|
||||||
final Optional<Account> account = auth.map(AuthenticatedAccount::getAccount);
|
final Optional<Account> account = auth.map(AuthenticatedAccount::getAccount);
|
||||||
|
final Optional<Account> maybeTarget = accounts.getByServiceIdentifier(targetIdentifier);
|
||||||
|
|
||||||
final Account target;
|
if (groupSendToken.isPresent()) {
|
||||||
{
|
if (auth.isPresent() || accessKey.isPresent()) {
|
||||||
final Optional<Account> maybeTarget = accounts.getByServiceIdentifier(targetIdentifier);
|
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);
|
OptionalAccess.verify(account, accessKey, maybeTarget, deviceId);
|
||||||
|
|
||||||
target = maybeTarget.orElseThrow();
|
|
||||||
}
|
}
|
||||||
|
final Account target = maybeTarget.orElseThrow(NotFoundException::new);
|
||||||
|
|
||||||
if (account.isPresent()) {
|
if (account.isPresent()) {
|
||||||
rateLimiters.getPreKeysLimiter().validate(
|
rateLimiters.getPreKeysLimiter().validate(
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,15 +9,20 @@ import com.google.protobuf.ByteString;
|
||||||
import io.grpc.Status;
|
import io.grpc.Status;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.time.Clock;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.signal.chat.keys.CheckIdentityKeyRequest;
|
import org.signal.chat.keys.CheckIdentityKeyRequest;
|
||||||
import org.signal.chat.keys.CheckIdentityKeyResponse;
|
import org.signal.chat.keys.CheckIdentityKeyResponse;
|
||||||
import org.signal.chat.keys.GetPreKeysAnonymousRequest;
|
import org.signal.chat.keys.GetPreKeysAnonymousRequest;
|
||||||
import org.signal.chat.keys.GetPreKeysResponse;
|
import org.signal.chat.keys.GetPreKeysResponse;
|
||||||
import org.signal.chat.keys.ReactorKeysAnonymousGrpc;
|
import org.signal.chat.keys.ReactorKeysAnonymousGrpc;
|
||||||
import org.signal.libsignal.protocol.IdentityKey;
|
import org.signal.libsignal.protocol.IdentityKey;
|
||||||
|
import org.signal.libsignal.zkgroup.ServerSecretParams;
|
||||||
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil;
|
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil;
|
||||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.KeysManager;
|
import org.whispersystems.textsecuregcm.storage.KeysManager;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
|
@ -28,11 +33,14 @@ public class KeysAnonymousGrpcService extends ReactorKeysAnonymousGrpc.KeysAnony
|
||||||
|
|
||||||
private final AccountsManager accountsManager;
|
private final AccountsManager accountsManager;
|
||||||
private final KeysManager keysManager;
|
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.accountsManager = accountsManager;
|
||||||
this.keysManager = keysManager;
|
this.keysManager = keysManager;
|
||||||
}
|
this.groupSendTokenUtil = new GroupSendTokenUtil(serverSecretParams, clock);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<GetPreKeysResponse> getPreKeys(final GetPreKeysAnonymousRequest request) {
|
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());
|
final byte deviceId = DeviceIdUtil.validate(request.getRequest().getDeviceId());
|
||||||
|
|
||||||
return Mono.fromFuture(() -> accountsManager.getByServiceIdentifierAsync(serviceIdentifier))
|
return switch (request.getAuthorizationCase()) {
|
||||||
.flatMap(Mono::justOrEmpty)
|
case GROUP_SEND_TOKEN ->
|
||||||
.switchIfEmpty(Mono.error(Status.UNAUTHENTICATED.asException()))
|
groupSendTokenUtil.checkGroupSendToken(request.getGroupSendToken(), List.of(serviceIdentifier))
|
||||||
.flatMap(targetAccount ->
|
.then(lookUpAccount(serviceIdentifier, Status.NOT_FOUND))
|
||||||
UnidentifiedAccessUtil.checkUnidentifiedAccess(targetAccount, request.getUnidentifiedAccessKey().toByteArray())
|
.flatMap(targetAccount -> KeysGrpcHelper.getPreKeys(targetAccount, serviceIdentifier.identityType(), deviceId, keysManager));
|
||||||
? KeysGrpcHelper.getPreKeys(targetAccount, serviceIdentifier.identityType(), deviceId, keysManager)
|
case UNIDENTIFIED_ACCESS_KEY ->
|
||||||
: Mono.error(Status.UNAUTHENTICATED.asException()));
|
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
|
@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) {
|
private static boolean fingerprintMatches(final IdentityKey identityKey, final byte[] fingerprint) {
|
||||||
final byte[] digest;
|
final byte[] digest;
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -164,9 +164,19 @@ message GetPreKeysAnonymousRequest {
|
||||||
GetPreKeysRequest request = 1;
|
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 {
|
message GetPreKeysResponse {
|
||||||
|
|
|
@ -29,13 +29,18 @@ import java.nio.ByteBuffer;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.OptionalInt;
|
import java.util.OptionalInt;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import javax.ws.rs.client.Entity;
|
import javax.ws.rs.client.Entity;
|
||||||
|
import javax.ws.rs.client.Invocation;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import org.glassfish.jersey.server.ServerProperties;
|
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.IdentityKey;
|
||||||
import org.signal.libsignal.protocol.ecc.Curve;
|
import org.signal.libsignal.protocol.ecc.Curve;
|
||||||
import org.signal.libsignal.protocol.ecc.ECKeyPair;
|
import org.signal.libsignal.protocol.ecc.ECKeyPair;
|
||||||
|
import org.signal.libsignal.zkgroup.ServerSecretParams;
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||||
import org.whispersystems.textsecuregcm.entities.CheckKeysRequest;
|
import org.whispersystems.textsecuregcm.entities.CheckKeysRequest;
|
||||||
import org.whispersystems.textsecuregcm.entities.ECPreKey;
|
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.AciServiceIdentifier;
|
||||||
import org.whispersystems.textsecuregcm.identity.IdentityType;
|
import org.whispersystems.textsecuregcm.identity.IdentityType;
|
||||||
import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier;
|
import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier;
|
||||||
|
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
import org.whispersystems.textsecuregcm.mappers.CompletionExceptionMapper;
|
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.ByteArrayAdapter;
|
||||||
import org.whispersystems.textsecuregcm.util.CompletableFutureTestUtil;
|
import org.whispersystems.textsecuregcm.util.CompletableFutureTestUtil;
|
||||||
import org.whispersystems.textsecuregcm.util.HeaderUtils;
|
import org.whispersystems.textsecuregcm.util.HeaderUtils;
|
||||||
|
import org.whispersystems.textsecuregcm.util.TestClock;
|
||||||
|
|
||||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||||
class KeysControllerTest {
|
class KeysControllerTest {
|
||||||
|
@ -86,8 +94,13 @@ class KeysControllerTest {
|
||||||
private static final String EXISTS_NUMBER = "+14152222222";
|
private static final String EXISTS_NUMBER = "+14152222222";
|
||||||
private static final UUID EXISTS_UUID = UUID.randomUUID();
|
private static final UUID EXISTS_UUID = UUID.randomUUID();
|
||||||
private static final UUID EXISTS_PNI = 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 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_ID = 1;
|
||||||
private static final byte SAMPLE_DEVICE_ID2 = 2;
|
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 RateLimiters rateLimiters = mock(RateLimiters.class);
|
||||||
private static final RateLimiter rateLimiter = mock(RateLimiter.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()
|
private static final ResourceExtension resources = ResourceExtension.builder()
|
||||||
.addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE)
|
.addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE)
|
||||||
.addProvider(AuthHelper.getAuthFilter())
|
.addProvider(AuthHelper.getAuthFilter())
|
||||||
|
@ -143,7 +160,7 @@ class KeysControllerTest {
|
||||||
.addProvider(new AuthValueFactoryProvider.Binder<>(AuthenticatedAccount.class))
|
.addProvider(new AuthValueFactoryProvider.Binder<>(AuthenticatedAccount.class))
|
||||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||||
.addResource(new ServerRejectedExceptionMapper())
|
.addResource(new ServerRejectedExceptionMapper())
|
||||||
.addResource(new KeysController(rateLimiters, KEYS, accounts))
|
.addResource(new KeysController(rateLimiters, KEYS, accounts, serverSecretParams, clock))
|
||||||
.addResource(new RateLimitExceededExceptionMapper())
|
.addResource(new RateLimitExceededExceptionMapper())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
@ -183,6 +200,8 @@ class KeysControllerTest {
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setup() {
|
void setup() {
|
||||||
|
clock.unpin();
|
||||||
|
|
||||||
sampleDevice = mock(Device.class);
|
sampleDevice = mock(Device.class);
|
||||||
final Device sampleDevice2 = mock(Device.class);
|
final Device sampleDevice2 = mock(Device.class);
|
||||||
final Device sampleDevice3 = mock(Device.class);
|
final Device sampleDevice3 = mock(Device.class);
|
||||||
|
@ -529,6 +548,68 @@ class KeysControllerTest {
|
||||||
verifyNoMoreInteractions(KEYS);
|
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
|
@Test
|
||||||
void testNoDevices() {
|
void testNoDevices() {
|
||||||
|
|
||||||
|
|
|
@ -433,7 +433,7 @@ class MessageControllerTest {
|
||||||
.queryParam("story", story)
|
.queryParam("story", story)
|
||||||
.request()
|
.request()
|
||||||
.header(HeaderUtils.GROUP_SEND_TOKEN,
|
.header(HeaderUtils.GROUP_SEND_TOKEN,
|
||||||
validGroupSendTokenHeader(List.of(authorizedRecipient), expiration));
|
AuthHelper.validGroupSendTokenHeader(serverSecretParams, List.of(authorizedRecipient), expiration));
|
||||||
|
|
||||||
if (includeUak) {
|
if (includeUak) {
|
||||||
builder = builder.header(HeaderUtils.UNIDENTIFIED_ACCESS_KEY, Base64.getEncoder().encodeToString(UNIDENTIFIED_ACCESS_BYTES));
|
builder = builder.header(HeaderUtils.UNIDENTIFIED_ACCESS_KEY, Base64.getEncoder().encodeToString(UNIDENTIFIED_ACCESS_BYTES));
|
||||||
|
@ -1351,8 +1351,8 @@ class MessageControllerTest {
|
||||||
.queryParam("urgent", false)
|
.queryParam("urgent", false)
|
||||||
.request()
|
.request()
|
||||||
.header(HttpHeaders.USER_AGENT, "FIXME")
|
.header(HttpHeaders.USER_AGENT, "FIXME")
|
||||||
.header(HeaderUtils.GROUP_SEND_TOKEN, validGroupSendTokenHeader(
|
.header(HeaderUtils.GROUP_SEND_TOKEN, AuthHelper.validGroupSendTokenHeader(
|
||||||
List.of(SINGLE_DEVICE_ACI_ID, MULTI_DEVICE_ACI_ID), Instant.parse("2024-04-10T00:00:00.00Z")))
|
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));
|
.put(Entity.entity(stream, MultiRecipientMessageProvider.MEDIA_TYPE));
|
||||||
|
|
||||||
assertThat("Unexpected response", response.getStatus(), is(equalTo(200)));
|
assertThat("Unexpected response", response.getStatus(), is(equalTo(200)));
|
||||||
|
@ -1389,8 +1389,8 @@ class MessageControllerTest {
|
||||||
.queryParam("urgent", false)
|
.queryParam("urgent", false)
|
||||||
.request()
|
.request()
|
||||||
.header(HttpHeaders.USER_AGENT, "FIXME")
|
.header(HttpHeaders.USER_AGENT, "FIXME")
|
||||||
.header(HeaderUtils.GROUP_SEND_TOKEN, validGroupSendTokenHeader(
|
.header(HeaderUtils.GROUP_SEND_TOKEN, AuthHelper.validGroupSendTokenHeader(
|
||||||
List.of(MULTI_DEVICE_ACI_ID), Instant.parse("2024-04-10T00:00:00.00Z")))
|
serverSecretParams, List.of(MULTI_DEVICE_ACI_ID), Instant.parse("2024-04-10T00:00:00.00Z")))
|
||||||
.put(Entity.entity(stream, MultiRecipientMessageProvider.MEDIA_TYPE));
|
.put(Entity.entity(stream, MultiRecipientMessageProvider.MEDIA_TYPE));
|
||||||
|
|
||||||
assertThat("Unexpected response", response.getStatus(), is(equalTo(401)));
|
assertThat("Unexpected response", response.getStatus(), is(equalTo(401)));
|
||||||
|
@ -1419,39 +1419,14 @@ class MessageControllerTest {
|
||||||
.queryParam("urgent", false)
|
.queryParam("urgent", false)
|
||||||
.request()
|
.request()
|
||||||
.header(HttpHeaders.USER_AGENT, "FIXME")
|
.header(HttpHeaders.USER_AGENT, "FIXME")
|
||||||
.header(HeaderUtils.GROUP_SEND_TOKEN, validGroupSendTokenHeader(
|
.header(HeaderUtils.GROUP_SEND_TOKEN, AuthHelper.validGroupSendTokenHeader(
|
||||||
List.of(SINGLE_DEVICE_ACI_ID, MULTI_DEVICE_ACI_ID), Instant.parse("2024-04-10T00:00:00.00Z")))
|
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));
|
.put(Entity.entity(stream, MultiRecipientMessageProvider.MEDIA_TYPE));
|
||||||
|
|
||||||
assertThat("Unexpected response", response.getStatus(), is(equalTo(401)));
|
assertThat("Unexpected response", response.getStatus(), is(equalTo(401)));
|
||||||
verifyNoMoreInteractions(messageSender);
|
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
|
@ParameterizedTest
|
||||||
@ValueSource(booleans = {true, false})
|
@ValueSource(booleans = {true, false})
|
||||||
void testMultiRecipientRedisBombProtection(final boolean useExplicitIdentifier) throws Exception {
|
void testMultiRecipientRedisBombProtection(final boolean useExplicitIdentifier) throws Exception {
|
||||||
|
|
|
@ -6,23 +6,27 @@
|
||||||
package org.whispersystems.textsecuregcm.grpc;
|
package org.whispersystems.textsecuregcm.grpc;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
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.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyByte;
|
import static org.mockito.ArgumentMatchers.anyByte;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertStatusException;
|
import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertStatusException;
|
||||||
|
|
||||||
import com.google.protobuf.ByteString;
|
import com.google.protobuf.ByteString;
|
||||||
import io.grpc.Status;
|
import io.grpc.Status;
|
||||||
import io.grpc.StatusRuntimeException;
|
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.ValueSource;
|
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.InvalidKeyException;
|
||||||
import org.signal.libsignal.protocol.ecc.Curve;
|
import org.signal.libsignal.protocol.ecc.Curve;
|
||||||
import org.signal.libsignal.protocol.ecc.ECKeyPair;
|
import org.signal.libsignal.protocol.ecc.ECKeyPair;
|
||||||
|
import org.signal.libsignal.zkgroup.ServerSecretParams;
|
||||||
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil;
|
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil;
|
||||||
import org.whispersystems.textsecuregcm.entities.ECPreKey;
|
import org.whispersystems.textsecuregcm.entities.ECPreKey;
|
||||||
import org.whispersystems.textsecuregcm.entities.ECSignedPreKey;
|
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.AccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
import org.whispersystems.textsecuregcm.storage.KeysManager;
|
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.tests.util.KeysHelper;
|
||||||
|
import org.whispersystems.textsecuregcm.util.TestClock;
|
||||||
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
|
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
|
||||||
import org.whispersystems.textsecuregcm.util.UUIDUtil;
|
import org.whispersystems.textsecuregcm.util.UUIDUtil;
|
||||||
import org.whispersystems.textsecuregcm.util.Util;
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
|
@ -60,6 +68,9 @@ import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
class KeysAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<KeysAnonymousGrpcService, KeysAnonymousGrpc.KeysAnonymousBlockingStub> {
|
class KeysAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<KeysAnonymousGrpcService, KeysAnonymousGrpc.KeysAnonymousBlockingStub> {
|
||||||
|
|
||||||
|
private static final ServerSecretParams SERVER_SECRET_PARAMS = ServerSecretParams.generate();
|
||||||
|
private static final TestClock CLOCK = TestClock.now();
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private AccountsManager accountsManager;
|
private AccountsManager accountsManager;
|
||||||
|
|
||||||
|
@ -68,71 +79,54 @@ class KeysAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<KeysAnonymousGrpcS
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected KeysAnonymousGrpcService createServiceBeforeEachTest() {
|
protected KeysAnonymousGrpcService createServiceBeforeEachTest() {
|
||||||
return new KeysAnonymousGrpcService(accountsManager, keysManager);
|
return new KeysAnonymousGrpcService(accountsManager, keysManager, SERVER_SECRET_PARAMS, CLOCK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getPreKeys() {
|
void getPreKeysUnidentifiedAccessKey() {
|
||||||
final Account targetAccount = mock(Account.class);
|
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 ECKeyPair identityKeyPair = Curve.generateKeyPair();
|
||||||
final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey());
|
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);
|
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.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(targetAccount.getIdentityKey(IdentityType.ACI)).thenReturn(identityKey);
|
||||||
when(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(identifier)))
|
when(accountsManager.getByServiceIdentifierAsync(identifier))
|
||||||
.thenReturn(CompletableFuture.completedFuture(Optional.of(targetAccount)));
|
.thenReturn(CompletableFuture.completedFuture(Optional.of(targetAccount)));
|
||||||
|
|
||||||
final ECPreKey ecPreKey = new ECPreKey(1, Curve.generateKeyPair().getPublicKey());
|
final ECPreKey ecPreKey = new ECPreKey(1, Curve.generateKeyPair().getPublicKey());
|
||||||
final ECSignedPreKey ecSignedPreKey = KeysHelper.signedECPreKey(2, identityKeyPair);
|
final ECSignedPreKey ecSignedPreKey = KeysHelper.signedECPreKey(2, identityKeyPair);
|
||||||
final KEMSignedPreKey kemSignedPreKey = KeysHelper.signedKEMPreKey(3, 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)));
|
.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)));
|
.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)));
|
.thenReturn(CompletableFuture.completedFuture(Optional.of(ecSignedPreKey)));
|
||||||
|
|
||||||
final GetPreKeysResponse response = unauthenticatedServiceStub().getPreKeys(GetPreKeysAnonymousRequest.newBuilder()
|
final GetPreKeysResponse response = unauthenticatedServiceStub().getPreKeys(GetPreKeysAnonymousRequest.newBuilder()
|
||||||
.setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey))
|
.setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey))
|
||||||
.setRequest(GetPreKeysRequest.newBuilder()
|
.setRequest(GetPreKeysRequest.newBuilder()
|
||||||
.setTargetIdentifier(ServiceIdentifier.newBuilder()
|
.setTargetIdentifier(ServiceIdentifierUtil.toGrpcServiceIdentifier(identifier))
|
||||||
.setIdentityType(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI)
|
.setDeviceId(Device.PRIMARY_ID))
|
||||||
.setUuid(UUIDUtil.toByteString(identifier))
|
|
||||||
.build())
|
|
||||||
.setDeviceId(Device.PRIMARY_ID)
|
|
||||||
.build())
|
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
final GetPreKeysResponse expectedResponse = GetPreKeysResponse.newBuilder()
|
final GetPreKeysResponse expectedResponse = GetPreKeysResponse.newBuilder()
|
||||||
.setIdentityKey(ByteString.copyFrom(identityKey.serialize()))
|
.setIdentityKey(ByteString.copyFrom(identityKey.serialize()))
|
||||||
.putPreKeys(Device.PRIMARY_ID, GetPreKeysResponse.PreKeyBundle.newBuilder()
|
.putPreKeys(Device.PRIMARY_ID, GetPreKeysResponse.PreKeyBundle.newBuilder()
|
||||||
.setEcOneTimePreKey(EcPreKey.newBuilder()
|
.setEcOneTimePreKey(toGrpcEcPreKey(ecPreKey))
|
||||||
.setKeyId(ecPreKey.keyId())
|
.setEcSignedPreKey(toGrpcEcSignedPreKey(ecSignedPreKey))
|
||||||
.setPublicKey(ByteString.copyFrom(ecPreKey.serializedPublicKey()))
|
.setKemOneTimePreKey(toGrpcKemSignedPreKey(kemSignedPreKey))
|
||||||
.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())
|
|
||||||
.build())
|
.build())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
@ -140,50 +134,177 @@ class KeysAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<KeysAnonymousGrpcS
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getPreKeysIncorrectUnidentifiedAccessKey() {
|
void getPreKeysGroupSendEndorsement() throws Exception {
|
||||||
final Account targetAccount = mock(Account.class);
|
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 ECKeyPair identityKeyPair = Curve.generateKeyPair();
|
||||||
final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey());
|
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);
|
final byte[] unidentifiedAccessKey = TestRandomUtil.nextBytes(UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH);
|
||||||
|
|
||||||
when(targetAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey));
|
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(targetAccount.getIdentityKey(IdentityType.ACI)).thenReturn(identityKey);
|
||||||
when(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(identifier)))
|
when(accountsManager.getByServiceIdentifierAsync(identifier))
|
||||||
.thenReturn(CompletableFuture.completedFuture(Optional.of(targetAccount)));
|
.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()
|
.setRequest(GetPreKeysRequest.newBuilder()
|
||||||
.setTargetIdentifier(ServiceIdentifier.newBuilder()
|
.setTargetIdentifier(ServiceIdentifierUtil.toGrpcServiceIdentifier(identifier))
|
||||||
.setIdentityType(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI)
|
.setDeviceId(Device.PRIMARY_ID))
|
||||||
.setUuid(UUIDUtil.toByteString(identifier))
|
.build());
|
||||||
.build())
|
|
||||||
.setDeviceId(Device.PRIMARY_ID)
|
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()));
|
.build();
|
||||||
|
|
||||||
|
assertEquals(expectedResponse, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getPreKeysAccountNotFound() {
|
void getPreKeysNoAuth() {
|
||||||
when(accountsManager.getByServiceIdentifierAsync(any()))
|
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()));
|
.thenReturn(CompletableFuture.completedFuture(Optional.empty()));
|
||||||
|
|
||||||
final StatusRuntimeException exception =
|
assertGetKeysFailure(Status.UNAUTHENTICATED,
|
||||||
assertThrows(StatusRuntimeException.class,
|
GetPreKeysAnonymousRequest.newBuilder()
|
||||||
() -> unauthenticatedServiceStub().getPreKeys(GetPreKeysAnonymousRequest.newBuilder()
|
.setUnidentifiedAccessKey(UUIDUtil.toByteString(UUID.randomUUID()))
|
||||||
.setUnidentifiedAccessKey(UUIDUtil.toByteString(UUID.randomUUID()))
|
.setRequest(GetPreKeysRequest.newBuilder()
|
||||||
.setRequest(GetPreKeysRequest.newBuilder()
|
.setTargetIdentifier(ServiceIdentifierUtil.toGrpcServiceIdentifier(nonexistentAci)))
|
||||||
.setTargetIdentifier(ServiceIdentifier.newBuilder()
|
.build());
|
||||||
.setIdentityType(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI)
|
|
||||||
.setUuid(UUIDUtil.toByteString(UUID.randomUUID()))
|
|
||||||
.build())
|
|
||||||
.build())
|
|
||||||
.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
|
@ParameterizedTest
|
||||||
|
@ -203,16 +324,14 @@ class KeysAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<KeysAnonymousGrpcS
|
||||||
when(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(accountIdentifier)))
|
when(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(accountIdentifier)))
|
||||||
.thenReturn(CompletableFuture.completedFuture(Optional.of(targetAccount)));
|
.thenReturn(CompletableFuture.completedFuture(Optional.of(targetAccount)));
|
||||||
|
|
||||||
assertStatusException(Status.NOT_FOUND, () -> unauthenticatedServiceStub().getPreKeys(GetPreKeysAnonymousRequest.newBuilder()
|
assertGetKeysFailure(Status.NOT_FOUND, GetPreKeysAnonymousRequest.newBuilder()
|
||||||
.setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey))
|
.setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey))
|
||||||
.setRequest(GetPreKeysRequest.newBuilder()
|
.setRequest(GetPreKeysRequest.newBuilder()
|
||||||
.setTargetIdentifier(ServiceIdentifier.newBuilder()
|
.setTargetIdentifier(ServiceIdentifier.newBuilder()
|
||||||
.setIdentityType(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI)
|
.setIdentityType(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI)
|
||||||
.setUuid(UUIDUtil.toByteString(accountIdentifier))
|
.setUuid(UUIDUtil.toByteString(accountIdentifier)))
|
||||||
.build())
|
.setDeviceId(deviceId))
|
||||||
.setDeviceId(deviceId)
|
.build());
|
||||||
.build())
|
|
||||||
.build()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -291,4 +410,32 @@ class KeysAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<KeysAnonymousGrpcS
|
||||||
throw new AssertionError("All Java implementations must support SHA-256 MessageDigest algorithm", e);
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,8 @@ import io.dropwizard.auth.PolymorphicAuthDynamicFeature;
|
||||||
import io.dropwizard.auth.basic.BasicCredentialAuthFilter;
|
import io.dropwizard.auth.basic.BasicCredentialAuthFilter;
|
||||||
import io.dropwizard.auth.basic.BasicCredentials;
|
import io.dropwizard.auth.basic.BasicCredentials;
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
@ -26,17 +28,31 @@ import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.junit.jupiter.api.extension.AfterEachCallback;
|
import org.junit.jupiter.api.extension.AfterEachCallback;
|
||||||
import org.junit.jupiter.api.extension.ExtensionContext;
|
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||||
import org.signal.libsignal.protocol.IdentityKey;
|
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.Curve;
|
||||||
import org.signal.libsignal.protocol.ecc.ECKeyPair;
|
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.AccountAuthenticator;
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||||
import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
|
import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
|
||||||
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
|
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
|
||||||
import org.whispersystems.textsecuregcm.identity.IdentityType;
|
import org.whispersystems.textsecuregcm.identity.IdentityType;
|
||||||
import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier;
|
import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier;
|
||||||
|
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
|
@ -317,4 +333,33 @@ public class AuthHelper {
|
||||||
EXTENSION_TEST_ACCOUNTS.clear();
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue