diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceCheckController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceCheckController.java index 1d4533be2..d6eee5cb8 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceCheckController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceCheckController.java @@ -14,6 +14,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.ForbiddenException; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.PUT; @@ -35,6 +36,7 @@ import org.whispersystems.textsecuregcm.backup.BackupAuthManager; import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.devicecheck.AppleDeviceCheckManager; import org.whispersystems.textsecuregcm.storage.devicecheck.ChallengeNotFoundException; import org.whispersystems.textsecuregcm.storage.devicecheck.DeviceCheckKeyIdNotFoundException; @@ -139,6 +141,10 @@ public class DeviceCheckController { @RequestBody(description = "The attestation data, created by [attestKey](https://developer.apple.com/documentation/devicecheck/dcappattestservice/attestkey(_:clientdatahash:completionhandler:))") @NotNull final byte[] attestation) { + if (authenticatedDevice.deviceId() != Device.PRIMARY_ID) { + throw new ForbiddenException("Only primary devices may register attestations"); + } + final Account account = accountsManager.getByAccountIdentifier(authenticatedDevice.accountIdentifier()) .orElseThrow(() -> new WebApplicationException(Response.Status.UNAUTHORIZED)); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/devicecheck/AppleDeviceCheckManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/devicecheck/AppleDeviceCheckManager.java index 4c1088cd4..442c47a00 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/devicecheck/AppleDeviceCheckManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/devicecheck/AppleDeviceCheckManager.java @@ -30,8 +30,11 @@ import java.util.Base64; import java.util.List; import java.util.UUID; import javax.annotation.Nullable; +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.Metrics; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.metrics.MetricsUtil; import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisClusterClient; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.util.ResilienceUtil; @@ -56,7 +59,9 @@ public class AppleDeviceCheckManager { // How many distinct device keys we're willing to accept for a single Account @VisibleForTesting - static final int MAX_DEVICE_KEYS = 100; + static final int MAX_DEVICE_KEYS = 200; + + private static final String KEY_COUNT_DISTRIBUTION_NAME = MetricsUtil.name(AppleDeviceCheckManager.class, "keyCount"); private final AppleDeviceChecks appleDeviceChecks; private final FaultTolerantRedisClusterClient redisClient; @@ -109,6 +114,10 @@ public class AppleDeviceCheckManager { return; } + DistributionSummary.builder(KEY_COUNT_DISTRIBUTION_NAME) + .register(Metrics.globalRegistry) + .record(existingKeys.size()); + if (existingKeys.size() >= MAX_DEVICE_KEYS) { // This is best-effort, since we don't check the number of keys transactionally. We just don't want to allow // the keys for an account to grow arbitrarily large diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/DeviceCheckControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/DeviceCheckControllerTest.java index 328a25347..3c58f2023 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/DeviceCheckControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/DeviceCheckControllerTest.java @@ -12,6 +12,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import io.dropwizard.auth.AuthValueFactoryProvider; @@ -193,6 +194,24 @@ class DeviceCheckControllerTest { .registerAttestation(any(), eq(keyId), eq(attestation)); } + @Test + public void registerKeyNonPrimary() { + final byte[] keyId = TestRandomUtil.nextBytes(16); + final byte[] attestation = TestRandomUtil.nextBytes(32); + final Response response = resources.getJerseyTest() + .target("v1/devicecheck/attest") + .queryParam("keyId", Base64.getUrlEncoder().encodeToString(keyId)) + .request() + .header("Authorization", AuthHelper.getAuthHeader( + AuthHelper.VALID_UUID_3, + AuthHelper.VALID_DEVICE_3_LINKED_ID, + AuthHelper.VALID_PASSWORD_3_LINKED)) + .put(Entity.entity(attestation, MediaType.APPLICATION_OCTET_STREAM)); + + assertThat(response.getStatus()).isEqualTo(403); + verifyNoInteractions(appleDeviceCheckManager); + } + @Test public void checkAssertion() throws DeviceCheckKeyIdNotFoundException, DeviceCheckVerificationFailedException, ChallengeNotFoundException, RequestReuseException {