Profile gRPC: Define getExpiringProfileKeyCredential endpoint

This commit is contained in:
Katherine Yen
2023-08-30 14:56:43 -07:00
committed by GitHub
parent dd18fcaea2
commit 6a37b73463
13 changed files with 696 additions and 168 deletions

View File

@@ -648,8 +648,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
.addService(ServerInterceptors.intercept(new KeysGrpcService(accountsManager, keys, rateLimiters), basicCredentialAuthenticationInterceptor))
.addService(new KeysAnonymousGrpcService(accountsManager, keys))
.addService(ServerInterceptors.intercept(new ProfileGrpcService(clock, accountsManager, profilesManager, dynamicConfigurationManager,
config.getBadges(), asyncCdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner, profileBadgeConverter, rateLimiters, config.getCdnConfiguration().bucket()), basicCredentialAuthenticationInterceptor))
.addService(new ProfileAnonymousGrpcService(accountsManager, profilesManager, profileBadgeConverter));
config.getBadges(), asyncCdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner, profileBadgeConverter, rateLimiters, zkProfileOperations, config.getCdnConfiguration().bucket()), basicCredentialAuthenticationInterceptor))
.addService(new ProfileAnonymousGrpcService(accountsManager, profilesManager, profileBadgeConverter, zkProfileOperations));
RemoteDeprecationFilter remoteDeprecationFilter = new RemoteDeprecationFilter(dynamicConfigurationManager);
environment.servlets()

View File

