Skip to content

Commit

Permalink
Add DeviceCheck API for iOS Testflight backup enablement
Browse files Browse the repository at this point in the history
  • Loading branch information
ravi-signal committed Dec 3, 2024
1 parent fb6c4ec commit 2c16335
Show file tree
Hide file tree
Showing 29 changed files with 1,877 additions and 7 deletions.
12 changes: 12 additions & 0 deletions service/config/sample.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ appleAppStore:
productIdToLevel: {}
appleRootCerts: []

appleDeviceCheck:
production: false
teamId: 0123456789
bundleId: bundle.name

deviceCheck:
backupRedemptionDuration: P30D
backupRedemptionLevel: 201

dynamoDbClient:
region: us-west-2 # AWS Region
Expand All @@ -103,6 +111,10 @@ dynamoDbTables:
phoneNumberIdentifierTableName: Example_Accounts_PhoneNumberIdentifiers
usernamesTableName: Example_Accounts_Usernames
usedLinkDeviceTokensTableName: Example_Accounts_UsedLinkDeviceTokens
appleDeviceChecks:
tableName: Example_AppleDeviceChecks
appleDeviceCheckPublicKeys:
tableName: Example_AppleDeviceCheckPublicKeys
backups:
tableName: Example_Backups
clientReleases:
Expand Down
6 changes: 6 additions & 0 deletions service/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<java-uuid-generator.version>5.1.0</java-uuid-generator.version>
<google-androidpublisher.version>v3-rev20241016-2.0.0</google-androidpublisher.version>
<storekit.version>3.2.0</storekit.version>
<webauthn4j.version>0.28.0.RELEASE</webauthn4j.version>
</properties>

<dependencies>
Expand All @@ -35,6 +36,11 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.webauthn4j</groupId>
<artifactId>webauthn4j-appattest</artifactId>
<version>${webauthn4j.version}</version>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-jaxrs2-jakarta</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.whispersystems.textsecuregcm.attachments.TusConfiguration;
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
import org.whispersystems.textsecuregcm.configuration.AppleAppStoreConfiguration;
import org.whispersystems.textsecuregcm.configuration.AppleDeviceCheckConfiguration;
import org.whispersystems.textsecuregcm.configuration.ArtServiceConfiguration;
import org.whispersystems.textsecuregcm.configuration.AwsCredentialsProviderFactory;
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
Expand All @@ -25,6 +26,7 @@
import org.whispersystems.textsecuregcm.configuration.ClientReleaseConfiguration;
import org.whispersystems.textsecuregcm.configuration.DatadogConfiguration;
import org.whispersystems.textsecuregcm.configuration.DefaultAwsCredentialsFactory;
import org.whispersystems.textsecuregcm.configuration.DeviceCheckConfiguration;
import org.whispersystems.textsecuregcm.configuration.DirectoryV2Configuration;
import org.whispersystems.textsecuregcm.configuration.DogstatsdConfiguration;
import org.whispersystems.textsecuregcm.configuration.DynamoDbClientFactory;
Expand Down Expand Up @@ -96,6 +98,16 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty
private AppleAppStoreConfiguration appleAppStore;

@NotNull
@Valid
@JsonProperty
private AppleDeviceCheckConfiguration appleDeviceCheck;

@NotNull
@Valid
@JsonProperty
private DeviceCheckConfiguration deviceCheck;

@NotNull
@Valid
@JsonProperty
Expand Down Expand Up @@ -359,6 +371,14 @@ public AppleAppStoreConfiguration getAppleAppStore() {
return appleAppStore;
}

public AppleDeviceCheckConfiguration getAppleDeviceCheck() {
return appleDeviceCheck;
}

public DeviceCheckConfiguration getDeviceCheck() {
return deviceCheck;
}

public DynamoDbClientFactory getDynamoDbClientConfiguration() {
return dynamoDbClient;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import static java.util.Objects.requireNonNull;

import com.google.common.collect.Lists;
import com.webauthn4j.appattest.DeviceCheckManager;
import io.dropwizard.auth.AuthDynamicFeature;
import io.dropwizard.auth.AuthFilter;
import io.dropwizard.auth.AuthValueFactoryProvider;
Expand Down Expand Up @@ -114,6 +115,7 @@
import org.whispersystems.textsecuregcm.controllers.CallRoutingControllerV2;
import org.whispersystems.textsecuregcm.controllers.CertificateController;
import org.whispersystems.textsecuregcm.controllers.ChallengeController;
import org.whispersystems.textsecuregcm.controllers.DeviceCheckController;
import org.whispersystems.textsecuregcm.controllers.DeviceController;
import org.whispersystems.textsecuregcm.controllers.DirectoryV2Controller;
import org.whispersystems.textsecuregcm.controllers.DonationController;
Expand Down Expand Up @@ -213,6 +215,9 @@
import org.whispersystems.textsecuregcm.storage.AccountPrincipalSupplier;
import org.whispersystems.textsecuregcm.storage.Accounts;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.devicecheck.AppleDeviceCheckManager;
import org.whispersystems.textsecuregcm.storage.devicecheck.AppleDeviceCheckTrustAnchor;
import org.whispersystems.textsecuregcm.storage.devicecheck.AppleDeviceChecks;
import org.whispersystems.textsecuregcm.storage.ChangeNumberManager;
import org.whispersystems.textsecuregcm.storage.ClientPublicKeys;
import org.whispersystems.textsecuregcm.storage.ClientPublicKeysManager;
Expand Down Expand Up @@ -789,6 +794,20 @@ public void run(WhisperServerConfiguration config, Environment environment) thro
cdn3RemoteStorageManager,
clock);

