diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeyTransparencyController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeyTransparencyController.java index d24d491f5..1b4f510ac 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeyTransparencyController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeyTransparencyController.java @@ -76,8 +76,10 @@ public class KeyTransparencyController { ) @ApiResponse(responseCode = "200", description = "The ACI was found and its mapped value matched the provided ACI identity key", useReturnTypeSchema = true) @ApiResponse(responseCode = "400", description = "Invalid request. See response for any available details.") - @ApiResponse(responseCode = "403", description = "The ACI was found but its mapped value did not match the provided ACI identity key") - @ApiResponse(responseCode = "404", description = "The ACI was not found in the log") + @ApiResponse(responseCode = "403", description = """ + The ACI was found but its mapped value did not match the provided ACI identity key + or the ACI was not found in the log. + """) @ApiResponse(responseCode = "422", description = "Invalid request format") @ApiResponse(responseCode = "429", description = "Rate-limited") @POST @@ -108,7 +110,10 @@ public class KeyTransparencyController { request.usernameHash().map(ByteString::copyFrom), maybeE164SearchRequest, request.lastTreeHeadSize(), - request.distinguishedTreeHeadSize()) + request.distinguishedTreeHeadSize(), + request.aciVersion(), + request.e164Version(), + request.usernameHashVersion()) .toByteArray()); } catch (final StatusRuntimeException exception) { handleKeyTransparencyServiceError(exception); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/KeyTransparencySearchRequest.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/KeyTransparencySearchRequest.java index f767d9e68..dc0cd42ff 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/KeyTransparencySearchRequest.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/KeyTransparencySearchRequest.java @@ -9,6 +9,8 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import java.util.Optional; @@ -51,8 +53,20 @@ public record KeyTransparencySearchRequest( Optional<@Positive Long> lastTreeHeadSize, @Schema(description = "The distinguished tree head size to prove consistency against.") - @Positive long distinguishedTreeHeadSize + @Positive long distinguishedTreeHeadSize, + + @Schema(description = "The version of the ACI to look up.") + Optional<@Max(MAX_UINT32) @Min(0) Long> aciVersion, + + @Schema(description = "The version of the E164 to look up.") + Optional<@Max(MAX_UINT32) @Min(0) Long> e164Version, + + @Schema(description = "The version of the username hash to look up.") + Optional<@Max(MAX_UINT32) @Min(0) Long> usernameHashVersion ) { + // This is the max value for a protobuf uint32 field + private static final long MAX_UINT32 = 0xFFFFFFFFL; + @AssertTrue @Schema(hidden = true) public boolean isUnidentifiedAccessKeyProvidedWithE164() { diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/keytransparency/KeyTransparencyServiceClient.java b/service/src/main/java/org/whispersystems/textsecuregcm/keytransparency/KeyTransparencyServiceClient.java index e455fb4ae..4312971d2 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/keytransparency/KeyTransparencyServiceClient.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/keytransparency/KeyTransparencyServiceClient.java @@ -116,7 +116,10 @@ public class KeyTransparencyServiceClient implements Managed { final Optional usernameHash, final Optional e164SearchRequest, final Optional lastTreeHeadSize, - final long distinguishedTreeHeadSize) { + final long distinguishedTreeHeadSize, + final Optional aciVersion, + final Optional e164Version, + final Optional usernameHashVersion) { final SearchRequest.Builder searchRequestBuilder = SearchRequest.newBuilder() .setAci(aci) .setAciIdentityKey(aciIdentityKey); @@ -124,6 +127,10 @@ public class KeyTransparencyServiceClient implements Managed { usernameHash.ifPresent(searchRequestBuilder::setUsernameHash); e164SearchRequest.ifPresent(searchRequestBuilder::setE164SearchRequest); + aciVersion.ifPresent(version -> searchRequestBuilder.setAciVersion(version.intValue())); + e164Version.ifPresent(version -> searchRequestBuilder.setE164Version(version.intValue())); + usernameHashVersion.ifPresent(version -> searchRequestBuilder.setUsernameHashVersion(version.intValue())); + final ConsistencyParameters.Builder consistency = ConsistencyParameters.newBuilder() .setDistinguished(distinguishedTreeHeadSize); lastTreeHeadSize.ifPresent(consistency::setLast); diff --git a/service/src/main/proto/KeyTransparencyService.proto b/service/src/main/proto/KeyTransparencyService.proto index ad0a46194..92c3b7834 100644 --- a/service/src/main/proto/KeyTransparencyService.proto +++ b/service/src/main/proto/KeyTransparencyService.proto @@ -68,6 +68,18 @@ message SearchRequest { * The tree head size(s) to prove consistency against. */ ConsistencyParameters consistency = 5 [(org.signal.chat.require.present) = true]; + /** + * The version of the ACI to look up. + */ + optional uint32 aci_version = 6; + /** + * The version of the E164 to look up. + */ + optional uint32 e164_version = 7; + /** + * The version of the username hash to look up. + */ + optional uint32 username_hash_version = 8; } /** diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/KeyTransparencyControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/KeyTransparencyControllerTest.java index f141469db..d38e26f69 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/KeyTransparencyControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/KeyTransparencyControllerTest.java @@ -35,6 +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.ArrayList; +import java.util.List; import java.util.Optional; import java.util.UUID; import java.util.stream.Stream; @@ -123,7 +125,13 @@ public class KeyTransparencyControllerTest { @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @ParameterizedTest @MethodSource - void searchSuccess(final Optional e164, final Optional usernameHash) { + void searchSuccess( + final Optional e164, + final Optional usernameHash, + final Optional aciVersion, + final Optional e164Version, + final Optional usernameHashVersion + ) { final CondensedTreeSearchResponse aciSearchResponse = CondensedTreeSearchResponse.newBuilder() .setOpening(ByteString.copyFrom(TestRandomUtil.nextBytes(16))) .setSearch(SearchProof.getDefaultInstance()) @@ -139,7 +147,7 @@ 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())) + when(keyTransparencyServiceClient.search(any(), any(), any(), any(), any(), anyLong(), any(), any(), any())) .thenReturn(searchResponseBuilder.build()); final Invocation.Builder request = resources.getJerseyTest() @@ -149,7 +157,7 @@ public class KeyTransparencyControllerTest { final Optional unidentifiedAccessKey = e164.isPresent() ? Optional.of(UNIDENTIFIED_ACCESS_KEY) : Optional.empty(); final String searchJson = createRequestJson( new KeyTransparencySearchRequest(ACI, e164, usernameHash, ACI_IDENTITY_KEY, - unidentifiedAccessKey, Optional.of(3L), 4L)); + unidentifiedAccessKey, Optional.of(3L), 4L, aciVersion, e164Version, usernameHashVersion)); try (Response response = request.post(Entity.json(searchJson))) { assertEquals(200, response.getStatus()); @@ -163,9 +171,13 @@ public class KeyTransparencyControllerTest { ArgumentCaptor aciIdentityKeyArgument = ArgumentCaptor.forClass(ByteString.class); ArgumentCaptor> usernameHashArgument = ArgumentCaptor.forClass(Optional.class); ArgumentCaptor> e164Argument = ArgumentCaptor.forClass(Optional.class); + ArgumentCaptor> aciVersionArgument = ArgumentCaptor.forClass(Optional.class); + ArgumentCaptor> e164VersionArgument = ArgumentCaptor.forClass(Optional.class); + ArgumentCaptor> usernameHashVersionArgument = ArgumentCaptor.forClass(Optional.class); + verify(keyTransparencyServiceClient).search(aciArgument.capture(), aciIdentityKeyArgument.capture(), - usernameHashArgument.capture(), e164Argument.capture(), eq(Optional.of(3L)), eq(4L)); + usernameHashArgument.capture(), e164Argument.capture(), eq(Optional.of(3L)), eq(4L), aciVersionArgument.capture(), e164VersionArgument.capture(), usernameHashVersionArgument.capture()); assertArrayEquals(ACI.toCompactByteArray(), aciArgument.getValue().toByteArray()); assertArrayEquals(ACI_IDENTITY_KEY.serialize(), aciIdentityKeyArgument.getValue().toByteArray()); @@ -185,6 +197,18 @@ public class KeyTransparencyControllerTest { } else { assertTrue(e164Argument.getValue().isEmpty()); } + + final List>> versionArgumentCaptors = List.of(aciVersionArgument, e164VersionArgument, usernameHashVersionArgument); + final List> providedVersions = List.of(aciVersion, e164Version, usernameHashVersion); + + for (int i = 0; i < versionArgumentCaptors.size(); i++) { + if (providedVersions.get(i).isPresent()) { + assertEquals(providedVersions.get(i), versionArgumentCaptors.get(i).getValue()); + } else { + assertTrue(versionArgumentCaptors.get(i).getValue().isEmpty()); + } + } + } catch (InvalidProtocolBufferException e) { throw new RuntimeException(e); } @@ -192,10 +216,12 @@ public class KeyTransparencyControllerTest { private static Stream searchSuccess() { return Stream.of( - Arguments.of(Optional.of(NUMBER), Optional.empty()), - Arguments.of(Optional.empty(), Optional.of(USERNAME_HASH)), - Arguments.of(Optional.of(NUMBER), Optional.of(USERNAME_HASH)) - ); + Arguments.argumentSet("Search for ACI and E164", Optional.of(NUMBER), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()), + Arguments.argumentSet("Search for ACI and username hash", Optional.empty(), Optional.of(USERNAME_HASH), Optional.empty(), Optional.empty(), Optional.empty()), + Arguments.argumentSet("Search for ACI, E164, and username hash", Optional.of(NUMBER), Optional.of(USERNAME_HASH), Optional.empty(), Optional.empty(), Optional.empty()), + Arguments.argumentSet("Search for version 0 of the ACI", Optional.of(NUMBER), Optional.of(USERNAME_HASH), Optional.of(0L), Optional.empty(), Optional.empty()), + Arguments.argumentSet("Search for multiple versioned identifiers", Optional.of(NUMBER), Optional.of(USERNAME_HASH), Optional.empty(), Optional.of(0L), Optional.of(1L)) + ); } @Test @@ -206,7 +232,8 @@ public class KeyTransparencyControllerTest { .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)); try (Response response = request.post( Entity.json(createRequestJson(new KeyTransparencySearchRequest(ACI, Optional.empty(), Optional.empty(), - ACI_IDENTITY_KEY, Optional.empty(), Optional.empty(), 4L))))) { + ACI_IDENTITY_KEY, Optional.empty(), Optional.empty(), 4L, Optional.empty(), Optional.empty(), + Optional.empty()))))) { assertEquals(400, response.getStatus()); } verifyNoInteractions(keyTransparencyServiceClient); @@ -215,7 +242,7 @@ public class KeyTransparencyControllerTest { @ParameterizedTest @MethodSource void searchGrpcErrors(final Status grpcStatus, final int httpStatus) { - when(keyTransparencyServiceClient.search(any(), any(), any(), any(), any(), anyLong())) + when(keyTransparencyServiceClient.search(any(), any(), any(), any(), any(), anyLong(), any(), any(), any())) .thenThrow(new StatusRuntimeException(grpcStatus)); final Invocation.Builder request = resources.getJerseyTest() @@ -223,15 +250,14 @@ public class KeyTransparencyControllerTest { .request(); try (Response response = request.post( Entity.json(createRequestJson(new KeyTransparencySearchRequest(ACI, Optional.empty(), Optional.empty(), - ACI_IDENTITY_KEY, Optional.empty(), Optional.empty(), 4L))))) { + ACI_IDENTITY_KEY, Optional.empty(), Optional.empty(), 4L, Optional.empty(), Optional.empty(), Optional.empty()))))) { assertEquals(httpStatus, response.getStatus()); - verify(keyTransparencyServiceClient, times(1)).search(any(), any(), any(), any(), any(), anyLong()); + verify(keyTransparencyServiceClient, times(1)).search(any(), any(), any(), any(), any(), anyLong(), any(), any(), any()); } } private static Stream searchGrpcErrors() { return Stream.of( - Arguments.of(Status.NOT_FOUND, 404), Arguments.of(Status.PERMISSION_DENIED, 403), Arguments.of(Status.INVALID_ARGUMENT, 422), Arguments.of(Status.UNKNOWN, 500) @@ -246,13 +272,16 @@ public class KeyTransparencyControllerTest { final Optional e164, final Optional unidentifiedAccessKey, final Optional lastTreeHeadSize, - final long distinguishedTreeHeadSize) { + final long distinguishedTreeHeadSize, + final Optional aciVersion, + final Optional e164Version, + final Optional usernameHashVersion) { final Invocation.Builder request = resources.getJerseyTest() .target("/v1/key-transparency/search") .request(); try (Response response = request.post(Entity.json( createRequestJson(new KeyTransparencySearchRequest(aci, e164, Optional.empty(), - aciIdentityKey, unidentifiedAccessKey, lastTreeHeadSize, distinguishedTreeHeadSize))))) { + aciIdentityKey, unidentifiedAccessKey, lastTreeHeadSize, distinguishedTreeHeadSize, aciVersion, e164Version, usernameHashVersion))))) { assertEquals(422, response.getStatus()); verifyNoInteractions(keyTransparencyServiceClient); } @@ -260,18 +289,18 @@ public class KeyTransparencyControllerTest { private static Stream searchInvalidRequest() { return Stream.of( - // ACI can't be null - Arguments.of(null, ACI_IDENTITY_KEY, Optional.empty(), Optional.empty(), Optional.empty(), 4L), - // ACI identity key can't be null - Arguments.of(ACI, null, Optional.empty(), Optional.empty(), Optional.empty(), 4L), - // lastNonDistinguishedTreeHeadSize must be positive - Arguments.of(ACI, ACI_IDENTITY_KEY, Optional.empty(), Optional.empty(), Optional.of(0L), 4L), - // lastDistinguishedTreeHeadSize must be positive - Arguments.of(ACI, ACI_IDENTITY_KEY, Optional.empty(), Optional.empty(), Optional.empty(), 0L), - // E164 can't be provided without an unidentified access key - Arguments.of(ACI, ACI_IDENTITY_KEY, Optional.of(NUMBER), Optional.empty(), Optional.empty(), 4L), - // ...and an unidentified access key can't be provided without an E164 - Arguments.of(ACI, ACI_IDENTITY_KEY, Optional.empty(), Optional.of(UNIDENTIFIED_ACCESS_KEY), Optional.empty(), 4L) + Arguments.argumentSet("ACI can't be null", null, ACI_IDENTITY_KEY, Optional.empty(), Optional.empty(), Optional.empty(), 4L, Optional.empty(), Optional.empty(), Optional.empty()), + Arguments.argumentSet("ACI identity key can't be null", ACI, null, Optional.empty(), Optional.empty(), Optional.empty(), 4L, Optional.empty(), Optional.empty(), Optional.empty()), + Arguments.argumentSet("lastNonDistinguishedTreeHeadSize must be positive", ACI, ACI_IDENTITY_KEY, Optional.empty(), Optional.empty(), Optional.of(0L), 4L, Optional.empty(), Optional.empty(), Optional.empty()), + Arguments.argumentSet("lastDistinguishedTreeHeadSize must be positive", ACI, ACI_IDENTITY_KEY, Optional.empty(), Optional.empty(), Optional.empty(), 0L, Optional.empty(), Optional.empty(), Optional.empty()), + Arguments.argumentSet("E164 can't be provided without an unidentified access key", ACI, ACI_IDENTITY_KEY, Optional.of(NUMBER), Optional.empty(), Optional.empty(), 4L, Optional.empty(), Optional.empty(), Optional.empty()), + Arguments.argumentSet("An unidentified access key can't be provided without an E164", ACI, ACI_IDENTITY_KEY, Optional.empty(), Optional.of(UNIDENTIFIED_ACCESS_KEY), Optional.empty(), 4L, Optional.empty(), Optional.empty(), Optional.empty()), + Arguments.argumentSet("ACI version cannot be negative", ACI, ACI_IDENTITY_KEY, Optional.empty(), Optional.of(UNIDENTIFIED_ACCESS_KEY), Optional.empty(), 4L, Optional.of(-1L), Optional.empty(), Optional.empty()), + Arguments.argumentSet("ACI version cannot be greater than max uint32 protobuf value", ACI, ACI_IDENTITY_KEY, Optional.empty(), Optional.of(UNIDENTIFIED_ACCESS_KEY), Optional.empty(), 4L, Optional.of(0xFFFFFFFFL), Optional.empty(), Optional.empty()), + Arguments.argumentSet("E164 version cannot be negative", ACI, ACI_IDENTITY_KEY, Optional.empty(), Optional.of(UNIDENTIFIED_ACCESS_KEY), Optional.empty(), 4L, Optional.empty(), Optional.of(-1L), Optional.empty()), + Arguments.argumentSet("E164 version cannot be greater than max uint32 protobuf value", ACI, ACI_IDENTITY_KEY, Optional.empty(), Optional.of(UNIDENTIFIED_ACCESS_KEY), Optional.empty(), 4L, Optional.empty(), Optional.of(0xFFFFFFFFL), Optional.empty()), + Arguments.argumentSet("Username hash version cannot be negative", ACI, ACI_IDENTITY_KEY, Optional.empty(), Optional.of(UNIDENTIFIED_ACCESS_KEY), Optional.empty(), 4L, Optional.empty(), Optional.empty(), Optional.of(-1L)), + Arguments.argumentSet("Username hash cannot be greater than max uint32 protobuf value", ACI, ACI_IDENTITY_KEY, Optional.empty(), Optional.of(UNIDENTIFIED_ACCESS_KEY), Optional.empty(), 4L, Optional.empty(), Optional.empty(), Optional.of(0xFFFFFFFFL)) ); } @@ -284,7 +313,7 @@ public class KeyTransparencyControllerTest { .request(); try (Response response = request.post( Entity.json(createRequestJson(new KeyTransparencySearchRequest(ACI, Optional.empty(), Optional.empty(), - ACI_IDENTITY_KEY, Optional.empty(), Optional.empty(), 4L))))) { + ACI_IDENTITY_KEY, Optional.empty(), Optional.empty(), 4L, Optional.empty(), Optional.empty(), Optional.empty()))))) { assertEquals(429, response.getStatus()); verifyNoInteractions(keyTransparencyServiceClient); } @@ -373,105 +402,93 @@ public class KeyTransparencyControllerTest { private static Stream monitorInvalidRequest() { return Stream.of( - // aci monitor cannot be null - Arguments.of(createRequestJson( + Arguments.argumentSet("aci monitor cannot be null", createRequestJson( new KeyTransparencyMonitorRequest(null, Optional.empty(), Optional.empty(), 3L, 4L))), - // aci monitor fields can't be null - Arguments.of(createRequestJson( + Arguments.argumentSet("aci monitor fields can't be null - null value and commitment index", createRequestJson( new KeyTransparencyMonitorRequest(new KeyTransparencyMonitorRequest.AciMonitor(null, 4, null), Optional.empty(), Optional.empty(), 3L, 4L))), - Arguments.of(createRequestJson( + Arguments.argumentSet("aci monitor fields can't be null - null value", createRequestJson( new KeyTransparencyMonitorRequest( new KeyTransparencyMonitorRequest.AciMonitor(null, 4, COMMITMENT_INDEX), Optional.empty(), Optional.empty(), 3L, 4L))), - Arguments.of(createRequestJson( + Arguments.argumentSet("aci monitor fields can't be null - null commitment index", createRequestJson( new KeyTransparencyMonitorRequest(new KeyTransparencyMonitorRequest.AciMonitor(ACI, 4, null), Optional.empty(), Optional.empty(), 3L, 4L))), - // aciPosition must be positive - Arguments.of(createRequestJson(new KeyTransparencyMonitorRequest( + Arguments.argumentSet("aciPosition must be positive", createRequestJson(new KeyTransparencyMonitorRequest( new KeyTransparencyMonitorRequest.AciMonitor(ACI, 0, COMMITMENT_INDEX), Optional.empty(), Optional.empty(), 3L, 4L))), - // aci commitment index must be the correct size - Arguments.of(createRequestJson(new KeyTransparencyMonitorRequest( + Arguments.argumentSet("aci commitment index must be the correct size - too small", createRequestJson(new KeyTransparencyMonitorRequest( new KeyTransparencyMonitorRequest.AciMonitor(ACI, 4, new byte[0]), Optional.empty(), Optional.empty(), 3L, 4L))), - Arguments.of(createRequestJson(new KeyTransparencyMonitorRequest( + Arguments.argumentSet("aci commitment index must be the correct size - too large", createRequestJson(new KeyTransparencyMonitorRequest( new KeyTransparencyMonitorRequest.AciMonitor(ACI, 0, new byte[33]), Optional.empty(), Optional.empty(), 3L, 4L))), - // username monitor fields cannot be null - Arguments.of(createRequestJson( + Arguments.argumentSet("username monitor fields cannot be null - null value and commitment index", createRequestJson( new KeyTransparencyMonitorRequest( new KeyTransparencyMonitorRequest.AciMonitor(ACI, 4, COMMITMENT_INDEX), Optional.empty(), Optional.of(new KeyTransparencyMonitorRequest.UsernameHashMonitor(null, 5, null)), 3L, 4L))), - Arguments.of(createRequestJson( + Arguments.argumentSet("username monitor fields cannot be null - null value", createRequestJson( new KeyTransparencyMonitorRequest( new KeyTransparencyMonitorRequest.AciMonitor(ACI, 4, COMMITMENT_INDEX), Optional.empty(), Optional.of(new KeyTransparencyMonitorRequest.UsernameHashMonitor(null, 5, COMMITMENT_INDEX)), 3L, 4L))), - Arguments.of(createRequestJson( + Arguments.argumentSet("username monitor fields cannot be null - null commitment index", createRequestJson( new KeyTransparencyMonitorRequest( new KeyTransparencyMonitorRequest.AciMonitor(ACI, 4, COMMITMENT_INDEX), Optional.empty(), Optional.of(new KeyTransparencyMonitorRequest.UsernameHashMonitor(USERNAME_HASH, 5, null)), 3L, 4L))), - // usernameHashPosition must be positive - Arguments.of(createRequestJson( + Arguments.argumentSet("usernameHashPosition must be positive", createRequestJson( new KeyTransparencyMonitorRequest( new KeyTransparencyMonitorRequest.AciMonitor(ACI, 4, COMMITMENT_INDEX), Optional.empty(), Optional.of(new KeyTransparencyMonitorRequest.UsernameHashMonitor(USERNAME_HASH, 0, COMMITMENT_INDEX)), 3L, 4L))), - // username commitment index must be the correct size - Arguments.of(createRequestJson( + Arguments.argumentSet("username commitment index must be the correct size - too small", createRequestJson( new KeyTransparencyMonitorRequest( new KeyTransparencyMonitorRequest.AciMonitor(ACI, 4, new byte[0]), Optional.empty(), Optional.of(new KeyTransparencyMonitorRequest.UsernameHashMonitor(USERNAME_HASH, 5, new byte[0])), 3L, 4L))), - Arguments.of(createRequestJson( + Arguments.argumentSet("username commitment index must be the correct size - too large", createRequestJson( new KeyTransparencyMonitorRequest(new KeyTransparencyMonitorRequest.AciMonitor(ACI, 4, null), Optional.empty(), Optional.of(new KeyTransparencyMonitorRequest.UsernameHashMonitor(USERNAME_HASH, 5, new byte[33])), 3L, 4L))), - // e164 fields cannot be null - Arguments.of( + Arguments.argumentSet("e164 fields cannot be null - null value and commitment index", createRequestJson(new KeyTransparencyMonitorRequest( new KeyTransparencyMonitorRequest.AciMonitor(ACI, 4, COMMITMENT_INDEX), Optional.of(new KeyTransparencyMonitorRequest.E164Monitor(null, 5, null)), Optional.empty(), 3L, 4L))), - Arguments.of( + Arguments.argumentSet("e164 fields cannot be null - null value", createRequestJson(new KeyTransparencyMonitorRequest( new KeyTransparencyMonitorRequest.AciMonitor(ACI, 4, COMMITMENT_INDEX), Optional.of(new KeyTransparencyMonitorRequest.E164Monitor(null, 5, COMMITMENT_INDEX)), Optional.empty(), 3L, 4L))), - Arguments.of( + Arguments.argumentSet("e164 fields cannot be null - null commitment index", createRequestJson(new KeyTransparencyMonitorRequest( new KeyTransparencyMonitorRequest.AciMonitor(ACI, 4, COMMITMENT_INDEX), Optional.of(new KeyTransparencyMonitorRequest.E164Monitor(NUMBER, 5, null)), Optional.empty(), 3L, 4L))), - // e164Position must be positive - Arguments.of(createRequestJson(new KeyTransparencyMonitorRequest( + Arguments.argumentSet("e164Position must be positive", createRequestJson(new KeyTransparencyMonitorRequest( new KeyTransparencyMonitorRequest.AciMonitor(ACI, 4, COMMITMENT_INDEX), Optional.of( new KeyTransparencyMonitorRequest.E164Monitor(NUMBER, 0, COMMITMENT_INDEX)), Optional.empty(), 3L, 4L))), - // e164 commitment index must be the correct size - Arguments.of(createRequestJson(new KeyTransparencyMonitorRequest( + Arguments.argumentSet("e164 commitment index must be the correct size - too small", createRequestJson(new KeyTransparencyMonitorRequest( new KeyTransparencyMonitorRequest.AciMonitor(ACI, 4, COMMITMENT_INDEX), Optional.of( new KeyTransparencyMonitorRequest.E164Monitor(NUMBER, 5, new byte[0])), Optional.empty(), 3L, 4L))), - Arguments.of(createRequestJson(new KeyTransparencyMonitorRequest( + Arguments.argumentSet("e164 commitment index must be the correct size - too large", createRequestJson(new KeyTransparencyMonitorRequest( new KeyTransparencyMonitorRequest.AciMonitor(ACI, 4, COMMITMENT_INDEX), Optional.of( new KeyTransparencyMonitorRequest.E164Monitor(NUMBER, 5, new byte[33])), Optional.empty(), 3L, 4L))), - // lastNonDistinguishedTreeHeadSize must be positive - Arguments.of(createRequestJson(new KeyTransparencyMonitorRequest( + Arguments.argumentSet("lastNonDistinguishedTreeHeadSize must be positive", createRequestJson(new KeyTransparencyMonitorRequest( new KeyTransparencyMonitorRequest.AciMonitor(ACI, 4, COMMITMENT_INDEX), Optional.empty(), Optional.empty(), 0L, 4L))), - // lastDistinguishedTreeHeadSize must be positive - Arguments.of(createRequestJson(new KeyTransparencyMonitorRequest( + Arguments.argumentSet("lastDistinguishedTreeHeadSize must be positive", createRequestJson(new KeyTransparencyMonitorRequest( new KeyTransparencyMonitorRequest.AciMonitor(ACI, 4, COMMITMENT_INDEX), Optional.empty(), Optional.empty(), 3L, 0L))) );