Skip to content

Establishing app integrity

Play Integrity for Android and Apple Device Check for iOS both come with their mannerisms. In this document, guidance on their proper usage will be provided.

Android

Play Integrity has been established by Google to enable integrity checks. Previously introduced as SafetyNet, it has since evolved to support additional features, and its use has been simplified.

Requirements

On the client side, it should be noted, that Google Play services are required to be installed on respective Android devices. The service cannot be used without Google Play services. Google Play services might be out of date or unavailable for some other reason, therefore steps should be taken to ensure proper error handling. Furthermore, since the client device will always have to talk to a network service, appropriate error-handling should also cover loss/change of network as well as exponential back-off. When distributed outside of Google Play Store, the Cloud project number must be configured using the setCloudProjectNumber() method.

A Google Cloud Project is required to enable Play Integrity API services. This can then be linked to the Play Console.

Daily limit

There is a limit of 10 000 requests per app/day which can only be increased bilaterally by reaching out to Google.

Classic vs Standard requests

Play Integrity offers two variants: classic requests and standard requests. The biggest difference between the two is how they are verified. While in standard requests the backend can only verify the integrity tokens by contacting the appropriate Google services, the classic requests optionally allows for retrieving cryptographic keys from Google allowing verification of integrity tokens in the backend without contacting Google.

Therefore, classic request must be enabled and used by the wallet backend for enhanced privacy, and their appropriate keys must be retrieved in the Play Console.

Requesting a verdict - classic requests

In order to request a integrity verdict using the classic request method the backend needs to supply a nonce to the app. This nonce is used for replay-protection and to ensure the freshness of the response. Furthermore, it may be used to bind critical data to a server request and protect those against tampering. For nonces following restrictions apply: - String - URL-safe - Encoded as Base64 and non-wrapping - Minimum of 16 characters - Maximum of 500 characters

Please have a look at the documentation for classic integrity requests for the code required on the Android device.

Verification guideline - classic requests

An Integrity token is provided in the form of a JWT/JWS that is signed with a VERIFICATION_KEY and wrapped inside a JWE that is encrypted using an DECRYPTION_KEY (should decryption fail, the request should be discarded). The VERIFICATION_KEY and DECRYPTION_KEY are provided by the Google Play Console in an encrypted text file and must be made available to the backend in a secure way.

Note: The validity of the VERIFICATION_KEY is not established through a PKI but rather through the secure retrieval from the Google Play Console, therefore there is no chain of certificates to a trusted root.

If successful, the payload of the integrity token can now be inspected, which has the following structure:

{
  requestDetails: { ... }
  appIntegrity: { ... }
  deviceIntegrity: { ... }
  accountDetails: { ... }
  environmentDetails: { ... }
}
We will discuss every name/value pair in the following sections:

“requestDetails”

requestPackageName: "com.package.name"

The value must match the originally implemented package name of the Android app.

nonce: "aGVsbG8gd29scmQgdGhlcmU"

The value must match the nonce originally created for the request.

timestampMillis: "1617893780"

The value must not differ from the current time too much. We propose a range from -1 to +5 in minutes. Note: It must be ensured that the server time is as accurate as possible.

“appIntegrity”

appRecognitionVerdict: "PLAY_RECOGNIZED"

The value must be either: PLAY_RECOGNIZED, if installed from the Play Store; or UNRECOGNIZED_VERSION, if distributing outside of the Play Store.

packageName: "com.package.name"

The value must match the original package name.

certificateSha256Digest: ["6a6a1474b5cbbb2b1aa57e0bc3"]

The value must match the hash of your app’s signing key. The signing keys must be protected properly and only used for releases. A different key should be used for debug purposes.

versionCode: "42"

The value must be within the range of supported versions.

“deviceIntegrity”

For devices running Android 13 or higher:

deviceRecognitionVerdict: ["MEETS_STRONG_INTEGRITY"]

The value must match "MEETS_STRONG_INTEGRITY".

Strong integrity check has to be enabled in the Play Console. It will ensure hardware-backed proof of boot integrity for Android 13 and up.

For devices running Android 9 to 12:

