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

View File

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

View File

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

View File

@ -211,10 +211,18 @@ message GetUnversionedProfileAnonymousRequest {
* Contains the data necessary to request an unversioned profile. * Contains the data necessary to request an unversioned profile.
*/ */
GetUnversionedProfileRequest request = 1; GetUnversionedProfileRequest request = 1;
/**
* The unidentified access key for the targeted account. oneof authentication {
*/ /**
bytes unidentified_access_key = 2; * 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 { message GetUnversionedProfileResponse {

View File

@ -44,6 +44,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.stream.Stream; import java.util.stream.Stream;
import javax.ws.rs.client.Entity; import javax.ws.rs.client.Entity;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedHashMap; import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
@ -117,7 +118,7 @@ import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
@ExtendWith(DropwizardExtensionsSupport.class) @ExtendWith(DropwizardExtensionsSupport.class)
class ProfileControllerTest { 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 AccountsManager accountsManager = mock(AccountsManager.class);
private static final ProfilesManager profilesManager = mock(ProfilesManager.class); private static final ProfilesManager profilesManager = mock(ProfilesManager.class);
private static final RateLimiters rateLimiters = mock(RateLimiters.class); private static final RateLimiters rateLimiters = mock(RateLimiters.class);
@ -129,6 +130,7 @@ class ProfileControllerTest {
"accessKey"); "accessKey");
private static final PolicySigner policySigner = new PolicySigner("accessSecret", "us-west-1"); private static final PolicySigner policySigner = new PolicySigner("accessSecret", "us-west-1");
private static final ServerZkProfileOperations zkProfileOperations = mock(ServerZkProfileOperations.class); 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 byte[] UNIDENTIFIED_ACCESS_KEY = "sixteenbytes1234".getBytes(StandardCharsets.UTF_8);
private static final IdentityKey ACCOUNT_IDENTITY_KEY = new IdentityKey(Curve.generateKeyPair().getPublicKey()); private static final IdentityKey ACCOUNT_IDENTITY_KEY = new IdentityKey(Curve.generateKeyPair().getPublicKey());
@ -170,6 +172,7 @@ class ProfileControllerTest {
postPolicyGenerator, postPolicyGenerator,
policySigner, policySigner,
"profilesBucket", "profilesBucket",
serverSecretParams,
zkProfileOperations, zkProfileOperations,
Executors.newSingleThreadExecutor())) Executors.newSingleThreadExecutor()))
.build(); .build();
@ -177,7 +180,7 @@ class ProfileControllerTest {
@BeforeEach @BeforeEach
void setup() { void setup() {
reset(s3client); reset(s3client);
clock.pin(Instant.ofEpochSecond(42));
AccountsHelper.setupMockUpdate(accountsManager); AccountsHelper.setupMockUpdate(accountsManager);
dynamicPaymentsConfiguration = mock(DynamicPaymentsConfiguration.class); dynamicPaymentsConfiguration = mock(DynamicPaymentsConfiguration.class);
@ -311,6 +314,65 @@ class ProfileControllerTest {
assertThat(response.getStatus()).isEqualTo(401); 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 @Test
void testProfileGetByPni() throws RateLimitExceededException { void testProfileGetByPni() throws RateLimitExceededException {
final BaseProfileResponse profile = resources.getJerseyTest() 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 com.google.protobuf.ByteString;
import io.grpc.Status; import io.grpc.Status;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.Collections; 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.AccountsManager;
import org.whispersystems.textsecuregcm.storage.ProfilesManager; import org.whispersystems.textsecuregcm.storage.ProfilesManager;
import org.whispersystems.textsecuregcm.storage.VersionedProfile; import org.whispersystems.textsecuregcm.storage.VersionedProfile;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.tests.util.ProfileTestHelper; import org.whispersystems.textsecuregcm.tests.util.ProfileTestHelper;
import org.whispersystems.textsecuregcm.util.TestClock;
import org.whispersystems.textsecuregcm.util.TestRandomUtil; import org.whispersystems.textsecuregcm.util.TestRandomUtil;
import org.whispersystems.textsecuregcm.util.UUIDUtil; import org.whispersystems.textsecuregcm.util.UUIDUtil;
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException; 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> { public class ProfileAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<ProfileAnonymousGrpcService, ProfileAnonymousGrpc.ProfileAnonymousBlockingStub> {
private final ServerSecretParams SERVER_SECRET_PARAMS = ServerSecretParams.generate();
@Mock @Mock
private Account account; private Account account;
@ -93,10 +98,6 @@ public class ProfileAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<ProfileA
@Mock @Mock
private ProfileBadgeConverter profileBadgeConverter; private ProfileBadgeConverter profileBadgeConverter;
@Mock
private ServerZkProfileOperations serverZkProfileOperations;
@Override @Override
protected ProfileAnonymousGrpcService createServiceBeforeEachTest() { protected ProfileAnonymousGrpcService createServiceBeforeEachTest() {
getMockRequestAttributesInterceptor().setAcceptLanguage(Locale.LanguageRange.parse("en-us")); getMockRequestAttributesInterceptor().setAcceptLanguage(Locale.LanguageRange.parse("en-us"));
@ -111,12 +112,12 @@ public class ProfileAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<ProfileA
accountsManager, accountsManager,
profilesManager, profilesManager,
profileBadgeConverter, profileBadgeConverter,
serverZkProfileOperations SERVER_SECRET_PARAMS
); );
} }
@Test @Test
void getUnversionedProfile() { void getUnversionedProfileUnidentifiedAccessKey() {
final UUID targetUuid = UUID.randomUUID(); final UUID targetUuid = UUID.randomUUID();
final org.whispersystems.textsecuregcm.identity.ServiceIdentifier serviceIdentifier = new AciServiceIdentifier(targetUuid); final org.whispersystems.textsecuregcm.identity.ServiceIdentifier serviceIdentifier = new AciServiceIdentifier(targetUuid);
@ -169,9 +170,71 @@ public class ProfileAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<ProfileA
assertEquals(expectedResponse, response); 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 @ParameterizedTest
@MethodSource @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); final byte[] unidentifiedAccessKey = TestRandomUtil.nextBytes(UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH);
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey)); when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey));
@ -179,22 +242,21 @@ public class ProfileAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<ProfileA
when(accountsManager.getByServiceIdentifierAsync(any())).thenReturn( when(accountsManager.getByServiceIdentifierAsync(any())).thenReturn(
CompletableFuture.completedFuture(accountNotFound ? Optional.empty() : Optional.of(account))); 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() .setRequest(GetUnversionedProfileRequest.newBuilder()
.setServiceIdentifier(ServiceIdentifier.newBuilder() .setServiceIdentifier(ServiceIdentifier.newBuilder()
.setIdentityType(identityType) .setIdentityType(identityType)
.setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID()))) .setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID())))))
.build()) .build();
.build());
if (!missingUnidentifiedAccessKey) { assertStatusException(Status.UNAUTHENTICATED, () -> unauthenticatedServiceStub().getUnversionedProfile(request));
requestBuilder.setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey));
}
assertStatusException(Status.UNAUTHENTICATED, () -> unauthenticatedServiceStub().getUnversionedProfile(requestBuilder.build()));
} }
private static Stream<Arguments> getUnversionedProfileUnauthenticated() { private static Stream<Arguments> getUnversionedProfileIncorrectUnidentifiedAccessKey() {
return Stream.of( return Stream.of(
Arguments.of(IdentityType.IDENTITY_TYPE_PNI, false, false), Arguments.of(IdentityType.IDENTITY_TYPE_PNI, false, false),
Arguments.of(IdentityType.IDENTITY_TYPE_ACI, true, 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 @ParameterizedTest
@MethodSource @MethodSource
void getVersionedProfile(final String requestVersion, 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 byte[] unidentifiedAccessKey = TestRandomUtil.nextBytes(UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH);
final UUID targetUuid = UUID.randomUUID(); final UUID targetUuid = UUID.randomUUID();
final ServerSecretParams serverSecretParams = ServerSecretParams.generate(); final ClientZkProfileOperations clientZkProfile = new ClientZkProfileOperations(SERVER_SECRET_PARAMS.getPublicParams());
final ServerPublicParams serverPublicParams = serverSecretParams.getPublicParams();
final ServerZkProfileOperations serverZkProfile = new ServerZkProfileOperations(serverSecretParams);
final ClientZkProfileOperations clientZkProfile = new ClientZkProfileOperations(serverPublicParams);
final byte[] profileKeyBytes = TestRandomUtil.nextBytes(32); final byte[] profileKeyBytes = TestRandomUtil.nextBytes(32);
final ProfileKey profileKey = new ProfileKey(profileKeyBytes); 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) final Instant expiration = Instant.now().plus(org.whispersystems.textsecuregcm.util.ProfileHelper.EXPIRING_PROFILE_KEY_CREDENTIAL_EXPIRATION)
.truncatedTo(ChronoUnit.DAYS); .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() final GetExpiringProfileKeyCredentialAnonymousRequest request = GetExpiringProfileKeyCredentialAnonymousRequest.newBuilder()
.setRequest(GetExpiringProfileKeyCredentialRequest.newBuilder() .setRequest(GetExpiringProfileKeyCredentialRequest.newBuilder()
.setAccountIdentifier(ServiceIdentifier.newBuilder() .setAccountIdentifier(ServiceIdentifier.newBuilder()
@ -389,13 +497,8 @@ public class ProfileAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<ProfileA
final GetExpiringProfileKeyCredentialResponse response = unauthenticatedServiceStub().getExpiringProfileKeyCredential(request); 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(() -> assertThatNoException().isThrownBy(() ->
clientZkProfileCipher.receiveExpiringProfileKeyCredential(profileKeyCredentialRequestContext, new ExpiringProfileKeyCredentialResponse(response.getProfileKeyCredential().toByteArray()))); clientZkProfile.receiveExpiringProfileKeyCredential(profileKeyCredentialRequestContext, new ExpiringProfileKeyCredentialResponse(response.getProfileKeyCredential().toByteArray())));
} }
@ParameterizedTest @ParameterizedTest
@ -471,10 +574,6 @@ public class ProfileAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<ProfileA
final UUID targetUuid = UUID.randomUUID(); final UUID targetUuid = UUID.randomUUID();
final byte[] unidentifiedAccessKey = TestRandomUtil.nextBytes(UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH); 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); final VersionedProfile profile = mock(VersionedProfile.class);
when(profile.commitment()).thenReturn("commitment".getBytes(StandardCharsets.UTF_8)); when(profile.commitment()).thenReturn("commitment".getBytes(StandardCharsets.UTF_8));
when(account.getUuid()).thenReturn(targetUuid); when(account.getUuid()).thenReturn(targetUuid);