Define identity key check endpoint in keys anonymous service
This commit is contained in:
parent
c11b74e9c0
commit
cbc3887226
|
@ -20,4 +20,11 @@ public class IdentityTypeUtil {
|
|||
case IDENTITY_TYPE_UNSPECIFIED, UNRECOGNIZED -> throw Status.INVALID_ARGUMENT.asRuntimeException();
|
||||
};
|
||||
}
|
||||
|
||||
public static org.signal.chat.common.IdentityType toGrpcIdentityType(final IdentityType identityType) {
|
||||
return switch (identityType) {
|
||||
case ACI -> org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI;
|
||||
case PNI -> org.signal.chat.common.IdentityType.IDENTITY_TYPE_PNI;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,15 +5,24 @@
|
|||
|
||||
package org.whispersystems.textsecuregcm.grpc;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
import io.grpc.Status;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import org.signal.chat.keys.CheckIdentityKeyRequest;
|
||||
import org.signal.chat.keys.CheckIdentityKeyResponse;
|
||||
import org.signal.chat.keys.GetPreKeysAnonymousRequest;
|
||||
import org.signal.chat.keys.GetPreKeysResponse;
|
||||
import org.signal.chat.keys.ReactorKeysAnonymousGrpc;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil;
|
||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.KeysManager;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.function.Tuples;
|
||||
|
||||
public class KeysAnonymousGrpcService extends ReactorKeysAnonymousGrpc.KeysAnonymousImplBase {
|
||||
|
||||
|
@ -38,4 +47,35 @@ public class KeysAnonymousGrpcService extends ReactorKeysAnonymousGrpc.KeysAnony
|
|||
? KeysGrpcHelper.getPreKeys(targetAccount, serviceIdentifier.identityType(), request.getRequest().getDeviceId(), keysManager)
|
||||
: Mono.error(Status.UNAUTHENTICATED.asException()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<CheckIdentityKeyResponse> checkIdentityKeys(final Flux<CheckIdentityKeyRequest> requests) {
|
||||
return requests
|
||||
.map(request -> Tuples.of(ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getTargetIdentifier()),
|
||||
request.getFingerprint().toByteArray()))
|
||||
.flatMap(serviceIdentifierAndFingerprint -> Mono.fromFuture(
|
||||
() -> accountsManager.getByServiceIdentifierAsync(serviceIdentifierAndFingerprint.getT1()))
|
||||
.flatMap(Mono::justOrEmpty)
|
||||
.filter(account -> !fingerprintMatches(account.getIdentityKey(serviceIdentifierAndFingerprint.getT1()
|
||||
.identityType()), serviceIdentifierAndFingerprint.getT2()))
|
||||
.map(account -> CheckIdentityKeyResponse.newBuilder()
|
||||
.setTargetIdentifier(
|
||||
ServiceIdentifierUtil.toGrpcServiceIdentifier(serviceIdentifierAndFingerprint.getT1()))
|
||||
.setIdentityKey(ByteString.copyFrom(account.getIdentityKey(serviceIdentifierAndFingerprint.getT1()
|
||||
.identityType()).serialize()))
|
||||
.build())
|
||||
);
|
||||
}
|
||||
|
||||
private static boolean fingerprintMatches(final IdentityKey identityKey, final byte[] fingerprint) {
|
||||
final byte[] digest;
|
||||
try {
|
||||
digest = MessageDigest.getInstance("SHA-256").digest(identityKey.serialize());
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// SHA-256 should always be supported as an algorithm
|
||||
throw new AssertionError("All Java implementations must support the SHA-256 message digest");
|
||||
}
|
||||
|
||||
return Arrays.equals(digest, 0, 4, fingerprint, 0, 4);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,8 +5,10 @@
|
|||
|
||||
package org.whispersystems.textsecuregcm.grpc;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
import io.grpc.Status;
|
||||
import java.util.UUID;
|
||||
import org.signal.chat.common.IdentityType;
|
||||
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
|
@ -31,4 +33,11 @@ public class ServiceIdentifierUtil {
|
|||
case PNI -> new PniServiceIdentifier(uuid);
|
||||
};
|
||||
}
|
||||
|
||||
public static org.signal.chat.common.ServiceIdentifier toGrpcServiceIdentifier(final ServiceIdentifier serviceIdentifier) {
|
||||
return org.signal.chat.common.ServiceIdentifier.newBuilder()
|
||||
.setIdentityType(IdentityTypeUtil.toGrpcIdentityType(serviceIdentifier.identityType()))
|
||||
.setUuid(UUIDUtil.toByteString(serviceIdentifier.uuid()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,6 +104,14 @@ service KeysAnonymous {
|
|||
* devices.
|
||||
*/
|
||||
rpc GetPreKeys(GetPreKeysAnonymousRequest) returns (GetPreKeysResponse) {}
|
||||
|
||||
/**
|
||||
* Checks identity key fingerprints of the target accounts.
|
||||
*
|
||||
* Returns a stream of elements, each one representing an account that had a mismatched
|
||||
* identity key fingerprint with the server and the corresponding identity key stored by the server.
|
||||
*/
|
||||
rpc CheckIdentityKeys(stream CheckIdentityKeyRequest) returns (stream CheckIdentityKeyResponse) {}
|
||||
}
|
||||
|
||||
message GetPreKeyCountRequest {
|
||||
|
@ -247,3 +255,25 @@ message SetKemLastResortPreKeyRequest {
|
|||
message SetPreKeyResponse {
|
||||
}
|
||||
|
||||
message CheckIdentityKeyRequest {
|
||||
/**
|
||||
* The service identifier of the account for which we want to check the associated identity key fingerprint.
|
||||
*/
|
||||
common.ServiceIdentifier target_identifier = 1;
|
||||
/**
|
||||
* The most significant 4 bytes of the SHA-256 hash of the identity key associated with the target account/identity type.
|
||||
*/
|
||||
bytes fingerprint = 2;
|
||||
}
|
||||
|
||||
message CheckIdentityKeyResponse {
|
||||
/**
|
||||
* The service identifier of the account for which there is a mismatch between the client and server identity key fingerprints.
|
||||
*/
|
||||
common.ServiceIdentifier target_identifier = 1;
|
||||
/**
|
||||
* The identity key that is stored by the server for the target account/identity type.
|
||||
*/
|
||||
bytes identity_key = 2;
|
||||
}
|
||||
|
||||
|
|
|
@ -96,14 +96,6 @@ service ProfileAnonymous {
|
|||
* access key, or an `INVALID_ARGUMENT` status if the given credential type is invalid.
|
||||
*/
|
||||
rpc GetExpiringProfileKeyCredential(GetExpiringProfileKeyCredentialAnonymousRequest) returns (GetExpiringProfileKeyCredentialResponse) {}
|
||||
|
||||
/**
|
||||
* Checks identity key fingerprints of the target accounts.
|
||||
*
|
||||
* Returns a stream of elements, each one representing an account that had a mismatched
|
||||
* identity key fingerprint with the server and the corresponding identity key stored by the server.
|
||||
*/
|
||||
rpc CheckIdentityKeys(stream CheckIdentityKeysRequest) returns (stream CheckIdentityKeysResponse) {}
|
||||
}
|
||||
|
||||
message SetProfileRequest {
|
||||
|
@ -278,28 +270,6 @@ message GetExpiringProfileKeyCredentialResponse {
|
|||
bytes profileKeyCredential = 1;
|
||||
}
|
||||
|
||||
message CheckIdentityKeysRequest {
|
||||
/**
|
||||
* The service identifier of the account for which we want to check the associated identity key fingerprint.
|
||||
*/
|
||||
common.ServiceIdentifier target_identifier = 1;
|
||||
/**
|
||||
* The most significant 4 bytes of the SHA-256 hash of the identity key associated with the target account/identity type.
|
||||
*/
|
||||
bytes fingerprint = 2;
|
||||
}
|
||||
|
||||
message CheckIdentityKeysResponse {
|
||||
/**
|
||||
* The service identifier of the account for which there is a mismatch between the client and server identity key fingerprints.
|
||||
*/
|
||||
common.ServiceIdentifier target_identifier = 1;
|
||||
/**
|
||||
* The identity key that is stored by the server for the target account/identity type.
|
||||
*/
|
||||
bytes identity_key = 2;
|
||||
}
|
||||
|
||||
message ProfileAvatarUploadAttributes {
|
||||
/**
|
||||
* The S3 upload path for the profile's avatar.
|
||||
|
|
|
@ -16,8 +16,11 @@ import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertStatusEx
|
|||
import com.google.protobuf.ByteString;
|
||||
import io.grpc.Status;
|
||||
import io.grpc.StatusRuntimeException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
@ -29,11 +32,14 @@ import org.signal.chat.common.EcPreKey;
|
|||
import org.signal.chat.common.EcSignedPreKey;
|
||||
import org.signal.chat.common.KemSignedPreKey;
|
||||
import org.signal.chat.common.ServiceIdentifier;
|
||||
import org.signal.chat.keys.CheckIdentityKeyRequest;
|
||||
import org.signal.chat.keys.GetPreKeysAnonymousRequest;
|
||||
import org.signal.chat.keys.GetPreKeysRequest;
|
||||
import org.signal.chat.keys.GetPreKeysResponse;
|
||||
import org.signal.chat.keys.KeysAnonymousGrpc;
|
||||
import org.signal.chat.keys.ReactorKeysAnonymousGrpc;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.ecc.Curve;
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair;
|
||||
import org.whispersystems.textsecuregcm.entities.ECPreKey;
|
||||
|
@ -41,12 +47,15 @@ import org.whispersystems.textsecuregcm.entities.ECSignedPreKey;
|
|||
import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.identity.IdentityType;
|
||||
import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.KeysManager;
|
||||
import org.whispersystems.textsecuregcm.tests.util.KeysHelper;
|
||||
import org.whispersystems.textsecuregcm.util.UUIDUtil;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
class KeysAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<KeysAnonymousGrpcService, KeysAnonymousGrpc.KeysAnonymousBlockingStub> {
|
||||
|
||||
|
@ -56,7 +65,6 @@ class KeysAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<KeysAnonymousGrpcS
|
|||
@Mock
|
||||
private KeysManager keysManager;
|
||||
|
||||
|
||||
@Override
|
||||
protected KeysAnonymousGrpcService createServiceBeforeEachTest() {
|
||||
return new KeysAnonymousGrpcService(accountsManager, keysManager);
|
||||
|
@ -203,4 +211,81 @@ class KeysAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<KeysAnonymousGrpcS
|
|||
.build())
|
||||
.build()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkIdentityKeys() {
|
||||
final ReactorKeysAnonymousGrpc.ReactorKeysAnonymousStub reactiveKeysAnonymousStub = ReactorKeysAnonymousGrpc.newReactorStub(SimpleBaseGrpcTest.GRPC_SERVER_EXTENSION_UNAUTHENTICATED.getChannel());
|
||||
when(accountsManager.getByServiceIdentifierAsync(any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(Optional.empty()));
|
||||
|
||||
final Account mismatchedAciFingerprintAccount = mock(Account.class);
|
||||
final UUID mismatchedAciFingerprintAccountIdentifier = UUID.randomUUID();
|
||||
final IdentityKey mismatchedAciFingerprintAccountIdentityKey = new IdentityKey(Curve.generateKeyPair().getPublicKey());
|
||||
|
||||
final Account matchingAciFingerprintAccount = mock(Account.class);
|
||||
final UUID matchingAciFingerprintAccountIdentifier = UUID.randomUUID();
|
||||
final IdentityKey matchingAciFingerprintAccountIdentityKey = new IdentityKey(Curve.generateKeyPair().getPublicKey());
|
||||
|
||||
final Account mismatchedPniFingerprintAccount = mock(Account.class);
|
||||
final UUID mismatchedPniFingerprintAccountIdentifier = UUID.randomUUID();
|
||||
final IdentityKey mismatchedPniFingerpringAccountIdentityKey = new IdentityKey(Curve.generateKeyPair().getPublicKey());
|
||||
|
||||
when(mismatchedAciFingerprintAccount.getIdentityKey(IdentityType.ACI)).thenReturn(mismatchedAciFingerprintAccountIdentityKey);
|
||||
when(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(mismatchedAciFingerprintAccountIdentifier)))
|
||||
.thenReturn(CompletableFuture.completedFuture(Optional.of(mismatchedAciFingerprintAccount)));
|
||||
|
||||
when(matchingAciFingerprintAccount.getIdentityKey(IdentityType.ACI)).thenReturn(matchingAciFingerprintAccountIdentityKey);
|
||||
when(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(matchingAciFingerprintAccountIdentifier)))
|
||||
.thenReturn(CompletableFuture.completedFuture(Optional.of(matchingAciFingerprintAccount)));
|
||||
|
||||
when(mismatchedPniFingerprintAccount.getIdentityKey(IdentityType.PNI)).thenReturn(mismatchedPniFingerpringAccountIdentityKey);
|
||||
when(accountsManager.getByServiceIdentifierAsync(new PniServiceIdentifier(mismatchedPniFingerprintAccountIdentifier)))
|
||||
.thenReturn(CompletableFuture.completedFuture(Optional.of(mismatchedPniFingerprintAccount)));
|
||||
|
||||
final Flux<CheckIdentityKeyRequest> requests = Flux.just(
|
||||
buildCheckIdentityKeyRequest(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI, mismatchedAciFingerprintAccountIdentifier,
|
||||
new IdentityKey(Curve.generateKeyPair().getPublicKey())),
|
||||
buildCheckIdentityKeyRequest(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI, matchingAciFingerprintAccountIdentifier,
|
||||
matchingAciFingerprintAccountIdentityKey),
|
||||
buildCheckIdentityKeyRequest(org.signal.chat.common.IdentityType.IDENTITY_TYPE_PNI, UUID.randomUUID(),
|
||||
new IdentityKey(Curve.generateKeyPair().getPublicKey())),
|
||||
buildCheckIdentityKeyRequest(org.signal.chat.common.IdentityType.IDENTITY_TYPE_PNI, mismatchedPniFingerprintAccountIdentifier,
|
||||
new IdentityKey(Curve.generateKeyPair().getPublicKey()))
|
||||
);
|
||||
|
||||
final Map<UUID, IdentityKey> expectedResponses = Map.of(
|
||||
mismatchedAciFingerprintAccountIdentifier, mismatchedAciFingerprintAccountIdentityKey,
|
||||
mismatchedPniFingerprintAccountIdentifier, mismatchedPniFingerpringAccountIdentityKey);
|
||||
|
||||
final Map<UUID, IdentityKey> responses = reactiveKeysAnonymousStub.checkIdentityKeys(requests)
|
||||
.collectMap(response -> ServiceIdentifierUtil.fromGrpcServiceIdentifier(response.getTargetIdentifier()).uuid(),
|
||||
response -> {
|
||||
try {
|
||||
return new IdentityKey(response.getIdentityKey().toByteArray());
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
})
|
||||
.block();
|
||||
|
||||
assertEquals(expectedResponses, responses);
|
||||
}
|
||||
|
||||
private static CheckIdentityKeyRequest buildCheckIdentityKeyRequest(final org.signal.chat.common.IdentityType identityType,
|
||||
final UUID uuid, final IdentityKey identityKey) {
|
||||
return CheckIdentityKeyRequest.newBuilder()
|
||||
.setTargetIdentifier(ServiceIdentifier.newBuilder()
|
||||
.setIdentityType(identityType)
|
||||
.setUuid(ByteString.copyFrom(UUIDUtil.toBytes(uuid))))
|
||||
.setFingerprint(ByteString.copyFrom(getFingerprint(identityKey)))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static byte[] getFingerprint(final IdentityKey publicKey) {
|
||||
try {
|
||||
return Util.truncate(MessageDigest.getInstance("SHA-256").digest(publicKey.serialize()), 4);
|
||||
} catch (final NoSuchAlgorithmException e) {
|
||||
throw new AssertionError("All Java implementations must support SHA-256 MessageDigest algorithm", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue