Update chat to send three search keys in one request to KT
This commit is contained in:
parent
89292e238b
commit
712f3affd9
|
@ -21,7 +21,6 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
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;
|
||||
|
@ -38,6 +37,7 @@ 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.E164SearchRequest;
|
||||
import org.signal.keytransparency.client.MonitorKey;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -74,17 +74,25 @@ public class KeyTransparencyController {
|
|||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Search for the given search keys in the key transparency log",
|
||||
summary = "Search for the given identifiers in the key transparency log",
|
||||
description = """
|
||||
Enforced unauthenticated endpoint. Returns a response if all search keys exist in the key transparency log.
|
||||
Returns a response if the ACI exists in the transparency log and its mapped value matches the provided
|
||||
ACI identity key.
|
||||
|
||||
The username hash search response field is populated if it is found in the log and its mapped value matches
|
||||
the provided ACI. The E164 search response is populated similarly, with some additional requirements:
|
||||
- The account associated with the provided ACI must be discoverable by phone number.
|
||||
- The provided unidentified access key must match the one on the account.
|
||||
|
||||
Enforced unauthenticated endpoint.
|
||||
"""
|
||||
)
|
||||
@ApiResponse(responseCode = "200", description = "All search key lookups were successful", useReturnTypeSchema = true)
|
||||
@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 = "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 = "Rate-limited")
|
||||
@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 = "422", description = "Invalid request format")
|
||||
@ApiResponse(responseCode = "429", description = "Rate-limited")
|
||||
@POST
|
||||
@Path("/search")
|
||||
@RateLimitedByIp(RateLimiters.For.KEY_TRANSPARENCY_SEARCH_PER_IP)
|
||||
|
@ -97,41 +105,29 @@ public class KeyTransparencyController {
|
|||
requireNotAuthenticated(authenticatedAccount);
|
||||
|
||||
try {
|
||||
final CompletableFuture<byte[]> aciSearchKeyResponseFuture = keyTransparencyServiceClient.search(
|
||||
getFullSearchKeyByteString(ACI_PREFIX, request.aci().toCompactByteArray()),
|
||||
ByteString.copyFrom(request.aciIdentityKey().serialize()),
|
||||
Optional.empty(),
|
||||
request.lastTreeHeadSize(),
|
||||
request.distinguishedTreeHeadSize(),
|
||||
KEY_TRANSPARENCY_RPC_TIMEOUT);
|
||||
final Optional<E164SearchRequest> maybeE164SearchRequest =
|
||||
request.e164().flatMap(e164 -> request.unidentifiedAccessKey().map(uak ->
|
||||
E164SearchRequest.newBuilder()
|
||||
.setE164(e164)
|
||||
.setUnidentifiedAccessKey(ByteString.copyFrom(request.unidentifiedAccessKey().get()))
|
||||
.build()
|
||||
));
|
||||
|
||||
final CompletableFuture<byte[]> e164SearchKeyResponseFuture = request.e164()
|
||||
.map(e164 -> keyTransparencyServiceClient.search(
|
||||
getFullSearchKeyByteString(E164_PREFIX, e164.getBytes(StandardCharsets.UTF_8)),
|
||||
return keyTransparencyServiceClient.search(
|
||||
ByteString.copyFrom(request.aci().toCompactByteArray()),
|
||||
Optional.of(ByteString.copyFrom(request.unidentifiedAccessKey().get())),
|
||||
ByteString.copyFrom(request.aciIdentityKey().serialize()),
|
||||
request.usernameHash().map(ByteString::copyFrom),
|
||||
maybeE164SearchRequest,
|
||||
request.lastTreeHeadSize(),
|
||||
request.distinguishedTreeHeadSize(),
|
||||
KEY_TRANSPARENCY_RPC_TIMEOUT))
|
||||
.orElse(CompletableFuture.completedFuture(null));
|
||||
|
||||
final CompletableFuture<byte[]> usernameHashSearchKeyResponseFuture = request.usernameHash()
|
||||
.map(usernameHash -> keyTransparencyServiceClient.search(
|
||||
getFullSearchKeyByteString(USERNAME_PREFIX, request.usernameHash().get()),
|
||||
ByteString.copyFrom(request.aci().toCompactByteArray()),
|
||||
Optional.empty(),
|
||||
request.lastTreeHeadSize(),
|
||||
request.distinguishedTreeHeadSize(),
|
||||
KEY_TRANSPARENCY_RPC_TIMEOUT))
|
||||
.orElse(CompletableFuture.completedFuture(null));
|
||||
|
||||
return CompletableFuture.allOf(aciSearchKeyResponseFuture, e164SearchKeyResponseFuture,
|
||||
usernameHashSearchKeyResponseFuture)
|
||||
.thenApply(ignored ->
|
||||
new KeyTransparencySearchResponse(aciSearchKeyResponseFuture.join(),
|
||||
Optional.ofNullable(e164SearchKeyResponseFuture.join()),
|
||||
Optional.ofNullable(usernameHashSearchKeyResponseFuture.join())))
|
||||
.join();
|
||||
KEY_TRANSPARENCY_RPC_TIMEOUT)
|
||||
.thenApply(searchResponse ->
|
||||
new KeyTransparencySearchResponse(
|
||||
searchResponse.getTreeHead().toByteArray(),
|
||||
searchResponse.getAci().toByteArray(),
|
||||
searchResponse.hasE164() ? Optional.of(searchResponse.getE164().toByteArray()) : Optional.empty(),
|
||||
searchResponse.hasUsernameHash() ? Optional.of(searchResponse.getUsernameHash().toByteArray()) : Optional.empty())
|
||||
).join();
|
||||
} catch (final CancellationException exception) {
|
||||
LOGGER.error("Unexpected cancellation from key transparency service", exception);
|
||||
throw new ServerErrorException(Response.Status.SERVICE_UNAVAILABLE, exception);
|
||||
|
@ -143,10 +139,10 @@ public class KeyTransparencyController {
|
|||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Monitor the given search keys in the key transparency log",
|
||||
summary = "Monitor the given identifiers in the key transparency log",
|
||||
description = """
|
||||
Enforced unauthenticated endpoint. Return proofs proving that the log tree
|
||||
has been constructed correctly in later entries for each of the given search keys .
|
||||
Return proofs proving that the log tree has been constructed correctly in later entries for each of the given
|
||||
identifiers. Enforced unauthenticated endpoint.
|
||||
"""
|
||||
)
|
||||
@ApiResponse(responseCode = "200", description = "All search keys exist in the log", useReturnTypeSchema = true)
|
||||
|
@ -199,8 +195,9 @@ public class KeyTransparencyController {
|
|||
@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`.
|
||||
The response contains the distinguished tree head to prove consistency
|
||||
against for future calls to `/search`, `/monitor`, and `/distinguished`.
|
||||
Enforced unauthenticated endpoint.
|
||||
"""
|
||||
)
|
||||
@ApiResponse(responseCode = "200", description = "The `distinguished` search key exists in the log", useReturnTypeSchema = true)
|
||||
|
|
|
@ -25,11 +25,11 @@ public record KeyTransparencySearchRequest(
|
|||
@NotNull
|
||||
@JsonSerialize(using = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class)
|
||||
@JsonDeserialize(using = ServiceIdentifierAdapter.AciServiceIdentifierDeserializer.class)
|
||||
@Schema(description = "The aci identifier to look up")
|
||||
@Schema(description = "The ACI to look up")
|
||||
AciServiceIdentifier aci,
|
||||
|
||||
@E164
|
||||
@Schema(description = "The e164-formatted phone number to look up")
|
||||
@Schema(description = "The E164-formatted phone number to look up")
|
||||
Optional<String> e164,
|
||||
|
||||
@JsonSerialize(contentUsing = ByteArrayBase64UrlAdapter.Serializing.class)
|
||||
|
@ -40,7 +40,7 @@ public record KeyTransparencySearchRequest(
|
|||
@NotNull
|
||||
@JsonSerialize(using = IdentityKeyAdapter.Serializer.class)
|
||||
@JsonDeserialize(using = IdentityKeyAdapter.Deserializer.class)
|
||||
@Schema(description="The public aci identity key associated with the provided aci")
|
||||
@Schema(description="The public ACI identity key associated with the provided ACI")
|
||||
IdentityKey aciIdentityKey,
|
||||
|
||||
@JsonSerialize(contentUsing = ByteArrayBase64WithPaddingAdapter.Serializing.class)
|
||||
|
|
|
@ -17,16 +17,22 @@ public record KeyTransparencySearchResponse(
|
|||
@NotNull
|
||||
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
|
||||
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
|
||||
@Schema(description = "The search response for the aci search key encoded in standard un-padded base64")
|
||||
@Schema(description = "The `FullTreeHead` protobuf encoded in standard un-padded base64. This should be used across all identifiers.")
|
||||
byte[] fullTreeHead,
|
||||
|
||||
@NotNull
|
||||
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
|
||||
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
|
||||
@Schema(description = "The `TreeSearchResponse` protobuf for the ACI identifier encoded in standard un-padded base64")
|
||||
byte[] aciSearchResponse,
|
||||
|
||||
@JsonSerialize(contentUsing = ByteArrayAdapter.Serializing.class)
|
||||
@JsonDeserialize(contentUsing = ByteArrayAdapter.Deserializing.class)
|
||||
@Schema(description = "The search response for the e164 search key encoded in standard un-padded base64")
|
||||
@Schema(description = "The `TreeSearchResponse` protobuf for the E164 encoded in standard un-padded base64")
|
||||
Optional<byte[]> e164SearchResponse,
|
||||
|
||||
@JsonSerialize(contentUsing = ByteArrayAdapter.Serializing.class)
|
||||
@JsonDeserialize(contentUsing = ByteArrayAdapter.Deserializing.class)
|
||||
@Schema(description = "The search response for the username hash search key encoded in standard un-padded base64")
|
||||
@Schema(description = "The `TreeSearchResponse` protobuf for the username hash encoded in standard un-padded base64")
|
||||
Optional<byte[]> usernameHashSearchResponse
|
||||
) {}
|
||||
|
|
|
@ -26,10 +26,12 @@ import java.util.concurrent.Executor;
|
|||
import java.util.concurrent.TimeUnit;
|
||||
import org.signal.keytransparency.client.ConsistencyParameters;
|
||||
import org.signal.keytransparency.client.DistinguishedRequest;
|
||||
import org.signal.keytransparency.client.E164SearchRequest;
|
||||
import org.signal.keytransparency.client.KeyTransparencyQueryServiceGrpc;
|
||||
import org.signal.keytransparency.client.MonitorKey;
|
||||
import org.signal.keytransparency.client.MonitorRequest;
|
||||
import org.signal.keytransparency.client.SearchRequest;
|
||||
import org.signal.keytransparency.client.SearchResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||
|
@ -111,28 +113,29 @@ public class KeyTransparencyServiceClient implements Managed {
|
|||
}
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public CompletableFuture<byte[]> search(
|
||||
final ByteString searchKey,
|
||||
final ByteString mappedValue,
|
||||
final Optional<ByteString> unidentifiedAccessKey,
|
||||
public CompletableFuture<SearchResponse> search(
|
||||
final ByteString aci,
|
||||
final ByteString aciIdentityKey,
|
||||
final Optional<ByteString> usernameHash,
|
||||
final Optional<E164SearchRequest> e164SearchRequest,
|
||||
final Optional<Long> lastTreeHeadSize,
|
||||
final Optional<Long> distinguishedTreeHeadSize,
|
||||
final Duration timeout) {
|
||||
final SearchRequest.Builder searchRequestBuilder = SearchRequest.newBuilder()
|
||||
.setSearchKey(searchKey)
|
||||
.setMappedValue(mappedValue);
|
||||
final SearchRequest.Builder searchKeysRequestBuilder = SearchRequest.newBuilder()
|
||||
.setAci(aci)
|
||||
.setAciIdentityKey(aciIdentityKey);
|
||||
|
||||
unidentifiedAccessKey.ifPresent(searchRequestBuilder::setUnidentifiedAccessKey);
|
||||
usernameHash.ifPresent(searchKeysRequestBuilder::setUsernameHash);
|
||||
e164SearchRequest.ifPresent(searchKeysRequestBuilder::setE164SearchRequest);
|
||||
|
||||
final ConsistencyParameters.Builder consistency = ConsistencyParameters.newBuilder();
|
||||
lastTreeHeadSize.ifPresent(consistency::setLast);
|
||||
distinguishedTreeHeadSize.ifPresent(consistency::setDistinguished);
|
||||
|
||||
searchRequestBuilder.setConsistency(consistency);
|
||||
searchKeysRequestBuilder.setConsistency(consistency.build());
|
||||
|
||||
return CompletableFutureUtil.toCompletableFuture(stub.withDeadline(toDeadline(timeout))
|
||||
.search(searchRequestBuilder.build()), callbackExecutor)
|
||||
.thenApply(AbstractMessageLite::toByteArray);
|
||||
.search(searchKeysRequestBuilder.build()), callbackExecutor);
|
||||
}
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
|
|
|
@ -11,9 +11,15 @@ option java_package = "org.signal.keytransparency.client";
|
|||
package kt_query;
|
||||
|
||||
/**
|
||||
* An external-facing, read-only key transparency service used by Signal's chat server
|
||||
* to look up and monitor search keys.
|
||||
*/
|
||||
* An external-facing, read-only key transparency service used by Signal's chat server
|
||||
* to look up and monitor identifiers.
|
||||
* There are three types of identifier mappings stored by the key transparency log:
|
||||
* - An ACI which maps to an ACI identity key
|
||||
* - An E164-formatted phone number which maps to an ACI
|
||||
* - A username hash which also maps to an ACI
|
||||
* Separately, the log also stores and periodically updates a fixed value known as the `distinguished` key.
|
||||
* Clients use the verified tree head from looking up this key for future calls to the Search and Monitor endpoints.
|
||||
*/
|
||||
service KeyTransparencyQueryService {
|
||||
/**
|
||||
* An endpoint used by clients to retrieve the most recent distinguished tree
|
||||
|
@ -21,19 +27,86 @@ service KeyTransparencyQueryService {
|
|||
* subsequent Search and Monitor requests. It should be the first key
|
||||
* transparency RPC a client calls.
|
||||
*/
|
||||
rpc Distinguished(DistinguishedRequest) returns (SearchResponse) {}
|
||||
rpc Distinguished(DistinguishedRequest) returns (DistinguishedResponse) {}
|
||||
/**
|
||||
* An endpoint used by clients to search for a given key in the transparency log.
|
||||
* The server returns proof that the search key exists in the log.
|
||||
* An endpoint used by clients to search for one or more identifiers in the transparency log.
|
||||
* The server returns proof that the identifier(s) exist in the log.
|
||||
*/
|
||||
rpc Search(SearchRequest) returns (SearchResponse) {}
|
||||
/**
|
||||
* An endpoint that allows users to monitor a set of search keys by returning proof that the log continues to be
|
||||
* constructed correctly in later entries for those search keys.
|
||||
* An endpoint that allows users to monitor a set of identifiers by returning proof that the log continues to be
|
||||
* constructed correctly in later entries for those identifiers.
|
||||
*/
|
||||
rpc Monitor(MonitorRequest) returns (MonitorResponse) {}
|
||||
}
|
||||
|
||||
message SearchRequest {
|
||||
/**
|
||||
* The ACI to look up in the log.
|
||||
*/
|
||||
bytes aci = 1;
|
||||
/**
|
||||
* The ACI identity key that the client thinks the ACI maps to in the log.
|
||||
*/
|
||||
bytes aci_identity_key = 2;
|
||||
/**
|
||||
* The username hash to look up in the log.
|
||||
*/
|
||||
optional bytes username_hash = 3;
|
||||
/**
|
||||
* The E164 to look up in the log along with associated data.
|
||||
*/
|
||||
optional E164SearchRequest e164_search_request = 4;
|
||||
/**
|
||||
* The tree head size(s) to prove consistency against.
|
||||
*/
|
||||
ConsistencyParameters consistency = 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* E164SearchRequest contains the data that the user must provide when looking up an E164.
|
||||
*/
|
||||
message E164SearchRequest {
|
||||
/**
|
||||
* The E164 that the client wishes to look up in the transparency log.
|
||||
*/
|
||||
string e164 = 1;
|
||||
/**
|
||||
* The unidentified access key of the account associated with the provided E164.
|
||||
*/
|
||||
bytes unidentified_access_key = 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* SearchResponse contains search proofs for each of the requested identifiers.
|
||||
* Callers should use the top-level `FullTreeHead` for verification;
|
||||
* the `FullTreeHead` field on the individual `TreeSearchResponse`s will be empty.
|
||||
*/
|
||||
message SearchResponse {
|
||||
/**
|
||||
* A signed representation of the log tree's current state along with some
|
||||
* additional information necessary for validation such as a consistency proof and an auditor-signed tree head.
|
||||
*/
|
||||
FullTreeHead tree_head = 1;
|
||||
/**
|
||||
* The ACI search response is always provided.
|
||||
*/
|
||||
TreeSearchResponse aci = 2;
|
||||
/**
|
||||
* This response is only provided if all of the conditions are met:
|
||||
* - the E164 exists in the log
|
||||
* - its mapped ACI matches the one provided in the request
|
||||
* - the account associated with the ACI is discoverable
|
||||
* - the unidentified access key provided in E164SearchRequest matches the one on the account
|
||||
*/
|
||||
optional TreeSearchResponse e164 = 3;
|
||||
/**
|
||||
* This response is only provided if the username hash exists in the log and
|
||||
* its mapped ACI matches the one provided in the request.
|
||||
*/
|
||||
optional TreeSearchResponse username_hash = 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* The tree head size(s) to prove consistency against. A client's very first
|
||||
* key transparency request should be looking up the "distinguished" key;
|
||||
|
@ -43,7 +116,7 @@ service KeyTransparencyQueryService {
|
|||
message ConsistencyParameters {
|
||||
/**
|
||||
* The non-distinguished tree head size to prove consistency against.
|
||||
* This field may be omitted if the client is looking up a search key
|
||||
* This field may be omitted if the client is looking up an identifier
|
||||
* for the first time.
|
||||
*/
|
||||
optional uint64 last = 1;
|
||||
|
@ -68,57 +141,47 @@ message DistinguishedRequest {
|
|||
optional uint64 last = 1;
|
||||
}
|
||||
|
||||
message SearchRequest {
|
||||
/**
|
||||
* DistinguishedResponse contains the tree head and search proof for the most
|
||||
* recent `distinguished` key in the log. Callers should use the top-level
|
||||
* `FullTreeHead` for verification; the `FullTreeHead` field on
|
||||
* `TreeSearchResponse` will be empty.
|
||||
*/
|
||||
message DistinguishedResponse {
|
||||
/**
|
||||
* The key to look up in the log tree.
|
||||
* A key can be an ACI, an E164 phone number, or a username hash and
|
||||
* is prefixed with a unique identifier to indicate its type.
|
||||
* A signed representation of the log tree's current state along with some
|
||||
* additional information necessary for validation such as a consistency proof and an auditor-signed tree head.
|
||||
*/
|
||||
bytes search_key = 1;
|
||||
FullTreeHead full_tree_head = 1;
|
||||
/**
|
||||
* TODO: Remove this protobuf field in the KT server
|
||||
* The version of the key to look up. If not specified, the key transparency server
|
||||
* defaults to returning the most recent version of the key.
|
||||
* This search response is always provided.
|
||||
*/
|
||||
optional uint32 version = 2;
|
||||
/**
|
||||
* The tree head size(s) to prove consistency against.
|
||||
*/
|
||||
ConsistencyParameters consistency = 3;
|
||||
/**
|
||||
* Clients need to prove that they know the search key to value mapping
|
||||
* to avoid the key transparency service leaking user data.
|
||||
* If the client is looking up the distinguished key, this field may be empty.
|
||||
*/
|
||||
optional bytes mapped_value = 4;
|
||||
/**
|
||||
* Clients should only provide the unidentified_access_key if the search key is a phone number.
|
||||
*/
|
||||
optional bytes unidentified_access_key = 5;
|
||||
TreeSearchResponse distinguished_response = 2;
|
||||
}
|
||||
|
||||
message SearchResponse {
|
||||
message TreeSearchResponse {
|
||||
/**
|
||||
* A signed representation of the log tree's current state along with some
|
||||
* additional information necessary for validation such as a consistency proof and an auditor-signed tree head.
|
||||
*/
|
||||
FullTreeHead tree_head = 1;
|
||||
/**
|
||||
* A proof that is combined with the original requested search key and the VRF public key
|
||||
* A proof that is combined with the original requested identifier and the VRF public key
|
||||
* and outputs whether the proof is valid, and if so, the commitment index.
|
||||
*/
|
||||
bytes vrf_proof = 2;
|
||||
/**
|
||||
* A proof that the binary search for the given search key was done correctly.
|
||||
* A proof that the binary search for the given identifier was done correctly.
|
||||
*/
|
||||
SearchProof search = 3;
|
||||
/**
|
||||
* A 32 byte value computed based on the log position and a random 32 byte key that is only known by
|
||||
* the key transparency service. It is provided so that clients can recompute and verify the commitment.
|
||||
* A 32-byte value computed based on the log position of the identifier
|
||||
* and a random 32 byte key that is only known by the key transparency service.
|
||||
* It is provided so that clients can recompute and verify the commitment.
|
||||
*/
|
||||
bytes opening = 4;
|
||||
/**
|
||||
* The new or updated value of the search key.
|
||||
* The new or updated value that the identifier maps to.
|
||||
*/
|
||||
UpdateValue value = 5;
|
||||
}
|
||||
|
@ -193,17 +256,17 @@ message ProofStep {
|
|||
|
||||
message SearchProof {
|
||||
/**
|
||||
* The position in the log tree of the first occurrence of the requested search key.
|
||||
* The position in the log tree of the first occurrence of the requested identifier.
|
||||
*/
|
||||
uint64 pos = 1;
|
||||
/**
|
||||
* The steps of a binary search through the entries of the log tree for the given search key version.
|
||||
* The steps of a binary search through the entries of the log tree for the given identifier version.
|
||||
* Each ProofStep corresponds to a log entry and provides the information necessary to recompute a log tree
|
||||
* leaf hash.
|
||||
*/
|
||||
repeated ProofStep steps = 2;
|
||||
/**
|
||||
* A batch inclusion proof for all log tree leaves involved in the binary search for the given search key.
|
||||
* A batch inclusion proof for all log tree leaves involved in the binary search for the given identifier.
|
||||
*/
|
||||
repeated bytes inclusion = 3;
|
||||
}
|
||||
|
@ -215,19 +278,19 @@ message UpdateValue {
|
|||
*/
|
||||
// optional bytes signature = 1;
|
||||
/**
|
||||
* The new value for a search key.
|
||||
* The new value for a identifier.
|
||||
*/
|
||||
bytes value = 2;
|
||||
}
|
||||
|
||||
message PrefixSearchResult {
|
||||
/**
|
||||
* A proof from a prefix tree that indicates a search was done correctly for a given search key.
|
||||
* A proof from a prefix tree that indicates a search was done correctly for a given identifier.
|
||||
* The elements of this array are the copath of the prefix tree leaf node in bottom-to-top order.
|
||||
*/
|
||||
repeated bytes proof = 1;
|
||||
/**
|
||||
* The version of the requested search key in the prefix tree.
|
||||
* The version of the requested identifier in the prefix tree.
|
||||
*/
|
||||
uint32 counter = 2;
|
||||
}
|
||||
|
@ -238,7 +301,7 @@ message MonitorKey {
|
|||
*/
|
||||
bytes search_key = 1;
|
||||
/**
|
||||
* A list of log tree positions maintained by a client for the search key being monitored.
|
||||
* A list of log tree positions maintained by a client for the identifier being monitored.
|
||||
* Each position is in the direct path to a key version and corresponds to a tree head
|
||||
* that has been verified to contain that version or greater.
|
||||
* The key transparency server uses this list to compute which log entries to return
|
||||
|
@ -246,7 +309,7 @@ message MonitorKey {
|
|||
*/
|
||||
repeated uint64 entries = 2;
|
||||
/**
|
||||
* The commitment index for the search key. This is derived from vrf_proof in
|
||||
* The commitment index for the identifier. This is derived from vrf_proof in
|
||||
* the SearchResponse.
|
||||
*/
|
||||
bytes commitment_index = 3;
|
||||
|
@ -259,8 +322,8 @@ message MonitorRequest {
|
|||
*/
|
||||
repeated MonitorKey owned_keys = 1;
|
||||
/**
|
||||
* The list of search keys that the client would like to monitor.
|
||||
* All search keys *must* belong to the same user.
|
||||
* The list of identifiers that the client would like to monitor.
|
||||
* All identifiers *must* belong to the same user.
|
||||
*/
|
||||
repeated MonitorKey contact_keys = 2;
|
||||
/**
|
||||
|
@ -292,8 +355,8 @@ message MonitorResponse {
|
|||
*/
|
||||
repeated MonitorProof owned_proofs = 2;
|
||||
/**
|
||||
* A list of proofs, one for each key in MonitorRequest.contact_keys, each proving that the given search key
|
||||
* continues to be constructed correctly in later entries of hte log tree.
|
||||
* A list of proofs, one for each identifier in MonitorRequest.contact_keys, each proving that the given identifier
|
||||
* continues to be constructed correctly in later entries of the log tree.
|
||||
*/
|
||||
repeated MonitorProof contact_proofs = 3;
|
||||
/**
|
||||
|
|
|
@ -12,34 +12,29 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
|||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.reset;
|
||||
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.KeyTransparencyController.ACI_PREFIX;
|
||||
import static org.whispersystems.textsecuregcm.controllers.KeyTransparencyController.E164_PREFIX;
|
||||
import static org.whispersystems.textsecuregcm.controllers.KeyTransparencyController.USERNAME_PREFIX;
|
||||
import static org.whispersystems.textsecuregcm.controllers.KeyTransparencyController.getFullSearchKeyByteString;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.google.common.net.HttpHeaders;
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import io.dropwizard.auth.AuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||
import io.grpc.Status;
|
||||
import io.grpc.StatusRuntimeException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
|
@ -59,6 +54,12 @@ import org.junit.jupiter.params.provider.Arguments;
|
|||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.signal.keytransparency.client.E164SearchRequest;
|
||||
import org.signal.keytransparency.client.FullTreeHead;
|
||||
import org.signal.keytransparency.client.SearchProof;
|
||||
import org.signal.keytransparency.client.SearchResponse;
|
||||
import org.signal.keytransparency.client.TreeSearchResponse;
|
||||
import org.signal.keytransparency.client.UpdateValue;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.signal.libsignal.protocol.ecc.Curve;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
||||
|
@ -137,12 +138,25 @@ public class KeyTransparencyControllerTest {
|
|||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void searchSuccess(final Optional<String> e164, final Optional<byte[]> usernameHash, final int expectedNumClientCalls,
|
||||
final Set<ByteString> expectedSearchKeys,
|
||||
final Set<ByteString> expectedValues,
|
||||
final List<Optional<ByteString>> expectedUnidentifiedAccessKey) {
|
||||
when(keyTransparencyServiceClient.search(any(), any(), any(), any(), any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(TestRandomUtil.nextBytes(16)));
|
||||
void searchSuccess(final Optional<String> e164, final Optional<byte[]> usernameHash) {
|
||||
final TreeSearchResponse aciSearchResponse = TreeSearchResponse.newBuilder()
|
||||
.setOpening(ByteString.copyFrom(TestRandomUtil.nextBytes(16)))
|
||||
.setTreeHead(FullTreeHead.getDefaultInstance())
|
||||
.setSearch(SearchProof.getDefaultInstance())
|
||||
.setValue(UpdateValue.newBuilder()
|
||||
.setValue(ByteString.copyFrom(TestRandomUtil.nextBytes(16)))
|
||||
.build())
|
||||
.build();
|
||||
|
||||
final SearchResponse.Builder searchResponseBuilder = SearchResponse.newBuilder()
|
||||
.setTreeHead(FullTreeHead.getDefaultInstance())
|
||||
.setAci(aciSearchResponse);
|
||||
|
||||
e164.ifPresent(ignored -> searchResponseBuilder.setE164(TreeSearchResponse.getDefaultInstance()));
|
||||
usernameHash.ifPresent(ignored -> searchResponseBuilder.setUsernameHash(TreeSearchResponse.getDefaultInstance()));
|
||||
|
||||
when(keyTransparencyServiceClient.search(any(), any(), any(), any(), any(), any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(searchResponseBuilder.build()));
|
||||
|
||||
final Invocation.Builder request = resources.getJerseyTest()
|
||||
.target("/v1/key-transparency/search")
|
||||
|
@ -158,54 +172,51 @@ public class KeyTransparencyControllerTest {
|
|||
|
||||
final KeyTransparencySearchResponse keyTransparencySearchResponse = response.readEntity(
|
||||
KeyTransparencySearchResponse.class);
|
||||
assertNotNull(keyTransparencySearchResponse.fullTreeHead());
|
||||
assertNotNull(keyTransparencySearchResponse.aciSearchResponse());
|
||||
|
||||
usernameHash.ifPresentOrElse(
|
||||
ignored -> assertTrue(keyTransparencySearchResponse.usernameHashSearchResponse().isPresent()),
|
||||
() -> assertTrue(keyTransparencySearchResponse.usernameHashSearchResponse().isEmpty()));
|
||||
assertEquals(aciSearchResponse, TreeSearchResponse.parseFrom(keyTransparencySearchResponse.aciSearchResponse()));
|
||||
|
||||
e164.ifPresentOrElse(ignored -> assertTrue(keyTransparencySearchResponse.e164SearchResponse().isPresent()),
|
||||
() -> assertTrue(keyTransparencySearchResponse.e164SearchResponse().isEmpty()));
|
||||
e164.ifPresent(ignored -> assertNotNull(keyTransparencySearchResponse.e164SearchResponse()));
|
||||
usernameHash.ifPresent(ignored -> assertNotNull(keyTransparencySearchResponse.usernameHashSearchResponse()));
|
||||
|
||||
ArgumentCaptor<ByteString> valueArguments = ArgumentCaptor.forClass(ByteString.class);
|
||||
ArgumentCaptor<ByteString> searchKeyArguments = ArgumentCaptor.forClass(ByteString.class);
|
||||
ArgumentCaptor<Optional<ByteString>> unidentifiedAccessKeyArgument = ArgumentCaptor.forClass(Optional.class);
|
||||
ArgumentCaptor<ByteString> aciArgument = ArgumentCaptor.forClass(ByteString.class);
|
||||
ArgumentCaptor<ByteString> aciIdentityKeyArgument = ArgumentCaptor.forClass(ByteString.class);
|
||||
ArgumentCaptor<Optional<ByteString>> usernameHashArgument = ArgumentCaptor.forClass(Optional.class);
|
||||
ArgumentCaptor<Optional<E164SearchRequest>> e164Argument = ArgumentCaptor.forClass(Optional.class);
|
||||
|
||||
verify(keyTransparencyServiceClient, times(expectedNumClientCalls)).search(searchKeyArguments.capture(), valueArguments.capture(), unidentifiedAccessKeyArgument.capture(), eq(Optional.of(3L)), eq(Optional.of(4L)),
|
||||
verify(keyTransparencyServiceClient).search(aciArgument.capture(), aciIdentityKeyArgument.capture(),
|
||||
usernameHashArgument.capture(), e164Argument.capture(), eq(Optional.of(3L)), eq(Optional.of(4L)),
|
||||
eq(KeyTransparencyController.KEY_TRANSPARENCY_RPC_TIMEOUT));
|
||||
|
||||
assertEquals(expectedSearchKeys, new HashSet<>(searchKeyArguments.getAllValues()));
|
||||
assertEquals(expectedValues, new HashSet<>(valueArguments.getAllValues()));
|
||||
assertEquals(expectedUnidentifiedAccessKey, unidentifiedAccessKeyArgument.getAllValues());
|
||||
assertArrayEquals(ACI.toCompactByteArray(), aciArgument.getValue().toByteArray());
|
||||
assertArrayEquals(ACI_IDENTITY_KEY.serialize(), aciIdentityKeyArgument.getValue().toByteArray());
|
||||
|
||||
if (usernameHash.isPresent()) {
|
||||
assertArrayEquals(USERNAME_HASH, usernameHashArgument.getValue().orElseThrow().toByteArray());
|
||||
} else {
|
||||
assertTrue(usernameHashArgument.getValue().isEmpty());
|
||||
}
|
||||
|
||||
if (e164.isPresent()) {
|
||||
final E164SearchRequest expected = E164SearchRequest.newBuilder()
|
||||
.setE164(e164.get())
|
||||
.setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey.get()))
|
||||
.build();
|
||||
assertEquals(expected, e164Argument.getValue().orElseThrow());
|
||||
} else {
|
||||
assertTrue(e164Argument.getValue().isEmpty());
|
||||
}
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Stream<Arguments> searchSuccess() {
|
||||
final byte[] aciBytes = ACI.toCompactByteArray();
|
||||
final ByteString aciValueByteString = ByteString.copyFrom(aciBytes);
|
||||
|
||||
final byte[] aciIdentityKeyBytes = ACI_IDENTITY_KEY.serialize();
|
||||
final ByteString aciIdentityKeyValueByteString = ByteString.copyFrom(aciIdentityKeyBytes);
|
||||
|
||||
|
||||
return Stream.of(
|
||||
// Only looking up ACI; ACI identity key should be the only value provided; no UAK
|
||||
Arguments.of(Optional.empty(), Optional.empty(), 1,
|
||||
Set.of(getFullSearchKeyByteString(ACI_PREFIX, aciBytes)),
|
||||
Set.of(aciIdentityKeyValueByteString),
|
||||
List.of(Optional.empty())),
|
||||
// Looking up ACI and username hash; ACI identity key and ACI should be the values provided; no UAK
|
||||
Arguments.of(Optional.empty(), Optional.of(USERNAME_HASH), 2,
|
||||
Set.of(getFullSearchKeyByteString(ACI_PREFIX, aciBytes),
|
||||
getFullSearchKeyByteString(USERNAME_PREFIX, USERNAME_HASH)),
|
||||
Set.of(aciIdentityKeyValueByteString, aciValueByteString),
|
||||
List.of(Optional.empty(), Optional.empty())),
|
||||
// Looking up ACI and phone number; ACI identity key and ACI should be the values provided; must provide UAK
|
||||
Arguments.of(Optional.of(NUMBER), Optional.empty(), 2,
|
||||
Set.of(getFullSearchKeyByteString(ACI_PREFIX, aciBytes),
|
||||
getFullSearchKeyByteString(E164_PREFIX, NUMBER.getBytes(StandardCharsets.UTF_8))),
|
||||
Set.of(aciValueByteString, aciIdentityKeyValueByteString),
|
||||
List.of(Optional.empty(), Optional.of(ByteString.copyFrom(UNIDENTIFIED_ACCESS_KEY))))
|
||||
Arguments.of(Optional.of(NUMBER), Optional.empty()),
|
||||
Arguments.of(Optional.empty(), Optional.of(USERNAME_HASH)),
|
||||
Arguments.of(Optional.of(NUMBER), Optional.of(USERNAME_HASH))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -226,7 +237,7 @@ public class KeyTransparencyControllerTest {
|
|||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void searchGrpcErrors(final Status grpcStatus, final int httpStatus) {
|
||||
when(keyTransparencyServiceClient.search(any(), any(), any(), any(), any(), any()))
|
||||
when(keyTransparencyServiceClient.search(any(), any(), any(), any(), any(), any(), any()))
|
||||
.thenReturn(CompletableFuture.failedFuture(new CompletionException(new StatusRuntimeException(grpcStatus))));
|
||||
|
||||
final Invocation.Builder request = resources.getJerseyTest()
|
||||
|
@ -236,7 +247,7 @@ public class KeyTransparencyControllerTest {
|
|||
Entity.json(createRequestJson(new KeyTransparencySearchRequest(ACI, Optional.empty(), Optional.empty(),
|
||||
ACI_IDENTITY_KEY, Optional.empty(), Optional.empty(), Optional.empty()))))) {
|
||||
assertEquals(httpStatus, response.getStatus());
|
||||
verify(keyTransparencyServiceClient, times(1)).search(any(), any(), any(), any(), any(), any());
|
||||
verify(keyTransparencyServiceClient, times(1)).search(any(), any(), any(), any(), any(), any(), any());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue