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 d8e931a14..82734575b 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeyTransparencyController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeyTransparencyController.java @@ -168,19 +168,18 @@ public class KeyTransparencyController { try { final List monitorKeys = new ArrayList<>(List.of( createMonitorKey(getFullSearchKeyByteString(ACI_PREFIX, request.aci().value().toCompactByteArray()), - request.aci().positions()) + request.aci().positions(), + ByteString.copyFrom(request.aci().commitmentIndex())) )); request.usernameHash().ifPresent(usernameHash -> monitorKeys.add(createMonitorKey(getFullSearchKeyByteString(USERNAME_PREFIX, usernameHash.value()), - usernameHash.positions())) - ); + usernameHash.positions(), ByteString.copyFrom(usernameHash.commitmentIndex())))); request.e164().ifPresent(e164 -> monitorKeys.add( createMonitorKey(getFullSearchKeyByteString(E164_PREFIX, e164.value().getBytes(StandardCharsets.UTF_8)), - e164.positions())) - ); + e164.positions(), ByteString.copyFrom(e164.commitmentIndex())))); return new KeyTransparencyMonitorResponse(keyTransparencyServiceClient.monitor( monitorKeys, @@ -252,10 +251,12 @@ public class KeyTransparencyController { throw new ServerErrorException(Response.Status.INTERNAL_SERVER_ERROR, unwrapped); } - private static MonitorKey createMonitorKey(final ByteString fullSearchKey, final List positions) { + private static MonitorKey createMonitorKey(final ByteString fullSearchKey, final List positions, + final ByteString commitmentIndex) { return MonitorKey.newBuilder() .setSearchKey(fullSearchKey) .addAllEntries(positions) + .setCommitmentIndex(commitmentIndex) .build(); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/KeyTransparencyMonitorRequest.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/KeyTransparencyMonitorRequest.java index eaed0eaae..0a3280fb9 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/KeyTransparencyMonitorRequest.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/KeyTransparencyMonitorRequest.java @@ -17,6 +17,7 @@ import javax.validation.constraints.NotNull; import javax.validation.constraints.Positive; import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; import org.whispersystems.textsecuregcm.util.ByteArrayBase64UrlAdapter; +import org.whispersystems.textsecuregcm.util.ExactlySize; import org.whispersystems.textsecuregcm.util.ServiceIdentifierAdapter; public record KeyTransparencyMonitorRequest( @@ -53,7 +54,14 @@ public record KeyTransparencyMonitorRequest( @Valid @NotNull @NotEmpty - List<@Positive Long> positions + List<@Positive Long> positions, + + @Schema(description = "The commitment index derived from a previous search request") + @JsonSerialize(using = ByteArrayBase64UrlAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64UrlAdapter.Deserializing.class) + @NotNull + @ExactlySize(32) + byte[] commitmentIndex ) {} public record E164Monitor( @@ -65,7 +73,14 @@ public record KeyTransparencyMonitorRequest( @NotNull @NotEmpty @Valid - List<@Positive Long> positions + List<@Positive Long> positions, + + @Schema(description = "The commitment index derived from a previous search or monitor request") + @JsonSerialize(using = ByteArrayBase64UrlAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64UrlAdapter.Deserializing.class) + @NotNull + @ExactlySize(32) + byte[] commitmentIndex ) {} public record UsernameHashMonitor( @@ -80,6 +95,13 @@ public record KeyTransparencyMonitorRequest( @Schema(description = "A list of log tree positions maintained by the client for the username hash search key.") @NotNull @NotEmpty - @Valid List<@Positive Long> positions + @Valid List<@Positive Long> positions, + + @Schema(description = "The commitment index derived from a previous search or monitor request") + @JsonSerialize(using = ByteArrayBase64UrlAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64UrlAdapter.Deserializing.class) + @NotNull + @ExactlySize(32) + byte[] commitmentIndex ) {} } 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 ae3955873..474d9cbf1 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/KeyTransparencyControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/KeyTransparencyControllerTest.java @@ -89,6 +89,7 @@ public class KeyTransparencyControllerTest { 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()); + private static final byte[] COMMITMENT_INDEX = new byte[32]; private static final byte[] UNIDENTIFIED_ACCESS_KEY = new byte[16]; private final KeyTransparencyServiceClient keyTransparencyServiceClient = mock(KeyTransparencyServiceClient.class); private static final RateLimiters rateLimiters = mock(RateLimiters.class); @@ -311,7 +312,8 @@ public class KeyTransparencyControllerTest { try (Response response = request.post(Entity.json( createRequestJson( - new KeyTransparencyMonitorRequest(new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(3L)), + new KeyTransparencyMonitorRequest( + new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(3L), COMMITMENT_INDEX), Optional.empty(), Optional.empty(), Optional.of(3L), Optional.of(4L)))))) { assertEquals(200, response.getStatus()); @@ -332,7 +334,8 @@ public class KeyTransparencyControllerTest { .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)); try (Response response = request.post( Entity.json(createRequestJson( - new KeyTransparencyMonitorRequest(new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(3L)), + new KeyTransparencyMonitorRequest( + new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(3L), COMMITMENT_INDEX), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()))))) { assertEquals(400, response.getStatus()); verifyNoInteractions(keyTransparencyServiceClient); @@ -350,7 +353,8 @@ public class KeyTransparencyControllerTest { .request(); try (Response response = request.post( Entity.json(createRequestJson( - new KeyTransparencyMonitorRequest(new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(3L)), + new KeyTransparencyMonitorRequest( + new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(3L), COMMITMENT_INDEX), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()))))) { assertEquals(httpStatus, response.getStatus()); verify(keyTransparencyServiceClient, times(1)).monitor(any(), any(), any(), any()); @@ -380,54 +384,121 @@ public class KeyTransparencyControllerTest { private static Stream monitorInvalidRequest() { return Stream.of( - // aci and aciPositions can't be empty + // aci monitor cannot be null Arguments.of(createRequestJson( - new KeyTransparencyMonitorRequest(new KeyTransparencyMonitorRequest.AciMonitor(null, null), + new KeyTransparencyMonitorRequest(null, Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty()))), + // aci monitor fields can't be null + Arguments.of(createRequestJson( + new KeyTransparencyMonitorRequest(new KeyTransparencyMonitorRequest.AciMonitor(null, null, null), + Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()))), + Arguments.of(createRequestJson( + new KeyTransparencyMonitorRequest( + new KeyTransparencyMonitorRequest.AciMonitor(null, List.of(4L), COMMITMENT_INDEX), + Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()))), + Arguments.of(createRequestJson( + new KeyTransparencyMonitorRequest(new KeyTransparencyMonitorRequest.AciMonitor(ACI, null, COMMITMENT_INDEX), + Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()))), + Arguments.of(createRequestJson( + new KeyTransparencyMonitorRequest(new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(4L), null), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()))), // aciPositions list can't be empty Arguments.of(createRequestJson(new KeyTransparencyMonitorRequest( - new KeyTransparencyMonitorRequest.AciMonitor(ACI, Collections.emptyList()), + new KeyTransparencyMonitorRequest.AciMonitor(ACI, Collections.emptyList(), COMMITMENT_INDEX), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()))), - // usernameHash cannot be empty if usernameHashPositions isn't + // aci commitment index must be the correct size + Arguments.of(createRequestJson(new KeyTransparencyMonitorRequest( + new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(4L), new byte[0]), + Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()))), + Arguments.of(createRequestJson(new KeyTransparencyMonitorRequest( + new KeyTransparencyMonitorRequest.AciMonitor(ACI, Collections.emptyList(), new byte[33]), + Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()))), + // username monitor fields cannot be null Arguments.of(createRequestJson( - new KeyTransparencyMonitorRequest(new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(4L)), - Optional.empty(), - Optional.of(new KeyTransparencyMonitorRequest.UsernameHashMonitor(null, List.of(5L))), + new KeyTransparencyMonitorRequest( + new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(4L), COMMITMENT_INDEX), Optional.empty(), + Optional.of(new KeyTransparencyMonitorRequest.UsernameHashMonitor(null, null, null)), Optional.empty(), Optional.empty()))), - // usernameHashPosition cannot be empty if usernameHash isn't Arguments.of(createRequestJson( - new KeyTransparencyMonitorRequest(new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(4L)), - Optional.empty(), Optional.of(new KeyTransparencyMonitorRequest.UsernameHashMonitor(USERNAME_HASH, - null)), Optional.empty(), Optional.empty()))), + new KeyTransparencyMonitorRequest( + new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(4L), COMMITMENT_INDEX), Optional.empty(), + Optional.of(new KeyTransparencyMonitorRequest.UsernameHashMonitor(null, List.of(5L), COMMITMENT_INDEX)), + Optional.empty(), Optional.empty()))), + Arguments.of(createRequestJson( + new KeyTransparencyMonitorRequest( + new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(4L), COMMITMENT_INDEX), Optional.empty(), + Optional.of( + new KeyTransparencyMonitorRequest.UsernameHashMonitor(USERNAME_HASH, null, COMMITMENT_INDEX)), + Optional.empty(), Optional.empty()))), + Arguments.of(createRequestJson( + new KeyTransparencyMonitorRequest( + new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(4L), COMMITMENT_INDEX), Optional.empty(), + Optional.of(new KeyTransparencyMonitorRequest.UsernameHashMonitor(USERNAME_HASH, List.of(5L), null)), + Optional.empty(), Optional.empty()))), // usernameHashPositions list cannot be empty Arguments.of(createRequestJson( - new KeyTransparencyMonitorRequest(new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(4L)), + new KeyTransparencyMonitorRequest( + new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(4L), COMMITMENT_INDEX), Optional.empty(), Optional.of(new KeyTransparencyMonitorRequest.UsernameHashMonitor(USERNAME_HASH, - Collections.emptyList())), Optional.empty(), Optional.empty()))), - // e164 cannot be empty if e164Positions isn't + Collections.emptyList(), COMMITMENT_INDEX)), Optional.empty(), Optional.empty()))), + // username commitment index must be the correct size + Arguments.of(createRequestJson( + new KeyTransparencyMonitorRequest( + new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(4L), new byte[0]), + Optional.empty(), + Optional.of(new KeyTransparencyMonitorRequest.UsernameHashMonitor(USERNAME_HASH, + List.of(5L), new byte[0])), Optional.empty(), Optional.empty()))), + Arguments.of(createRequestJson( + new KeyTransparencyMonitorRequest(new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(4L), null), + Optional.empty(), + Optional.of(new KeyTransparencyMonitorRequest.UsernameHashMonitor(USERNAME_HASH, + List.of(5L), new byte[33])), Optional.empty(), Optional.empty()))), + // e164 fields cannot be null Arguments.of( createRequestJson(new KeyTransparencyMonitorRequest( - new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(4L)), - Optional.of(new KeyTransparencyMonitorRequest.E164Monitor(null, List.of(5L))), + new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(4L), COMMITMENT_INDEX), + Optional.of(new KeyTransparencyMonitorRequest.E164Monitor(null, null, null)), + Optional.empty(), Optional.empty(), Optional.empty()))), + Arguments.of( + createRequestJson(new KeyTransparencyMonitorRequest( + new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(4L), COMMITMENT_INDEX), + Optional.of(new KeyTransparencyMonitorRequest.E164Monitor(null, List.of(5L), COMMITMENT_INDEX)), + Optional.empty(), Optional.empty(), Optional.empty()))), + Arguments.of( + createRequestJson(new KeyTransparencyMonitorRequest( + new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(4L), COMMITMENT_INDEX), + Optional.of(new KeyTransparencyMonitorRequest.E164Monitor(NUMBER, null, COMMITMENT_INDEX)), + Optional.empty(), Optional.empty(), Optional.empty()))), + Arguments.of( + createRequestJson(new KeyTransparencyMonitorRequest( + new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(4L), COMMITMENT_INDEX), + Optional.of(new KeyTransparencyMonitorRequest.E164Monitor(NUMBER, List.of(5L), null)), Optional.empty(), Optional.empty(), Optional.empty()))), - // e164Positions cannot be empty if e164 isn't - Arguments.of(createRequestJson(new KeyTransparencyMonitorRequest( - new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(4L)), - Optional.of(new KeyTransparencyMonitorRequest.E164Monitor(NUMBER, null)), Optional.empty(), - Optional.empty(), Optional.empty()))), // e164Positions list cannot empty Arguments.of(createRequestJson(new KeyTransparencyMonitorRequest( - new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(4L)), - Optional.of(new KeyTransparencyMonitorRequest.E164Monitor(NUMBER, Collections.emptyList())), + new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(4L), COMMITMENT_INDEX), + Optional.of( + new KeyTransparencyMonitorRequest.E164Monitor(NUMBER, Collections.emptyList(), COMMITMENT_INDEX)), + Optional.empty(), Optional.empty(), Optional.empty()))), + // e164 commitment index must be the correct size + Arguments.of(createRequestJson(new KeyTransparencyMonitorRequest( + new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(4L), COMMITMENT_INDEX), + Optional.of( + new KeyTransparencyMonitorRequest.E164Monitor(NUMBER, List.of(5L), new byte[0])), + Optional.empty(), Optional.empty(), Optional.empty()))), + Arguments.of(createRequestJson(new KeyTransparencyMonitorRequest( + new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(4L), COMMITMENT_INDEX), + Optional.of( + new KeyTransparencyMonitorRequest.E164Monitor(NUMBER, List.of(5L), new byte[33])), Optional.empty(), Optional.empty(), Optional.empty()))), // lastNonDistinguishedTreeHeadSize must be positive Arguments.of(createRequestJson(new KeyTransparencyMonitorRequest( - new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(4L)), Optional.empty(), + new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(4L), COMMITMENT_INDEX), Optional.empty(), Optional.empty(), Optional.of(0L), Optional.empty()))), // lastDistinguishedTreeHeadSize must be positive Arguments.of(createRequestJson(new KeyTransparencyMonitorRequest( - new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(4L)), Optional.empty(), + new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(4L), COMMITMENT_INDEX), Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(-1L)))) ); } @@ -441,7 +512,7 @@ public class KeyTransparencyControllerTest { .request(); try (Response response = request.post( Entity.json(createRequestJson( - new KeyTransparencyMonitorRequest(new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(3L)), + new KeyTransparencyMonitorRequest(new KeyTransparencyMonitorRequest.AciMonitor(ACI, List.of(3L), null), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()))))) { assertEquals(429, response.getStatus());