mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-21 14:58:06 +01:00
Implement key transparency endpoints using simple-grpc
This commit is contained in:
@@ -35,12 +35,8 @@ import jakarta.ws.rs.client.WebTarget;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.stream.Stream;
|
||||
import javax.annotation.Nullable;
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
@@ -54,8 +50,10 @@ import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.signal.keytransparency.client.CondensedTreeSearchResponse;
|
||||
import org.signal.keytransparency.client.DistinguishedResponse;
|
||||
import org.signal.keytransparency.client.E164SearchRequest;
|
||||
import org.signal.keytransparency.client.FullTreeHead;
|
||||
import org.signal.keytransparency.client.MonitorResponse;
|
||||
import org.signal.keytransparency.client.SearchProof;
|
||||
import org.signal.keytransparency.client.SearchResponse;
|
||||
import org.signal.keytransparency.client.UpdateValue;
|
||||
@@ -81,16 +79,16 @@ import org.whispersystems.textsecuregcm.util.TestRemoteAddressFilterProvider;
|
||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||
public class KeyTransparencyControllerTest {
|
||||
|
||||
private static final String NUMBER = PhoneNumberUtil.getInstance().format(
|
||||
public static final String NUMBER = PhoneNumberUtil.getInstance().format(
|
||||
PhoneNumberUtil.getInstance().getExampleNumber("US"),
|
||||
PhoneNumberUtil.PhoneNumberFormat.E164);
|
||||
private static final AciServiceIdentifier ACI = new AciServiceIdentifier(UUID.randomUUID());
|
||||
private static final byte[] USERNAME_HASH = TestRandomUtil.nextBytes(20);
|
||||
public static final AciServiceIdentifier ACI = new AciServiceIdentifier(UUID.randomUUID());
|
||||
public static final byte[] USERNAME_HASH = TestRandomUtil.nextBytes(20);
|
||||
private static final TestRemoteAddressFilterProvider TEST_REMOTE_ADDRESS_FILTER_PROVIDER
|
||||
= new TestRemoteAddressFilterProvider("127.0.0.1");
|
||||
private static final IdentityKey ACI_IDENTITY_KEY = new IdentityKey(Curve.generateKeyPair().getPublicKey());
|
||||
public static final IdentityKey ACI_IDENTITY_KEY = new IdentityKey(Curve.generateKeyPair().getPublicKey());
|
||||
private static final byte[] COMMITMENT_INDEX = new byte[32];
|
||||
private static final byte[] UNIDENTIFIED_ACCESS_KEY = new byte[16];
|
||||
public static final byte[] UNIDENTIFIED_ACCESS_KEY = new byte[16];
|
||||
private final KeyTransparencyServiceClient keyTransparencyServiceClient = mock(KeyTransparencyServiceClient.class);
|
||||
private static final RateLimiters rateLimiters = mock(RateLimiters.class);
|
||||
private static final RateLimiter searchRatelimiter = mock(RateLimiter.class);
|
||||
@@ -141,8 +139,8 @@ public class KeyTransparencyControllerTest {
|
||||
e164.ifPresent(ignored -> searchResponseBuilder.setE164(CondensedTreeSearchResponse.getDefaultInstance()));
|
||||
usernameHash.ifPresent(ignored -> searchResponseBuilder.setUsernameHash(CondensedTreeSearchResponse.getDefaultInstance()));
|
||||
|
||||
when(keyTransparencyServiceClient.search(any(), any(), any(), any(), any(), anyLong(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(searchResponseBuilder.build().toByteArray()));
|
||||
when(keyTransparencyServiceClient.search(any(), any(), any(), any(), any(), anyLong()))
|
||||
.thenReturn(searchResponseBuilder.build());
|
||||
|
||||
final Invocation.Builder request = resources.getJerseyTest()
|
||||
.target("/v1/key-transparency/search")
|
||||
@@ -167,8 +165,7 @@ public class KeyTransparencyControllerTest {
|
||||
ArgumentCaptor<Optional<E164SearchRequest>> e164Argument = ArgumentCaptor.forClass(Optional.class);
|
||||
|
||||
verify(keyTransparencyServiceClient).search(aciArgument.capture(), aciIdentityKeyArgument.capture(),
|
||||
usernameHashArgument.capture(), e164Argument.capture(), eq(Optional.of(3L)), eq(4L),
|
||||
eq(KeyTransparencyController.KEY_TRANSPARENCY_RPC_TIMEOUT));
|
||||
usernameHashArgument.capture(), e164Argument.capture(), eq(Optional.of(3L)), eq(4L));
|
||||
|
||||
assertArrayEquals(ACI.toCompactByteArray(), aciArgument.getValue().toByteArray());
|
||||
assertArrayEquals(ACI_IDENTITY_KEY.serialize(), aciIdentityKeyArgument.getValue().toByteArray());
|
||||
@@ -218,8 +215,8 @@ public class KeyTransparencyControllerTest {
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void searchGrpcErrors(final Status grpcStatus, final int httpStatus) {
|
||||
when(keyTransparencyServiceClient.search(any(), any(), any(), any(), any(), anyLong(), any()))
|
||||
.thenReturn(CompletableFuture.failedFuture(new CompletionException(new StatusRuntimeException(grpcStatus))));
|
||||
when(keyTransparencyServiceClient.search(any(), any(), any(), any(), any(), anyLong()))
|
||||
.thenThrow(new StatusRuntimeException(grpcStatus));
|
||||
|
||||
final Invocation.Builder request = resources.getJerseyTest()
|
||||
.target("/v1/key-transparency/search")
|
||||
@@ -228,7 +225,7 @@ public class KeyTransparencyControllerTest {
|
||||
Entity.json(createRequestJson(new KeyTransparencySearchRequest(ACI, Optional.empty(), Optional.empty(),
|
||||
ACI_IDENTITY_KEY, Optional.empty(), Optional.empty(), 4L))))) {
|
||||
assertEquals(httpStatus, response.getStatus());
|
||||
verify(keyTransparencyServiceClient, times(1)).search(any(), any(), any(), any(), any(), anyLong(), any());
|
||||
verify(keyTransparencyServiceClient, times(1)).search(any(), any(), any(), any(), any(), anyLong());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,8 +292,8 @@ public class KeyTransparencyControllerTest {
|
||||
|
||||
@Test
|
||||
void monitorSuccess() {
|
||||
when(keyTransparencyServiceClient.monitor(any(), any(), any(), anyLong(), anyLong(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(TestRandomUtil.nextBytes(16)));
|
||||
when(keyTransparencyServiceClient.monitor(any(), any(), any(), anyLong(), anyLong()))
|
||||
.thenReturn(MonitorResponse.getDefaultInstance());
|
||||
|
||||
final Invocation.Builder request = resources.getJerseyTest()
|
||||
.target("/v1/key-transparency/monitor")
|
||||
@@ -314,7 +311,7 @@ public class KeyTransparencyControllerTest {
|
||||
assertNotNull(keyTransparencyMonitorResponse.serializedResponse());
|
||||
|
||||
verify(keyTransparencyServiceClient, times(1)).monitor(
|
||||
any(), any(), any(), eq(3L), eq(4L), eq(KeyTransparencyController.KEY_TRANSPARENCY_RPC_TIMEOUT));
|
||||
any(), any(), any(), eq(3L), eq(4L));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,8 +334,8 @@ public class KeyTransparencyControllerTest {
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void monitorGrpcErrors(final Status grpcStatus, final int httpStatus) {
|
||||
when(keyTransparencyServiceClient.monitor(any(), any(), any(), anyLong(), anyLong(), any()))
|
||||
.thenReturn(CompletableFuture.failedFuture(new CompletionException(new StatusRuntimeException(grpcStatus))));
|
||||
when(keyTransparencyServiceClient.monitor(any(), any(), any(), anyLong(), anyLong()))
|
||||
.thenThrow(new StatusRuntimeException(grpcStatus));
|
||||
|
||||
final Invocation.Builder request = resources.getJerseyTest()
|
||||
.target("/v1/key-transparency/monitor")
|
||||
@@ -349,7 +346,7 @@ public class KeyTransparencyControllerTest {
|
||||
new KeyTransparencyMonitorRequest.AciMonitor(ACI, 3, COMMITMENT_INDEX),
|
||||
Optional.empty(), Optional.empty(), 3L, 4L))))) {
|
||||
assertEquals(httpStatus, response.getStatus());
|
||||
verify(keyTransparencyServiceClient, times(1)).monitor(any(), any(), any(), anyLong(), anyLong(), any());
|
||||
verify(keyTransparencyServiceClient, times(1)).monitor(any(), any(), any(), anyLong(), anyLong());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,8 +497,8 @@ public class KeyTransparencyControllerTest {
|
||||
@ParameterizedTest
|
||||
@CsvSource(", 1")
|
||||
void distinguishedSuccess(@Nullable Long lastTreeHeadSize) {
|
||||
when(keyTransparencyServiceClient.getDistinguishedKey(any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(TestRandomUtil.nextBytes(16)));
|
||||
when(keyTransparencyServiceClient.getDistinguishedKey(any()))
|
||||
.thenReturn(DistinguishedResponse.getDefaultInstance());
|
||||
|
||||
WebTarget webTarget = resources.getJerseyTest()
|
||||
.target("/v1/key-transparency/distinguished");
|
||||
@@ -518,8 +515,7 @@ public class KeyTransparencyControllerTest {
|
||||
assertNotNull(distinguishedKeyResponse.serializedResponse());
|
||||
|
||||
verify(keyTransparencyServiceClient, times(1))
|
||||
.getDistinguishedKey(eq(Optional.ofNullable(lastTreeHeadSize)),
|
||||
eq(KeyTransparencyController.KEY_TRANSPARENCY_RPC_TIMEOUT));
|
||||
.getDistinguishedKey(eq(Optional.ofNullable(lastTreeHeadSize)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -538,15 +534,15 @@ public class KeyTransparencyControllerTest {
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void distinguishedGrpcErrors(final Status grpcStatus, final int httpStatus) {
|
||||
when(keyTransparencyServiceClient.getDistinguishedKey(any(), any()))
|
||||
.thenReturn(CompletableFuture.failedFuture(new CompletionException(new StatusRuntimeException(grpcStatus))));
|
||||
when(keyTransparencyServiceClient.getDistinguishedKey(any()))
|
||||
.thenThrow(new StatusRuntimeException(grpcStatus));
|
||||
|
||||
final Invocation.Builder request = resources.getJerseyTest()
|
||||
.target("/v1/key-transparency/distinguished")
|
||||
.request();
|
||||
try (Response response = request.get()) {
|
||||
assertEquals(httpStatus, response.getStatus());
|
||||
verify(keyTransparencyServiceClient).getDistinguishedKey(any(), any());
|
||||
verify(keyTransparencyServiceClient).getDistinguishedKey(any());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -561,8 +557,8 @@ public class KeyTransparencyControllerTest {
|
||||
|
||||
@Test
|
||||
void distinguishedInvalidRequest() {
|
||||
when(keyTransparencyServiceClient.getDistinguishedKey(any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(TestRandomUtil.nextBytes(16)));
|
||||
when(keyTransparencyServiceClient.getDistinguishedKey(any()))
|
||||
.thenReturn(DistinguishedResponse.getDefaultInstance());
|
||||
|
||||
final Invocation.Builder request = resources.getJerseyTest()
|
||||
.target("/v1/key-transparency/distinguished")
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.grpc;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
import io.grpc.Channel;
|
||||
import io.grpc.Status;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.signal.keytransparency.client.AciMonitorRequest;
|
||||
import org.signal.keytransparency.client.ConsistencyParameters;
|
||||
import org.signal.keytransparency.client.DistinguishedRequest;
|
||||
import org.signal.keytransparency.client.DistinguishedResponse;
|
||||
import org.signal.keytransparency.client.E164MonitorRequest;
|
||||
import org.signal.keytransparency.client.E164SearchRequest;
|
||||
import org.signal.keytransparency.client.KeyTransparencyQueryServiceGrpc;
|
||||
import org.signal.keytransparency.client.MonitorRequest;
|
||||
import org.signal.keytransparency.client.MonitorResponse;
|
||||
import org.signal.keytransparency.client.SearchRequest;
|
||||
import org.signal.keytransparency.client.SearchResponse;
|
||||
import org.signal.keytransparency.client.UsernameHashMonitorRequest;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||
import org.whispersystems.textsecuregcm.keytransparency.KeyTransparencyServiceClient;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.whispersystems.textsecuregcm.controllers.KeyTransparencyControllerTest.ACI;
|
||||
import static org.whispersystems.textsecuregcm.controllers.KeyTransparencyControllerTest.ACI_IDENTITY_KEY;
|
||||
import static org.whispersystems.textsecuregcm.controllers.KeyTransparencyControllerTest.NUMBER;
|
||||
import static org.whispersystems.textsecuregcm.controllers.KeyTransparencyControllerTest.UNIDENTIFIED_ACCESS_KEY;
|
||||
import static org.whispersystems.textsecuregcm.controllers.KeyTransparencyControllerTest.USERNAME_HASH;
|
||||
import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertRateLimitExceeded;
|
||||
import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertStatusException;
|
||||
import static org.whispersystems.textsecuregcm.grpc.KeyTransparencyGrpcService.COMMITMENT_INDEX_LENGTH;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public class KeyTransparencyGrpcServiceTest extends SimpleBaseGrpcTest<KeyTransparencyGrpcService, KeyTransparencyQueryServiceGrpc.KeyTransparencyQueryServiceBlockingStub>{
|
||||
@Mock
|
||||
private KeyTransparencyServiceClient keyTransparencyServiceClient;
|
||||
@Mock
|
||||
private RateLimiter rateLimiter;
|
||||
|
||||
@Override
|
||||
protected KeyTransparencyGrpcService createServiceBeforeEachTest() {
|
||||
final RateLimiters rateLimiters = mock(RateLimiters.class);
|
||||
when(rateLimiters.getKeyTransparencySearchLimiter()).thenReturn(rateLimiter);
|
||||
when(rateLimiters.getKeyTransparencyDistinguishedLimiter()).thenReturn(rateLimiter);
|
||||
when(rateLimiters.getKeyTransparencyMonitorLimiter()).thenReturn(rateLimiter);
|
||||
|
||||
return new KeyTransparencyGrpcService(rateLimiters, keyTransparencyServiceClient);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected KeyTransparencyQueryServiceGrpc.KeyTransparencyQueryServiceBlockingStub createStub(final Channel channel) {
|
||||
return KeyTransparencyQueryServiceGrpc.newBlockingStub(channel);
|
||||
}
|
||||
|
||||
@Test
|
||||
void searchSuccess() throws RateLimitExceededException {
|
||||
when(keyTransparencyServiceClient.search(any())).thenReturn(SearchResponse.getDefaultInstance());
|
||||
Mockito.doNothing().when(rateLimiter).validate(any(String.class));
|
||||
final SearchRequest request = SearchRequest.newBuilder()
|
||||
.setAci(ByteString.copyFrom(ACI.toCompactByteArray()))
|
||||
.setAciIdentityKey(ByteString.copyFrom(ACI_IDENTITY_KEY.serialize()))
|
||||
.setConsistency(ConsistencyParameters.newBuilder()
|
||||
.setDistinguished(10)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
assertDoesNotThrow(() -> unauthenticatedServiceStub().search(request));
|
||||
verify(keyTransparencyServiceClient, times(1)).search(eq(request));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void searchInvalidRequest(final Optional<byte[]> aciServiceIdentifier,
|
||||
final Optional<IdentityKey> aciIdentityKey,
|
||||
final Optional<String> e164,
|
||||
final Optional<byte[]> unidentifiedAccessKey,
|
||||
final Optional<byte[]> usernameHash,
|
||||
final Optional<Long> lastTreeHeadSize,
|
||||
final Optional<Long> distinguishedTreeHeadSize) {
|
||||
|
||||
final SearchRequest.Builder requestBuilder = SearchRequest.newBuilder();
|
||||
|
||||
aciServiceIdentifier.ifPresent(v -> requestBuilder.setAci(ByteString.copyFrom(v)));
|
||||
aciIdentityKey.ifPresent(v -> requestBuilder.setAciIdentityKey(ByteString.copyFrom(v.serialize())));
|
||||
usernameHash.ifPresent(v -> requestBuilder.setUsernameHash(ByteString.copyFrom(v)));
|
||||
|
||||
final E164SearchRequest.Builder e164RequestBuilder = E164SearchRequest.newBuilder();
|
||||
|
||||
e164.ifPresent(e164RequestBuilder::setE164);
|
||||
unidentifiedAccessKey.ifPresent(v -> e164RequestBuilder.setUnidentifiedAccessKey(ByteString.copyFrom(v)));
|
||||
requestBuilder.setE164SearchRequest(e164RequestBuilder.build());
|
||||
|
||||
final ConsistencyParameters.Builder consistencyBuilder = ConsistencyParameters.newBuilder();
|
||||
distinguishedTreeHeadSize.ifPresent(consistencyBuilder::setDistinguished);
|
||||
lastTreeHeadSize.ifPresent(consistencyBuilder::setLast);
|
||||
requestBuilder.setConsistency(consistencyBuilder.build());
|
||||
|
||||
assertStatusException(Status.INVALID_ARGUMENT, () -> unauthenticatedServiceStub().search(requestBuilder.build()));
|
||||
verifyNoInteractions(keyTransparencyServiceClient);
|
||||
}
|
||||
|
||||
private static Stream<Arguments> searchInvalidRequest() {
|
||||
byte[] aciBytes = ACI.toCompactByteArray();
|
||||
return Stream.of(
|
||||
Arguments.argumentSet("Empty ACI", Optional.empty(), Optional.of(ACI_IDENTITY_KEY), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(4L)),
|
||||
Arguments.argumentSet("Null ACI identity key", Optional.of(aciBytes), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(4L)),
|
||||
Arguments.argumentSet("Invalid ACI", Optional.of(new byte[15]), Optional.of(ACI_IDENTITY_KEY), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(4L)),
|
||||
Arguments.argumentSet("Non-positive consistency.last", Optional.of(aciBytes), Optional.of(ACI_IDENTITY_KEY), Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(0L), Optional.of(4L)),
|
||||
Arguments.argumentSet("consistency.distinguished not provided",Optional.of(aciBytes), Optional.of(ACI_IDENTITY_KEY), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()),
|
||||
Arguments.argumentSet("Non-positive consistency.distinguished",Optional.of(aciBytes), Optional.of(ACI_IDENTITY_KEY), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(0L)),
|
||||
Arguments.argumentSet("E164 can't be provided without an unidentified access key", Optional.of(aciBytes), Optional.of(ACI_IDENTITY_KEY), Optional.of(NUMBER), Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(4L)),
|
||||
Arguments.argumentSet("Unidentified access key can't be provided without E164", Optional.of(aciBytes), Optional.of(ACI_IDENTITY_KEY), Optional.empty(), Optional.of(UNIDENTIFIED_ACCESS_KEY), Optional.empty(), Optional.empty(), Optional.of(4L)),
|
||||
Arguments.argumentSet("Invalid username hash", Optional.of(aciBytes), Optional.of(ACI_IDENTITY_KEY), Optional.empty(), Optional.empty(), Optional.of(new byte[19]), Optional.empty(), Optional.of(4L))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void searchRatelimited() throws RateLimitExceededException {
|
||||
final Duration retryAfterDuration = Duration.ofMinutes(7);
|
||||
Mockito.doThrow(new RateLimitExceededException(retryAfterDuration)).when(rateLimiter).validate(any(String.class));
|
||||
|
||||
final SearchRequest request = SearchRequest.newBuilder()
|
||||
.setAci(ByteString.copyFrom(ACI.toCompactByteArray()))
|
||||
.setAciIdentityKey(ByteString.copyFrom(ACI_IDENTITY_KEY.serialize()))
|
||||
.setConsistency(ConsistencyParameters.newBuilder()
|
||||
.setDistinguished(10)
|
||||
.build())
|
||||
.build();
|
||||
assertRateLimitExceeded(retryAfterDuration, () -> unauthenticatedServiceStub().search(request));
|
||||
verifyNoInteractions(keyTransparencyServiceClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
void monitorSuccess() {
|
||||
when(keyTransparencyServiceClient.monitor(any())).thenReturn(MonitorResponse.getDefaultInstance());
|
||||
when(rateLimiter.validateReactive(any(String.class)))
|
||||
.thenReturn(Mono.empty());
|
||||
final AciMonitorRequest aciMonitorRequest = AciMonitorRequest.newBuilder()
|
||||
.setAci(ByteString.copyFrom(ACI.toCompactByteArray()))
|
||||
.setCommitmentIndex(ByteString.copyFrom(new byte[COMMITMENT_INDEX_LENGTH]))
|
||||
.setEntryPosition(10)
|
||||
.build();
|
||||
|
||||
final MonitorRequest request = MonitorRequest.newBuilder()
|
||||
.setAci(aciMonitorRequest)
|
||||
.setConsistency(ConsistencyParameters.newBuilder()
|
||||
.setDistinguished(10)
|
||||
.setLast(10)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
assertDoesNotThrow(() -> unauthenticatedServiceStub().monitor(request));
|
||||
verify(keyTransparencyServiceClient, times(1)).monitor(eq(request));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void monitorInvalidRequest(final Optional<AciMonitorRequest> aciMonitorRequest,
|
||||
final Optional<E164MonitorRequest> e164MonitorRequest,
|
||||
final Optional<UsernameHashMonitorRequest> usernameHashMonitorRequest,
|
||||
final Optional<Long> lastTreeHeadSize,
|
||||
final Optional<Long> distinguishedTreeHeadSize) {
|
||||
|
||||
final MonitorRequest.Builder requestBuilder = MonitorRequest.newBuilder();
|
||||
|
||||
aciMonitorRequest.ifPresent(requestBuilder::setAci);
|
||||
e164MonitorRequest.ifPresent(requestBuilder::setE164);
|
||||
usernameHashMonitorRequest.ifPresent(requestBuilder::setUsernameHash);
|
||||
|
||||
final ConsistencyParameters.Builder consistencyBuilder = ConsistencyParameters.newBuilder();
|
||||
lastTreeHeadSize.ifPresent(consistencyBuilder::setLast);
|
||||
distinguishedTreeHeadSize.ifPresent(consistencyBuilder::setDistinguished);
|
||||
|
||||
requestBuilder.setConsistency(consistencyBuilder.build());
|
||||
|
||||
assertStatusException(Status.INVALID_ARGUMENT, () -> unauthenticatedServiceStub().monitor(requestBuilder.build()));
|
||||
}
|
||||
|
||||
private static Stream<Arguments> monitorInvalidRequest() {
|
||||
final Optional<AciMonitorRequest> validAciMonitorRequest = Optional.of(constructAciMonitorRequest(ACI.toCompactByteArray(), new byte[32], 10));
|
||||
return Stream.of(
|
||||
Arguments.argumentSet("ACI monitor request can't be unset", Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(4L), Optional.of(4L)),
|
||||
Arguments.argumentSet("ACI can't be empty",Optional.of(AciMonitorRequest.newBuilder().build()), Optional.empty(), Optional.empty(), Optional.of(4L), Optional.of(4L)),
|
||||
Arguments.argumentSet("Empty ACI on ACI monitor request",Optional.of(constructAciMonitorRequest(new byte[0], new byte[32], 10)), Optional.empty(), Optional.empty(), Optional.of(4L), Optional.of(4L)),
|
||||
Arguments.argumentSet("Invalid ACI", Optional.of(constructAciMonitorRequest(new byte[15], new byte[32], 10)), Optional.empty(), Optional.empty(), Optional.of(4L), Optional.of(4L)),
|
||||
Arguments.argumentSet("Invalid commitment index on ACI monitor request", Optional.of(constructAciMonitorRequest(ACI.toCompactByteArray(), new byte[31], 10)), Optional.empty(), Optional.empty(), Optional.of(4L), Optional.of(4L)),
|
||||
Arguments.argumentSet("Invalid entry position on ACI monitor request", Optional.of(constructAciMonitorRequest(ACI.toCompactByteArray(), new byte[32], 0)), Optional.empty(), Optional.empty(), Optional.of(4L), Optional.of(4L)),
|
||||
Arguments.argumentSet("E164 can't be blank", validAciMonitorRequest, Optional.of(constructE164MonitorRequest("", new byte[32], 10)), Optional.empty(), Optional.of(4L), Optional.of(4L)),
|
||||
Arguments.argumentSet("Invalid commitment index on E164 monitor request", validAciMonitorRequest, Optional.of(constructE164MonitorRequest(NUMBER, new byte[31], 10)), Optional.empty(), Optional.of(4L), Optional.of(4L)),
|
||||
Arguments.argumentSet("Invalid entry position on E164 monitor request", validAciMonitorRequest, Optional.of(constructE164MonitorRequest(NUMBER, new byte[32], 0)), Optional.empty(), Optional.of(4L), Optional.of(4L)),
|
||||
Arguments.argumentSet("Username hash can't be empty", validAciMonitorRequest, Optional.empty(), Optional.of(constructUsernameHashMonitorRequest(new byte[0], new byte[32], 10)), Optional.of(4L), Optional.of(4L)),
|
||||
Arguments.argumentSet("Invalid username hash length", validAciMonitorRequest, Optional.empty(), Optional.of(constructUsernameHashMonitorRequest(new byte[31], new byte[32], 10)), Optional.of(4L), Optional.of(4L)),
|
||||
Arguments.argumentSet("Invalid commitment index on username hash monitor request", validAciMonitorRequest, Optional.empty(), Optional.of(constructUsernameHashMonitorRequest(USERNAME_HASH, new byte[31], 10)), Optional.of(4L), Optional.of(4L)),
|
||||
Arguments.argumentSet("Invalid entry position on username hash monitor request", validAciMonitorRequest, Optional.empty(), Optional.of(constructUsernameHashMonitorRequest(USERNAME_HASH, new byte[32], 0)), Optional.of(4L), Optional.of(4L)),
|
||||
Arguments.argumentSet("consistency.last must be provided", validAciMonitorRequest, Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(4L),
|
||||
Arguments.argumentSet("consistency.last must be positive", validAciMonitorRequest, Optional.empty(), Optional.empty(), Optional.of(0L), Optional.of(4L)),
|
||||
Arguments.argumentSet("consistency.distinguished must be provided", validAciMonitorRequest, Optional.empty(), Optional.empty(), Optional.of(4L)), Optional.empty()),
|
||||
Arguments.argumentSet("consistency.distinguished must be positive", validAciMonitorRequest, Optional.empty(), Optional.empty(), Optional.of(4L), Optional.of(0L))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void monitorRatelimited() throws RateLimitExceededException {
|
||||
final Duration retryAfterDuration = Duration.ofMinutes(7);
|
||||
Mockito.doThrow(new RateLimitExceededException(retryAfterDuration)).when(rateLimiter).validate(any(String.class));
|
||||
|
||||
final AciMonitorRequest aciMonitorRequest = AciMonitorRequest.newBuilder()
|
||||
.setAci(ByteString.copyFrom(ACI.toCompactByteArray()))
|
||||
.setCommitmentIndex(ByteString.copyFrom(new byte[COMMITMENT_INDEX_LENGTH]))
|
||||
.setEntryPosition(10)
|
||||
.build();
|
||||
|
||||
final MonitorRequest request = MonitorRequest.newBuilder()
|
||||
.setAci(aciMonitorRequest)
|
||||
.setConsistency(ConsistencyParameters.newBuilder()
|
||||
.setDistinguished(10)
|
||||
.setLast(10)
|
||||
.build())
|
||||
.build();
|
||||
assertRateLimitExceeded(retryAfterDuration, () -> unauthenticatedServiceStub().monitor(request));
|
||||
verifyNoInteractions(keyTransparencyServiceClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
void distinguishedSuccess() {
|
||||
when(keyTransparencyServiceClient.distinguished(any())).thenReturn(DistinguishedResponse.getDefaultInstance());
|
||||
when(rateLimiter.validateReactive(any(String.class)))
|
||||
.thenReturn(Mono.empty());
|
||||
final DistinguishedRequest request = DistinguishedRequest.newBuilder().build();
|
||||
|
||||
assertDoesNotThrow(() -> unauthenticatedServiceStub().distinguished(request));
|
||||
verify(keyTransparencyServiceClient, times(1)).distinguished(eq(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
void distinguishedInvalidRequest() {
|
||||
final DistinguishedRequest request = DistinguishedRequest.newBuilder()
|
||||
.setLast(0)
|
||||
.build();
|
||||
|
||||
assertStatusException(Status.INVALID_ARGUMENT, () -> unauthenticatedServiceStub().distinguished(request));
|
||||
verifyNoInteractions(keyTransparencyServiceClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
void distinguishedRatelimited() throws RateLimitExceededException {
|
||||
final Duration retryAfterDuration = Duration.ofMinutes(7);
|
||||
Mockito.doThrow(new RateLimitExceededException(retryAfterDuration)).when(rateLimiter).validate(any(String.class));
|
||||
|
||||
final DistinguishedRequest request = DistinguishedRequest.newBuilder()
|
||||
.setLast(10)
|
||||
.build();
|
||||
|
||||
assertRateLimitExceeded(retryAfterDuration, () -> unauthenticatedServiceStub().distinguished(request));
|
||||
verifyNoInteractions(keyTransparencyServiceClient);
|
||||
}
|
||||
|
||||
private static AciMonitorRequest constructAciMonitorRequest(final byte[] aci, final byte[] commitmentIndex, final long entryPosition) {
|
||||
return AciMonitorRequest.newBuilder()
|
||||
.setAci(ByteString.copyFrom(aci))
|
||||
.setCommitmentIndex(ByteString.copyFrom(commitmentIndex))
|
||||
.setEntryPosition(entryPosition)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static E164MonitorRequest constructE164MonitorRequest(final String e164, final byte[] commitmentIndex, final long entryPosition) {
|
||||
return E164MonitorRequest.newBuilder()
|
||||
.setE164(e164)
|
||||
.setCommitmentIndex(ByteString.copyFrom(commitmentIndex))
|
||||
.setEntryPosition(entryPosition)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static UsernameHashMonitorRequest constructUsernameHashMonitorRequest(final byte[] usernameHash, final byte[] commitmentIndex, final long entryPosition) {
|
||||
return UsernameHashMonitorRequest.newBuilder()
|
||||
.setUsernameHash(ByteString.copyFrom(usernameHash))
|
||||
.setCommitmentIndex(ByteString.copyFrom(commitmentIndex))
|
||||
.setEntryPosition(entryPosition)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user