Group Send Endorsement support for unversioned profile fetch

This commit is contained in:
Jonathan Klabunde Tomer 2024-04-23 14:58:19 -07:00 committed by GitHub
parent 9ef1fee172
commit f0dcd8e07b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 274 additions and 66 deletions

View File

@ -798,7 +798,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
.addService(new KeysAnonymousGrpcService(accountsManager, keysManager, zkSecretParams, Clock.systemUTC()))
.addService(new PaymentsGrpcService(currencyManager))
.addService(ExternalServiceCredentialsAnonymousGrpcService.create(accountsManager, config))
.addService(new ProfileAnonymousGrpcService(accountsManager, profilesManager, profileBadgeConverter, zkProfileOperations));
.addService(new ProfileAnonymousGrpcService(accountsManager, profilesManager, profileBadgeConverter, zkSecretParams));
}
};
@ -969,7 +969,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
new PaymentsController(currencyManager, paymentsCredentialsGenerator),
new ProfileController(clock, rateLimiters, accountsManager, profilesManager, dynamicConfigurationManager,
profileBadgeConverter, config.getBadges(), cdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner,
config.getCdnConfiguration().bucket(), zkProfileOperations, batchIdentityCheckExecutor),
config.getCdnConfiguration().bucket(), zkSecretParams, zkProfileOperations, batchIdentityCheckExecutor),
new ProvisioningController(rateLimiters, provisioningManager),
new RegistrationController(accountsManager, phoneVerificationTokenManager, registrationLockVerificationManager,
rateLimiters),

View File

@ -58,13 +58,17 @@ import org.glassfish.jersey.server.ManagedAsync;
import org.signal.libsignal.protocol.IdentityKey;
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.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialResponse;
import org.signal.libsignal.zkgroup.profiles.ServerZkProfileOperations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.Anonymous;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.GroupSendTokenHeader;
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessChecksum;
import org.whispersystems.textsecuregcm.badges.ProfileBadgeConverter;
@ -109,19 +113,20 @@ import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
public class ProfileController {
private final Logger logger = LoggerFactory.getLogger(ProfileController.class);
private final Clock clock;
private final RateLimiters rateLimiters;
private final ProfilesManager profilesManager;
private final AccountsManager accountsManager;
private final RateLimiters rateLimiters;
private final ProfilesManager profilesManager;
private final AccountsManager accountsManager;
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
private final ProfileBadgeConverter profileBadgeConverter;
private final Map<String, BadgeConfiguration> badgeConfigurationMap;
private final PolicySigner policySigner;
private final PostPolicyGenerator policyGenerator;
private final PolicySigner policySigner;
private final PostPolicyGenerator policyGenerator;
private final ServerSecretParams serverSecretParams;
private final ServerZkProfileOperations zkProfileOperations;
private final S3Client s3client;
private final String bucket;
private final S3Client s3client;
private final String bucket;
private final Executor batchIdentityCheckExecutor;
@ -142,21 +147,23 @@ public class ProfileController {
PostPolicyGenerator policyGenerator,
PolicySigner policySigner,
String bucket,
ServerSecretParams serverSecretParams,
ServerZkProfileOperations zkProfileOperations,
Executor batchIdentityCheckExecutor) {
this.clock = clock;
this.rateLimiters = rateLimiters;
this.accountsManager = accountsManager;
this.profilesManager = profilesManager;
this.rateLimiters = rateLimiters;
this.accountsManager = accountsManager;
this.profilesManager = profilesManager;
this.dynamicConfigurationManager = dynamicConfigurationManager;
this.profileBadgeConverter = profileBadgeConverter;
this.badgeConfigurationMap = badgesConfiguration.getBadges().stream().collect(Collectors.toMap(
BadgeConfiguration::getId, Function.identity()));
this.serverSecretParams = serverSecretParams;
this.zkProfileOperations = zkProfileOperations;
this.bucket = bucket;
this.s3client = s3client;
this.policyGenerator = policyGenerator;
this.policySigner = policySigner;
this.bucket = bucket;
this.s3client = s3client;
this.policyGenerator = policyGenerator;
this.policySigner = policySigner;
this.batchIdentityCheckExecutor = Preconditions.checkNotNull(batchIdentityCheckExecutor);
}
@ -282,6 +289,7 @@ public class ProfileController {
public BaseProfileResponse getUnversionedProfile(
@ReadOnly @Auth Optional<AuthenticatedAccount> auth,
@HeaderParam(HeaderUtils.UNIDENTIFIED_ACCESS_KEY) Optional<Anonymous> accessKey,
@HeaderParam(HeaderUtils.GROUP_SEND_TOKEN) Optional<GroupSendTokenHeader> groupSendToken,
@Context ContainerRequestContext containerRequestContext,
@HeaderParam(HttpHeaders.USER_AGENT) String userAgent,
@PathParam("identifier") ServiceIdentifier identifier,
@ -290,8 +298,22 @@ public class ProfileController {
final Optional<Account> maybeRequester = auth.map(AuthenticatedAccount::getAccount);
final Account targetAccount = verifyPermissionToReceiveProfile(
maybeRequester, accessKey.filter(ignored -> identifier.identityType() == IdentityType.ACI), identifier);
final Account targetAccount;
if (groupSendToken.isPresent()) {
if (accessKey.isPresent()) {
throw new BadRequestException("may not provide both group send token and unidentified access key");
}
try {
final GroupSendFullToken token = groupSendToken.get().token();
token.verify(List.of(identifier.toLibsignal()), clock.instant(), GroupSendDerivedKeyPair.forExpiration(token.getExpiration(), serverSecretParams));
targetAccount = accountsManager.getByServiceIdentifier(identifier).orElseThrow(NotFoundException::new);
} catch (VerificationFailedException e) {
throw new NotAuthorizedException(e);
}
} else {
targetAccount = verifyPermissionToReceiveProfile(
maybeRequester, accessKey.filter(ignored -> identifier.identityType() == IdentityType.ACI), identifier);
}
return switch (identifier.identityType()) {
case ACI -> {
yield buildBaseProfileResponseForAccountIdentity(targetAccount,

View File

@ -6,6 +6,10 @@
package org.whispersystems.textsecuregcm.grpc;
import io.grpc.Status;
import java.time.Clock;
import java.util.List;
import org.signal.chat.profile.CredentialType;
import org.signal.chat.profile.GetExpiringProfileKeyCredentialAnonymousRequest;
import org.signal.chat.profile.GetExpiringProfileKeyCredentialResponse;
@ -14,6 +18,7 @@ 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.ServerSecretParams;
import org.signal.libsignal.zkgroup.profiles.ServerZkProfileOperations;
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil;
import org.whispersystems.textsecuregcm.badges.ProfileBadgeConverter;
@ -29,16 +34,18 @@ public class ProfileAnonymousGrpcService extends ReactorProfileAnonymousGrpc.Pro
private final ProfilesManager profilesManager;
private final ProfileBadgeConverter profileBadgeConverter;
private final ServerZkProfileOperations zkProfileOperations;
private final GroupSendTokenUtil groupSendTokenUtil;
public ProfileAnonymousGrpcService(
final AccountsManager accountsManager,
final ProfilesManager profilesManager,
final ProfileBadgeConverter profileBadgeConverter,
final ServerZkProfileOperations zkProfileOperations) {
final ServerSecretParams serverSecretParams) {
this.accountsManager = accountsManager;
this.profilesManager = profilesManager;
this.profileBadgeConverter = profileBadgeConverter;
this.zkProfileOperations = zkProfileOperations;
this.zkProfileOperations = new ServerZkProfileOperations(serverSecretParams);
this.groupSendTokenUtil = new GroupSendTokenUtil(serverSecretParams, Clock.systemUTC());
}
@Override
@ -51,8 +58,18 @@ public class ProfileAnonymousGrpcService extends ReactorProfileAnonymousGrpc.Pro
throw Status.UNAUTHENTICATED.asRuntimeException();
}
return getTargetAccountAndValidateUnidentifiedAccess(targetIdentifier, request.getUnidentifiedAccessKey().toByteArray())
.map(targetAccount -> ProfileGrpcHelper.buildUnversionedProfileResponse(targetIdentifier,
final Mono<Account> account = switch (request.getAuthenticationCase()) {
case GROUP_SEND_TOKEN ->
groupSendTokenUtil.checkGroupSendToken(request.getGroupSendToken(), List.of(targetIdentifier))
.then(Mono.fromFuture(() -> accountsManager.getByServiceIdentifierAsync(targetIdentifier)))
.flatMap(Mono::justOrEmpty)
.switchIfEmpty(Mono.error(Status.NOT_FOUND.asException()));
case UNIDENTIFIED_ACCESS_KEY ->
getTargetAccountAndValidateUnidentifiedAccess(targetIdentifier, request.getUnidentifiedAccessKey().toByteArray());
default -> Mono.error(Status.INVALID_ARGUMENT.asException());
};
return account.map(targetAccount -> ProfileGrpcHelper.buildUnversionedProfileResponse(targetIdentifier,
null,
targetAccount,
profileBadgeConverter));

View File

@ -211,10 +211,18 @@ message GetUnversionedProfileAnonymousRequest {
* Contains the data necessary to request an unversioned profile.
*/
GetUnversionedProfileRequest request = 1;
/**
* The unidentified access key for the targeted account.
*/
bytes unidentified_access_key = 2;
oneof authentication {
/**
* The unidentified access key for the targeted account.
*/
bytes unidentified_access_key = 2;
/**
* A group send endorsement token for the targeted account.
*/
bytes group_send_token = 3;
}
}
message GetUnversionedProfileResponse {

View File

@ -44,6 +44,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.stream.Stream;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
@ -117,7 +118,7 @@ import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
@ExtendWith(DropwizardExtensionsSupport.class)
class ProfileControllerTest {
private static final Clock clock = TestClock.pinned(Instant.ofEpochSecond(42));
private static final TestClock clock = TestClock.now();
private static final AccountsManager accountsManager = mock(AccountsManager.class);
private static final ProfilesManager profilesManager = mock(ProfilesManager.class);
private static final RateLimiters rateLimiters = mock(RateLimiters.class);
@ -129,6 +130,7 @@ class ProfileControllerTest {
"accessKey");
private static final PolicySigner policySigner = new PolicySigner("accessSecret", "us-west-1");
private static final ServerZkProfileOperations zkProfileOperations = mock(ServerZkProfileOperations.class);
private static final ServerSecretParams serverSecretParams = ServerSecretParams.generate();
private static final byte[] UNIDENTIFIED_ACCESS_KEY = "sixteenbytes1234".getBytes(StandardCharsets.UTF_8);
private static final IdentityKey ACCOUNT_IDENTITY_KEY = new IdentityKey(Curve.generateKeyPair().getPublicKey());
@ -170,6 +172,7 @@ class ProfileControllerTest {
postPolicyGenerator,
policySigner,
"profilesBucket",
serverSecretParams,
zkProfileOperations,
Executors.newSingleThreadExecutor()))
.build();
@ -177,7 +180,7 @@ class ProfileControllerTest {
@BeforeEach
void setup() {
reset(s3client);
clock.pin(Instant.ofEpochSecond(42));
AccountsHelper.setupMockUpdate(accountsManager);
dynamicPaymentsConfiguration = mock(DynamicPaymentsConfiguration.class);
@ -311,6 +314,65 @@ class ProfileControllerTest {
assertThat(response.getStatus()).isEqualTo(401);
}
@ParameterizedTest
@MethodSource
void testProfileGetWithGroupSendEndorsement(
UUID target, UUID 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("/v1/profile/" + target)
.queryParam("pq", "true")
.request()
.header(
HeaderUtils.GROUP_SEND_TOKEN,
AuthHelper.validGroupSendTokenHeader(serverSecretParams, List.of(new AciServiceIdentifier(authorizedTarget)), expiration));
if (includeUak) {
builder = builder.header(HeaderUtils.UNIDENTIFIED_ACCESS_KEY, AuthHelper.getUnidentifiedAccessHeader(UNIDENTIFIED_ACCESS_KEY));
}
Response response = builder.get();
assertThat(response.getStatus()).isEqualTo(expectedResponse);
if (expectedResponse == 200) {
final BaseProfileResponse profile = response.readEntity(BaseProfileResponse.class);
assertThat(profile.getIdentityKey()).isEqualTo(ACCOUNT_TWO_IDENTITY_KEY);
assertThat(profile.getBadges()).hasSize(1).element(0).has(new Condition<>(
badge -> "Test Badge".equals(badge.getName()), "has badge with expected name"));
}
verifyNoMoreInteractions(rateLimiter);
}
private static Stream<Arguments> testProfileGetWithGroupSendEndorsement() {
UUID notExistsUuid = UUID.randomUUID();
return Stream.of(
// valid endorsement
Arguments.of(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_UUID_TWO, Duration.ofHours(1), false, 200),
// expired endorsement, not authorized
Arguments.of(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_UUID_TWO, Duration.ofHours(-1), false, 401),
// endorsement for the wrong recipient, not authorized
Arguments.of(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_UUID, Duration.ofHours(1), false, 401),
// expired endorsement for the wrong recipient, not authorized
Arguments.of(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_UUID, Duration.ofHours(-1), false, 401),
// valid endorsement for the right recipient but they aren't registered, not found
Arguments.of(notExistsUuid, notExistsUuid, Duration.ofHours(1), false, 404),
// expired endorsement for the right recipient but they aren't registered, not authorized (NOT not found)
Arguments.of(notExistsUuid, notExistsUuid, Duration.ofHours(-1), false, 401),
// valid endorsement but also a UAK, bad request
Arguments.of(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_UUID_TWO, Duration.ofHours(1), true, 400));
}
@Test
void testProfileGetByPni() throws RateLimitExceededException {
final BaseProfileResponse profile = resources.getJerseyTest()

View File

@ -19,6 +19,7 @@ import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertStatusEx
import com.google.protobuf.ByteString;
import io.grpc.Status;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
@ -73,7 +74,9 @@ import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
import org.whispersystems.textsecuregcm.storage.VersionedProfile;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.tests.util.ProfileTestHelper;
import org.whispersystems.textsecuregcm.util.TestClock;
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
import org.whispersystems.textsecuregcm.util.UUIDUtil;
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
@ -81,6 +84,8 @@ import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
public class ProfileAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<ProfileAnonymousGrpcService, ProfileAnonymousGrpc.ProfileAnonymousBlockingStub> {
private final ServerSecretParams SERVER_SECRET_PARAMS = ServerSecretParams.generate();
@Mock
private Account account;
@ -93,10 +98,6 @@ public class ProfileAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<ProfileA
@Mock
private ProfileBadgeConverter profileBadgeConverter;
@Mock
private ServerZkProfileOperations serverZkProfileOperations;
@Override
protected ProfileAnonymousGrpcService createServiceBeforeEachTest() {
getMockRequestAttributesInterceptor().setAcceptLanguage(Locale.LanguageRange.parse("en-us"));
@ -111,12 +112,12 @@ public class ProfileAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<ProfileA
accountsManager,
profilesManager,
profileBadgeConverter,
serverZkProfileOperations
SERVER_SECRET_PARAMS
);
}
@Test
void getUnversionedProfile() {
void getUnversionedProfileUnidentifiedAccessKey() {
final UUID targetUuid = UUID.randomUUID();
final org.whispersystems.textsecuregcm.identity.ServiceIdentifier serviceIdentifier = new AciServiceIdentifier(targetUuid);
@ -169,9 +170,71 @@ public class ProfileAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<ProfileA
assertEquals(expectedResponse, response);
}
@Test
void getUnversionedProfileGroupSendEndorsement() throws Exception {
final UUID targetUuid = UUID.randomUUID();
final org.whispersystems.textsecuregcm.identity.ServiceIdentifier serviceIdentifier = new AciServiceIdentifier(targetUuid);
// Expiration must be on a day boundary; we want one in the future
final Instant expiration = Instant.now().plus(Duration.ofDays(1)).truncatedTo(ChronoUnit.DAYS);
final byte[] token = AuthHelper.validGroupSendToken(SERVER_SECRET_PARAMS, List.of(serviceIdentifier), expiration);
final ECKeyPair identityKeyPair = Curve.generateKeyPair();
final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey());
final List<Badge> badges = List.of(new Badge(
"TEST",
"other",
"Test Badge",
"This badge is in unit tests.",
List.of("l", "m", "h", "x", "xx", "xxx"),
"SVG",
List.of(
new BadgeSvg("sl", "sd"),
new BadgeSvg("ml", "md"),
new BadgeSvg("ll", "ld")))
);
when(account.getBadges()).thenReturn(Collections.emptyList());
when(profileBadgeConverter.convert(any(), any(), anyBoolean())).thenReturn(badges);
when(account.isUnrestrictedUnidentifiedAccess()).thenReturn(false);
when(account.getIdentityKey(org.whispersystems.textsecuregcm.identity.IdentityType.ACI)).thenReturn(identityKey);
when(accountsManager.getByServiceIdentifierAsync(serviceIdentifier)).thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
final GetUnversionedProfileAnonymousRequest request = GetUnversionedProfileAnonymousRequest.newBuilder()
.setGroupSendToken(ByteString.copyFrom(token))
.setRequest(GetUnversionedProfileRequest.newBuilder()
.setServiceIdentifier(
ServiceIdentifierUtil.toGrpcServiceIdentifier(serviceIdentifier))
.build())
.build();
final GetUnversionedProfileResponse response = unauthenticatedServiceStub().getUnversionedProfile(request);
final GetUnversionedProfileResponse expectedResponse = GetUnversionedProfileResponse.newBuilder()
.setIdentityKey(ByteString.copyFrom(identityKey.serialize()))
.setUnrestrictedUnidentifiedAccess(false)
.setCapabilities(ProfileGrpcHelper.buildUserCapabilities(new UserCapabilities()))
.addAllBadges(ProfileGrpcHelper.buildBadges(badges))
.build();
verify(accountsManager).getByServiceIdentifierAsync(serviceIdentifier);
assertEquals(expectedResponse, response);
}
@Test
void getUnversionedProfileNoAuth() {
final GetUnversionedProfileAnonymousRequest request = GetUnversionedProfileAnonymousRequest.newBuilder()
.setRequest(GetUnversionedProfileRequest.newBuilder()
.setServiceIdentifier(ServiceIdentifierUtil.toGrpcServiceIdentifier(new AciServiceIdentifier(UUID.randomUUID()))))
.build();
assertStatusException(Status.INVALID_ARGUMENT, () -> unauthenticatedServiceStub().getUnversionedProfile(request));
}
@ParameterizedTest
@MethodSource
void getUnversionedProfileUnauthenticated(final IdentityType identityType, final boolean missingUnidentifiedAccessKey, final boolean accountNotFound) {
void getUnversionedProfileIncorrectUnidentifiedAccessKey(final IdentityType identityType, final boolean wrongUnidentifiedAccessKey, final boolean accountNotFound) {
final byte[] unidentifiedAccessKey = TestRandomUtil.nextBytes(UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH);
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey));
@ -179,22 +242,21 @@ public class ProfileAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<ProfileA
when(accountsManager.getByServiceIdentifierAsync(any())).thenReturn(
CompletableFuture.completedFuture(accountNotFound ? Optional.empty() : Optional.of(account)));
final GetUnversionedProfileAnonymousRequest.Builder requestBuilder = GetUnversionedProfileAnonymousRequest.newBuilder()
final GetUnversionedProfileAnonymousRequest request = GetUnversionedProfileAnonymousRequest.newBuilder()
.setUnidentifiedAccessKey(
ByteString.copyFrom(wrongUnidentifiedAccessKey
? new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH]
: unidentifiedAccessKey))
.setRequest(GetUnversionedProfileRequest.newBuilder()
.setServiceIdentifier(ServiceIdentifier.newBuilder()
.setIdentityType(identityType)
.setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID())))
.build())
.build());
.setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID())))))
.build();
if (!missingUnidentifiedAccessKey) {
requestBuilder.setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey));
}
assertStatusException(Status.UNAUTHENTICATED, () -> unauthenticatedServiceStub().getUnversionedProfile(requestBuilder.build()));
assertStatusException(Status.UNAUTHENTICATED, () -> unauthenticatedServiceStub().getUnversionedProfile(request));
}
private static Stream<Arguments> getUnversionedProfileUnauthenticated() {
private static Stream<Arguments> getUnversionedProfileIncorrectUnidentifiedAccessKey() {
return Stream.of(
Arguments.of(IdentityType.IDENTITY_TYPE_PNI, false, false),
Arguments.of(IdentityType.IDENTITY_TYPE_ACI, true, false),
@ -202,6 +264,62 @@ public class ProfileAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<ProfileA
);
}
@Test
void getUnversionedProfileExpiredGroupSendEndorsement() throws Exception {
final AciServiceIdentifier serviceIdentifier = new AciServiceIdentifier(UUID.randomUUID());
// Expirations must be on a day boundary; pick one in the recent past
final Instant expiration = Instant.now().truncatedTo(ChronoUnit.DAYS);
final byte[] token = AuthHelper.validGroupSendToken(SERVER_SECRET_PARAMS, List.of(serviceIdentifier), expiration);
final GetUnversionedProfileAnonymousRequest request = GetUnversionedProfileAnonymousRequest.newBuilder()
.setGroupSendToken(ByteString.copyFrom(token))
.setRequest(GetUnversionedProfileRequest.newBuilder()
.setServiceIdentifier(
ServiceIdentifierUtil.toGrpcServiceIdentifier(serviceIdentifier)))
.build();
assertStatusException(Status.UNAUTHENTICATED, () -> unauthenticatedServiceStub().getUnversionedProfile(request));
}
@Test
void getUnversionedProfileIncorrectGroupSendEndorsement() throws Exception {
final AciServiceIdentifier targetServiceIdentifier = new AciServiceIdentifier(UUID.randomUUID());
final AciServiceIdentifier authorizedServiceIdentifier = new AciServiceIdentifier(UUID.randomUUID());
// Expiration must be on a day boundary; we want one in the future
final Instant expiration = Instant.now().plus(Duration.ofDays(1)).truncatedTo(ChronoUnit.DAYS);
final byte[] token = AuthHelper.validGroupSendToken(SERVER_SECRET_PARAMS, List.of(authorizedServiceIdentifier), expiration);
when(accountsManager.getByServiceIdentifierAsync(any())).thenReturn(
CompletableFuture.completedFuture(Optional.empty()));
final GetUnversionedProfileAnonymousRequest request = GetUnversionedProfileAnonymousRequest.newBuilder()
.setGroupSendToken(ByteString.copyFrom(token))
.setRequest(GetUnversionedProfileRequest.newBuilder()
.setServiceIdentifier(
ServiceIdentifierUtil.toGrpcServiceIdentifier(targetServiceIdentifier)))
.build();
assertStatusException(Status.UNAUTHENTICATED, () -> unauthenticatedServiceStub().getUnversionedProfile(request));
}
@Test
void getUnversionedProfileGroupSendEndorsementAccountNotFound() throws Exception {
final AciServiceIdentifier serviceIdentifier = new AciServiceIdentifier(UUID.randomUUID());
// Expiration must be on a day boundary; we want one in the future
final Instant expiration = Instant.now().plus(Duration.ofDays(1)).truncatedTo(ChronoUnit.DAYS);
final byte[] token = AuthHelper.validGroupSendToken(SERVER_SECRET_PARAMS, List.of(serviceIdentifier), expiration);
when(accountsManager.getByServiceIdentifierAsync(any())).thenReturn(CompletableFuture.completedFuture(Optional.empty()));
final GetUnversionedProfileAnonymousRequest request = GetUnversionedProfileAnonymousRequest.newBuilder()
.setGroupSendToken(ByteString.copyFrom(token))
.setRequest(GetUnversionedProfileRequest.newBuilder()
.setServiceIdentifier(ServiceIdentifierUtil.toGrpcServiceIdentifier(serviceIdentifier)))
.build();
assertStatusException(Status.NOT_FOUND, () -> unauthenticatedServiceStub().getUnversionedProfile(request));
}
@ParameterizedTest
@MethodSource
void getVersionedProfile(final String requestVersion,
@ -343,11 +461,7 @@ public class ProfileAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<ProfileA
final byte[] unidentifiedAccessKey = TestRandomUtil.nextBytes(UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH);
final UUID targetUuid = UUID.randomUUID();
final ServerSecretParams serverSecretParams = ServerSecretParams.generate();
final ServerPublicParams serverPublicParams = serverSecretParams.getPublicParams();
final ServerZkProfileOperations serverZkProfile = new ServerZkProfileOperations(serverSecretParams);
final ClientZkProfileOperations clientZkProfile = new ClientZkProfileOperations(serverPublicParams);
final ClientZkProfileOperations clientZkProfile = new ClientZkProfileOperations(SERVER_SECRET_PARAMS.getPublicParams());
final byte[] profileKeyBytes = TestRandomUtil.nextBytes(32);
final ProfileKey profileKey = new ProfileKey(profileKeyBytes);
@ -368,12 +482,6 @@ public class ProfileAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<ProfileA
final Instant expiration = Instant.now().plus(org.whispersystems.textsecuregcm.util.ProfileHelper.EXPIRING_PROFILE_KEY_CREDENTIAL_EXPIRATION)
.truncatedTo(ChronoUnit.DAYS);
final ExpiringProfileKeyCredentialResponse credentialResponse =
serverZkProfile.issueExpiringProfileKeyCredential(credentialRequest, new ServiceId.Aci(targetUuid), profileKeyCommitment, expiration);
when(serverZkProfileOperations.issueExpiringProfileKeyCredential(credentialRequest, new ServiceId.Aci(targetUuid), profileKeyCommitment, expiration))
.thenReturn(credentialResponse);
final GetExpiringProfileKeyCredentialAnonymousRequest request = GetExpiringProfileKeyCredentialAnonymousRequest.newBuilder()
.setRequest(GetExpiringProfileKeyCredentialRequest.newBuilder()
.setAccountIdentifier(ServiceIdentifier.newBuilder()
@ -389,13 +497,8 @@ public class ProfileAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<ProfileA
final GetExpiringProfileKeyCredentialResponse response = unauthenticatedServiceStub().getExpiringProfileKeyCredential(request);
assertArrayEquals(credentialResponse.serialize(), response.getProfileKeyCredential().toByteArray());
verify(serverZkProfileOperations).issueExpiringProfileKeyCredential(credentialRequest, new ServiceId.Aci(targetUuid), profileKeyCommitment, expiration);
final ClientZkProfileOperations clientZkProfileCipher = new ClientZkProfileOperations(serverPublicParams);
assertThatNoException().isThrownBy(() ->
clientZkProfileCipher.receiveExpiringProfileKeyCredential(profileKeyCredentialRequestContext, new ExpiringProfileKeyCredentialResponse(response.getProfileKeyCredential().toByteArray())));
clientZkProfile.receiveExpiringProfileKeyCredential(profileKeyCredentialRequestContext, new ExpiringProfileKeyCredentialResponse(response.getProfileKeyCredential().toByteArray())));
}
@ParameterizedTest
@ -471,10 +574,6 @@ public class ProfileAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<ProfileA
final UUID targetUuid = UUID.randomUUID();
final byte[] unidentifiedAccessKey = TestRandomUtil.nextBytes(UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH);
if (throwZkVerificationException) {
when(serverZkProfileOperations.issueExpiringProfileKeyCredential(any(), any(), any(), any())).thenThrow(new VerificationFailedException());
}
final VersionedProfile profile = mock(VersionedProfile.class);
when(profile.commitment()).thenReturn("commitment".getBytes(StandardCharsets.UTF_8));
when(account.getUuid()).thenReturn(targetUuid);