deviceRecognitionVerdict: ["MEETS_DEVICE_INTEGRITY"]

The value must match "MEETS_DEVICE_INTEGRITY".

“accountDetails”

Account details field involves licensing information for paid apps on the Google Play Store. It is therefore not relevant to our security requirements.

“environmentDetails”

The Environment details object contains optional information of other apps detected on the device. It should not be used, as Play Protect services can be disabled on the client side manually. Furthermore, excluding users with screen readers or assistance options may impact legal accessibility requirements.

Summary

The previous sections have introduced the different terms and values used in the Play Integrity system. To properly verify integrity tokens in the backend, one should therefore perform the following steps:

  1. Decrypt a given integrity token using the DECRYPTION_KEY from the Play Console
  2. Verify its signature using the VERIFICATION_KEY from the Play Console.
  3. requestPackageName equals packageName equals
  4. nonce matches original challenge
  5. timestampMillis is within acceptable range
  6. appRecognitionVerdict equals “PLAY_RECOGNIZED”
  7. certificateSha256Digest equals your signing key
  8. versionCode is greater than minimum supported version
  9. deviceRecognitionVerdict is MEETS_STRONG_INTEGRITY (Android 13 and higher) or MEETS_DEVICE_INTEGRITY (Android 9 to 12)

Additional remarks

Hardware-backed proof of boot integrity can also be established using “KeyAttestation”. Also, third-party distributions lacking Google Play services could be supported this way.

iOS

Apple’s iOS offers a system referred to as App Attest under the umbrella term “DeviceCheck”. Its terms are sometimes used interchangeably but while DeviceCheck offers validating per-device transaction fraud detection, the App Attest service offers establishing integrity of the installed client app. It is also under the umbrella of DeviceCheck, referred to as DCAppAttestService.

Attestations vs. Assertions

The App Attest service revolves around both Attestations and Assertions. In conjunction they are used to establish an app’s integrity. An Attestation is run only once upon initially setting up the app. When the user first opens the app, an attest key is generated. The key is then attested by Apple services and registered on the app’s server. This involves verification steps outlined in its respective section. An Attestation establishes a baseline for running Assertions later on. An Attestation should only be performed once per installation or after a factory reset.

Assertions on the other hand are performed using an attested key. They require a successful Attestation beforehand. They can be run often and should be tied to specific user-interactions within the app. The server challenges the client to sign a given nonce, establishing the app has not been tampered with. Further verification details will be provided in the respective section.

Requirements

Not all Apple devices support App Attest, while the iOS ecosystem benefits from not having to support a multitude of OEM like Android, the service requires use of a Secure Enclave, available since the iPhone 8/iPhone X. Since iOS 14, the App Attest API is available, and appropriate support can be checked using the isSupported flag.

For Attestations, there is a limit of 10 million users per day per app. At the time of writing an official process of increasing it could not be ascertained. Additionally, the documentation says that there may be throttling after more than 100 requests per second.

For Assertions, there is "no restriction on the number of assertions that you can make with a key".

Client-side:

Upon first use of the app, a key must be prepared for signing App Attest challenges. This involves generating and attesting a key using respective methods.

If the assertion feature will be used the attested key must be recorded on the server with proper verification.

The Attestation functionality of the App Attest system involves contacting an Apple network service, therefore appropriate error handling should be implemented for network issues, as well as exponential backoff mechanisms.

Verification guideline for attestations:

The attestations should be performed when first setting up an App Attest key.

The attestation object is a CBOR structure, following the WebAuthN standard for Apple Anonymous Attestation Statement Format.

The following code snippet illustrates the structure of an attestation object:

{
  fmt: 'apple-appattest',
  attStmt: {
    x5c: [
      <Buffer 30 82 02 cc ... > of client certificate (*credCert*),
        - OID 1.2.840.113635.100.8.2 -> nonce bytes
      <Buffer 30 82 02 36 ... > intermediate,
    ],
    receipt: <Buffer 30 80 06 09 ... >
  },
  authData: <Buffer 21 c9 9e 00 ... >
}
The attestation statement (attStmt) contains the certificate chain (x5c), which are encoded as X509 certificates, and the receipt, which contains information regarding the risk metrics offered by Apple via their server. The authentication data (authData) will be a byte string comprised of: - an RP ID (32 bytes) - a counter (4 bytes) - an aaguid (16 bytes) - a credentialId (32 bytes).

To verify the attestation object, verify that it is in the Concise Binary Object Representation (CBOR) data format with the expected syntax. To do this, the object must first be decoded and then the existence of the fields attStmt, authData and fmt must be checked. In addition, the field fmt should contain the string 'apple-appattest'.

The following fields must be checked:

x5c

The “x5c” field consists of an array, it must contain a certificate chain containing the attested key’s public certificate as a leaf and an intermediate certificate. The root (Apple App Attestation Root CA) is not included in the chain and must be acquired manually from https://www.apple.com/certificateauthority/private/ Apple does not provide any infrastructure or information for checking the revocation status of these certificates.

credCert

The first certificate (which is the public part of the attestKey) must contain an ASN.1 extension with an object identifier (OID) of 1.2.840.113635.100.8.2 pointing to the nonce.

Verify that hash(authData||hash(clientData)) equals the nonce from the client certificate otherwise the attestation is invalid.

RP ID

Must match a hash of your app’s App ID prefix, a period and your CFBundleIdentifier

counter

Counts the number of times the attested key was used to sign an assertion. Should be 0 initially.

aaguid (16 bytes)

This constant should match the environment, either development or production.

credentialId (32 bytes)

“A hash of the public key part of the attested cryptographic key pair.” Must match the certificate from the chain stored in “x5c”. This should be stored for verification of assertions later.

After successful verification, the server must store the public part of the attested key for later verification of assertions.

Verification guideline for assertions:

After establishing an attested key in the previous section, the app is now ready to sign challenges sent by the server whenever the app's integrity is to be verified. Assertions should be established whenever a specific user-interaction involving the server is to be performed, establishing the app’s integrity at the time of request.

The following code snippet illustrates an assertion object:

{
  signature: <Buffer 30 45 02 20 ... >,
  authenticatorData: <Buffer 21 c9 9e 00 ... >
}

The authenticatorData will only contain: - RP ID (32 bytes) - counter (4 bytes).

Similarly to the attestation, once again verify the nonce by hashing clientData and challenge as described in the Apple documentation

signature

Verify the signatures validity using the public key previously stored during Attesation.

RP ID

Must match a hash of your app’s App ID prefix, a period and your CFBundleIdentifier

counter

The counter should only be the direct successor to the previously stored counter. It should not be higher, unless other backends are involved.

authenticatorData

Ensure the embedded challenge matches the one issued. This is done by calculating nonce = hash(authenticatorData||hash(clientData)) on the server side and verify the signature is valid for the calculated nonce. clientData represents the challenge issued to the client

Summary:

The App Attest system involves two phases, both of which come with their intricacies during development. They can only be used in conjunction with one another. An Attestation is used to prepare a key once to be used to establish the app’s integrity later using Assertions. We therefore have two checklists to be used accordingly:

When establishing an Attestation, e.g. a new device is registered: 1. “x5c” certificate chain is valid and terminates to Apple’s App Attest root certificate 2. Calculate nonce server side: hash(authData+hash(clientData)) 3. Obtain credCert value, ensure it equals nonce 4. Ensure hash(credCert) matches key identifier from your app 5. Ensure “RP ID” matches hash(.) 6. Ensure counter is 0 7. aaguid corresponds to environment (development or production) 8. Verify credentialId equals key identifier

If successful, store the credCert on the server, preparing it to verify assertions.

When establishing an Assertion:

  1. Compute nonce on server side hash(authenticatorData+hash(clientData)), whereby clientData has to be the same as the challenge previously issued to the client.
  2. Check signature is valid for nonce using public key stored for that user (established during Attestation)
  3. Ensure “RP ID” matches hash(.)
  4. Ensure counter is the direct successor of previous Assertions, update accordingly

Additional remarks

iOS does feature some hardware-backed security options using the secure enclave, however key attestation similarly to Android is not available at the time of writing. Attestation cannot be used for purposes other than DC App Attest.