diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java index 1ef94f677..522395973 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java @@ -61,6 +61,7 @@ public class KeysController { private final Keys keys; 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_TYPE_TAG_NAME = "identityType"; @@ -85,6 +86,7 @@ public class KeysController { @Timed @PUT @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) @ChangesDeviceEnabledState public void setKeys(@Auth final DisabledPermittedAuthenticatedAccount disabledPermittedAuth, @NotNull @Valid final PreKeyState preKeys, @@ -100,21 +102,21 @@ public class KeysController { 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; + + 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()) { - 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(); throw new ForbiddenException(); } + Metrics.counter(IDENTITY_KEY_CHANGE_COUNTER_NAME, tags).increment(); } if (updateAccount) { diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/ChangeNumberRequest.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/ChangeNumberRequest.java index 3d0d63042..153c4e54a 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/ChangeNumberRequest.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/ChangeNumberRequest.java @@ -11,6 +11,7 @@ import java.util.List; import java.util.Map; import javax.annotation.Nullable; import javax.validation.Valid; +import javax.validation.constraints.AssertTrue; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import org.whispersystems.textsecuregcm.util.ByteArrayAdapter; @@ -24,4 +25,12 @@ public record ChangeNumberRequest(String sessionId, @NotNull @Valid Map devicePniSignedPrekeys, @NotNull Map pniRegistrationIds) implements PhoneVerificationRequest { + @AssertTrue + public boolean isSignatureValidOnEachSignedPreKey() { + if (devicePniSignedPrekeys == null) { + return true; + } + return devicePniSignedPrekeys.values().parallelStream() + .allMatch(spk -> PreKeySignatureValidator.validatePreKeySignature(pniIdentityKey, spk)); + } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/ChangePhoneNumberRequest.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/ChangePhoneNumberRequest.java index 49970b270..0a3efb99a 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/ChangePhoneNumberRequest.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/ChangePhoneNumberRequest.java @@ -8,6 +8,7 @@ package org.whispersystems.textsecuregcm.entities; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; import java.util.Map; +import javax.validation.constraints.AssertTrue; import javax.annotation.Nullable; import javax.validation.constraints.NotBlank; @@ -18,4 +19,14 @@ public record ChangePhoneNumberRequest(@NotBlank String number, @Nullable List deviceMessages, @Nullable Map devicePniSignedPrekeys, @Nullable Map pniRegistrationIds) { + + @AssertTrue + public boolean isSignatureValidOnEachSignedPreKey() { + if (devicePniSignedPrekeys == null) { + return true; + } + return devicePniSignedPrekeys.values().parallelStream() + .allMatch(spk -> PreKeySignatureValidator.validatePreKeySignature(pniIdentityKey, spk)); + } + } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/PhoneNumberIdentityKeyDistributionRequest.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/PhoneNumberIdentityKeyDistributionRequest.java index f32fc04ea..619143fe3 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/PhoneNumberIdentityKeyDistributionRequest.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/PhoneNumberIdentityKeyDistributionRequest.java @@ -11,6 +11,7 @@ import java.util.List; import java.util.Map; import javax.annotation.Nullable; import javax.validation.Valid; +import javax.validation.constraints.AssertTrue; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import org.whispersystems.textsecuregcm.util.ByteArrayAdapter; @@ -34,4 +35,11 @@ public record PhoneNumberIdentityKeyDistributionRequest( @Valid @Schema(description="The new registration ID to use for the phone-number identity of each device") Map pniRegistrationIds) { + + @AssertTrue + public boolean isSignatureValidOnEachSignedPreKey() { + return devicePniSignedPrekeys.values().parallelStream() + .allMatch(spk -> PreKeySignatureValidator.validatePreKeySignature(pniIdentityKey, spk)); + } + } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeySignatureValidator.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeySignatureValidator.java new file mode 100644 index 000000000..16db6085e --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeySignatureValidator.java @@ -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; + } + } +} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyState.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyState.java index 18b4cab1c..b68c64f4f 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyState.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyState.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.annotations.VisibleForTesting; import java.util.List; import javax.validation.Valid; +import javax.validation.constraints.AssertTrue; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; @@ -48,4 +49,8 @@ public class PreKeyState { return identityKey; } + @AssertTrue + public boolean isSignatureValid() { + return PreKeySignatureValidator.validatePreKeySignature(identityKey, signedPreKey); + } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java index 0a228934e..bdf28d888 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java @@ -48,6 +48,8 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nullable; 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.mockito.ArgumentCaptor; 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.whispersystems.textsecuregcm.auth.AuthenticatedAccount; 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.tests.util.AccountsHelper; 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.SystemMapper; import org.whispersystems.textsecuregcm.util.TestClock; @@ -1622,7 +1627,8 @@ class AccountControllerTest { void testChangePhoneNumberChangePrekeys() throws Exception { final String number = "+18005559876"; 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); Device device2 = mock(Device.class); @@ -1648,7 +1654,7 @@ class AccountControllerTest { var deviceMessages = List.of( new IncomingMessage(1, 2, 2, "content2"), 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 registrationIds = Map.of(1L, 17, 2L, 47, 3L, 89); @@ -1674,7 +1680,8 @@ class AccountControllerTest { @Test void testChangePhoneNumberSameNumberChangePrekeys() throws Exception { 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); Device device2 = mock(Device.class); @@ -1700,7 +1707,7 @@ class AccountControllerTest { var deviceMessages = List.of( new IncomingMessage(1, 2, 2, "content2"), 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 registrationIds = Map.of(1L, 17, 2L, 47, 3L, 89); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerChangeNumberIntegrationTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerChangeNumberIntegrationTest.java index 9d17176c5..f4511b0b9 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerChangeNumberIntegrationTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerChangeNumberIntegrationTest.java @@ -23,6 +23,8 @@ import java.util.concurrent.CompletableFuture; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; 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.controllers.MismatchedDevicesException; 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.securevaluerecovery.SecureValueRecovery2Client; import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema.Tables; +import org.whispersystems.textsecuregcm.tests.util.KeysHelper; class AccountsManagerChangeNumberIntegrationTest { @@ -144,7 +147,8 @@ class AccountsManagerChangeNumberIntegrationTest { final String originalNumber = "+18005551111"; final String secondNumber = "+18005552222"; 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 Account account = accountsManager.create(originalNumber, "password", null, accountAttributes, new ArrayList<>()); @@ -153,7 +157,7 @@ class AccountsManagerChangeNumberIntegrationTest { final UUID originalUuid = account.getUuid(); final UUID originalPni = account.getPhoneNumberIdentifier(); - final String pniIdentityKey = "changed-pni-identity-key"; + final String pniIdentityKey = KeysHelper.serializeIdentityKey(pniIdentityKeyPair); final Map preKeys = Map.of(Device.MASTER_ID, rotatedSignedPreKey); final Map registrationIds = Map.of(Device.MASTER_ID, rotatedPniRegistrationId); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/KeysControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/KeysControllerTest.java index 22b5e4e18..d4c7410f9 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/KeysControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/KeysControllerTest.java @@ -24,6 +24,7 @@ import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; import io.dropwizard.testing.junit5.ResourceExtension; import java.time.Duration; +import java.util.Base64; import java.util.Collections; import java.util.LinkedList; 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.extension.ExtendWith; 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.DisabledPermittedAuthenticatedAccount; 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.tests.util.AccountsHelper; import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import org.whispersystems.textsecuregcm.tests.util.KeysHelper; @ExtendWith(DropwizardExtensionsSupport.class) class KeysControllerTest { @@ -76,6 +80,12 @@ class KeysControllerTest { 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_KEY2 = new PreKey(5667, "test3"); 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 SignedPreKey SAMPLE_SIGNED_KEY = new SignedPreKey( 1111, "foofoo", "sig11" ); - private final SignedPreKey SAMPLE_SIGNED_KEY2 = new SignedPreKey( 2222, "foobar", "sig22" ); - private final SignedPreKey SAMPLE_SIGNED_KEY3 = new SignedPreKey( 3333, "barfoo", "sig33" ); - private final SignedPreKey SAMPLE_SIGNED_PNI_KEY = new SignedPreKey( 4444, "foofoopni", "sig44" ); - private final SignedPreKey SAMPLE_SIGNED_PNI_KEY2 = new SignedPreKey( 5555, "foobarpni", "sig55" ); - private final SignedPreKey SAMPLE_SIGNED_PNI_KEY3 = new SignedPreKey( 6666, "barfoopni", "sig66" ); - private final SignedPreKey VALID_DEVICE_SIGNED_KEY = new SignedPreKey(89898, "zoofarb", "sigvalid"); - private final SignedPreKey VALID_DEVICE_PNI_SIGNED_KEY = new SignedPreKey(7777, "zoofarber", "sigvalidest"); + private final SignedPreKey SAMPLE_SIGNED_KEY = KeysHelper.signedPreKey( 1111, IDENTITY_KEY_PAIR); + private final SignedPreKey SAMPLE_SIGNED_KEY2 = KeysHelper.signedPreKey( 2222, IDENTITY_KEY_PAIR); + private final SignedPreKey SAMPLE_SIGNED_KEY3 = KeysHelper.signedPreKey( 3333, IDENTITY_KEY_PAIR); + private final SignedPreKey SAMPLE_SIGNED_PNI_KEY = KeysHelper.signedPreKey( 4444, PNI_IDENTITY_KEY_PAIR); + private final SignedPreKey SAMPLE_SIGNED_PNI_KEY2 = KeysHelper.signedPreKey( 5555, PNI_IDENTITY_KEY_PAIR); + private final SignedPreKey SAMPLE_SIGNED_PNI_KEY3 = KeysHelper.signedPreKey( 6666, PNI_IDENTITY_KEY_PAIR); + private final SignedPreKey VALID_DEVICE_SIGNED_KEY = KeysHelper.signedPreKey(89898, IDENTITY_KEY_PAIR); + 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 AccountsManager accounts = mock(AccountsManager.class ); @@ -153,8 +163,8 @@ class KeysControllerTest { when(existsAccount.getDevice(22L)).thenReturn(Optional.empty()); when(existsAccount.getDevices()).thenReturn(allDevices); when(existsAccount.isEnabled()).thenReturn(true); - when(existsAccount.getIdentityKey()).thenReturn("existsidentitykey"); - when(existsAccount.getPhoneNumberIdentityKey()).thenReturn("existspniidentitykey"); + when(existsAccount.getIdentityKey()).thenReturn(IDENTITY_KEY); + when(existsAccount.getPhoneNumberIdentityKey()).thenReturn(PNI_IDENTITY_KEY); when(existsAccount.getNumber()).thenReturn(EXISTS_NUMBER); when(existsAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of("1337".getBytes())); @@ -234,7 +244,7 @@ class KeysControllerTest { @Test void putSignedPreKeyV2() { - SignedPreKey test = new SignedPreKey(9998, "fooozzz", "baaarzzz"); + SignedPreKey test = KeysHelper.signedPreKey(9998, IDENTITY_KEY_PAIR); Response response = resources.getJerseyTest() .target("/v2/keys/signed") .request() @@ -250,7 +260,7 @@ class KeysControllerTest { @Test void putPhoneNumberIdentitySignedPreKeyV2() { - final SignedPreKey replacementKey = new SignedPreKey(9998, "fooozzz", "baaarzzz"); + final SignedPreKey replacementKey = KeysHelper.signedPreKey(9998, PNI_IDENTITY_KEY_PAIR); Response response = resources.getJerseyTest() .target("/v2/keys/signed") @@ -268,7 +278,7 @@ class KeysControllerTest { @Test void disabledPutSignedPreKeyV2() { - SignedPreKey test = new SignedPreKey(9999, "fooozzz", "baaarzzz"); + SignedPreKey test = KeysHelper.signedPreKey(9999, IDENTITY_KEY_PAIR); Response response = resources.getJerseyTest() .target("/v2/keys/signed") .request() @@ -514,8 +524,9 @@ class KeysControllerTest { @Test void putKeysTestV2() { final PreKey preKey = new PreKey(31337, "foobar"); - final SignedPreKey signedPreKey = new SignedPreKey(31338, "foobaz", "myvalidsig"); - final String identityKey = "barbar"; + final ECKeyPair identityKeyPair = Curve.generateKeyPair(); + final SignedPreKey signedPreKey = KeysHelper.signedPreKey(31338, identityKeyPair); + final String identityKey = KeysHelper.serializeIdentityKey(identityKeyPair); List preKeys = new LinkedList() {{ add(preKey); @@ -540,15 +551,16 @@ class KeysControllerTest { assertThat(capturedList.get(0).getKeyId()).isEqualTo(31337); 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(accounts).update(eq(AuthHelper.VALID_ACCOUNT), any()); } @Test void putKeysByPhoneNumberIdentifierTestV2() { - final SignedPreKey signedPreKey = new SignedPreKey(31338, "foobaz", "myvalidsig"); - final String identityKey = "barbar"; + final ECKeyPair identityKeyPair = Curve.generateKeyPair(); + final SignedPreKey signedPreKey = KeysHelper.signedPreKey(31338, identityKeyPair); + final String identityKey = KeysHelper.serializeIdentityKey(identityKeyPair); List preKeys = List.of(new PreKey(31337, "foobar")); @@ -572,16 +584,32 @@ class KeysControllerTest { assertThat(capturedList.get(0).getKeyId()).isEqualTo(31337); 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(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 void disabledPutKeysTestV2() { final PreKey preKey = new PreKey(31337, "foobar"); - final SignedPreKey signedPreKey = new SignedPreKey(31338, "foobaz", "myvalidsig"); - final String identityKey = "barbar"; + final ECKeyPair identityKeyPair = Curve.generateKeyPair(); + final SignedPreKey signedPreKey = KeysHelper.signedPreKey(31338, identityKeyPair); + final String identityKey = KeysHelper.serializeIdentityKey(identityKeyPair); List preKeys = new LinkedList() {{ add(preKey); @@ -606,7 +634,7 @@ class KeysControllerTest { assertThat(capturedList.get(0).getKeyId()).isEqualTo(31337); 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(accounts).update(eq(AuthHelper.DISABLED_ACCOUNT), any()); } @@ -614,12 +642,11 @@ class KeysControllerTest { @Test void putIdentityKeyNonPrimary() { final PreKey preKey = new PreKey(31337, "foobar"); - final SignedPreKey signedPreKey = new SignedPreKey(31338, "foobaz", "myvalidsig"); - final String identityKey = "barbar"; + final SignedPreKey signedPreKey = KeysHelper.signedPreKey(31338, IDENTITY_KEY_PAIR); List preKeys = List.of(preKey); - PreKeyState preKeyState = new PreKeyState(identityKey, signedPreKey, preKeys); + PreKeyState preKeyState = new PreKeyState(IDENTITY_KEY, signedPreKey, preKeys); Response response = resources.getJerseyTest() diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/KeysHelper.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/KeysHelper.java new file mode 100644 index 000000000..f05f2d234 --- /dev/null +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/KeysHelper.java @@ -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)); + } +}