key transparency: add distinguished key endpoint

This commit is contained in:
Chris Eager
2024-10-24 11:49:20 -05:00
committed by GitHub
parent 1959ca2d96
commit 155f3d6231
6 changed files with 267 additions and 72 deletions

View File

@@ -11,34 +11,9 @@ import io.dropwizard.auth.Auth;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.signal.keytransparency.client.MonitorKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
import org.whispersystems.textsecuregcm.entities.KeyTransparencyMonitorRequest;
import org.whispersystems.textsecuregcm.entities.KeyTransparencyMonitorResponse;
import org.whispersystems.textsecuregcm.entities.KeyTransparencySearchRequest;
import org.whispersystems.textsecuregcm.entities.KeyTransparencySearchResponse;
import org.whispersystems.textsecuregcm.keytransparency.KeyTransparencyServiceClient;
import org.whispersystems.textsecuregcm.limits.RateLimitedByIp;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
import org.whispersystems.websocket.auth.ReadOnly;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.ForbiddenException;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.ServerErrorException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
@@ -48,6 +23,35 @@ import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.ForbiddenException;
import javax.ws.rs.GET;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.ServerErrorException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.signal.keytransparency.client.MonitorKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
import org.whispersystems.textsecuregcm.entities.KeyTransparencyDistinguishedKeyResponse;
import org.whispersystems.textsecuregcm.entities.KeyTransparencyMonitorRequest;
import org.whispersystems.textsecuregcm.entities.KeyTransparencyMonitorResponse;
import org.whispersystems.textsecuregcm.entities.KeyTransparencySearchRequest;
import org.whispersystems.textsecuregcm.entities.KeyTransparencySearchResponse;
import org.whispersystems.textsecuregcm.keytransparency.KeyTransparencyServiceClient;
import org.whispersystems.textsecuregcm.limits.RateLimitedByIp;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
import org.whispersystems.websocket.auth.ReadOnly;
@Path("/v1/key-transparency")
@Tag(name = "KeyTransparency")
@@ -76,9 +80,10 @@ public class KeyTransparencyController {
"""
)
@ApiResponse(responseCode = "200", description = "All search key lookups were successful", useReturnTypeSchema = true)
@ApiResponse(responseCode = "400", description = "Invalid request. See response for any available details.")
@ApiResponse(responseCode = "403", description = "At least one search key lookup to value mapping was invalid")
@ApiResponse(responseCode = "404", description = "At least one search key lookup did not find the key")
@ApiResponse(responseCode = "429", description = "Ratelimited")
@ApiResponse(responseCode = "429", description = "Rate-limited")
@ApiResponse(responseCode = "422", description = "Invalid request format")
@POST
@Path("/search")
@@ -145,8 +150,9 @@ public class KeyTransparencyController {
"""
)
@ApiResponse(responseCode = "200", description = "All search keys exist in the log", useReturnTypeSchema = true)
@ApiResponse(responseCode = "400", description = "Invalid request. See response for any available details.")
@ApiResponse(responseCode = "404", description = "At least one search key lookup did not find the key")
@ApiResponse(responseCode = "429", description = "Ratelimited")
@ApiResponse(responseCode = "429", description = "Rate-limited")
@ApiResponse(responseCode = "422", description = "Invalid request format")
@POST
@Path("/monitor")
@@ -191,6 +197,44 @@ public class KeyTransparencyController {
return null;
}
@Operation(
summary = "Get the current value of the distinguished key",
description = """
Enforced unauthenticated endpoint. The response contains the distinguished tree head to prove consistency
against for future calls to `/search` and `/distinguished`.
"""
)
@ApiResponse(responseCode = "200", description = "The `distinguished` search key exists in the log", useReturnTypeSchema = true)
@ApiResponse(responseCode = "400", description = "Invalid request. See response for any available details.")
@ApiResponse(responseCode = "422", description = "Invalid request format")
@ApiResponse(responseCode = "429", description = "Rate-limited")
@GET
@Path("/distinguished")
@RateLimitedByIp(RateLimiters.For.KEY_TRANSPARENCY_DISTINGUISHED_PER_IP)
@Produces(MediaType.APPLICATION_JSON)
public KeyTransparencyDistinguishedKeyResponse getDistinguishedKey(
@ReadOnly @Auth final Optional<AuthenticatedDevice> authenticatedAccount,
@Parameter(description = "The distinguished tree head size returned by a previously verified call")
@QueryParam("lastTreeHeadSize") @Valid final Optional<@Positive Long> lastTreeHeadSize) {
// Disallow clients from making authenticated requests to this endpoint
requireNotAuthenticated(authenticatedAccount);
try {
return keyTransparencyServiceClient.getDistinguishedKey(lastTreeHeadSize, KEY_TRANSPARENCY_RPC_TIMEOUT)
.thenApply(KeyTransparencyDistinguishedKeyResponse::new)
.join();
} catch (final CancellationException exception) {
LOGGER.error("Unexpected cancellation from key transparency service", exception);
throw new ServerErrorException(Response.Status.SERVICE_UNAVAILABLE, exception);
} catch (final CompletionException exception) {
handleKeyTransparencyServiceError(exception);
}
// This is unreachable
return null;
}
private void handleKeyTransparencyServiceError(final CompletionException exception) {
final Throwable unwrapped = ExceptionUtils.unwrap(exception);