final AppleDeviceChecks appleDeviceChecks = new AppleDeviceChecks(
dynamoDbClient,
DeviceCheckManager.createObjectConverter(),
config.getDynamoDbTables().getAppleDeviceChecks().getTableName(),
config.getDynamoDbTables().getAppleDeviceCheckPublicKeys().getTableName());
final DeviceCheckManager deviceCheckManager = new DeviceCheckManager(new AppleDeviceCheckTrustAnchor());
deviceCheckManager.getAttestationDataValidator().setProduction(config.getAppleDeviceCheck().production());
final AppleDeviceCheckManager appleDeviceCheckManager = new AppleDeviceCheckManager(
appleDeviceChecks,
cacheCluster,
deviceCheckManager,
config.getAppleDeviceCheck().teamId(),
config.getAppleDeviceCheck().bundleId());

final DynamicConfigTurnRouter configTurnRouter = new DynamicConfigTurnRouter(dynamicConfigurationManager);

MaxMindDatabaseManager geoIpCityDatabaseManager = new MaxMindDatabaseManager(
Expand Down Expand Up @@ -1092,6 +1111,9 @@ protected void configureServer(final ServerBuilder<?> serverBuilder) {
zkAuthOperations, callingGenericZkSecretParams, clock),
new ChallengeController(rateLimitChallengeManager, challengeConstraintChecker),
new DeviceController(accountsManager, clientPublicKeysManager, rateLimiters, config.getMaxDevices()),
new DeviceCheckController(clock, backupAuthManager, appleDeviceCheckManager, rateLimiters,
config.getDeviceCheck().backupRedemptionLevel(),
config.getDeviceCheck().backupRedemptionDuration()),
new DirectoryV2Controller(directoryV2CredentialsGenerator),
new DonationController(clock, zkReceiptOperations, redeemedReceiptsManager, accountsManager, config.getBadges(),
ReceiptCredentialPresentation::new),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,13 +235,24 @@ public CompletableFuture<Void> redeemReceipt(
.withDescription("receipt serial is already redeemed")
.asRuntimeException();
}
return accountsManager.updateAsync(account, a -> {
final Account.BackupVoucher newPayment = new Account.BackupVoucher(receiptLevel, receiptExpiration);
final Account.BackupVoucher existingPayment = a.getBackupVoucher();
a.setBackupVoucher(merge(existingPayment, newPayment));
});
})
.thenRun(Util.NOOP);
return extendBackupVoucher(account, new Account.BackupVoucher(receiptLevel, receiptExpiration));
});
}

/**
* Extend the duration of the backup voucher on an account.
*
* @param account The account to update
* @param backupVoucher The backup voucher to apply to this account
* @return A future that completes once the account has been updated to have at least the level and expiration
* in the provided voucher.
*/
public CompletableFuture<Void> extendBackupVoucher(final Account account, final Account.BackupVoucher backupVoucher) {
return accountsManager.updateAsync(account, a -> {
final Account.BackupVoucher newPayment = backupVoucher;
final Account.BackupVoucher existingPayment = a.getBackupVoucher();
a.setBackupVoucher(merge(existingPayment, newPayment));
}).thenRun(Util.NOOP);
}

private static Account.BackupVoucher merge(@Nullable final Account.BackupVoucher prev,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;

import java.time.Duration;

/**
* Configuration for Apple DeviceCheck
*
* @param production Whether this is for production or sandbox attestations
* @param teamId The teamId to validate attestations against
* @param bundleId The bundleId to validation attestations against
*/
public record AppleDeviceCheckConfiguration(
boolean production,
String teamId,
String bundleId) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;

import java.time.Duration;

/**
* Configuration for Device Check operations
*
* @param backupRedemptionDuration How long to grant backup access for redemptions via device check
* @param backupRedemptionLevel What backup level to grant redemptions via device check
*/
public record DeviceCheckConfiguration(Duration backupRedemptionDuration, long backupRedemptionLevel) {}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ public Duration getExpiration() {

private final AccountsTableConfiguration accounts;

private final Table appleDeviceChecks;
private final Table appleDeviceCheckPublicKeys;
private final Table backups;
private final Table clientPublicKeys;
private final Table clientReleases;
Expand All @@ -74,6 +76,8 @@ public Duration getExpiration() {

public DynamoDbTables(
@JsonProperty("accounts") final AccountsTableConfiguration accounts,
@JsonProperty("appleDeviceChecks") final Table appleDeviceChecks,
@JsonProperty("appleDeviceCheckPublicKeys") final Table appleDeviceCheckPublicKeys,
@JsonProperty("backups") final Table backups,
@JsonProperty("clientPublicKeys") final Table clientPublicKeys,
@JsonProperty("clientReleases") final Table clientReleases,
Expand All @@ -99,6 +103,8 @@ public DynamoDbTables(
@JsonProperty("verificationSessions") final Table verificationSessions) {

this.accounts = accounts;
this.appleDeviceChecks = appleDeviceChecks;
this.appleDeviceCheckPublicKeys = appleDeviceCheckPublicKeys;
this.backups = backups;
this.clientPublicKeys = clientPublicKeys;
this.clientReleases = clientReleases;
Expand Down Expand Up @@ -130,6 +136,18 @@ public AccountsTableConfiguration getAccounts() {
return accounts;
}

@NotNull
@Valid
public Table getAppleDeviceChecks() {
return appleDeviceChecks;
}

@NotNull
@Valid
public Table getAppleDeviceCheckPublicKeys() {
return appleDeviceCheckPublicKeys;
}

@NotNull
@Valid
public Table getBackups() {
Expand Down
Loading

0 comments on commit 2c16335

Please # to comment.