Allow callers to request a combined group auth credential
This commit is contained in:
parent
c1f9bedf2f
commit
e38e5fa17d
|
@ -86,6 +86,7 @@ import org.whispersystems.textsecuregcm.controllers.DeviceController;
|
|||
import org.whispersystems.textsecuregcm.controllers.DirectoryController;
|
||||
import org.whispersystems.textsecuregcm.controllers.DirectoryV2Controller;
|
||||
import org.whispersystems.textsecuregcm.controllers.DonationController;
|
||||
import org.whispersystems.textsecuregcm.controllers.GroupController;
|
||||
import org.whispersystems.textsecuregcm.controllers.KeepAliveController;
|
||||
import org.whispersystems.textsecuregcm.controllers.KeysController;
|
||||
import org.whispersystems.textsecuregcm.controllers.MessageController;
|
||||
|
@ -631,6 +632,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
new DirectoryV2Controller(directoryV2CredentialsGenerator),
|
||||
new DonationController(clock, zkReceiptOperations, redeemedReceiptsManager, accountsManager, config.getBadges(),
|
||||
ReceiptCredentialPresentation::new, stripeExecutor, config.getDonationConfiguration(), config.getStripe()),
|
||||
new GroupController(zkAuthOperations),
|
||||
new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, deletedAccountsManager,
|
||||
messagesManager, apnFallbackManager, reportMessageManager, multiRecipientMessageExecutor),
|
||||
new PaymentsController(currencyManager, paymentsCredentialsGenerator),
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Copyright 2013-2022 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.dropwizard.auth.Auth;
|
||||
import javax.ws.rs.BadRequestException;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import org.signal.libsignal.zkgroup.auth.ServerZkAuthOperations;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.entities.GroupCredentials;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Path("/v1/group")
|
||||
public class GroupController {
|
||||
|
||||
private final ServerZkAuthOperations serverZkAuthOperations;
|
||||
private final Clock clock;
|
||||
|
||||
@VisibleForTesting
|
||||
static final Duration MAX_REDEMPTION_DURATION = Duration.ofDays(7);
|
||||
|
||||
public GroupController(final ServerZkAuthOperations serverZkAuthOperations) {
|
||||
this(serverZkAuthOperations, Clock.systemUTC());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
GroupController(final ServerZkAuthOperations serverZkAuthOperations, final Clock clock) {
|
||||
this.serverZkAuthOperations = serverZkAuthOperations;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/auth")
|
||||
public GroupCredentials getGroupAuthenticationCredentials(@Auth AuthenticatedAccount auth,
|
||||
@QueryParam("redemptionStartSeconds") int startSeconds,
|
||||
@QueryParam("redemptionEndSeconds") int endSeconds) {
|
||||
|
||||
final Instant startOfDay = clock.instant().truncatedTo(ChronoUnit.DAYS);
|
||||
final Instant redemptionStart = Instant.ofEpochSecond(startSeconds);
|
||||
final Instant redemptionEnd = Instant.ofEpochSecond(endSeconds);
|
||||
|
||||
if (redemptionStart.isAfter(redemptionEnd) ||
|
||||
redemptionStart.isBefore(startOfDay) ||
|
||||
redemptionEnd.isAfter(startOfDay.plus(MAX_REDEMPTION_DURATION)) ||
|
||||
!redemptionStart.equals(redemptionStart.truncatedTo(ChronoUnit.DAYS)) ||
|
||||
!redemptionEnd.equals(redemptionEnd.truncatedTo(ChronoUnit.DAYS))) {
|
||||
|
||||
throw new BadRequestException();
|
||||
}
|
||||
|
||||
final List<GroupCredentials.GroupCredential> credentials = new ArrayList<>();
|
||||
|
||||
Instant redemption = redemptionStart;
|
||||
|
||||
while (!redemption.isAfter(redemptionEnd)) {
|
||||
credentials.add(new GroupCredentials.GroupCredential(serverZkAuthOperations.issueAuthCredentialWithPni(
|
||||
auth.getAccount().getUuid(),
|
||||
auth.getAccount().getPhoneNumberIdentifier(),
|
||||
redemption).serialize(),
|
||||
(int) redemption.getEpochSecond()));
|
||||
|
||||
redemption = redemption.plus(Duration.ofDays(1));
|
||||
}
|
||||
|
||||
return new GroupCredentials(credentials);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* Copyright 2013-2022 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.stream.Stream;
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
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.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.entities.GroupCredentials;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||
class GroupControllerTest {
|
||||
|
||||
private static final ServerSecretParams SERVER_SECRET_PARAMS = ServerSecretParams.generate();
|
||||
private static final ServerZkAuthOperations SERVER_ZK_AUTH_OPERATIONS = new ServerZkAuthOperations(SERVER_SECRET_PARAMS);
|
||||
|
||||
private static final Clock CLOCK = Clock.fixed(Instant.now(), ZoneId.systemDefault());
|
||||
|
||||
private static final ResourceExtension RESOURCE_EXTENSION = ResourceExtension.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
.addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(
|
||||
ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class)))
|
||||
.setMapper(SystemMapper.getMapper())
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new GroupController(SERVER_ZK_AUTH_OPERATIONS, CLOCK))
|
||||
.build();
|
||||
|
||||
@Test
|
||||
void testGetSingleGroupCredential() {
|
||||
final Instant startOfDay = CLOCK.instant().truncatedTo(ChronoUnit.DAYS);
|
||||
|
||||
final GroupCredentials credentials = RESOURCE_EXTENSION.getJerseyTest()
|
||||
.target("/v1/group/auth")
|
||||
.queryParam("redemptionStartSeconds", startOfDay.getEpochSecond())
|
||||
.queryParam("redemptionEndSeconds", startOfDay.getEpochSecond())
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(GroupCredentials.class);
|
||||
|
||||
assertEquals(1, credentials.getCredentials().size());
|
||||
assertEquals(startOfDay.getEpochSecond(), credentials.getCredentials().get(0).getRedemptionTime());
|
||||
|
||||
final ClientZkAuthOperations clientZkAuthOperations =
|
||||
new ClientZkAuthOperations(SERVER_SECRET_PARAMS.getPublicParams());
|
||||
|
||||
assertDoesNotThrow(() -> {
|
||||
clientZkAuthOperations.receiveAuthCredentialWithPni(AuthHelper.VALID_UUID,
|
||||
AuthHelper.VALID_PNI,
|
||||
(int) startOfDay.getEpochSecond(),
|
||||
new AuthCredentialWithPniResponse(credentials.getCredentials().get(0).getCredential()));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetWeekLongGroupCredentials() {
|
||||
final Instant startOfDay = CLOCK.instant().truncatedTo(ChronoUnit.DAYS);
|
||||
|
||||
final GroupCredentials credentials = RESOURCE_EXTENSION.getJerseyTest()
|
||||
.target("/v1/group/auth")
|
||||
.queryParam("redemptionStartSeconds", startOfDay.getEpochSecond())
|
||||
.queryParam("redemptionEndSeconds", startOfDay.plus(Duration.ofDays(7)).getEpochSecond())
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(GroupCredentials.class);
|
||||
|
||||
assertEquals(8, credentials.getCredentials().size());
|
||||
|
||||
final ClientZkAuthOperations clientZkAuthOperations =
|
||||
new ClientZkAuthOperations(SERVER_SECRET_PARAMS.getPublicParams());
|
||||
|
||||
for (int i = 0; i < 8; i++) {
|
||||
final Instant redemptionTime = startOfDay.plus(Duration.ofDays(i));
|
||||
assertEquals(redemptionTime.getEpochSecond(), credentials.getCredentials().get(i).getRedemptionTime());
|
||||
|
||||
final int index = i;
|
||||
|
||||
assertDoesNotThrow(() -> {
|
||||
clientZkAuthOperations.receiveAuthCredentialWithPni(AuthHelper.VALID_UUID,
|
||||
AuthHelper.VALID_PNI,
|
||||
redemptionTime.getEpochSecond(),
|
||||
new AuthCredentialWithPniResponse(credentials.getCredentials().get(index).getCredential()));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void testBadRedemptionTimes(final Instant redemptionStart, final Instant redemptionEnd) {
|
||||
final Response response = RESOURCE_EXTENSION.getJerseyTest()
|
||||
.target("/v1/group/auth")
|
||||
.queryParam("redemptionStartSeconds", redemptionStart.getEpochSecond())
|
||||
.queryParam("redemptionEndSeconds", redemptionEnd.getEpochSecond())
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get();
|
||||
|
||||
assertEquals(400, response.getStatus());
|
||||
}
|
||||
|
||||
private static Stream<Arguments> testBadRedemptionTimes() {
|
||||
return Stream.of(
|
||||
// Start is after end
|
||||
Arguments.of(CLOCK.instant().plus(Duration.ofDays(1)), CLOCK.instant()),
|
||||
|
||||
// Start is in the past
|
||||
Arguments.of(CLOCK.instant().minus(Duration.ofDays(1)), CLOCK.instant()),
|
||||
|
||||
// End is too far in the future
|
||||
Arguments.of(CLOCK.instant(), CLOCK.instant().plus(GroupController.MAX_REDEMPTION_DURATION).plus(Duration.ofDays(1))),
|
||||
|
||||
// Start is not at a day boundary
|
||||
Arguments.of(CLOCK.instant().plusSeconds(17), CLOCK.instant().plus(Duration.ofDays(1))),
|
||||
|
||||
// End is not at a day boundary
|
||||
Arguments.of(CLOCK.instant(), CLOCK.instant().plusSeconds(17))
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue