mirror of
https://github.com/signalapp/Signal-Server
synced 2026-07-07 07:16:58 +01:00
Port CertificateController to gRPC
This commit is contained in:
@@ -153,11 +153,12 @@ import org.whispersystems.textsecuregcm.grpc.BackupsAnonymousGrpcService;
|
||||
import org.whispersystems.textsecuregcm.grpc.BackupsGrpcService;
|
||||
import org.whispersystems.textsecuregcm.grpc.CallQualitySurveyGrpcService;
|
||||
import org.whispersystems.textsecuregcm.grpc.ChallengeGrpcService;
|
||||
import org.whispersystems.textsecuregcm.grpc.CredentialsAnonymousGrpcService;
|
||||
import org.whispersystems.textsecuregcm.grpc.CredentialsGrpcService;
|
||||
import org.whispersystems.textsecuregcm.grpc.DevicesGrpcService;
|
||||
import org.whispersystems.textsecuregcm.grpc.ErrorConformanceInterceptor;
|
||||
import org.whispersystems.textsecuregcm.grpc.ErrorMappingInterceptor;
|
||||
import org.whispersystems.textsecuregcm.grpc.ExternalServiceCredentialsAnonymousGrpcService;
|
||||
import org.whispersystems.textsecuregcm.grpc.ExternalServiceCredentialsGrpcService;
|
||||
import org.whispersystems.textsecuregcm.grpc.ExternalServiceDefinitions;
|
||||
import org.whispersystems.textsecuregcm.grpc.GroupSendTokenUtil;
|
||||
import org.whispersystems.textsecuregcm.grpc.GrpcAllowListInterceptor;
|
||||
import org.whispersystems.textsecuregcm.grpc.KeysAnonymousGrpcService;
|
||||
@@ -986,9 +987,15 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
final ProhibitAuthenticationInterceptor prohibitAuthenticationInterceptor = new ProhibitAuthenticationInterceptor();
|
||||
final GroupSendTokenUtil groupSendTokenUtil = new GroupSendTokenUtil(zkSecretParams, Clock.systemUTC());
|
||||
|
||||
final CertificateGenerator certificateGenerator =
|
||||
new CertificateGenerator(config.getDeliveryCertificate().certificate(),
|
||||
config.getDeliveryCertificate().ecPrivateKey(),
|
||||
config.getDeliveryCertificate().expiresDays(),
|
||||
config.getDeliveryCertificate().embedSigner());
|
||||
|
||||
final List<ServerServiceDefinition> authenticatedServices = Stream.of(
|
||||
new AccountsGrpcService(accountsManager, rateLimiters, usernameHashZkProofVerifier, registrationRecoveryPasswordsManager),
|
||||
ExternalServiceCredentialsGrpcService.createForAllExternalServices(config, rateLimiters),
|
||||
new CredentialsGrpcService(accountsManager, certificateGenerator, zkAuthOperations, callingGenericZkSecretParams, rateLimiters, Clock.systemUTC(), ExternalServiceDefinitions.createExternalServiceList(config, Clock.systemUTC())),
|
||||
new KeysGrpcService(accountsManager, keysManager, rateLimiters),
|
||||
new ProfileGrpcService(clock, accountsManager, profilesManager, dynamicConfigurationManager, config.getBadges(), profileCdnPolicyGenerator, profileCdnPolicySigner, profileBadgeConverter, rateLimiters),
|
||||
new MessagesGrpcService(accountsManager, rateLimiters, messageSender, messageByteLimitCardinalityEstimator, spamChecker, Clock.systemUTC()),
|
||||
@@ -1017,7 +1024,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
new ProfileAnonymousGrpcService(accountsManager, profilesManager, profileBadgeConverter, zkSecretParams),
|
||||
new MessagesAnonymousGrpcService(accountsManager, rateLimiters, messageSender, groupSendTokenUtil, messageByteLimitCardinalityEstimator, spamChecker, Clock.systemUTC()),
|
||||
new BackupsAnonymousGrpcService(backupManager, backupMetrics, config.getAttachments().maxAttachmentUploadSizeInBytes(), config.getAttachments().maxMessageBackupUploadSizeInBytes()),
|
||||
ExternalServiceCredentialsAnonymousGrpcService.create(accountsManager, config))
|
||||
new CredentialsAnonymousGrpcService(accountsManager, ExternalServiceDefinitions.SVR.generatorFactory().apply(config, Clock.systemUTC())))
|
||||
.map(bindableService -> ServerInterceptors.intercept(bindableService,
|
||||
// Note: interceptors run in the reverse order they are added; the remote deprecation filter
|
||||
// depends on the user-agent context so it has to come first here!
|
||||
@@ -1140,9 +1147,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
new CallRoutingControllerV2(rateLimiters, cloudflareTurnCredentialsManager),
|
||||
new CallLinkController(rateLimiters, callingGenericZkSecretParams),
|
||||
new CallQualitySurveyController(callQualitySurveyManager),
|
||||
new CertificateController(accountsManager, new CertificateGenerator(config.getDeliveryCertificate().certificate(),
|
||||
config.getDeliveryCertificate().ecPrivateKey(), config.getDeliveryCertificate().expiresDays(), config.getDeliveryCertificate().embedSigner()),
|
||||
zkAuthOperations, callingGenericZkSecretParams, clock),
|
||||
new CertificateController(accountsManager, certificateGenerator, zkAuthOperations, callingGenericZkSecretParams, clock),
|
||||
new ChallengeController(accountsManager, rateLimitChallengeManager, challengeConstraintChecker),
|
||||
new DeviceController(accountsManager, rateLimiters, persistentTimer),
|
||||
new DeviceCheckController(clock, accountsManager, backupAuthManager, appleDeviceCheckManager, rateLimiters,
|
||||
|
||||
+1
-3
@@ -20,7 +20,6 @@ import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.WebApplicationException;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
@@ -76,8 +75,7 @@ public class CertificateController {
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/delivery")
|
||||
public DeliveryCertificate getDeliveryCertificate(@Auth AuthenticatedDevice auth,
|
||||
@QueryParam("includeE164") @DefaultValue("true") boolean includeE164)
|
||||
throws InvalidKeyException {
|
||||
@QueryParam("includeE164") @DefaultValue("true") boolean includeE164) {
|
||||
|
||||
Metrics.counter(GENERATE_DELIVERY_CERTIFICATE_COUNTER_NAME, INCLUDE_E164_TAG_NAME, String.valueOf(includeE164))
|
||||
.increment();
|
||||
|
||||
+19
-31
@@ -1,53 +1,35 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.grpc;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import java.time.Clock;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.signal.chat.credentials.AuthCheckResult;
|
||||
import org.signal.chat.credentials.CheckSvrCredentialsRequest;
|
||||
import org.signal.chat.credentials.CheckSvrCredentialsResponse;
|
||||
import org.signal.chat.credentials.SimpleExternalServiceCredentialsAnonymousGrpc;
|
||||
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
|
||||
import org.signal.chat.credentials.SimpleCredentialsAnonymousGrpc;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsSelector;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class ExternalServiceCredentialsAnonymousGrpcService extends
|
||||
SimpleExternalServiceCredentialsAnonymousGrpc.ExternalServiceCredentialsAnonymousImplBase {
|
||||
public class CredentialsAnonymousGrpcService extends SimpleCredentialsAnonymousGrpc.CredentialsAnonymousImplBase {
|
||||
|
||||
private final AccountsManager accountsManager;
|
||||
private final ExternalServiceCredentialsGenerator svrCredentialsGenerator;
|
||||
|
||||
private static final long MAX_SVR_PASSWORD_AGE_SECONDS = TimeUnit.DAYS.toSeconds(30);
|
||||
|
||||
private final ExternalServiceCredentialsGenerator svrCredentialsGenerator;
|
||||
|
||||
private final AccountsManager accountsManager;
|
||||
|
||||
|
||||
public static ExternalServiceCredentialsAnonymousGrpcService create(
|
||||
final AccountsManager accountsManager,
|
||||
final WhisperServerConfiguration chatConfiguration) {
|
||||
return new ExternalServiceCredentialsAnonymousGrpcService(
|
||||
accountsManager,
|
||||
ExternalServiceDefinitions.SVR.generatorFactory().apply(chatConfiguration, Clock.systemUTC())
|
||||
);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
ExternalServiceCredentialsAnonymousGrpcService(
|
||||
final AccountsManager accountsManager,
|
||||
public CredentialsAnonymousGrpcService(final AccountsManager accountsManager,
|
||||
final ExternalServiceCredentialsGenerator svrCredentialsGenerator) {
|
||||
this.accountsManager = requireNonNull(accountsManager);
|
||||
this.svrCredentialsGenerator = requireNonNull(svrCredentialsGenerator);
|
||||
|
||||
this.svrCredentialsGenerator = svrCredentialsGenerator;
|
||||
this.accountsManager = accountsManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -57,14 +39,18 @@ public class ExternalServiceCredentialsAnonymousGrpcService extends
|
||||
tokens,
|
||||
svrCredentialsGenerator,
|
||||
MAX_SVR_PASSWORD_AGE_SECONDS);
|
||||
|
||||
// the username associated with the provided number
|
||||
final Optional<String> maybeUsername = accountsManager.getByE164(request.getNumber())
|
||||
.map(Account::getUuid)
|
||||
.map(svrCredentialsGenerator::generateForUuid)
|
||||
.map(ExternalServiceCredentials::username);
|
||||
|
||||
final CheckSvrCredentialsResponse.Builder builder = CheckSvrCredentialsResponse.newBuilder();
|
||||
|
||||
for (ExternalServiceCredentialsSelector.CredentialInfo credentialInfo : credentials) {
|
||||
final AuthCheckResult authCheckResult;
|
||||
|
||||
if (!credentialInfo.valid()) {
|
||||
authCheckResult = AuthCheckResult.AUTH_CHECK_RESULT_INVALID;
|
||||
} else {
|
||||
@@ -74,8 +60,10 @@ public class ExternalServiceCredentialsAnonymousGrpcService extends
|
||||
? AuthCheckResult.AUTH_CHECK_RESULT_MATCH
|
||||
: AuthCheckResult.AUTH_CHECK_RESULT_NO_MATCH;
|
||||
}
|
||||
|
||||
builder.putMatches(credentialInfo.token(), authCheckResult);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.grpc;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import org.signal.chat.credentials.ExternalServiceType;
|
||||
import org.signal.chat.credentials.GetDeliveryCertificateRequest;
|
||||
import org.signal.chat.credentials.GetDeliveryCertificateResponse;
|
||||
import org.signal.chat.credentials.GetExternalServiceCredentialsRequest;
|
||||
import org.signal.chat.credentials.GetExternalServiceCredentialsResponse;
|
||||
import org.signal.chat.credentials.GetGroupCredentialsRequest;
|
||||
import org.signal.chat.credentials.GetGroupCredentialsResponse;
|
||||
import org.signal.chat.credentials.SimpleCredentialsGrpc;
|
||||
import org.signal.libsignal.protocol.ServiceId;
|
||||
import org.signal.libsignal.zkgroup.GenericServerSecretParams;
|
||||
import org.signal.libsignal.zkgroup.auth.ServerZkAuthOperations;
|
||||
import org.signal.libsignal.zkgroup.calllinks.CallLinkAuthCredentialResponse;
|
||||
import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.RedemptionRange;
|
||||
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice;
|
||||
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticationUtil;
|
||||
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||
import org.whispersystems.textsecuregcm.identity.IdentityType;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.util.UUIDUtil;
|
||||
|
||||
public class CredentialsGrpcService extends SimpleCredentialsGrpc.CredentialsImplBase {
|
||||
|
||||
private final AccountsManager accountsManager;
|
||||
private final CertificateGenerator certificateGenerator;
|
||||
private final ServerZkAuthOperations serverZkAuthOperations;
|
||||
private final GenericServerSecretParams serverSecretParams;
|
||||
private final RateLimiters rateLimiters;
|
||||
private final Clock clock;
|
||||
|
||||
private final Map<ExternalServiceType, ExternalServiceCredentialsGenerator> credentialsGeneratorByType;
|
||||
|
||||
public CredentialsGrpcService(final AccountsManager accountsManager,
|
||||
final CertificateGenerator certificateGenerator,
|
||||
final ServerZkAuthOperations serverZkAuthOperations,
|
||||
final GenericServerSecretParams serverSecretParams,
|
||||
final RateLimiters rateLimiters,
|
||||
final Clock clock,
|
||||
final Map<ExternalServiceType, ExternalServiceCredentialsGenerator> credentialsGeneratorByType) {
|
||||
|
||||
this.accountsManager = accountsManager;
|
||||
this.certificateGenerator = certificateGenerator;
|
||||
this.serverZkAuthOperations = serverZkAuthOperations;
|
||||
this.serverSecretParams = serverSecretParams;
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.clock = clock;
|
||||
this.credentialsGeneratorByType = credentialsGeneratorByType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GetExternalServiceCredentialsResponse getExternalServiceCredentials(final GetExternalServiceCredentialsRequest request)
|
||||
throws RateLimitExceededException {
|
||||
final ExternalServiceCredentialsGenerator credentialsGenerator = this.credentialsGeneratorByType
|
||||
.get(request.getExternalService());
|
||||
if (credentialsGenerator == null) {
|
||||
throw GrpcExceptions.fieldViolation("externalService", "Invalid external service type");
|
||||
}
|
||||
final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice();
|
||||
rateLimiters.forDescriptor(RateLimiters.For.EXTERNAL_SERVICE_CREDENTIALS).validate(authenticatedDevice.accountIdentifier());
|
||||
final ExternalServiceCredentials externalServiceCredentials = credentialsGenerator
|
||||
.generateForUuid(authenticatedDevice.accountIdentifier());
|
||||
return GetExternalServiceCredentialsResponse.newBuilder()
|
||||
.setUsername(externalServiceCredentials.username())
|
||||
.setPassword(externalServiceCredentials.password())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GetDeliveryCertificateResponse getDeliveryCertificate(final GetDeliveryCertificateRequest request) {
|
||||
final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice();
|
||||
|
||||
final Account account = accountsManager.getByAccountIdentifier(authenticatedDevice.accountIdentifier())
|
||||
.orElseThrow(() -> GrpcExceptions.invalidCredentials("invalid credentials"));
|
||||
|
||||
return GetDeliveryCertificateResponse.newBuilder()
|
||||
.setCertificateWithE164(ByteString.copyFrom(
|
||||
certificateGenerator.createFor(account, authenticatedDevice.deviceId(), true)))
|
||||
.setCertificateWithoutE164(ByteString.copyFrom(
|
||||
certificateGenerator.createFor(account, authenticatedDevice.deviceId(), false)))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GetGroupCredentialsResponse getGroupCredentials(final GetGroupCredentialsRequest request) {
|
||||
final RedemptionRange redemptionRange;
|
||||
|
||||
try {
|
||||
redemptionRange = RedemptionRange.inclusive(clock,
|
||||
Instant.ofEpochSecond(request.getRedemptionStartSeconds()),
|
||||
Instant.ofEpochSecond(request.getRedemptionEndSeconds()));
|
||||
} catch (final IllegalArgumentException e) {
|
||||
throw GrpcExceptions.invalidArguments(e.getMessage());
|
||||
}
|
||||
|
||||
final Account account =
|
||||
accountsManager.getByAccountIdentifier(AuthenticationUtil.requireAuthenticatedDevice().accountIdentifier())
|
||||
.orElseThrow(() -> GrpcExceptions.invalidCredentials("invalid credentials"));
|
||||
|
||||
final ServiceId.Aci aci = new ServiceId.Aci(account.getIdentifier(IdentityType.ACI));
|
||||
final ServiceId.Pni pni = new ServiceId.Pni(account.getIdentifier(IdentityType.PNI));
|
||||
|
||||
final GetGroupCredentialsResponse.Builder responseBuilder = GetGroupCredentialsResponse.newBuilder()
|
||||
.setPni(UUIDUtil.toByteString(pni.getRawUUID()));
|
||||
|
||||
for (final Instant redemption : redemptionRange) {
|
||||
responseBuilder.addGroupCredentials(GetGroupCredentialsResponse.CredentialAndRedemptionTime.newBuilder()
|
||||
.setRedemptionTimeSeconds(redemption.getEpochSecond())
|
||||
.setCredential(ByteString.copyFrom(
|
||||
serverZkAuthOperations.issueAuthCredentialWithPniZkc(aci, pni, redemption).serialize()))
|
||||
.build());
|
||||
|
||||
responseBuilder.addCallLinkAuthCredentials(GetGroupCredentialsResponse.CredentialAndRedemptionTime.newBuilder()
|
||||
.setRedemptionTimeSeconds(redemption.getEpochSecond())
|
||||
.setCredential(ByteString.copyFrom(
|
||||
CallLinkAuthCredentialResponse.issueCredential(aci, redemption, serverSecretParams).serialize()))
|
||||
.build());
|
||||
}
|
||||
|
||||
return responseBuilder.build();
|
||||
}
|
||||
}
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.grpc;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import java.time.Clock;
|
||||
import java.util.Map;
|
||||
import org.signal.chat.credentials.ExternalServiceType;
|
||||
import org.signal.chat.credentials.GetExternalServiceCredentialsRequest;
|
||||
import org.signal.chat.credentials.GetExternalServiceCredentialsResponse;
|
||||
import org.signal.chat.credentials.SimpleExternalServiceCredentialsGrpc;
|
||||
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice;
|
||||
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticationUtil;
|
||||
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
|
||||
public class ExternalServiceCredentialsGrpcService extends SimpleExternalServiceCredentialsGrpc.ExternalServiceCredentialsImplBase {
|
||||
|
||||
private final Map<ExternalServiceType, ExternalServiceCredentialsGenerator> credentialsGeneratorByType;
|
||||
|
||||
private final RateLimiters rateLimiters;
|
||||
|
||||
|
||||
public static ExternalServiceCredentialsGrpcService createForAllExternalServices(
|
||||
final WhisperServerConfiguration chatConfiguration,
|
||||
final RateLimiters rateLimiters) {
|
||||
return new ExternalServiceCredentialsGrpcService(
|
||||
ExternalServiceDefinitions.createExternalServiceList(chatConfiguration, Clock.systemUTC()),
|
||||
rateLimiters
|
||||
);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
ExternalServiceCredentialsGrpcService(
|
||||
final Map<ExternalServiceType, ExternalServiceCredentialsGenerator> credentialsGeneratorByType,
|
||||
final RateLimiters rateLimiters) {
|
||||
this.credentialsGeneratorByType = requireNonNull(credentialsGeneratorByType);
|
||||
this.rateLimiters = requireNonNull(rateLimiters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GetExternalServiceCredentialsResponse getExternalServiceCredentials(final GetExternalServiceCredentialsRequest request)
|
||||
throws RateLimitExceededException {
|
||||
final ExternalServiceCredentialsGenerator credentialsGenerator = this.credentialsGeneratorByType
|
||||
.get(request.getExternalService());
|
||||
if (credentialsGenerator == null) {
|
||||
throw GrpcExceptions.fieldViolation("externalService", "Invalid external service type");
|
||||
}
|
||||
final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice();
|
||||
rateLimiters.forDescriptor(RateLimiters.For.EXTERNAL_SERVICE_CREDENTIALS).validate(authenticatedDevice.accountIdentifier());
|
||||
final ExternalServiceCredentials externalServiceCredentials = credentialsGenerator
|
||||
.generateForUuid(authenticatedDevice.accountIdentifier());
|
||||
return GetExternalServiceCredentialsResponse.newBuilder()
|
||||
.setUsername(externalServiceCredentials.username())
|
||||
.setPassword(externalServiceCredentials.password())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -20,7 +20,7 @@ import org.whispersystems.textsecuregcm.configuration.DirectoryV2ClientConfigura
|
||||
import org.whispersystems.textsecuregcm.configuration.PaymentsServiceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureValueRecoveryConfiguration;
|
||||
|
||||
enum ExternalServiceDefinitions {
|
||||
public enum ExternalServiceDefinitions {
|
||||
DIRECTORY(ExternalServiceType.EXTERNAL_SERVICE_TYPE_DIRECTORY, (chatConfig, clock) -> {
|
||||
final DirectoryV2ClientConfiguration cfg = chatConfig.getDirectoryV2Configuration().getDirectoryV2ClientConfiguration();
|
||||
return ExternalServiceCredentialsGenerator
|
||||
@@ -30,7 +30,7 @@ enum ExternalServiceDefinitions {
|
||||
.withClock(clock)
|
||||
.build();
|
||||
}),
|
||||
PAYMENTS(ExternalServiceType.EXTERNAL_SERVICE_TYPE_PAYMENTS, (chatConfig, clock) -> {
|
||||
PAYMENTS(ExternalServiceType.EXTERNAL_SERVICE_TYPE_PAYMENTS, (chatConfig, _) -> {
|
||||
final PaymentsServiceConfiguration cfg = chatConfig.getPaymentsServiceConfiguration();
|
||||
return ExternalServiceCredentialsGenerator
|
||||
.builder(cfg.userAuthenticationTokenSharedSecret())
|
||||
@@ -47,7 +47,7 @@ enum ExternalServiceDefinitions {
|
||||
.withClock(clock)
|
||||
.build();
|
||||
}),
|
||||
STORAGE(ExternalServiceType.EXTERNAL_SERVICE_TYPE_STORAGE, (chatConfig, clock) -> {
|
||||
STORAGE(ExternalServiceType.EXTERNAL_SERVICE_TYPE_STORAGE, (chatConfig, _) -> {
|
||||
final PaymentsServiceConfiguration cfg = chatConfig.getPaymentsServiceConfiguration();
|
||||
return ExternalServiceCredentialsGenerator
|
||||
.builder(cfg.userAuthenticationTokenSharedSecret())
|
||||
|
||||
@@ -11,20 +11,35 @@ import "org/signal/chat/require.proto";
|
||||
|
||||
package org.signal.chat.credentials;
|
||||
|
||||
// Provides methods for obtaining and verifying credentials for "external" services
|
||||
// (i.e. services that are not a part of the chat server deployment).
|
||||
// All methods of this service require authentication.
|
||||
service ExternalServiceCredentials {
|
||||
// Provides methods for obtaining and verifying credentials that allow an
|
||||
// authenticated user to authenticate in another service or context without
|
||||
// revealing their identity.
|
||||
service Credentials {
|
||||
|
||||
// Generates and returns an external service credentials for the caller.
|
||||
rpc GetExternalServiceCredentials(GetExternalServiceCredentialsRequest)
|
||||
returns (GetExternalServiceCredentialsResponse) {}
|
||||
|
||||
// Generates a pair of delivery certificates that the holder can include to
|
||||
// identify themselves to a message recipient (but not the server) in a
|
||||
// sealed-sender message.
|
||||
rpc GetDeliveryCertificate(GetDeliveryCertificateRequest)
|
||||
returns (GetDeliveryCertificateResponse) {}
|
||||
|
||||
// Generates a set of zero-knowledge credentials for various group-related
|
||||
// actions.
|
||||
rpc GetGroupCredentials(GetGroupCredentialsRequest)
|
||||
returns (GetGroupCredentialsResponse) {}
|
||||
}
|
||||
|
||||
service ExternalServiceCredentialsAnonymous {
|
||||
// Given a list of secure value recovery (SVR) service credentials and a phone number,
|
||||
// checks, which of the provided credentials were generated by the user with the given phone number
|
||||
// and have not yet expired.
|
||||
// Provides methods for working with previously-generated credentials without
|
||||
// revealing an association between the credentials and the caller's identity to
|
||||
// the server.
|
||||
service CredentialsAnonymous {
|
||||
|
||||
// Given a list of secure value recovery (SVR) service credentials and a phone
|
||||
// number, checks and returns which of the provided credentials were generated
|
||||
// by the user with the given phone number and have not yet expired.
|
||||
rpc CheckSvrCredentials(CheckSvrCredentialsRequest)
|
||||
returns (CheckSvrCredentialsResponse) {}
|
||||
}
|
||||
@@ -79,3 +94,51 @@ message CheckSvrCredentialsResponse {
|
||||
|
||||
map<string, AuthCheckResult> matches = 1;
|
||||
}
|
||||
|
||||
message GetDeliveryCertificateRequest {
|
||||
}
|
||||
|
||||
// A pair of message delivery certificates. The response unconditionally
|
||||
// includes certificates with and without the caller's phone number so the
|
||||
// server never learns anything about the caller's intent to share their phone
|
||||
// number with their contacts.
|
||||
message GetDeliveryCertificateResponse {
|
||||
// A delivery receipt that includes the caller's phone number
|
||||
bytes certificate_with_e164 = 1;
|
||||
|
||||
// A delivery receipt that does not include the caller's phone number
|
||||
bytes certificate_without_e164 = 2;
|
||||
}
|
||||
|
||||
message GetGroupCredentialsRequest {
|
||||
// The earliest time for which to issue group credentials; must be aligned to
|
||||
// a UTC day boundary and may be no more than one day in the past at the time
|
||||
// of the call.
|
||||
uint64 redemption_start_seconds = 1;
|
||||
|
||||
// The latest time for which to issue group credentials; must be aligned to a
|
||||
// UTC day boundary and no more than seven days in the future at the time of
|
||||
// the call.
|
||||
uint64 redemption_end_seconds = 2;
|
||||
}
|
||||
|
||||
message GetGroupCredentialsResponse {
|
||||
// A zero-knowledge credential that may be redeemed at or up to one day after
|
||||
// the given redemption time
|
||||
message CredentialAndRedemptionTime {
|
||||
bytes credential = 1;
|
||||
uint64 redemption_time_seconds = 2;
|
||||
}
|
||||
|
||||
// A collection of credentials allowing the holder to anonymously
|
||||
// authenticate themselves for group-related actions
|
||||
repeated CredentialAndRedemptionTime group_credentials = 1;
|
||||
|
||||
// A collection of credentials allowing the holder to read, update, and delete
|
||||
// group call links
|
||||
repeated CredentialAndRedemptionTime call_link_auth_credentials = 2;
|
||||
|
||||
// The phone number identifier for which the included credentials were
|
||||
// generated
|
||||
bytes pni = 3;
|
||||
}
|
||||
|
||||
+26
-26
@@ -7,6 +7,8 @@ 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.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||
import io.grpc.Status;
|
||||
@@ -25,7 +27,7 @@ import org.mockito.Mockito;
|
||||
import org.signal.chat.credentials.AuthCheckResult;
|
||||
import org.signal.chat.credentials.CheckSvrCredentialsRequest;
|
||||
import org.signal.chat.credentials.CheckSvrCredentialsResponse;
|
||||
import org.signal.chat.credentials.ExternalServiceCredentialsAnonymousGrpc;
|
||||
import org.signal.chat.credentials.CredentialsAnonymousGrpc;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
@@ -34,8 +36,8 @@ import org.whispersystems.textsecuregcm.util.MockUtils;
|
||||
import org.whispersystems.textsecuregcm.util.MutableClock;
|
||||
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
|
||||
|
||||
class ExternalServiceCredentialsAnonymousGrpcServiceTest extends
|
||||
SimpleBaseGrpcTest<ExternalServiceCredentialsAnonymousGrpcService, ExternalServiceCredentialsAnonymousGrpc.ExternalServiceCredentialsAnonymousBlockingStub> {
|
||||
class CredentialsAnonymousGrpcServiceTest extends
|
||||
SimpleBaseGrpcTest<CredentialsAnonymousGrpcService, CredentialsAnonymousGrpc.CredentialsAnonymousBlockingStub> {
|
||||
|
||||
private static final UUID USER_UUID = UUID.randomUUID();
|
||||
|
||||
@@ -46,29 +48,33 @@ class ExternalServiceCredentialsAnonymousGrpcServiceTest extends
|
||||
|
||||
private static final MutableClock CLOCK = MockUtils.mutableClock(0);
|
||||
|
||||
private static final ExternalServiceCredentialsGenerator SVR_CREDENTIALS_GENERATOR = Mockito.spy(ExternalServiceCredentialsGenerator
|
||||
.builder(TestRandomUtil.nextBytes(32))
|
||||
.withUserDerivationKey(TestRandomUtil.nextBytes(32))
|
||||
.prependUsername(false)
|
||||
.withDerivedUsernameTruncateLength(16)
|
||||
.withClock(CLOCK)
|
||||
.build());
|
||||
private static final ExternalServiceCredentialsGenerator SVR_CREDENTIALS_GENERATOR =
|
||||
Mockito.spy(ExternalServiceCredentialsGenerator
|
||||
.builder(TestRandomUtil.nextBytes(32))
|
||||
.withUserDerivationKey(TestRandomUtil.nextBytes(32))
|
||||
.prependUsername(false)
|
||||
.withDerivedUsernameTruncateLength(16)
|
||||
.withClock(CLOCK)
|
||||
.build());
|
||||
|
||||
@Mock
|
||||
private AccountsManager accountsManager;
|
||||
|
||||
@Override
|
||||
protected ExternalServiceCredentialsAnonymousGrpcService createServiceBeforeEachTest() {
|
||||
return new ExternalServiceCredentialsAnonymousGrpcService(accountsManager, SVR_CREDENTIALS_GENERATOR);
|
||||
protected CredentialsAnonymousGrpcService createServiceBeforeEachTest() {
|
||||
return new CredentialsAnonymousGrpcService(accountsManager, SVR_CREDENTIALS_GENERATOR);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
Mockito.when(accountsManager.getByE164(USER_E164)).thenReturn(Optional.of(account(USER_UUID)));
|
||||
final Account account = mock(Account.class);
|
||||
when(account.getUuid()).thenReturn(USER_UUID);
|
||||
|
||||
when(accountsManager.getByE164(USER_E164)).thenReturn(Optional.of(account));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOneMatch() throws Exception {
|
||||
public void testOneMatch() {
|
||||
final UUID user2 = UUID.randomUUID();
|
||||
final UUID user3 = UUID.randomUUID();
|
||||
assertExpectedCredentialCheckResponse(Map.of(
|
||||
@@ -79,7 +85,7 @@ class ExternalServiceCredentialsAnonymousGrpcServiceTest extends
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoMatch() throws Exception {
|
||||
public void testNoMatch() {
|
||||
final UUID user2 = UUID.randomUUID();
|
||||
final UUID user3 = UUID.randomUUID();
|
||||
assertExpectedCredentialCheckResponse(Map.of(
|
||||
@@ -89,7 +95,7 @@ class ExternalServiceCredentialsAnonymousGrpcServiceTest extends
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSomeInvalid() throws Exception {
|
||||
public void testSomeInvalid() {
|
||||
final UUID user2 = UUID.randomUUID();
|
||||
final UUID user3 = UUID.randomUUID();
|
||||
final ExternalServiceCredentials user1Cred = credentials(USER_UUID, day(1));
|
||||
@@ -105,7 +111,7 @@ class ExternalServiceCredentialsAnonymousGrpcServiceTest extends
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSomeExpired() throws Exception {
|
||||
public void testSomeExpired() {
|
||||
final UUID user2 = UUID.randomUUID();
|
||||
final UUID user3 = UUID.randomUUID();
|
||||
assertExpectedCredentialCheckResponse(Map.of(
|
||||
@@ -117,7 +123,7 @@ class ExternalServiceCredentialsAnonymousGrpcServiceTest extends
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSomeHaveNewerVersions() throws Exception {
|
||||
public void testSomeHaveNewerVersions() {
|
||||
final UUID user2 = UUID.randomUUID();
|
||||
final UUID user3 = UUID.randomUUID();
|
||||
assertExpectedCredentialCheckResponse(Map.of(
|
||||
@@ -134,7 +140,7 @@ class ExternalServiceCredentialsAnonymousGrpcServiceTest extends
|
||||
public void testInvalidPasswordCount(int count) {
|
||||
final CheckSvrCredentialsRequest request = CheckSvrCredentialsRequest.newBuilder()
|
||||
.setNumber(USER_E164)
|
||||
.addAllPasswords(IntStream.range(0, count).mapToObj(i -> token(UUID.randomUUID(), day(10))).toList())
|
||||
.addAllPasswords(IntStream.range(0, count).mapToObj(_ -> token(UUID.randomUUID(), day(10))).toList())
|
||||
.build();
|
||||
final StatusRuntimeException status = assertThrows(StatusRuntimeException.class,
|
||||
() -> unauthenticatedServiceStub().checkSvrCredentials(request));
|
||||
@@ -143,7 +149,7 @@ class ExternalServiceCredentialsAnonymousGrpcServiceTest extends
|
||||
|
||||
private void assertExpectedCredentialCheckResponse(
|
||||
final Map<String, AuthCheckResult> expected,
|
||||
final long nowMillis) throws Exception {
|
||||
final long nowMillis) {
|
||||
CLOCK.setTimeMillis(nowMillis);
|
||||
final CheckSvrCredentialsRequest request = CheckSvrCredentialsRequest.newBuilder()
|
||||
.setNumber(USER_E164)
|
||||
@@ -170,10 +176,4 @@ class ExternalServiceCredentialsAnonymousGrpcServiceTest extends
|
||||
private static long day(final int n) {
|
||||
return Duration.ofDays(n).toMillis();
|
||||
}
|
||||
|
||||
private static Account account(final UUID uuid) {
|
||||
final Account a = new Account();
|
||||
a.setUuid(uuid);
|
||||
return a;
|
||||
}
|
||||
}
|
||||
+357
@@ -0,0 +1,357 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.grpc;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertRateLimitExceeded;
|
||||
import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertStatusException;
|
||||
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import io.grpc.Status;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.signal.chat.credentials.CredentialsGrpc;
|
||||
import org.signal.chat.credentials.ExternalServiceType;
|
||||
import org.signal.chat.credentials.GetDeliveryCertificateRequest;
|
||||
import org.signal.chat.credentials.GetDeliveryCertificateResponse;
|
||||
import org.signal.chat.credentials.GetExternalServiceCredentialsRequest;
|
||||
import org.signal.chat.credentials.GetExternalServiceCredentialsResponse;
|
||||
import org.signal.chat.credentials.GetGroupCredentialsRequest;
|
||||
import org.signal.chat.credentials.GetGroupCredentialsResponse;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.ServiceId;
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.signal.libsignal.zkgroup.GenericServerSecretParams;
|
||||
import org.signal.libsignal.zkgroup.ServerSecretParams;
|
||||
import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPniResponse;
|
||||
import org.signal.libsignal.zkgroup.auth.ClientZkAuthOperations;
|
||||
import org.signal.libsignal.zkgroup.auth.ServerZkAuthOperations;
|
||||
import org.signal.libsignal.zkgroup.calllinks.CallLinkAuthCredentialResponse;
|
||||
import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.controllers.CertificateController;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos;
|
||||
import org.whispersystems.textsecuregcm.identity.IdentityType;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.util.MockUtils;
|
||||
import org.whispersystems.textsecuregcm.util.TestClock;
|
||||
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
|
||||
import org.whispersystems.textsecuregcm.util.UUIDUtil;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public class CredentialsGrpcServiceTest
|
||||
extends SimpleBaseGrpcTest<CredentialsGrpcService, CredentialsGrpc.CredentialsBlockingStub> {
|
||||
|
||||
@Mock
|
||||
private AccountsManager accountsManager;
|
||||
|
||||
@Mock
|
||||
private RateLimiters rateLimiters;
|
||||
|
||||
@Mock
|
||||
private Account authenticatedAccount;
|
||||
|
||||
private static final ExternalServiceCredentialsGenerator DIRECTORY_CREDENTIALS_GENERATOR = Mockito.spy(ExternalServiceCredentialsGenerator
|
||||
.builder(TestRandomUtil.nextBytes(32))
|
||||
.withUserDerivationKey(TestRandomUtil.nextBytes(32))
|
||||
.prependUsername(false)
|
||||
.truncateSignature(false)
|
||||
.build());
|
||||
|
||||
private static final ExternalServiceCredentialsGenerator PAYMENTS_CREDENTIALS_GENERATOR = Mockito.spy(ExternalServiceCredentialsGenerator
|
||||
.builder(TestRandomUtil.nextBytes(32))
|
||||
.prependUsername(true)
|
||||
.build());
|
||||
|
||||
private static final UUID AUTHENTICATED_PNI = UUID.randomUUID();
|
||||
private static final String PHONE_NUMBER = PhoneNumberUtil.getInstance().format(
|
||||
PhoneNumberUtil.getInstance().getExampleNumber("US"), PhoneNumberUtil.PhoneNumberFormat.E164);
|
||||
|
||||
private static ECKeyPair IDENTITY_KEY_PAIR;
|
||||
|
||||
private static ECKeyPair SERVER_KEY_PAIR;
|
||||
private static MessageProtos.ServerCertificate SIGNED_SERVER_CERTIFICATE;
|
||||
|
||||
private static ServerSecretParams SERVER_SECRET_PARAMS;
|
||||
private static GenericServerSecretParams GENERIC_SERVER_SECRET_PARAMS;
|
||||
private static ServerZkAuthOperations SERVER_ZK_AUTH_OPERATIONS;
|
||||
|
||||
private static final Clock CLOCK = TestClock.pinned(Instant.now());
|
||||
|
||||
@BeforeAll
|
||||
static void setUpBeforeAll() {
|
||||
IDENTITY_KEY_PAIR = ECKeyPair.generate();
|
||||
|
||||
final ECKeyPair caKeyPair = ECKeyPair.generate();
|
||||
SERVER_KEY_PAIR = ECKeyPair.generate();
|
||||
|
||||
final MessageProtos.ServerCertificate.Certificate serverCertificate =
|
||||
MessageProtos.ServerCertificate.Certificate.newBuilder()
|
||||
.setId(ThreadLocalRandom.current().nextInt())
|
||||
.setKey(ByteString.copyFrom(SERVER_KEY_PAIR.getPublicKey().serialize()))
|
||||
.build();
|
||||
|
||||
final byte[] signature = caKeyPair.getPrivateKey().calculateSignature(serverCertificate.toByteArray());
|
||||
|
||||
SIGNED_SERVER_CERTIFICATE = MessageProtos.ServerCertificate.newBuilder()
|
||||
.setCertificate(ByteString.copyFrom(serverCertificate.toByteArray()))
|
||||
.setSignature(ByteString.copyFrom(signature))
|
||||
.build();
|
||||
|
||||
SERVER_SECRET_PARAMS = ServerSecretParams.generate();
|
||||
GENERIC_SERVER_SECRET_PARAMS = GenericServerSecretParams.generate();
|
||||
SERVER_ZK_AUTH_OPERATIONS = new ServerZkAuthOperations(SERVER_SECRET_PARAMS);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
when(authenticatedAccount.getUuid()).thenReturn(AUTHENTICATED_ACI);
|
||||
when(authenticatedAccount.getNumber()).thenReturn(PHONE_NUMBER);
|
||||
when(authenticatedAccount.getIdentifier(IdentityType.ACI)).thenReturn(AUTHENTICATED_ACI);
|
||||
when(authenticatedAccount.getIdentifier(IdentityType.PNI)).thenReturn(AUTHENTICATED_PNI);
|
||||
when(authenticatedAccount.getIdentityKey(IdentityType.ACI))
|
||||
.thenReturn(new IdentityKey(IDENTITY_KEY_PAIR.getPublicKey()));
|
||||
|
||||
when(accountsManager.getByAccountIdentifier(AUTHENTICATED_ACI)).thenReturn(Optional.of(authenticatedAccount));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CredentialsGrpcService createServiceBeforeEachTest() {
|
||||
final CertificateGenerator certificateGenerator;
|
||||
try {
|
||||
certificateGenerator = new CertificateGenerator(SIGNED_SERVER_CERTIFICATE.toByteArray(),
|
||||
SERVER_KEY_PAIR.getPrivateKey(),
|
||||
1,
|
||||
false);
|
||||
} catch (final InvalidProtocolBufferException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
|
||||
return new CredentialsGrpcService(accountsManager,
|
||||
certificateGenerator,
|
||||
SERVER_ZK_AUTH_OPERATIONS,
|
||||
GENERIC_SERVER_SECRET_PARAMS,
|
||||
rateLimiters,
|
||||
CLOCK,
|
||||
Map.of(
|
||||
ExternalServiceType.EXTERNAL_SERVICE_TYPE_DIRECTORY, DIRECTORY_CREDENTIALS_GENERATOR,
|
||||
ExternalServiceType.EXTERNAL_SERVICE_TYPE_PAYMENTS, PAYMENTS_CREDENTIALS_GENERATOR
|
||||
));
|
||||
}
|
||||
|
||||
static Stream<Arguments> testSuccess() {
|
||||
return Stream.of(
|
||||
Arguments.of(ExternalServiceType.EXTERNAL_SERVICE_TYPE_DIRECTORY, DIRECTORY_CREDENTIALS_GENERATOR),
|
||||
Arguments.of(ExternalServiceType.EXTERNAL_SERVICE_TYPE_PAYMENTS, PAYMENTS_CREDENTIALS_GENERATOR)
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
public void testSuccess(
|
||||
final ExternalServiceType externalServiceType,
|
||||
final ExternalServiceCredentialsGenerator credentialsGenerator) throws Exception {
|
||||
final RateLimiter limiter = mock(RateLimiter.class);
|
||||
doReturn(limiter).when(rateLimiters).forDescriptor(eq(RateLimiters.For.EXTERNAL_SERVICE_CREDENTIALS));
|
||||
doReturn(Mono.fromFuture(CompletableFuture.completedFuture(null))).when(limiter).validateReactive(eq(AUTHENTICATED_ACI));
|
||||
final GetExternalServiceCredentialsResponse artResponse = authenticatedServiceStub().getExternalServiceCredentials(
|
||||
GetExternalServiceCredentialsRequest.newBuilder()
|
||||
.setExternalService(externalServiceType)
|
||||
.build());
|
||||
final Optional<Long> artValidation = credentialsGenerator.validateAndGetTimestamp(
|
||||
new ExternalServiceCredentials(artResponse.getUsername(), artResponse.getPassword()));
|
||||
assertTrue(artValidation.isPresent());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(ints = { -1, 0, 1000 })
|
||||
public void testUnrecognizedService(final int externalServiceTypeValue) throws Exception {
|
||||
assertStatusException(Status.INVALID_ARGUMENT, () -> authenticatedServiceStub().getExternalServiceCredentials(
|
||||
GetExternalServiceCredentialsRequest.newBuilder()
|
||||
.setExternalServiceValue(externalServiceTypeValue)
|
||||
.build()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidRequest() throws Exception {
|
||||
assertStatusException(Status.INVALID_ARGUMENT, () -> authenticatedServiceStub().getExternalServiceCredentials(
|
||||
GetExternalServiceCredentialsRequest.newBuilder()
|
||||
.build()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRateLimitExceeded() throws Exception {
|
||||
final Duration retryAfter = MockUtils.updateRateLimiterResponseToFail(
|
||||
rateLimiters, RateLimiters.For.EXTERNAL_SERVICE_CREDENTIALS, AUTHENTICATED_ACI, Duration.ofSeconds(100));
|
||||
Mockito.reset(DIRECTORY_CREDENTIALS_GENERATOR);
|
||||
assertRateLimitExceeded(
|
||||
retryAfter,
|
||||
() -> authenticatedServiceStub().getExternalServiceCredentials(
|
||||
GetExternalServiceCredentialsRequest.newBuilder()
|
||||
.setExternalService(ExternalServiceType.EXTERNAL_SERVICE_TYPE_DIRECTORY)
|
||||
.build()),
|
||||
DIRECTORY_CREDENTIALS_GENERATOR
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* `ExternalServiceDefinitions` enum is supposed to have entries for all values in `ExternalServiceType`,
|
||||
* except for the `EXTERNAL_SERVICE_TYPE_UNSPECIFIED` and `UNRECOGNIZED`.
|
||||
* This test makes sure that is the case.
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@EnumSource(mode = EnumSource.Mode.EXCLUDE, names = { "UNRECOGNIZED", "EXTERNAL_SERVICE_TYPE_UNSPECIFIED" })
|
||||
public void testHaveExternalServiceDefinitionForServiceTypes(final ExternalServiceType externalServiceType) throws Exception {
|
||||
assertTrue(
|
||||
Arrays.stream(ExternalServiceDefinitions.values()).anyMatch(v -> v.externalService() == externalServiceType),
|
||||
"`ExternalServiceDefinitions` enum entry is missing for the `%s` value of `ExternalServiceType`".formatted(externalServiceType)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getDeliveryCertificate() throws InvalidProtocolBufferException, InvalidKeyException {
|
||||
final GetDeliveryCertificateResponse response =
|
||||
authenticatedServiceStub().getDeliveryCertificate(GetDeliveryCertificateRequest.getDefaultInstance());
|
||||
|
||||
checkDeliveryCertificate(response.getCertificateWithE164().toByteArray(), true);
|
||||
checkDeliveryCertificate(response.getCertificateWithoutE164().toByteArray(), false);
|
||||
}
|
||||
|
||||
private static void checkDeliveryCertificate(final byte[] deliveryCertificate, final boolean expectE164)
|
||||
throws InvalidProtocolBufferException, InvalidKeyException {
|
||||
final MessageProtos.SenderCertificate senderCertificateHolder =
|
||||
MessageProtos.SenderCertificate.parseFrom(deliveryCertificate);
|
||||
|
||||
final MessageProtos.SenderCertificate.Certificate senderCertificate =
|
||||
MessageProtos.SenderCertificate.Certificate.parseFrom(senderCertificateHolder.getCertificate());
|
||||
|
||||
final MessageProtos.ServerCertificate.Certificate serverCertificate =
|
||||
MessageProtos.ServerCertificate.Certificate.parseFrom(SIGNED_SERVER_CERTIFICATE.getCertificate());
|
||||
|
||||
assertEquals(serverCertificate.getId(), senderCertificate.getSignerId());
|
||||
|
||||
final ECPublicKey serverPublicKey = new ECPublicKey(serverCertificate.getKey().toByteArray());
|
||||
|
||||
assertTrue(serverPublicKey.verifySignature(senderCertificateHolder.getCertificate().toByteArray(),
|
||||
senderCertificateHolder.getSignature().toByteArray()));
|
||||
|
||||
assertEquals(expectE164, senderCertificate.hasSenderE164());
|
||||
|
||||
if (expectE164) {
|
||||
assertEquals(PHONE_NUMBER, senderCertificate.getSenderE164());
|
||||
}
|
||||
|
||||
assertEquals(AUTHENTICATED_DEVICE_ID, senderCertificate.getSenderDevice());
|
||||
assertTrue(senderCertificate.hasSenderUuid());
|
||||
assertEquals(UUIDUtil.toByteString(AUTHENTICATED_ACI), senderCertificate.getSenderUuid());
|
||||
assertArrayEquals(senderCertificate.getIdentityKey().toByteArray(),
|
||||
new IdentityKey(IDENTITY_KEY_PAIR.getPublicKey()).serialize());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(ints = {0, 1, 2, 3, 4, 5, 6, 7})
|
||||
void getGroupCredentials(final int redemptionEndOffsetDays) {
|
||||
final Instant startOfDay = CLOCK.instant().truncatedTo(ChronoUnit.DAYS);
|
||||
|
||||
final GetGroupCredentialsResponse response =
|
||||
authenticatedServiceStub().getGroupCredentials(GetGroupCredentialsRequest.newBuilder()
|
||||
.setRedemptionStartSeconds(startOfDay.getEpochSecond())
|
||||
.setRedemptionEndSeconds(startOfDay.plus(Duration.ofDays(redemptionEndOffsetDays)).getEpochSecond())
|
||||
.build());
|
||||
|
||||
assertEquals(AUTHENTICATED_PNI, UUIDUtil.fromByteString(response.getPni()));
|
||||
assertEquals(redemptionEndOffsetDays + 1, response.getGroupCredentialsCount());
|
||||
assertEquals(redemptionEndOffsetDays + 1, response.getCallLinkAuthCredentialsCount());
|
||||
|
||||
final ClientZkAuthOperations clientZkAuthOperations =
|
||||
new ClientZkAuthOperations(SERVER_SECRET_PARAMS.getPublicParams());
|
||||
|
||||
for (int i = 0; i < redemptionEndOffsetDays; i++) {
|
||||
final Instant redemptionTime = startOfDay.plus(Duration.ofDays(i));
|
||||
assertEquals(redemptionTime.getEpochSecond(), response.getGroupCredentials(i).getRedemptionTimeSeconds());
|
||||
assertEquals(redemptionTime.getEpochSecond(), response.getCallLinkAuthCredentials(i).getRedemptionTimeSeconds());
|
||||
|
||||
final int index = i;
|
||||
|
||||
assertDoesNotThrow(() -> {
|
||||
clientZkAuthOperations.receiveAuthCredentialWithPniAsServiceId(
|
||||
new ServiceId.Aci(AUTHENTICATED_ACI),
|
||||
new ServiceId.Pni(AUTHENTICATED_PNI),
|
||||
redemptionTime.getEpochSecond(),
|
||||
new AuthCredentialWithPniResponse(response.getGroupCredentials(index).getCredential().toByteArray()));
|
||||
});
|
||||
|
||||
assertDoesNotThrow(() -> {
|
||||
new CallLinkAuthCredentialResponse(response.getCallLinkAuthCredentials(index).getCredential().toByteArray())
|
||||
.receive(new ServiceId.Aci(AUTHENTICATED_ACI), redemptionTime, GENERIC_SERVER_SECRET_PARAMS.getPublicParams());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void getGroupCredentialsIllegalRedemptionTimes(final Instant redemptionStart, final Instant redemptionEnd) {
|
||||
//noinspection ThrowableNotThrown
|
||||
GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT, () -> {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
authenticatedServiceStub().getGroupCredentials(GetGroupCredentialsRequest.newBuilder()
|
||||
.setRedemptionStartSeconds(redemptionStart.getEpochSecond())
|
||||
.setRedemptionEndSeconds(redemptionEnd.getEpochSecond())
|
||||
.build());
|
||||
});
|
||||
}
|
||||
|
||||
private static Collection<Arguments> getGroupCredentialsIllegalRedemptionTimes() {
|
||||
final Instant startOfDay = CLOCK.instant().truncatedTo(ChronoUnit.DAYS);
|
||||
|
||||
return List.of(
|
||||
Arguments.argumentSet("Start is after end", startOfDay.plus(Duration.ofDays(1)), startOfDay),
|
||||
Arguments.argumentSet("Start is in the past", startOfDay.minus(Duration.ofDays(2)), startOfDay),
|
||||
Arguments.argumentSet("End is too far in the future", startOfDay,
|
||||
startOfDay.plus(CertificateController.MAX_REDEMPTION_DURATION).plus(Duration.ofDays(1))),
|
||||
Arguments.argumentSet("Start is not at a day boundary", startOfDay.plusSeconds(17),
|
||||
startOfDay.plus(Duration.ofDays(1))),
|
||||
Arguments.argumentSet("End is not at a day boundary", startOfDay, startOfDay.plusSeconds(17))
|
||||
);
|
||||
}
|
||||
}
|
||||
-138
@@ -1,138 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.grpc;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertRateLimitExceeded;
|
||||
import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertStatusException;
|
||||
import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertStatusUnauthenticated;
|
||||
|
||||
import io.grpc.Status;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.signal.chat.credentials.ExternalServiceCredentialsGrpc;
|
||||
import org.signal.chat.credentials.ExternalServiceType;
|
||||
import org.signal.chat.credentials.GetExternalServiceCredentialsRequest;
|
||||
import org.signal.chat.credentials.GetExternalServiceCredentialsResponse;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.util.MockUtils;
|
||||
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public class ExternalServiceCredentialsGrpcServiceTest
|
||||
extends SimpleBaseGrpcTest<ExternalServiceCredentialsGrpcService, ExternalServiceCredentialsGrpc.ExternalServiceCredentialsBlockingStub> {
|
||||
|
||||
private static final ExternalServiceCredentialsGenerator DIRECTORY_CREDENTIALS_GENERATOR = Mockito.spy(ExternalServiceCredentialsGenerator
|
||||
.builder(TestRandomUtil.nextBytes(32))
|
||||
.withUserDerivationKey(TestRandomUtil.nextBytes(32))
|
||||
.prependUsername(false)
|
||||
.truncateSignature(false)
|
||||
.build());
|
||||
|
||||
private static final ExternalServiceCredentialsGenerator PAYMENTS_CREDENTIALS_GENERATOR = Mockito.spy(ExternalServiceCredentialsGenerator
|
||||
.builder(TestRandomUtil.nextBytes(32))
|
||||
.prependUsername(true)
|
||||
.build());
|
||||
|
||||
@Mock
|
||||
private RateLimiters rateLimiters;
|
||||
|
||||
|
||||
@Override
|
||||
protected ExternalServiceCredentialsGrpcService createServiceBeforeEachTest() {
|
||||
return new ExternalServiceCredentialsGrpcService(Map.of(
|
||||
ExternalServiceType.EXTERNAL_SERVICE_TYPE_DIRECTORY, DIRECTORY_CREDENTIALS_GENERATOR,
|
||||
ExternalServiceType.EXTERNAL_SERVICE_TYPE_PAYMENTS, PAYMENTS_CREDENTIALS_GENERATOR
|
||||
), rateLimiters);
|
||||
}
|
||||
|
||||
static Stream<Arguments> testSuccess() {
|
||||
return Stream.of(
|
||||
Arguments.of(ExternalServiceType.EXTERNAL_SERVICE_TYPE_DIRECTORY, DIRECTORY_CREDENTIALS_GENERATOR),
|
||||
Arguments.of(ExternalServiceType.EXTERNAL_SERVICE_TYPE_PAYMENTS, PAYMENTS_CREDENTIALS_GENERATOR)
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
public void testSuccess(
|
||||
final ExternalServiceType externalServiceType,
|
||||
final ExternalServiceCredentialsGenerator credentialsGenerator) throws Exception {
|
||||
final RateLimiter limiter = mock(RateLimiter.class);
|
||||
doReturn(limiter).when(rateLimiters).forDescriptor(eq(RateLimiters.For.EXTERNAL_SERVICE_CREDENTIALS));
|
||||
doReturn(Mono.fromFuture(CompletableFuture.completedFuture(null))).when(limiter).validateReactive(eq(AUTHENTICATED_ACI));
|
||||
final GetExternalServiceCredentialsResponse artResponse = authenticatedServiceStub().getExternalServiceCredentials(
|
||||
GetExternalServiceCredentialsRequest.newBuilder()
|
||||
.setExternalService(externalServiceType)
|
||||
.build());
|
||||
final Optional<Long> artValidation = credentialsGenerator.validateAndGetTimestamp(
|
||||
new ExternalServiceCredentials(artResponse.getUsername(), artResponse.getPassword()));
|
||||
assertTrue(artValidation.isPresent());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(ints = { -1, 0, 1000 })
|
||||
public void testUnrecognizedService(final int externalServiceTypeValue) throws Exception {
|
||||
assertStatusException(Status.INVALID_ARGUMENT, () -> authenticatedServiceStub().getExternalServiceCredentials(
|
||||
GetExternalServiceCredentialsRequest.newBuilder()
|
||||
.setExternalServiceValue(externalServiceTypeValue)
|
||||
.build()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidRequest() throws Exception {
|
||||
assertStatusException(Status.INVALID_ARGUMENT, () -> authenticatedServiceStub().getExternalServiceCredentials(
|
||||
GetExternalServiceCredentialsRequest.newBuilder()
|
||||
.build()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRateLimitExceeded() throws Exception {
|
||||
final Duration retryAfter = MockUtils.updateRateLimiterResponseToFail(
|
||||
rateLimiters, RateLimiters.For.EXTERNAL_SERVICE_CREDENTIALS, AUTHENTICATED_ACI, Duration.ofSeconds(100));
|
||||
Mockito.reset(DIRECTORY_CREDENTIALS_GENERATOR);
|
||||
assertRateLimitExceeded(
|
||||
retryAfter,
|
||||
() -> authenticatedServiceStub().getExternalServiceCredentials(
|
||||
GetExternalServiceCredentialsRequest.newBuilder()
|
||||
.setExternalService(ExternalServiceType.EXTERNAL_SERVICE_TYPE_DIRECTORY)
|
||||
.build()),
|
||||
DIRECTORY_CREDENTIALS_GENERATOR
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* `ExternalServiceDefinitions` enum is supposed to have entries for all values in `ExternalServiceType`,
|
||||
* except for the `EXTERNAL_SERVICE_TYPE_UNSPECIFIED` and `UNRECOGNIZED`.
|
||||
* This test makes sure that is the case.
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@EnumSource(mode = EnumSource.Mode.EXCLUDE, names = { "UNRECOGNIZED", "EXTERNAL_SERVICE_TYPE_UNSPECIFIED" })
|
||||
public void testHaveExternalServiceDefinitionForServiceTypes(final ExternalServiceType externalServiceType) throws Exception {
|
||||
assertTrue(
|
||||
Arrays.stream(ExternalServiceDefinitions.values()).anyMatch(v -> v.externalService() == externalServiceType),
|
||||
"`ExternalServiceDefinitions` enum entry is missing for the `%s` value of `ExternalServiceType`".formatted(externalServiceType)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user