Add debugging context to signature validation failures
This commit is contained in:
		
							parent
							
								
									8a587d1d12
								
							
						
					
					
						commit
						a733f5c615
					
				|  | @ -22,6 +22,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; | ||||||
| import java.time.Instant; | import java.time.Instant; | ||||||
| import java.util.Optional; | import java.util.Optional; | ||||||
| import java.util.UUID; | import java.util.UUID; | ||||||
|  | import javax.annotation.Nullable; | ||||||
| import javax.validation.Valid; | import javax.validation.Valid; | ||||||
| import javax.validation.constraints.NotNull; | import javax.validation.constraints.NotNull; | ||||||
| import javax.ws.rs.BadRequestException; | import javax.ws.rs.BadRequestException; | ||||||
|  | @ -124,6 +125,10 @@ public class AccountControllerV2 { | ||||||
|     } catch (final UnrecognizedUserAgentException ignored) { |     } catch (final UnrecognizedUserAgentException ignored) { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     if (!request.isSignatureValidOnEachSignedPreKey(userAgentString)) { | ||||||
|  |       throw new WebApplicationException("Invalid signature", 422); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     final String number = request.number(); |     final String number = request.number(); | ||||||
| 
 | 
 | ||||||
|     // Only verify and check reglock if there's a data change to be made... |     // Only verify and check reglock if there's a data change to be made... | ||||||
|  | @ -195,12 +200,17 @@ public class AccountControllerV2 { | ||||||
|       content = @Content(schema = @Schema(implementation = StaleDevices.class))) |       content = @Content(schema = @Schema(implementation = StaleDevices.class))) | ||||||
|   public AccountIdentityResponse distributePhoneNumberIdentityKeys( |   public AccountIdentityResponse distributePhoneNumberIdentityKeys( | ||||||
|       @Mutable @Auth final AuthenticatedAccount authenticatedAccount, |       @Mutable @Auth final AuthenticatedAccount authenticatedAccount, | ||||||
|  |       @HeaderParam(HttpHeaders.USER_AGENT) @Nullable final String userAgentString, | ||||||
|       @NotNull @Valid final PhoneNumberIdentityKeyDistributionRequest request) { |       @NotNull @Valid final PhoneNumberIdentityKeyDistributionRequest request) { | ||||||
| 
 | 
 | ||||||
|     if (!authenticatedAccount.getAuthenticatedDevice().isPrimary()) { |     if (!authenticatedAccount.getAuthenticatedDevice().isPrimary()) { | ||||||
|       throw new ForbiddenException(); |       throw new ForbiddenException(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     if (!request.isSignatureValidOnEachSignedPreKey(userAgentString)) { | ||||||
|  |       throw new WebApplicationException("Invalid signature", 422); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     try { |     try { | ||||||
|       final Account updatedAccount = changeNumberManager.updatePniKeys( |       final Account updatedAccount = changeNumberManager.updatePniKeys( | ||||||
|           authenticatedAccount.getAccount(), |           authenticatedAccount.getAccount(), | ||||||
|  |  | ||||||
|  | @ -26,7 +26,7 @@ import java.util.List; | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
| import java.util.Optional; | import java.util.Optional; | ||||||
| import java.util.UUID; | import java.util.UUID; | ||||||
| import java.util.concurrent.CompletableFuture; | import javax.annotation.Nullable; | ||||||
| import javax.crypto.Mac; | import javax.crypto.Mac; | ||||||
| import javax.crypto.spec.SecretKeySpec; | import javax.crypto.spec.SecretKeySpec; | ||||||
| import javax.validation.Valid; | import javax.validation.Valid; | ||||||
|  | @ -64,7 +64,6 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager; | ||||||
| import org.whispersystems.textsecuregcm.storage.Device; | import org.whispersystems.textsecuregcm.storage.Device; | ||||||
| import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities; | import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities; | ||||||
| import org.whispersystems.textsecuregcm.storage.DeviceSpec; | import org.whispersystems.textsecuregcm.storage.DeviceSpec; | ||||||
| import org.whispersystems.textsecuregcm.util.Util; |  | ||||||
| import org.whispersystems.textsecuregcm.util.VerificationCode; | import org.whispersystems.textsecuregcm.util.VerificationCode; | ||||||
| import org.whispersystems.websocket.auth.Mutable; | import org.whispersystems.websocket.auth.Mutable; | ||||||
| import org.whispersystems.websocket.auth.ReadOnly; | import org.whispersystems.websocket.auth.ReadOnly; | ||||||
|  | @ -184,6 +183,7 @@ public class DeviceController { | ||||||
|       name = "Retry-After", |       name = "Retry-After", | ||||||
|       description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed")) |       description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed")) | ||||||
|   public DeviceResponse linkDevice(@HeaderParam(HttpHeaders.AUTHORIZATION) BasicAuthorizationHeader authorizationHeader, |   public DeviceResponse linkDevice(@HeaderParam(HttpHeaders.AUTHORIZATION) BasicAuthorizationHeader authorizationHeader, | ||||||
|  |       @HeaderParam(HttpHeaders.USER_AGENT) @Nullable String userAgent, | ||||||
|       @NotNull @Valid LinkDeviceRequest linkDeviceRequest, |       @NotNull @Valid LinkDeviceRequest linkDeviceRequest, | ||||||
|       @Context ContainerRequest containerRequest) |       @Context ContainerRequest containerRequest) | ||||||
|       throws RateLimitExceededException, DeviceLimitExceededException { |       throws RateLimitExceededException, DeviceLimitExceededException { | ||||||
|  | @ -199,9 +199,13 @@ public class DeviceController { | ||||||
| 
 | 
 | ||||||
|     final boolean allKeysValid = |     final boolean allKeysValid = | ||||||
|         PreKeySignatureValidator.validatePreKeySignatures(account.getIdentityKey(IdentityType.ACI), |         PreKeySignatureValidator.validatePreKeySignatures(account.getIdentityKey(IdentityType.ACI), | ||||||
|             List.of(deviceActivationRequest.aciSignedPreKey(), deviceActivationRequest.aciPqLastResortPreKey())) |             List.of(deviceActivationRequest.aciSignedPreKey(), deviceActivationRequest.aciPqLastResortPreKey()), | ||||||
|  |             userAgent, | ||||||
|  |             "link-device") | ||||||
|             && PreKeySignatureValidator.validatePreKeySignatures(account.getIdentityKey(IdentityType.PNI), |             && PreKeySignatureValidator.validatePreKeySignatures(account.getIdentityKey(IdentityType.PNI), | ||||||
|             List.of(deviceActivationRequest.pniSignedPreKey(), deviceActivationRequest.pniPqLastResortPreKey())); |             List.of(deviceActivationRequest.pniSignedPreKey(), deviceActivationRequest.pniPqLastResortPreKey()), | ||||||
|  |             userAgent, | ||||||
|  |             "link-device"); | ||||||
| 
 | 
 | ||||||
|     if (!allKeysValid) { |     if (!allKeysValid) { | ||||||
|       throw new WebApplicationException(Response.status(422).build()); |       throw new WebApplicationException(Response.status(422).build()); | ||||||
|  |  | ||||||
|  | @ -23,6 +23,7 @@ import java.util.List; | ||||||
| import java.util.Optional; | import java.util.Optional; | ||||||
| import java.util.UUID; | import java.util.UUID; | ||||||
| import java.util.concurrent.CompletableFuture; | import java.util.concurrent.CompletableFuture; | ||||||
|  | import javax.annotation.Nullable; | ||||||
| import javax.validation.Valid; | import javax.validation.Valid; | ||||||
| import javax.validation.constraints.NotNull; | import javax.validation.constraints.NotNull; | ||||||
| import javax.ws.rs.Consumes; | import javax.ws.rs.Consumes; | ||||||
|  | @ -130,7 +131,7 @@ public class KeysController { | ||||||
|     final Device device = auth.getAuthenticatedDevice(); |     final Device device = auth.getAuthenticatedDevice(); | ||||||
|     final UUID identifier = account.getIdentifier(identityType); |     final UUID identifier = account.getIdentifier(identityType); | ||||||
| 
 | 
 | ||||||
|     checkSignedPreKeySignatures(setKeysRequest, account.getIdentityKey(identityType)); |     checkSignedPreKeySignatures(setKeysRequest, account.getIdentityKey(identityType), userAgent); | ||||||
| 
 | 
 | ||||||
|     final Tag platformTag = UserAgentTagUtil.getPlatformTag(userAgent); |     final Tag platformTag = UserAgentTagUtil.getPlatformTag(userAgent); | ||||||
|     final Tag primaryDeviceTag = Tag.of(PRIMARY_DEVICE_TAG_NAME, String.valueOf(auth.getAuthenticatedDevice().isPrimary())); |     final Tag primaryDeviceTag = Tag.of(PRIMARY_DEVICE_TAG_NAME, String.valueOf(auth.getAuthenticatedDevice().isPrimary())); | ||||||
|  | @ -174,7 +175,10 @@ public class KeysController { | ||||||
|         .thenApply(Util.ASYNC_EMPTY_RESPONSE); |         .thenApply(Util.ASYNC_EMPTY_RESPONSE); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private void checkSignedPreKeySignatures(final SetKeysRequest setKeysRequest, final IdentityKey identityKey) { |   private void checkSignedPreKeySignatures(final SetKeysRequest setKeysRequest, | ||||||
|  |       final IdentityKey identityKey, | ||||||
|  |       @Nullable final String userAgent) { | ||||||
|  | 
 | ||||||
|     final List<SignedPreKey<?>> signedPreKeys = new ArrayList<>(); |     final List<SignedPreKey<?>> signedPreKeys = new ArrayList<>(); | ||||||
| 
 | 
 | ||||||
|     if (setKeysRequest.pqPreKeys() != null) { |     if (setKeysRequest.pqPreKeys() != null) { | ||||||
|  | @ -190,7 +194,7 @@ public class KeysController { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     final boolean allSignaturesValid = |     final boolean allSignaturesValid = | ||||||
|         signedPreKeys.isEmpty() || PreKeySignatureValidator.validatePreKeySignatures(identityKey, signedPreKeys); |         signedPreKeys.isEmpty() || PreKeySignatureValidator.validatePreKeySignatures(identityKey, signedPreKeys, userAgent, "set-keys"); | ||||||
| 
 | 
 | ||||||
|     if (!allSignaturesValid) { |     if (!allSignaturesValid) { | ||||||
|       throw new WebApplicationException("Invalid signature", 422); |       throw new WebApplicationException("Invalid signature", 422); | ||||||
|  | @ -397,13 +401,14 @@ public class KeysController { | ||||||
|   @Deprecated(forRemoval = true) |   @Deprecated(forRemoval = true) | ||||||
|   public CompletableFuture<Response> setSignedKey( |   public CompletableFuture<Response> setSignedKey( | ||||||
|       @ReadOnly @Auth final AuthenticatedAccount auth, |       @ReadOnly @Auth final AuthenticatedAccount auth, | ||||||
|  |       @HeaderParam(HttpHeaders.USER_AGENT) @Nullable final String userAgent, | ||||||
|       @Valid final ECSignedPreKey signedPreKey, |       @Valid final ECSignedPreKey signedPreKey, | ||||||
|       @QueryParam("identity") @DefaultValue("aci") final IdentityType identityType) { |       @QueryParam("identity") @DefaultValue("aci") final IdentityType identityType) { | ||||||
| 
 | 
 | ||||||
|     final UUID identifier = auth.getAccount().getIdentifier(identityType); |     final UUID identifier = auth.getAccount().getIdentifier(identityType); | ||||||
|     final byte deviceId = auth.getAuthenticatedDevice().getId(); |     final byte deviceId = auth.getAuthenticatedDevice().getId(); | ||||||
| 
 | 
 | ||||||
|     if (!PreKeySignatureValidator.validatePreKeySignatures(auth.getAccount().getIdentityKey(identityType), List.of(signedPreKey))) { |     if (!PreKeySignatureValidator.validatePreKeySignatures(auth.getAccount().getIdentityKey(identityType), List.of(signedPreKey), userAgent, "set-signed-pre-key")) { | ||||||
|       throw new WebApplicationException("Invalid signature", 422); |       throw new WebApplicationException("Invalid signature", 422); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -107,6 +107,10 @@ public class RegistrationController { | ||||||
|     final String number = authorizationHeader.getUsername(); |     final String number = authorizationHeader.getUsername(); | ||||||
|     final String password = authorizationHeader.getPassword(); |     final String password = authorizationHeader.getPassword(); | ||||||
| 
 | 
 | ||||||
|  |     if (!registrationRequest.isEverySignedKeyValid(userAgent)) { | ||||||
|  |       throw new WebApplicationException("Invalid signature", 422); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     RateLimiter.adaptLegacyException(() -> rateLimiters.getRegistrationLimiter().validate(number)); |     RateLimiter.adaptLegacyException(() -> rateLimiters.getRegistrationLimiter().validate(number)); | ||||||
| 
 | 
 | ||||||
|     final PhoneVerificationRequest.VerificationType verificationType = phoneVerificationTokenManager.verify(number, |     final PhoneVerificationRequest.VerificationType verificationType = phoneVerificationTokenManager.verify(number, | ||||||
|  |  | ||||||
|  | @ -68,8 +68,7 @@ public record ChangeNumberRequest( | ||||||
|     @Schema(description="the new phone-number-identity registration ID for each enabled device on the account, including this one") |     @Schema(description="the new phone-number-identity registration ID for each enabled device on the account, including this one") | ||||||
|     @NotNull Map<Byte, Integer> pniRegistrationIds) implements PhoneVerificationRequest { |     @NotNull Map<Byte, Integer> pniRegistrationIds) implements PhoneVerificationRequest { | ||||||
| 
 | 
 | ||||||
|   @AssertTrue |   public boolean isSignatureValidOnEachSignedPreKey(@Nullable final String userAgent) { | ||||||
|   public boolean isSignatureValidOnEachSignedPreKey() { |  | ||||||
|     List<SignedPreKey<?>> spks = new ArrayList<>(); |     List<SignedPreKey<?>> spks = new ArrayList<>(); | ||||||
|     if (devicePniSignedPrekeys != null) { |     if (devicePniSignedPrekeys != null) { | ||||||
|       spks.addAll(devicePniSignedPrekeys.values()); |       spks.addAll(devicePniSignedPrekeys.values()); | ||||||
|  | @ -77,7 +76,7 @@ public record ChangeNumberRequest( | ||||||
|     if (devicePniPqLastResortPrekeys != null) { |     if (devicePniPqLastResortPrekeys != null) { | ||||||
|       spks.addAll(devicePniPqLastResortPrekeys.values()); |       spks.addAll(devicePniPqLastResortPrekeys.values()); | ||||||
|     } |     } | ||||||
|     return spks.isEmpty() || PreKeySignatureValidator.validatePreKeySignatures(pniIdentityKey, spks); |     return spks.isEmpty() || PreKeySignatureValidator.validatePreKeySignatures(pniIdentityKey, spks, userAgent, "change-number"); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   @AssertTrue |   @AssertTrue | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ import io.swagger.v3.oas.annotations.media.Schema; | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
|  | import javax.annotation.Nullable; | ||||||
| import javax.validation.Valid; | import javax.validation.Valid; | ||||||
| import javax.validation.constraints.AssertTrue; | import javax.validation.constraints.AssertTrue; | ||||||
| import javax.validation.constraints.NotNull; | import javax.validation.constraints.NotNull; | ||||||
|  | @ -53,13 +54,12 @@ public record PhoneNumberIdentityKeyDistributionRequest( | ||||||
|     @Schema(description="The new registration ID to use for the phone-number identity of each device, including this one.") |     @Schema(description="The new registration ID to use for the phone-number identity of each device, including this one.") | ||||||
|     Map<Byte, Integer> pniRegistrationIds) { |     Map<Byte, Integer> pniRegistrationIds) { | ||||||
| 
 | 
 | ||||||
|   @AssertTrue |   public boolean isSignatureValidOnEachSignedPreKey(@Nullable final String userAgent) { | ||||||
|   public boolean isSignatureValidOnEachSignedPreKey() { |  | ||||||
|     List<SignedPreKey<?>> spks = new ArrayList<>(devicePniSignedPrekeys.values()); |     List<SignedPreKey<?>> spks = new ArrayList<>(devicePniSignedPrekeys.values()); | ||||||
|     if (devicePniPqLastResortPrekeys != null) { |     if (devicePniPqLastResortPrekeys != null) { | ||||||
|       spks.addAll(devicePniPqLastResortPrekeys.values()); |       spks.addAll(devicePniPqLastResortPrekeys.values()); | ||||||
|     } |     } | ||||||
|     return spks.isEmpty() || PreKeySignatureValidator.validatePreKeySignatures(pniIdentityKey, spks); |     return spks.isEmpty() || PreKeySignatureValidator.validatePreKeySignatures(pniIdentityKey, spks, userAgent, "distribute-pni-keys"); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -4,22 +4,31 @@ | ||||||
|  */ |  */ | ||||||
| package org.whispersystems.textsecuregcm.entities; | package org.whispersystems.textsecuregcm.entities; | ||||||
| 
 | 
 | ||||||
| import static com.codahale.metrics.MetricRegistry.name; |  | ||||||
| 
 |  | ||||||
| import io.micrometer.core.instrument.Counter; |  | ||||||
| import io.micrometer.core.instrument.Metrics; | import io.micrometer.core.instrument.Metrics; | ||||||
| import java.util.Collection; | import io.micrometer.core.instrument.Tag; | ||||||
|  | import io.micrometer.core.instrument.Tags; | ||||||
| import org.signal.libsignal.protocol.IdentityKey; | import org.signal.libsignal.protocol.IdentityKey; | ||||||
|  | import org.whispersystems.textsecuregcm.metrics.MetricsUtil; | ||||||
|  | import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; | ||||||
|  | 
 | ||||||
|  | import javax.annotation.Nullable; | ||||||
|  | import java.util.Collection; | ||||||
| 
 | 
 | ||||||
| public abstract class PreKeySignatureValidator { | public abstract class PreKeySignatureValidator { | ||||||
|   public static final Counter INVALID_SIGNATURE_COUNTER = |   public static final String INVALID_SIGNATURE_COUNTER_NAME = | ||||||
|       Metrics.counter(name(PreKeySignatureValidator.class, "invalidPreKeySignature")); |       MetricsUtil.name(PreKeySignatureValidator.class, "invalidPreKeySignature"); | ||||||
| 
 | 
 | ||||||
|   public static boolean validatePreKeySignatures(final IdentityKey identityKey, final Collection<SignedPreKey<?>> spks) { |   public static boolean validatePreKeySignatures(final IdentityKey identityKey, | ||||||
|     final boolean success = spks.stream().allMatch(spk -> spk.signatureValid(identityKey)); |       final Collection<SignedPreKey<?>> signedPreKeys, | ||||||
|  |       @Nullable final String userAgent, | ||||||
|  |       final String context) { | ||||||
|  | 
 | ||||||
|  |     final boolean success = signedPreKeys.stream().allMatch(signedPreKey -> signedPreKey.signatureValid(identityKey)); | ||||||
| 
 | 
 | ||||||
|     if (!success) { |     if (!success) { | ||||||
|       INVALID_SIGNATURE_COUNTER.increment(); |       Metrics.counter(INVALID_SIGNATURE_COUNTER_NAME, | ||||||
|  |               Tags.of(UserAgentTagUtil.getPlatformTag(userAgent), Tag.of("context", context))) | ||||||
|  |           .increment(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return success; |     return success; | ||||||
|  |  | ||||||
|  | @ -14,6 +14,7 @@ import com.google.common.annotations.VisibleForTesting; | ||||||
| import io.swagger.v3.oas.annotations.media.Schema; | import io.swagger.v3.oas.annotations.media.Schema; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Optional; | import java.util.Optional; | ||||||
|  | import javax.annotation.Nullable; | ||||||
| import javax.validation.Valid; | import javax.validation.Valid; | ||||||
| import javax.validation.constraints.AssertTrue; | import javax.validation.constraints.AssertTrue; | ||||||
| import javax.validation.constraints.NotNull; | import javax.validation.constraints.NotNull; | ||||||
|  | @ -96,8 +97,7 @@ public record RegistrationRequest(@Schema(requiredMode = Schema.RequiredMode.NOT | ||||||
|         new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, apnToken, gcmToken)); |         new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, apnToken, gcmToken)); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   @AssertTrue |   public boolean isEverySignedKeyValid(@Nullable final String userAgent) { | ||||||
|   public boolean isEverySignedKeyValid() { |  | ||||||
|     if (deviceActivationRequest().aciSignedPreKey() == null || |     if (deviceActivationRequest().aciSignedPreKey() == null || | ||||||
|         deviceActivationRequest().pniSignedPreKey() == null || |         deviceActivationRequest().pniSignedPreKey() == null || | ||||||
|         deviceActivationRequest().aciPqLastResortPreKey() == null || |         deviceActivationRequest().aciPqLastResortPreKey() == null || | ||||||
|  | @ -105,8 +105,8 @@ public record RegistrationRequest(@Schema(requiredMode = Schema.RequiredMode.NOT | ||||||
|       return false; |       return false; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return PreKeySignatureValidator.validatePreKeySignatures(aciIdentityKey(), List.of(deviceActivationRequest().aciSignedPreKey(), deviceActivationRequest().aciPqLastResortPreKey())) |     return PreKeySignatureValidator.validatePreKeySignatures(aciIdentityKey(), List.of(deviceActivationRequest().aciSignedPreKey(), deviceActivationRequest().aciPqLastResortPreKey()), userAgent, "register") | ||||||
|         && PreKeySignatureValidator.validatePreKeySignatures(pniIdentityKey(), List.of(deviceActivationRequest().pniSignedPreKey(), deviceActivationRequest().pniPqLastResortPreKey())); |         && PreKeySignatureValidator.validatePreKeySignatures(pniIdentityKey(), List.of(deviceActivationRequest().pniSignedPreKey(), deviceActivationRequest().pniPqLastResortPreKey()), userAgent, "register"); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   @VisibleForTesting |   @VisibleForTesting | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	 Jon Chambers
						Jon Chambers