From 279f877bf2c39f1b3648841de5f25ac573da38d1 Mon Sep 17 00:00:00 2001 From: Jon Chambers Date: Thu, 22 Feb 2024 14:32:45 -0500 Subject: [PATCH] Validate pre-key signatures via the legacy "set signed pre-key" endpoint --- .../controllers/KeysController.java | 6 ++ .../controllers/KeysControllerTest.java | 65 +++++++++---------- .../textsecuregcm/tests/util/AuthHelper.java | 11 +++- 3 files changed, 45 insertions(+), 37 deletions(-) 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 bd6bbf5b4..7907a4379 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java @@ -344,6 +344,8 @@ public class KeysController { @ApiResponse(responseCode = "200", description = "Indicates that new prekey was successfully stored.") @ApiResponse(responseCode = "401", description = "Account authentication check failed.") @ApiResponse(responseCode = "422", description = "Invalid request format.") + // TODO Remove this endpoint on or after 2024-05-24 + @Deprecated(forRemoval = true) public CompletableFuture setSignedKey( @ReadOnly @Auth final AuthenticatedAccount auth, @Valid final ECSignedPreKey signedPreKey, @@ -352,6 +354,10 @@ public class KeysController { final UUID identifier = auth.getAccount().getIdentifier(identityType); final byte deviceId = auth.getAuthenticatedDevice().getId(); + if (!PreKeySignatureValidator.validatePreKeySignatures(auth.getAccount().getIdentityKey(identityType), List.of(signedPreKey))) { + throw new WebApplicationException("Invalid signature", 422); + } + return keysManager.storeEcSignedPreKeys(identifier, deviceId, signedPreKey) .thenApply(Util.ASYNC_EMPTY_RESPONSE); } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/KeysControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/KeysControllerTest.java index 1e76cbceb..fc6830361 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/KeysControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/KeysControllerTest.java @@ -32,7 +32,6 @@ import java.util.Optional; import java.util.OptionalInt; import java.util.UUID; import java.util.concurrent.CompletableFuture; -import java.util.function.Consumer; import javax.ws.rs.client.Entity; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @@ -42,6 +41,9 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.ecc.Curve; @@ -269,8 +271,6 @@ class KeysControllerTest { when(KEYS.getEcCount(AuthHelper.VALID_UUID, sampleDeviceId)).thenReturn(CompletableFuture.completedFuture(5)); when(KEYS.getPqCount(AuthHelper.VALID_UUID, sampleDeviceId)).thenReturn(CompletableFuture.completedFuture(5)); - when(AuthHelper.VALID_ACCOUNT.getIdentityKey(IdentityType.ACI)).thenReturn(null); - when(KEYS.getEcSignedPreKey(AuthHelper.VALID_UUID, AuthHelper.VALID_DEVICE.getId())) .thenReturn(CompletableFuture.completedFuture(Optional.of(VALID_DEVICE_SIGNED_KEY))); @@ -309,7 +309,7 @@ class KeysControllerTest { @Test void putSignedPreKeyV2() { - final ECSignedPreKey signedPreKey = KeysHelper.signedECPreKey(9998, IDENTITY_KEY_PAIR); + final ECSignedPreKey signedPreKey = KeysHelper.signedECPreKey(9998, AuthHelper.VALID_IDENTITY_KEY_PAIR); try (final Response response = resources.getJerseyTest() .target("/v2/keys/signed") @@ -324,7 +324,7 @@ class KeysControllerTest { @Test void putPhoneNumberIdentitySignedPreKeyV2() { - final ECSignedPreKey pniSignedPreKey = KeysHelper.signedECPreKey(9998, PNI_IDENTITY_KEY_PAIR); + final ECSignedPreKey pniSignedPreKey = KeysHelper.signedECPreKey(9998, AuthHelper.VALID_PNI_IDENTITY_KEY_PAIR); try (final Response response = resources.getJerseyTest() .target("/v2/keys/signed") @@ -338,6 +338,23 @@ class KeysControllerTest { } } + @ParameterizedTest + @EnumSource(IdentityType.class) + void putSignedPreKeyV2BadSignature(final IdentityType identityType) { + final ECSignedPreKey signedPreKey = KeysHelper.signedECPreKey(9998, Curve.generateKeyPair()); + + try (final Response response = resources.getJerseyTest() + .target("/v2/keys/signed") + .queryParam("identity", identityType.name().toLowerCase()) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(signedPreKey, MediaType.APPLICATION_JSON_TYPE))) { + + assertThat(response.getStatus()).isEqualTo(422); + verify(KEYS, never()).storeEcSignedPreKeys(any(), anyByte(), any()); + } + } + @Test void validSingleRequestTestV2() { PreKeyResponse result = resources.getJerseyTest() @@ -740,14 +757,10 @@ class KeysControllerTest { @Test void putKeysTestV2() { final ECPreKey preKey = KeysHelper.ecPreKey(31337); - final ECKeyPair identityKeyPair = Curve.generateKeyPair(); - final ECSignedPreKey signedPreKey = KeysHelper.signedECPreKey(31338, identityKeyPair); - final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey()); + final ECSignedPreKey signedPreKey = KeysHelper.signedECPreKey(31338, AuthHelper.VALID_IDENTITY_KEY_PAIR); final SetKeysRequest setKeysRequest = new SetKeysRequest(List.of(preKey), signedPreKey, null, null); - when(AuthHelper.VALID_ACCOUNT.getIdentityKey(IdentityType.ACI)).thenReturn(identityKey); - Response response = resources.getJerseyTest() .target("/v2/keys") @@ -768,14 +781,10 @@ class KeysControllerTest { @Test void putKeysTestV2EmptySingleUseKeysList() { - final ECKeyPair identityKeyPair = Curve.generateKeyPair(); - final ECSignedPreKey signedPreKey = KeysHelper.signedECPreKey(31338, identityKeyPair); - final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey()); + final ECSignedPreKey signedPreKey = KeysHelper.signedECPreKey(31338, AuthHelper.VALID_IDENTITY_KEY_PAIR); final SetKeysRequest setKeysRequest = new SetKeysRequest(List.of(), signedPreKey, List.of(), null); - when(AuthHelper.VALID_ACCOUNT.getIdentityKey(IdentityType.ACI)).thenReturn(identityKey); - try (final Response response = resources.getJerseyTest() .target("/v2/keys") @@ -794,17 +803,13 @@ class KeysControllerTest { @Test void putKeysPqTestV2() { final ECPreKey preKey = KeysHelper.ecPreKey(31337); - final ECKeyPair identityKeyPair = Curve.generateKeyPair(); - final ECSignedPreKey signedPreKey = KeysHelper.signedECPreKey(31338, identityKeyPair); - final KEMSignedPreKey pqPreKey = KeysHelper.signedKEMPreKey(31339, identityKeyPair); - final KEMSignedPreKey pqLastResortPreKey = KeysHelper.signedKEMPreKey(31340, identityKeyPair); - final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey()); + final ECSignedPreKey signedPreKey = KeysHelper.signedECPreKey(31338, AuthHelper.VALID_IDENTITY_KEY_PAIR); + final KEMSignedPreKey pqPreKey = KeysHelper.signedKEMPreKey(31339, AuthHelper.VALID_IDENTITY_KEY_PAIR); + final KEMSignedPreKey pqLastResortPreKey = KeysHelper.signedKEMPreKey(31340, AuthHelper.VALID_IDENTITY_KEY_PAIR); final SetKeysRequest setKeysRequest = new SetKeysRequest(List.of(preKey), signedPreKey, List.of(pqPreKey), pqLastResortPreKey); - when(AuthHelper.VALID_ACCOUNT.getIdentityKey(IdentityType.ACI)).thenReturn(identityKey); - Response response = resources.getJerseyTest() .target("/v2/keys") @@ -901,14 +906,10 @@ class KeysControllerTest { @Test void putKeysByPhoneNumberIdentifierTestV2() { final ECPreKey preKey = KeysHelper.ecPreKey(31337); - final ECKeyPair identityKeyPair = Curve.generateKeyPair(); - final ECSignedPreKey signedPreKey = KeysHelper.signedECPreKey(31338, identityKeyPair); - final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey()); + final ECSignedPreKey signedPreKey = KeysHelper.signedECPreKey(31338, AuthHelper.VALID_PNI_IDENTITY_KEY_PAIR); final SetKeysRequest setKeysRequest = new SetKeysRequest(List.of(preKey), signedPreKey, null, null); - when(AuthHelper.VALID_ACCOUNT.getIdentityKey(IdentityType.PNI)).thenReturn(identityKey); - Response response = resources.getJerseyTest() .target("/v2/keys") @@ -930,17 +931,13 @@ class KeysControllerTest { @Test void putKeysByPhoneNumberIdentifierPqTestV2() { final ECPreKey preKey = KeysHelper.ecPreKey(31337); - final ECKeyPair identityKeyPair = Curve.generateKeyPair(); - final ECSignedPreKey signedPreKey = KeysHelper.signedECPreKey(31338, identityKeyPair); - final KEMSignedPreKey pqPreKey = KeysHelper.signedKEMPreKey(31339, identityKeyPair); - final KEMSignedPreKey pqLastResortPreKey = KeysHelper.signedKEMPreKey(31340, identityKeyPair); - final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey()); + final ECSignedPreKey signedPreKey = KeysHelper.signedECPreKey(31338, AuthHelper.VALID_PNI_IDENTITY_KEY_PAIR); + final KEMSignedPreKey pqPreKey = KeysHelper.signedKEMPreKey(31339, AuthHelper.VALID_PNI_IDENTITY_KEY_PAIR); + final KEMSignedPreKey pqLastResortPreKey = KeysHelper.signedKEMPreKey(31340, AuthHelper.VALID_PNI_IDENTITY_KEY_PAIR); final SetKeysRequest setKeysRequest = new SetKeysRequest(List.of(preKey), signedPreKey, List.of(pqPreKey), pqLastResortPreKey); - when(AuthHelper.VALID_ACCOUNT.getIdentityKey(IdentityType.PNI)).thenReturn(identityKey); - Response response = resources.getJerseyTest() .target("/v2/keys") diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java index 65917821b..116e3c1eb 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java @@ -29,7 +29,8 @@ import java.util.UUID; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.signal.libsignal.protocol.IdentityKey; -import org.signal.libsignal.protocol.ecc.ECPublicKey; +import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.protocol.ecc.ECKeyPair; import org.whispersystems.textsecuregcm.auth.AccountAuthenticator; import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; import org.whispersystems.textsecuregcm.auth.SaltedTokenHash; @@ -70,8 +71,11 @@ public class AuthHelper { public static final UUID UNDISCOVERABLE_UUID = UUID.randomUUID(); public static final String UNDISCOVERABLE_PASSWORD = "IT'S A SECRET TO EVERYBODY."; - public static final IdentityKey VALID_IDENTITY = new IdentityKey(ECPublicKey.fromPublicKeyBytes( - Base64.getDecoder().decode("BcxxDU9FGMda70E7+Uvm7pnQcEdXQ64aJCpPUeRSfcFo"))); + public static final ECKeyPair VALID_IDENTITY_KEY_PAIR = Curve.generateKeyPair(); + public static final IdentityKey VALID_IDENTITY = new IdentityKey(VALID_IDENTITY_KEY_PAIR.getPublicKey()); + + public static final ECKeyPair VALID_PNI_IDENTITY_KEY_PAIR = Curve.generateKeyPair(); + public static final IdentityKey VALID_PNI_IDENTITY = new IdentityKey(VALID_PNI_IDENTITY_KEY_PAIR.getPublicKey()); public static AccountsManager ACCOUNTS_MANAGER = mock(AccountsManager.class); public static Account VALID_ACCOUNT = mock(Account.class ); @@ -179,6 +183,7 @@ public class AuthHelper { when(VALID_ACCOUNT_3.isIdentifiedBy(new PniServiceIdentifier(VALID_PNI_3))).thenReturn(true); when(VALID_ACCOUNT.getIdentityKey(IdentityType.ACI)).thenReturn(VALID_IDENTITY); + when(VALID_ACCOUNT.getIdentityKey(IdentityType.PNI)).thenReturn(VALID_PNI_IDENTITY); reset(ACCOUNTS_MANAGER);