@@ -7,7 +7,6 @@ package org.whispersystems.textsecuregcm.controllers;
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import io.dropwizard.auth.Auth;
import io.micrometer.core.instrument.Counter;
@@ -18,11 +17,7 @@ import io.vavr.Tuple;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
@@ -66,8 +61,6 @@ import org.signal.libsignal.protocol.ServiceId;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialResponse;
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCommitment;
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequest;
import org.signal.libsignal.zkgroup.profiles.ServerZkProfileOperations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -130,9 +123,6 @@ public class ProfileController {
private final Executor batchIdentityCheckExecutor;
@VisibleForTesting
static final Duration EXPIRING_PROFILE_KEY_CREDENTIAL_EXPIRATION = Duration.ofDays(7);
private static final String EXPIRING_PROFILE_KEY_CREDENTIAL_TYPE = "expiringProfileKey";
private static final Counter VERSION_NOT_FOUND_COUNTER = Metrics.counter(name(ProfileController.class, "versionNotFound"));
@@ -276,7 +266,6 @@ public class ProfileController {
version,
credentialRequest,
isSelf,
Instant.now().plus(EXPIRING_PROFILE_KEY_CREDENTIAL_EXPIRATION).truncatedTo(ChronoUnit.DAYS),
containerRequestContext);
}
@@ -387,11 +376,19 @@ public class ProfileController {
final String version,
final String encodedCredentialRequest,
final boolean isSelf,
final Instant expiration,
final ContainerRequestContext containerRequestContext) {
final ExpiringProfileKeyCredentialResponse expiringProfileKeyCredentialResponse = profilesManager.get(account.getUuid(), version)
.map(profile -> getExpiringProfileKeyCredentialResponse(encodedCredentialRequest, profile, new ServiceId.Aci(account.getUuid()), expiration))
.map(profile -> {
final ExpiringProfileKeyCredentialResponse profileKeyCredentialResponse;
try {
profileKeyCredentialResponse = ProfileHelper.getExpiringProfileKeyCredential(HexFormat.of().parseHex(encodedCredentialRequest),
profile, new ServiceId.Aci(account.getUuid()), zkProfileOperations);
} catch (VerificationFailedException | InvalidInputException e) {
throw new BadRequestException(Response.status(Response.Status.BAD_REQUEST).build(), e);
}
return profileKeyCredentialResponse;
})
.orElse(null);
return new ExpiringProfileKeyCredentialProfileResponse(
@@ -453,23 +450,6 @@ public class ProfileController {
new PniServiceIdentifier(account.getPhoneNumberIdentifier()));
}
private ExpiringProfileKeyCredentialResponse getExpiringProfileKeyCredentialResponse(
final String encodedCredentialRequest,
final VersionedProfile profile,
final ServiceId.Aci accountIdentifier,
final Instant expiration) {
try {
final ProfileKeyCommitment commitment = new ProfileKeyCommitment(profile.commitment());
final ProfileKeyCredentialRequest request = new ProfileKeyCredentialRequest(
HexFormat.of().parseHex(encodedCredentialRequest));
return zkProfileOperations.issueExpiringProfileKeyCredential(request, accountIdentifier, commitment, expiration);
} catch (IllegalArgumentException | VerificationFailedException | InvalidInputException e) {
throw new WebApplicationException(e, Response.status(Response.Status.BAD_REQUEST).build());
}
}
private List<Locale> getAcceptableLanguagesForRequest(final ContainerRequestContext containerRequestContext) {
try {
return containerRequestContext.getAcceptableLanguages();

View File

@@ -1,11 +1,15 @@
package org.whispersystems.textsecuregcm.grpc;
import io.grpc.Status;
import org.signal.chat.profile.CredentialType;
import org.signal.chat.profile.GetExpiringProfileKeyCredentialAnonymousRequest;
import org.signal.chat.profile.GetExpiringProfileKeyCredentialResponse;
import org.signal.chat.profile.GetUnversionedProfileAnonymousRequest;
import org.signal.chat.profile.GetUnversionedProfileResponse;
import org.signal.chat.profile.GetVersionedProfileAnonymousRequest;
import org.signal.chat.profile.GetVersionedProfileResponse;
import org.signal.chat.profile.ReactorProfileAnonymousGrpc;
import org.signal.libsignal.zkgroup.profiles.ServerZkProfileOperations;
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil;
import org.whispersystems.textsecuregcm.badges.ProfileBadgeConverter;
import org.whispersystems.textsecuregcm.identity.IdentityType;
@@ -19,14 +23,17 @@ public class ProfileAnonymousGrpcService extends ReactorProfileAnonymousGrpc.Pro
private final AccountsManager accountsManager;
private final ProfilesManager profilesManager;
private final ProfileBadgeConverter profileBadgeConverter;
private final ServerZkProfileOperations zkProfileOperations;
public ProfileAnonymousGrpcService(
final AccountsManager accountsManager,
final ProfilesManager profilesManager,
final ProfileBadgeConverter profileBadgeConverter) {
final ProfileBadgeConverter profileBadgeConverter,
final ServerZkProfileOperations zkProfileOperations) {
this.accountsManager = accountsManager;
this.profilesManager = profilesManager;
this.profileBadgeConverter = profileBadgeConverter;
this.zkProfileOperations = zkProfileOperations;
}
@Override
@@ -58,10 +65,28 @@ public class ProfileAnonymousGrpcService extends ReactorProfileAnonymousGrpc.Pro
.flatMap(targetAccount -> ProfileGrpcHelper.getVersionedProfile(targetAccount, profilesManager, request.getRequest().getVersion()));
}
private Mono<Account> getTargetAccountAndValidateUnidentifiedAccess(final ServiceIdentifier targetIdentifier, final byte[] unidentifiedAccessKey) {
return Mono.fromFuture(() -> accountsManager.getByServiceIdentifierAsync(targetIdentifier))
.flatMap(Mono::justOrEmpty)
.filter(targetAccount -> UnidentifiedAccessUtil.checkUnidentifiedAccess(targetAccount, unidentifiedAccessKey))
.switchIfEmpty(Mono.error(Status.UNAUTHENTICATED.asException()));
@Override
public Mono<GetExpiringProfileKeyCredentialResponse> getExpiringProfileKeyCredential(
final GetExpiringProfileKeyCredentialAnonymousRequest request) {
final ServiceIdentifier targetIdentifier = ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getRequest().getAccountIdentifier());
if (targetIdentifier.identityType() != IdentityType.ACI) {
throw Status.INVALID_ARGUMENT.withDescription("Expected ACI service identifier").asRuntimeException();
}
if (request.getRequest().getCredentialType() != CredentialType.CREDENTIAL_TYPE_EXPIRING_PROFILE_KEY) {
throw Status.INVALID_ARGUMENT.withDescription("Expected expiring profile key credential type").asRuntimeException();
}
return getTargetAccountAndValidateUnidentifiedAccess(targetIdentifier, request.getUnidentifiedAccessKey().toByteArray())
.flatMap(account -> ProfileGrpcHelper.getExpiringProfileKeyCredentialResponse(account.getUuid(),
request.getRequest().getVersion(), request.getRequest().getCredentialRequest().toByteArray(), profilesManager, zkProfileOperations));
}
private Mono<Account> getTargetAccountAndValidateUnidentifiedAccess(final ServiceIdentifier targetIdentifier, final byte[] unidentifiedAccessKey) {
return Mono.fromFuture(() -> accountsManager.getByServiceIdentifierAsync(targetIdentifier))
.flatMap(Mono::justOrEmpty)
.filter(targetAccount -> UnidentifiedAccessUtil.checkUnidentifiedAccess(targetAccount, unidentifiedAccessKey))
.switchIfEmpty(Mono.error(Status.UNAUTHENTICATED.asException()));
}
}

View File

@@ -8,9 +8,15 @@ import java.util.UUID;
import io.grpc.Status;
import org.signal.chat.profile.Badge;
import org.signal.chat.profile.BadgeSvg;
import org.signal.chat.profile.GetExpiringProfileKeyCredentialResponse;
import org.signal.chat.profile.GetUnversionedProfileResponse;
import org.signal.chat.profile.GetVersionedProfileResponse;
import org.signal.chat.profile.UserCapabilities;
import org.signal.libsignal.protocol.ServiceId;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialResponse;
import org.signal.libsignal.zkgroup.profiles.ServerZkProfileOperations;
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessChecksum;
import org.whispersystems.textsecuregcm.badges.ProfileBadgeConverter;
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
@@ -119,4 +125,28 @@ public class ProfileGrpcHelper {
return responseBuilder.build();
}
static Mono<GetExpiringProfileKeyCredentialResponse> getExpiringProfileKeyCredentialResponse(
final UUID targetUuid,
final String version,
final byte[] encodedCredentialRequest,
final ProfilesManager profilesManager,
final ServerZkProfileOperations zkProfileOperations) {
return Mono.fromFuture(profilesManager.getAsync(targetUuid, version))
.flatMap(Mono::justOrEmpty)
.map(profile -> {
final ExpiringProfileKeyCredentialResponse profileKeyCredentialResponse;
try {
profileKeyCredentialResponse = ProfileHelper.getExpiringProfileKeyCredential(encodedCredentialRequest,
profile, new ServiceId.Aci(targetUuid), zkProfileOperations);
} catch (VerificationFailedException | InvalidInputException e) {
throw Status.INVALID_ARGUMENT.withCause(e).asRuntimeException();
}
return GetExpiringProfileKeyCredentialResponse.newBuilder()
.setProfileKeyCredential(ByteString.copyFrom(profileKeyCredentialResponse.serialize()))
.build();
})
.switchIfEmpty(Mono.error(Status.NOT_FOUND.withDescription("Profile version not found").asException()));
}
}

View File

@@ -2,6 +2,9 @@ package org.whispersystems.textsecuregcm.grpc;
import com.google.protobuf.ByteString;
import io.grpc.Status;
import org.signal.chat.profile.CredentialType;
import org.signal.chat.profile.GetExpiringProfileKeyCredentialRequest;
import org.signal.chat.profile.GetExpiringProfileKeyCredentialResponse;
import org.signal.chat.profile.GetUnversionedProfileRequest;
import org.signal.chat.profile.GetUnversionedProfileResponse;
import org.signal.chat.profile.GetVersionedProfileRequest;
@@ -11,6 +14,7 @@ import org.signal.chat.profile.ProfileAvatarUploadAttributes;
import org.signal.chat.profile.ReactorProfileGrpc;
import org.signal.chat.profile.SetProfileRequest;
import org.signal.chat.profile.SetProfileResponse;
import org.signal.libsignal.zkgroup.profiles.ServerZkProfileOperations;
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice;
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticationUtil;
import org.whispersystems.textsecuregcm.badges.ProfileBadgeConverter;
@@ -56,6 +60,7 @@ public class ProfileGrpcService extends ReactorProfileGrpc.ProfileImplBase {
private final PolicySigner policySigner;
private final ProfileBadgeConverter profileBadgeConverter;
private final RateLimiters rateLimiters;
private final ServerZkProfileOperations zkProfileOperations;
private final String bucket;
private record AvatarData(Optional<String> currentAvatar,
@@ -73,6 +78,7 @@ public class ProfileGrpcService extends ReactorProfileGrpc.ProfileImplBase {
final PolicySigner policySigner,
final ProfileBadgeConverter profileBadgeConverter,
final RateLimiters rateLimiters,
final ServerZkProfileOperations zkProfileOperations,
final String bucket) {
this.clock = clock;
this.accountsManager = accountsManager;
@@ -85,6 +91,7 @@ public class ProfileGrpcService extends ReactorProfileGrpc.ProfileImplBase {
this.policySigner = policySigner;
this.profileBadgeConverter = profileBadgeConverter;
this.rateLimiters = rateLimiters;
this.zkProfileOperations = zkProfileOperations;
this.bucket = bucket;
}
@@ -186,6 +193,26 @@ public class ProfileGrpcService extends ReactorProfileGrpc.ProfileImplBase {
.flatMap(account -> ProfileGrpcHelper.getVersionedProfile(account, profilesManager, request.getVersion()));
}
@Override
public Mono<GetExpiringProfileKeyCredentialResponse> getExpiringProfileKeyCredential(
final GetExpiringProfileKeyCredentialRequest request) {
final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice();
final ServiceIdentifier targetIdentifier = ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getAccountIdentifier());
if (targetIdentifier.identityType() != IdentityType.ACI) {
throw Status.INVALID_ARGUMENT.withDescription("Expected ACI service identifier").asRuntimeException();
}
if (request.getCredentialType() != CredentialType.CREDENTIAL_TYPE_EXPIRING_PROFILE_KEY) {
throw Status.INVALID_ARGUMENT.withDescription("Expected expiring profile key credential type").asRuntimeException();
}
return validateRateLimitAndGetAccount(authenticatedDevice.accountIdentifier(), targetIdentifier)
.flatMap(targetAccount -> ProfileGrpcHelper.getExpiringProfileKeyCredentialResponse(targetAccount.getUuid(),
request.getVersion(), request.getCredentialRequest().toByteArray(), profilesManager, zkProfileOperations));
}
private Mono<Account> validateRateLimitAndGetAccount(final UUID requesterUuid,
final ServiceIdentifier targetIdentifier) {
return rateLimiters.getProfileLimiter().validateReactive(requesterUuid)

View File

@@ -1,12 +1,23 @@
package org.whispersystems.textsecuregcm.util;
import com.google.common.annotations.VisibleForTesting;
import org.signal.libsignal.protocol.ServiceId;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialResponse;
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCommitment;
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequest;
import org.signal.libsignal.zkgroup.profiles.ServerZkProfileOperations;
import org.whispersystems.textsecuregcm.configuration.BadgeConfiguration;
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
import org.whispersystems.textsecuregcm.storage.AccountBadge;
import org.whispersystems.textsecuregcm.storage.VersionedProfile;
import javax.annotation.Nullable;
import java.security.SecureRandom;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Base64;
import java.util.LinkedHashMap;
@@ -16,6 +27,9 @@ import java.util.UUID;
public class ProfileHelper {
public static int MAX_PROFILE_AVATAR_SIZE_BYTES = 10 * 1024 * 1024;
@VisibleForTesting
public static final Duration EXPIRING_PROFILE_KEY_CREDENTIAL_EXPIRATION = Duration.ofDays(7);
public static List<AccountBadge> mergeBadgeIdsWithExistingAccountBadges(
final Clock clock,
final Map<String, BadgeConfiguration> badgeConfigurationMap,
@@ -70,4 +84,17 @@ public class ProfileHelper {
public static boolean isSelfProfileRequest(@Nullable final UUID requesterUuid, final AciServiceIdentifier targetIdentifier) {
return targetIdentifier.uuid().equals(requesterUuid);
}
public static ExpiringProfileKeyCredentialResponse getExpiringProfileKeyCredential(
final byte[] encodedCredentialRequest,
final VersionedProfile profile,
final ServiceId.Aci accountIdentifier,
final ServerZkProfileOperations zkProfileOperations) throws InvalidInputException, VerificationFailedException {
final Instant expiration = Instant.now().plus(EXPIRING_PROFILE_KEY_CREDENTIAL_EXPIRATION).truncatedTo(ChronoUnit.DAYS);
final ProfileKeyCommitment commitment = new ProfileKeyCommitment(profile.commitment());
final ProfileKeyCredentialRequest request = new ProfileKeyCredentialRequest(
encodedCredentialRequest);
return zkProfileOperations.issueExpiringProfileKeyCredential(request, accountIdentifier, commitment, expiration);
}
}