Always check prekey signatures when new prekeys are uploaded
This commit is contained in:
parent
bc68b67cdf
commit
e38911b2c5
|
@ -61,6 +61,7 @@ public class KeysController {
|
||||||
private final Keys keys;
|
private final Keys keys;
|
||||||
private final AccountsManager accounts;
|
private final AccountsManager accounts;
|
||||||
|
|
||||||
|
private static final String IDENTITY_KEY_CHANGE_COUNTER_NAME = name(KeysController.class, "identityKeyChange");
|
||||||
private static final String IDENTITY_KEY_CHANGE_FORBIDDEN_COUNTER_NAME = name(KeysController.class, "identityKeyChangeForbidden");
|
private static final String IDENTITY_KEY_CHANGE_FORBIDDEN_COUNTER_NAME = name(KeysController.class, "identityKeyChangeForbidden");
|
||||||
|
|
||||||
private static final String IDENTITY_TYPE_TAG_NAME = "identityType";
|
private static final String IDENTITY_TYPE_TAG_NAME = "identityType";
|
||||||
|
@ -85,6 +86,7 @@ public class KeysController {
|
||||||
@Timed
|
@Timed
|
||||||
@PUT
|
@PUT
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@ChangesDeviceEnabledState
|
@ChangesDeviceEnabledState
|
||||||
public void setKeys(@Auth final DisabledPermittedAuthenticatedAccount disabledPermittedAuth,
|
public void setKeys(@Auth final DisabledPermittedAuthenticatedAccount disabledPermittedAuth,
|
||||||
@NotNull @Valid final PreKeyState preKeys,
|
@NotNull @Valid final PreKeyState preKeys,
|
||||||
|
@ -100,21 +102,21 @@ public class KeysController {
|
||||||
updateAccount = true;
|
updateAccount = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!preKeys.getIdentityKey().equals(usePhoneNumberIdentity ? account.getPhoneNumberIdentityKey() : account.getIdentityKey())) {
|
final String oldIdentityKey = usePhoneNumberIdentity ? account.getPhoneNumberIdentityKey() : account.getIdentityKey();
|
||||||
|
if (!preKeys.getIdentityKey().equals(oldIdentityKey)) {
|
||||||
updateAccount = true;
|
updateAccount = true;
|
||||||
|
|
||||||
|
final boolean hasIdentityKey = StringUtils.isNotBlank(oldIdentityKey);
|
||||||
|
final Tags tags = Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))
|
||||||
|
.and(HAS_IDENTITY_KEY_TAG_NAME, String.valueOf(hasIdentityKey))
|
||||||
|
.and(IDENTITY_TYPE_TAG_NAME, usePhoneNumberIdentity ? "pni" : "aci");
|
||||||
|
|
||||||
if (!device.isMaster()) {
|
if (!device.isMaster()) {
|
||||||
final boolean hasIdentityKey = usePhoneNumberIdentity ?
|
|
||||||
StringUtils.isNotBlank(account.getPhoneNumberIdentityKey()) :
|
|
||||||
StringUtils.isNotBlank(account.getIdentityKey());
|
|
||||||
|
|
||||||
final Tags tags = Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))
|
|
||||||
.and(HAS_IDENTITY_KEY_TAG_NAME, String.valueOf(hasIdentityKey))
|
|
||||||
.and(IDENTITY_TYPE_TAG_NAME, usePhoneNumberIdentity ? "pni" : "aci");
|
|
||||||
|
|
||||||
Metrics.counter(IDENTITY_KEY_CHANGE_FORBIDDEN_COUNTER_NAME, tags).increment();
|
Metrics.counter(IDENTITY_KEY_CHANGE_FORBIDDEN_COUNTER_NAME, tags).increment();
|
||||||
|
|
||||||
throw new ForbiddenException();
|
throw new ForbiddenException();
|
||||||
}
|
}
|
||||||
|
Metrics.counter(IDENTITY_KEY_CHANGE_COUNTER_NAME, tags).increment();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updateAccount) {
|
if (updateAccount) {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
|
import javax.validation.constraints.AssertTrue;
|
||||||
import javax.validation.constraints.NotBlank;
|
import javax.validation.constraints.NotBlank;
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
|
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
|
||||||
|
@ -24,4 +25,12 @@ public record ChangeNumberRequest(String sessionId,
|
||||||
@NotNull @Valid Map<Long, @NotNull @Valid SignedPreKey> devicePniSignedPrekeys,
|
@NotNull @Valid Map<Long, @NotNull @Valid SignedPreKey> devicePniSignedPrekeys,
|
||||||
@NotNull Map<Long, Integer> pniRegistrationIds) implements PhoneVerificationRequest {
|
@NotNull Map<Long, Integer> pniRegistrationIds) implements PhoneVerificationRequest {
|
||||||
|
|
||||||
|
@AssertTrue
|
||||||
|
public boolean isSignatureValidOnEachSignedPreKey() {
|
||||||
|
if (devicePniSignedPrekeys == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return devicePniSignedPrekeys.values().parallelStream()
|
||||||
|
.allMatch(spk -> PreKeySignatureValidator.validatePreKeySignature(pniIdentityKey, spk));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ package org.whispersystems.textsecuregcm.entities;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import javax.validation.constraints.AssertTrue;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import javax.validation.constraints.NotBlank;
|
import javax.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
@ -18,4 +19,14 @@ public record ChangePhoneNumberRequest(@NotBlank String number,
|
||||||
@Nullable List<IncomingMessage> deviceMessages,
|
@Nullable List<IncomingMessage> deviceMessages,
|
||||||
@Nullable Map<Long, SignedPreKey> devicePniSignedPrekeys,
|
@Nullable Map<Long, SignedPreKey> devicePniSignedPrekeys,
|
||||||
@Nullable Map<Long, Integer> pniRegistrationIds) {
|
@Nullable Map<Long, Integer> pniRegistrationIds) {
|
||||||
|
|
||||||
|
@AssertTrue
|
||||||
|
public boolean isSignatureValidOnEachSignedPreKey() {
|
||||||
|
if (devicePniSignedPrekeys == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return devicePniSignedPrekeys.values().parallelStream()
|
||||||
|
.allMatch(spk -> PreKeySignatureValidator.validatePreKeySignature(pniIdentityKey, spk));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
|
import javax.validation.constraints.AssertTrue;
|
||||||
import javax.validation.constraints.NotBlank;
|
import javax.validation.constraints.NotBlank;
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
|
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
|
||||||
|
@ -34,4 +35,11 @@ public record PhoneNumberIdentityKeyDistributionRequest(
|
||||||
@Valid
|
@Valid
|
||||||
@Schema(description="The new registration ID to use for the phone-number identity of each device")
|
@Schema(description="The new registration ID to use for the phone-number identity of each device")
|
||||||
Map<Long, Integer> pniRegistrationIds) {
|
Map<Long, Integer> pniRegistrationIds) {
|
||||||
|
|
||||||
|
@AssertTrue
|
||||||
|
public boolean isSignatureValidOnEachSignedPreKey() {
|
||||||
|
return devicePniSignedPrekeys.values().parallelStream()
|
||||||
|
.allMatch(spk -> PreKeySignatureValidator.validatePreKeySignature(pniIdentityKey, spk));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2013-2020 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
import static com.codahale.metrics.MetricRegistry.name;
|
||||||
|
import io.micrometer.core.instrument.Metrics;
|
||||||
|
import java.util.Base64;
|
||||||
|
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||||
|
import org.signal.libsignal.protocol.ecc.Curve;
|
||||||
|
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||||
|
|
||||||
|
public abstract class PreKeySignatureValidator {
|
||||||
|
public static final boolean validatePreKeySignature(final String identityKeyB64, final SignedPreKey spk) {
|
||||||
|
try {
|
||||||
|
final byte[] identityKeyBytes = Base64.getDecoder().decode(identityKeyB64);
|
||||||
|
final byte[] prekeyBytes = Base64.getDecoder().decode(spk.getPublicKey());
|
||||||
|
final byte[] prekeySignatureBytes = Base64.getDecoder().decode(spk.getSignature());
|
||||||
|
|
||||||
|
final ECPublicKey identityKey = Curve.decodePoint(identityKeyBytes, 0);
|
||||||
|
|
||||||
|
return identityKey.verifySignature(prekeyBytes, prekeySignatureBytes);
|
||||||
|
} catch (IllegalArgumentException | InvalidKeyException e) {
|
||||||
|
Metrics.counter(name(PreKeySignatureValidator.class, "invalidPreKeySignature")).increment();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
|
import javax.validation.constraints.AssertTrue;
|
||||||
import javax.validation.constraints.NotEmpty;
|
import javax.validation.constraints.NotEmpty;
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
@ -48,4 +49,8 @@ public class PreKeyState {
|
||||||
return identityKey;
|
return identityKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AssertTrue
|
||||||
|
public boolean isSignatureValid() {
|
||||||
|
return PreKeySignatureValidator.validatePreKeySignature(identityKey, signedPreKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,8 @@ import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import javax.ws.rs.client.Entity;
|
import javax.ws.rs.client.Entity;
|
||||||
|
@ -65,6 +67,8 @@ import org.junit.jupiter.params.provider.CsvSource;
|
||||||
import org.junit.jupiter.params.provider.MethodSource;
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.mockito.stubbing.Answer;
|
import org.mockito.stubbing.Answer;
|
||||||
|
import org.signal.libsignal.protocol.ecc.Curve;
|
||||||
|
import org.signal.libsignal.protocol.ecc.ECKeyPair;
|
||||||
import org.signal.libsignal.usernames.BaseUsernameException;
|
import org.signal.libsignal.usernames.BaseUsernameException;
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
|
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
|
||||||
|
@ -122,6 +126,7 @@ import org.whispersystems.textsecuregcm.storage.UsernameHashNotAvailableExceptio
|
||||||
import org.whispersystems.textsecuregcm.storage.UsernameReservationNotFoundException;
|
import org.whispersystems.textsecuregcm.storage.UsernameReservationNotFoundException;
|
||||||
import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
|
import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
|
||||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||||
|
import org.whispersystems.textsecuregcm.tests.util.KeysHelper;
|
||||||
import org.whispersystems.textsecuregcm.util.MockUtils;
|
import org.whispersystems.textsecuregcm.util.MockUtils;
|
||||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||||
import org.whispersystems.textsecuregcm.util.TestClock;
|
import org.whispersystems.textsecuregcm.util.TestClock;
|
||||||
|
@ -1622,7 +1627,8 @@ class AccountControllerTest {
|
||||||
void testChangePhoneNumberChangePrekeys() throws Exception {
|
void testChangePhoneNumberChangePrekeys() throws Exception {
|
||||||
final String number = "+18005559876";
|
final String number = "+18005559876";
|
||||||
final String code = "987654";
|
final String code = "987654";
|
||||||
final String pniIdentityKey = "changed-pni-identity-key";
|
final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair();
|
||||||
|
final String pniIdentityKey = KeysHelper.serializeIdentityKey(pniIdentityKeyPair);
|
||||||
final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8);
|
final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
Device device2 = mock(Device.class);
|
Device device2 = mock(Device.class);
|
||||||
|
@ -1648,7 +1654,7 @@ class AccountControllerTest {
|
||||||
var deviceMessages = List.of(
|
var deviceMessages = List.of(
|
||||||
new IncomingMessage(1, 2, 2, "content2"),
|
new IncomingMessage(1, 2, 2, "content2"),
|
||||||
new IncomingMessage(1, 3, 3, "content3"));
|
new IncomingMessage(1, 3, 3, "content3"));
|
||||||
var deviceKeys = Map.of(1L, new SignedPreKey(), 2L, new SignedPreKey(), 3L, new SignedPreKey());
|
var deviceKeys = List.of(1L, 2L, 3L).stream().collect(Collectors.toMap(Function.identity(), n -> KeysHelper.signedPreKey(n + 100, pniIdentityKeyPair)));
|
||||||
|
|
||||||
final Map<Long, Integer> registrationIds = Map.of(1L, 17, 2L, 47, 3L, 89);
|
final Map<Long, Integer> registrationIds = Map.of(1L, 17, 2L, 47, 3L, 89);
|
||||||
|
|
||||||
|
@ -1674,7 +1680,8 @@ class AccountControllerTest {
|
||||||
@Test
|
@Test
|
||||||
void testChangePhoneNumberSameNumberChangePrekeys() throws Exception {
|
void testChangePhoneNumberSameNumberChangePrekeys() throws Exception {
|
||||||
final String code = "987654";
|
final String code = "987654";
|
||||||
final String pniIdentityKey = "changed-pni-identity-key";
|
final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair();
|
||||||
|
final String pniIdentityKey = KeysHelper.serializeIdentityKey(pniIdentityKeyPair);
|
||||||
final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8);
|
final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
Device device2 = mock(Device.class);
|
Device device2 = mock(Device.class);
|
||||||
|
@ -1700,7 +1707,7 @@ class AccountControllerTest {
|
||||||
var deviceMessages = List.of(
|
var deviceMessages = List.of(
|
||||||
new IncomingMessage(1, 2, 2, "content2"),
|
new IncomingMessage(1, 2, 2, "content2"),
|
||||||
new IncomingMessage(1, 3, 3, "content3"));
|
new IncomingMessage(1, 3, 3, "content3"));
|
||||||
var deviceKeys = Map.of(1L, new SignedPreKey(), 2L, new SignedPreKey(), 3L, new SignedPreKey());
|
var deviceKeys = List.of(1L, 2L, 3L).stream().collect(Collectors.toMap(Function.identity(), n -> KeysHelper.signedPreKey(n + 100, pniIdentityKeyPair)));
|
||||||
|
|
||||||
final Map<Long, Integer> registrationIds = Map.of(1L, 17, 2L, 47, 3L, 89);
|
final Map<Long, Integer> registrationIds = Map.of(1L, 17, 2L, 47, 3L, 89);
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,8 @@ import java.util.concurrent.CompletableFuture;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
|
import org.signal.libsignal.protocol.ecc.Curve;
|
||||||
|
import org.signal.libsignal.protocol.ecc.ECKeyPair;
|
||||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException;
|
import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException;
|
||||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||||
|
@ -34,6 +36,7 @@ import org.whispersystems.textsecuregcm.securebackup.SecureBackupClient;
|
||||||
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
|
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
|
||||||
import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client;
|
import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client;
|
||||||
import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema.Tables;
|
import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema.Tables;
|
||||||
|
import org.whispersystems.textsecuregcm.tests.util.KeysHelper;
|
||||||
|
|
||||||
class AccountsManagerChangeNumberIntegrationTest {
|
class AccountsManagerChangeNumberIntegrationTest {
|
||||||
|
|
||||||
|
@ -144,7 +147,8 @@ class AccountsManagerChangeNumberIntegrationTest {
|
||||||
final String originalNumber = "+18005551111";
|
final String originalNumber = "+18005551111";
|
||||||
final String secondNumber = "+18005552222";
|
final String secondNumber = "+18005552222";
|
||||||
final int rotatedPniRegistrationId = 17;
|
final int rotatedPniRegistrationId = 17;
|
||||||
final SignedPreKey rotatedSignedPreKey = new SignedPreKey(1, "test", "test");
|
final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair();
|
||||||
|
final SignedPreKey rotatedSignedPreKey = KeysHelper.signedPreKey(1L, pniIdentityKeyPair);
|
||||||
|
|
||||||
final AccountAttributes accountAttributes = new AccountAttributes(true, rotatedPniRegistrationId + 1, "test", null, true, new Device.DeviceCapabilities());
|
final AccountAttributes accountAttributes = new AccountAttributes(true, rotatedPniRegistrationId + 1, "test", null, true, new Device.DeviceCapabilities());
|
||||||
final Account account = accountsManager.create(originalNumber, "password", null, accountAttributes, new ArrayList<>());
|
final Account account = accountsManager.create(originalNumber, "password", null, accountAttributes, new ArrayList<>());
|
||||||
|
@ -153,7 +157,7 @@ class AccountsManagerChangeNumberIntegrationTest {
|
||||||
final UUID originalUuid = account.getUuid();
|
final UUID originalUuid = account.getUuid();
|
||||||
final UUID originalPni = account.getPhoneNumberIdentifier();
|
final UUID originalPni = account.getPhoneNumberIdentifier();
|
||||||
|
|
||||||
final String pniIdentityKey = "changed-pni-identity-key";
|
final String pniIdentityKey = KeysHelper.serializeIdentityKey(pniIdentityKeyPair);
|
||||||
final Map<Long, SignedPreKey> preKeys = Map.of(Device.MASTER_ID, rotatedSignedPreKey);
|
final Map<Long, SignedPreKey> preKeys = Map.of(Device.MASTER_ID, rotatedSignedPreKey);
|
||||||
final Map<Long, Integer> registrationIds = Map.of(Device.MASTER_ID, rotatedPniRegistrationId);
|
final Map<Long, Integer> registrationIds = Map.of(Device.MASTER_ID, rotatedPniRegistrationId);
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
|
||||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.util.Base64;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -39,6 +40,8 @@ import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.signal.libsignal.protocol.ecc.Curve;
|
||||||
|
import org.signal.libsignal.protocol.ecc.ECKeyPair;
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
|
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
|
||||||
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
|
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
|
||||||
|
@ -59,6 +62,7 @@ import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
import org.whispersystems.textsecuregcm.storage.Keys;
|
import org.whispersystems.textsecuregcm.storage.Keys;
|
||||||
import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
|
import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
|
||||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||||
|
import org.whispersystems.textsecuregcm.tests.util.KeysHelper;
|
||||||
|
|
||||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||||
class KeysControllerTest {
|
class KeysControllerTest {
|
||||||
|
@ -76,6 +80,12 @@ class KeysControllerTest {
|
||||||
|
|
||||||
private static final int SAMPLE_PNI_REGISTRATION_ID = 1717;
|
private static final int SAMPLE_PNI_REGISTRATION_ID = 1717;
|
||||||
|
|
||||||
|
private final ECKeyPair IDENTITY_KEY_PAIR = Curve.generateKeyPair();
|
||||||
|
private final String IDENTITY_KEY = KeysHelper.serializeIdentityKey(IDENTITY_KEY_PAIR);
|
||||||
|
|
||||||
|
private final ECKeyPair PNI_IDENTITY_KEY_PAIR = Curve.generateKeyPair();
|
||||||
|
private final String PNI_IDENTITY_KEY = KeysHelper.serializeIdentityKey(PNI_IDENTITY_KEY_PAIR);
|
||||||
|
|
||||||
private final PreKey SAMPLE_KEY = new PreKey(1234, "test1");
|
private final PreKey SAMPLE_KEY = new PreKey(1234, "test1");
|
||||||
private final PreKey SAMPLE_KEY2 = new PreKey(5667, "test3");
|
private final PreKey SAMPLE_KEY2 = new PreKey(5667, "test3");
|
||||||
private final PreKey SAMPLE_KEY3 = new PreKey(334, "test5");
|
private final PreKey SAMPLE_KEY3 = new PreKey(334, "test5");
|
||||||
|
@ -83,14 +93,14 @@ class KeysControllerTest {
|
||||||
|
|
||||||
private final PreKey SAMPLE_KEY_PNI = new PreKey(7777, "test7");
|
private final PreKey SAMPLE_KEY_PNI = new PreKey(7777, "test7");
|
||||||
|
|
||||||
private final SignedPreKey SAMPLE_SIGNED_KEY = new SignedPreKey( 1111, "foofoo", "sig11" );
|
private final SignedPreKey SAMPLE_SIGNED_KEY = KeysHelper.signedPreKey( 1111, IDENTITY_KEY_PAIR);
|
||||||
private final SignedPreKey SAMPLE_SIGNED_KEY2 = new SignedPreKey( 2222, "foobar", "sig22" );
|
private final SignedPreKey SAMPLE_SIGNED_KEY2 = KeysHelper.signedPreKey( 2222, IDENTITY_KEY_PAIR);
|
||||||
private final SignedPreKey SAMPLE_SIGNED_KEY3 = new SignedPreKey( 3333, "barfoo", "sig33" );
|
private final SignedPreKey SAMPLE_SIGNED_KEY3 = KeysHelper.signedPreKey( 3333, IDENTITY_KEY_PAIR);
|
||||||
private final SignedPreKey SAMPLE_SIGNED_PNI_KEY = new SignedPreKey( 4444, "foofoopni", "sig44" );
|
private final SignedPreKey SAMPLE_SIGNED_PNI_KEY = KeysHelper.signedPreKey( 4444, PNI_IDENTITY_KEY_PAIR);
|
||||||
private final SignedPreKey SAMPLE_SIGNED_PNI_KEY2 = new SignedPreKey( 5555, "foobarpni", "sig55" );
|
private final SignedPreKey SAMPLE_SIGNED_PNI_KEY2 = KeysHelper.signedPreKey( 5555, PNI_IDENTITY_KEY_PAIR);
|
||||||
private final SignedPreKey SAMPLE_SIGNED_PNI_KEY3 = new SignedPreKey( 6666, "barfoopni", "sig66" );
|
private final SignedPreKey SAMPLE_SIGNED_PNI_KEY3 = KeysHelper.signedPreKey( 6666, PNI_IDENTITY_KEY_PAIR);
|
||||||
private final SignedPreKey VALID_DEVICE_SIGNED_KEY = new SignedPreKey(89898, "zoofarb", "sigvalid");
|
private final SignedPreKey VALID_DEVICE_SIGNED_KEY = KeysHelper.signedPreKey(89898, IDENTITY_KEY_PAIR);
|
||||||
private final SignedPreKey VALID_DEVICE_PNI_SIGNED_KEY = new SignedPreKey(7777, "zoofarber", "sigvalidest");
|
private final SignedPreKey VALID_DEVICE_PNI_SIGNED_KEY = KeysHelper.signedPreKey(7777, PNI_IDENTITY_KEY_PAIR);
|
||||||
|
|
||||||
private final static Keys KEYS = mock(Keys.class );
|
private final static Keys KEYS = mock(Keys.class );
|
||||||
private final static AccountsManager accounts = mock(AccountsManager.class );
|
private final static AccountsManager accounts = mock(AccountsManager.class );
|
||||||
|
@ -153,8 +163,8 @@ class KeysControllerTest {
|
||||||
when(existsAccount.getDevice(22L)).thenReturn(Optional.empty());
|
when(existsAccount.getDevice(22L)).thenReturn(Optional.empty());
|
||||||
when(existsAccount.getDevices()).thenReturn(allDevices);
|
when(existsAccount.getDevices()).thenReturn(allDevices);
|
||||||
when(existsAccount.isEnabled()).thenReturn(true);
|
when(existsAccount.isEnabled()).thenReturn(true);
|
||||||
when(existsAccount.getIdentityKey()).thenReturn("existsidentitykey");
|
when(existsAccount.getIdentityKey()).thenReturn(IDENTITY_KEY);
|
||||||
when(existsAccount.getPhoneNumberIdentityKey()).thenReturn("existspniidentitykey");
|
when(existsAccount.getPhoneNumberIdentityKey()).thenReturn(PNI_IDENTITY_KEY);
|
||||||
when(existsAccount.getNumber()).thenReturn(EXISTS_NUMBER);
|
when(existsAccount.getNumber()).thenReturn(EXISTS_NUMBER);
|
||||||
when(existsAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of("1337".getBytes()));
|
when(existsAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of("1337".getBytes()));
|
||||||
|
|
||||||
|
@ -234,7 +244,7 @@ class KeysControllerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void putSignedPreKeyV2() {
|
void putSignedPreKeyV2() {
|
||||||
SignedPreKey test = new SignedPreKey(9998, "fooozzz", "baaarzzz");
|
SignedPreKey test = KeysHelper.signedPreKey(9998, IDENTITY_KEY_PAIR);
|
||||||
Response response = resources.getJerseyTest()
|
Response response = resources.getJerseyTest()
|
||||||
.target("/v2/keys/signed")
|
.target("/v2/keys/signed")
|
||||||
.request()
|
.request()
|
||||||
|
@ -250,7 +260,7 @@ class KeysControllerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void putPhoneNumberIdentitySignedPreKeyV2() {
|
void putPhoneNumberIdentitySignedPreKeyV2() {
|
||||||
final SignedPreKey replacementKey = new SignedPreKey(9998, "fooozzz", "baaarzzz");
|
final SignedPreKey replacementKey = KeysHelper.signedPreKey(9998, PNI_IDENTITY_KEY_PAIR);
|
||||||
|
|
||||||
Response response = resources.getJerseyTest()
|
Response response = resources.getJerseyTest()
|
||||||
.target("/v2/keys/signed")
|
.target("/v2/keys/signed")
|
||||||
|
@ -268,7 +278,7 @@ class KeysControllerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void disabledPutSignedPreKeyV2() {
|
void disabledPutSignedPreKeyV2() {
|
||||||
SignedPreKey test = new SignedPreKey(9999, "fooozzz", "baaarzzz");
|
SignedPreKey test = KeysHelper.signedPreKey(9999, IDENTITY_KEY_PAIR);
|
||||||
Response response = resources.getJerseyTest()
|
Response response = resources.getJerseyTest()
|
||||||
.target("/v2/keys/signed")
|
.target("/v2/keys/signed")
|
||||||
.request()
|
.request()
|
||||||
|
@ -514,8 +524,9 @@ class KeysControllerTest {
|
||||||
@Test
|
@Test
|
||||||
void putKeysTestV2() {
|
void putKeysTestV2() {
|
||||||
final PreKey preKey = new PreKey(31337, "foobar");
|
final PreKey preKey = new PreKey(31337, "foobar");
|
||||||
final SignedPreKey signedPreKey = new SignedPreKey(31338, "foobaz", "myvalidsig");
|
final ECKeyPair identityKeyPair = Curve.generateKeyPair();
|
||||||
final String identityKey = "barbar";
|
final SignedPreKey signedPreKey = KeysHelper.signedPreKey(31338, identityKeyPair);
|
||||||
|
final String identityKey = KeysHelper.serializeIdentityKey(identityKeyPair);
|
||||||
|
|
||||||
List<PreKey> preKeys = new LinkedList<PreKey>() {{
|
List<PreKey> preKeys = new LinkedList<PreKey>() {{
|
||||||
add(preKey);
|
add(preKey);
|
||||||
|
@ -540,15 +551,16 @@ class KeysControllerTest {
|
||||||
assertThat(capturedList.get(0).getKeyId()).isEqualTo(31337);
|
assertThat(capturedList.get(0).getKeyId()).isEqualTo(31337);
|
||||||
assertThat(capturedList.get(0).getPublicKey()).isEqualTo("foobar");
|
assertThat(capturedList.get(0).getPublicKey()).isEqualTo("foobar");
|
||||||
|
|
||||||
verify(AuthHelper.VALID_ACCOUNT).setIdentityKey(eq("barbar"));
|
verify(AuthHelper.VALID_ACCOUNT).setIdentityKey(eq(identityKey));
|
||||||
verify(AuthHelper.VALID_DEVICE).setSignedPreKey(eq(signedPreKey));
|
verify(AuthHelper.VALID_DEVICE).setSignedPreKey(eq(signedPreKey));
|
||||||
verify(accounts).update(eq(AuthHelper.VALID_ACCOUNT), any());
|
verify(accounts).update(eq(AuthHelper.VALID_ACCOUNT), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void putKeysByPhoneNumberIdentifierTestV2() {
|
void putKeysByPhoneNumberIdentifierTestV2() {
|
||||||
final SignedPreKey signedPreKey = new SignedPreKey(31338, "foobaz", "myvalidsig");
|
final ECKeyPair identityKeyPair = Curve.generateKeyPair();
|
||||||
final String identityKey = "barbar";
|
final SignedPreKey signedPreKey = KeysHelper.signedPreKey(31338, identityKeyPair);
|
||||||
|
final String identityKey = KeysHelper.serializeIdentityKey(identityKeyPair);
|
||||||
|
|
||||||
List<PreKey> preKeys = List.of(new PreKey(31337, "foobar"));
|
List<PreKey> preKeys = List.of(new PreKey(31337, "foobar"));
|
||||||
|
|
||||||
|
@ -572,16 +584,32 @@ class KeysControllerTest {
|
||||||
assertThat(capturedList.get(0).getKeyId()).isEqualTo(31337);
|
assertThat(capturedList.get(0).getKeyId()).isEqualTo(31337);
|
||||||
assertThat(capturedList.get(0).getPublicKey()).isEqualTo("foobar");
|
assertThat(capturedList.get(0).getPublicKey()).isEqualTo("foobar");
|
||||||
|
|
||||||
verify(AuthHelper.VALID_ACCOUNT).setPhoneNumberIdentityKey(eq("barbar"));
|
verify(AuthHelper.VALID_ACCOUNT).setPhoneNumberIdentityKey(eq(identityKey));
|
||||||
verify(AuthHelper.VALID_DEVICE).setPhoneNumberIdentitySignedPreKey(eq(signedPreKey));
|
verify(AuthHelper.VALID_DEVICE).setPhoneNumberIdentitySignedPreKey(eq(signedPreKey));
|
||||||
verify(accounts).update(eq(AuthHelper.VALID_ACCOUNT), any());
|
verify(accounts).update(eq(AuthHelper.VALID_ACCOUNT), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void putPrekeyWithInvalidSignature() {
|
||||||
|
final SignedPreKey badSignedPreKey = new SignedPreKey(1L, "foo", "bar");
|
||||||
|
PreKeyState preKeyState = new PreKeyState(IDENTITY_KEY, badSignedPreKey, List.of());
|
||||||
|
Response response =
|
||||||
|
resources.getJerseyTest()
|
||||||
|
.target("/v2/keys")
|
||||||
|
.queryParam("identity", "aci")
|
||||||
|
.request()
|
||||||
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||||
|
.put(Entity.entity(preKeyState, MediaType.APPLICATION_JSON_TYPE));
|
||||||
|
|
||||||
|
assertThat(response.getStatus()).isEqualTo(422);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void disabledPutKeysTestV2() {
|
void disabledPutKeysTestV2() {
|
||||||
final PreKey preKey = new PreKey(31337, "foobar");
|
final PreKey preKey = new PreKey(31337, "foobar");
|
||||||
final SignedPreKey signedPreKey = new SignedPreKey(31338, "foobaz", "myvalidsig");
|
final ECKeyPair identityKeyPair = Curve.generateKeyPair();
|
||||||
final String identityKey = "barbar";
|
final SignedPreKey signedPreKey = KeysHelper.signedPreKey(31338, identityKeyPair);
|
||||||
|
final String identityKey = KeysHelper.serializeIdentityKey(identityKeyPair);
|
||||||
|
|
||||||
List<PreKey> preKeys = new LinkedList<PreKey>() {{
|
List<PreKey> preKeys = new LinkedList<PreKey>() {{
|
||||||
add(preKey);
|
add(preKey);
|
||||||
|
@ -606,7 +634,7 @@ class KeysControllerTest {
|
||||||
assertThat(capturedList.get(0).getKeyId()).isEqualTo(31337);
|
assertThat(capturedList.get(0).getKeyId()).isEqualTo(31337);
|
||||||
assertThat(capturedList.get(0).getPublicKey()).isEqualTo("foobar");
|
assertThat(capturedList.get(0).getPublicKey()).isEqualTo("foobar");
|
||||||
|
|
||||||
verify(AuthHelper.DISABLED_ACCOUNT).setIdentityKey(eq("barbar"));
|
verify(AuthHelper.DISABLED_ACCOUNT).setIdentityKey(eq(identityKey));
|
||||||
verify(AuthHelper.DISABLED_DEVICE).setSignedPreKey(eq(signedPreKey));
|
verify(AuthHelper.DISABLED_DEVICE).setSignedPreKey(eq(signedPreKey));
|
||||||
verify(accounts).update(eq(AuthHelper.DISABLED_ACCOUNT), any());
|
verify(accounts).update(eq(AuthHelper.DISABLED_ACCOUNT), any());
|
||||||
}
|
}
|
||||||
|
@ -614,12 +642,11 @@ class KeysControllerTest {
|
||||||
@Test
|
@Test
|
||||||
void putIdentityKeyNonPrimary() {
|
void putIdentityKeyNonPrimary() {
|
||||||
final PreKey preKey = new PreKey(31337, "foobar");
|
final PreKey preKey = new PreKey(31337, "foobar");
|
||||||
final SignedPreKey signedPreKey = new SignedPreKey(31338, "foobaz", "myvalidsig");
|
final SignedPreKey signedPreKey = KeysHelper.signedPreKey(31338, IDENTITY_KEY_PAIR);
|
||||||
final String identityKey = "barbar";
|
|
||||||
|
|
||||||
List<PreKey> preKeys = List.of(preKey);
|
List<PreKey> preKeys = List.of(preKey);
|
||||||
|
|
||||||
PreKeyState preKeyState = new PreKeyState(identityKey, signedPreKey, preKeys);
|
PreKeyState preKeyState = new PreKeyState(IDENTITY_KEY, signedPreKey, preKeys);
|
||||||
|
|
||||||
Response response =
|
Response response =
|
||||||
resources.getJerseyTest()
|
resources.getJerseyTest()
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2013 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.tests.util;
|
||||||
|
|
||||||
|
import java.util.Base64;
|
||||||
|
import org.signal.libsignal.protocol.ecc.Curve;
|
||||||
|
import org.signal.libsignal.protocol.ecc.ECKeyPair;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
||||||
|
|
||||||
|
public final class KeysHelper {
|
||||||
|
public static String serializeIdentityKey(ECKeyPair keyPair) {
|
||||||
|
return Base64.getEncoder().encodeToString(keyPair.getPublicKey().serialize());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SignedPreKey signedPreKey(long id, final ECKeyPair signingKey) {
|
||||||
|
final byte[] pubKey = Curve.generateKeyPair().getPublicKey().serialize();
|
||||||
|
final byte[] sig = signingKey.getPrivateKey().calculateSignature(pubKey);
|
||||||
|
return new SignedPreKey(id, Base64.getEncoder().encodeToString(pubKey), Base64.getEncoder().encodeToString(sig));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue