Technical guide

How to verify mobile app attestation

Verify mobile app attestation on your backend: Play Integrity and App Attest checks, request binding, replay protection and outage handling.

  • Verify on the server

    Google and Apple sign the evidence. Your backend checks it with server-held credentials and trusts nothing the client says about its own result.

  • Bind the operation

    Tie each token or assertion to the exact action being authorised, so evidence from an earlier request cannot be reused.

  • Plan for no evidence

    Unsupported devices, quotas and outages are normal. Decide per endpoint what happens when evidence is missing.

Verify app attestation on your backend, never in the app. On Android, decode a Play Integrity token through Google’s server API and check that its request hash, package name and verdicts match the request you received. On iOS, validate the App Attest key once, then verify a signed assertion with that key on every sensitive request. In both cases, bind the evidence to the exact operation, reject replays and decide in advance what happens when no evidence is available. Attestation tells you the app and device are genuine. It does not authenticate the user or prove the account may perform the action.

Prepare the trust configuration

List the app identifiers, distribution environments and sensitive endpoints first. Assign owners for server credentials, trust material, policy changes and recovery. Keep development and production evidence apart.

PlatformWhat you set upWho checks what
Android, Play Integrity standard requestGoogle Cloud project linked to the app in Play Console; a service account allowed to call the decode APIGoogle signs and decrypts the token; your server checks request details and verdicts
iOS, App AttestApp Attest capability and the com.apple.developer.devicecheck.appattest-environment entitlement; server challenge and key storageYour server validates the first attestation and every later assertion

Agree a stable, canonical representation of the action to protect: operation name plus the parameters that matter for authorisation. App and server must serialise it the same way. A digest of two different serialisations fails even for a legitimate user.

Otukan app attestation supplies application authenticity evidence, and Otukan runtime evidence feeds the same server decision. Your backend combines it with the platform verdict and the account context before it authorises the request.

Android: Play Integrity standard requests

Warm up the token provider

Call prepareIntegrityToken() on a StandardIntegrityManager well before the protected action, for example at app start or when the user opens the flow. Preparation can take several seconds. The returned StandardIntegrityTokenProvider then issues tokens quickly when the user acts. If a request fails with INTEGRITY_TOKEN_PROVIDER_INVALID, prepare a new provider and retry once.

Request, decode and check

  1. For the protected action, compute a SHA-256 digest of its canonical form and pass it with setRequestHash(). Never place raw personal data in this field.
  2. Send the token with the request. On the server, call the Play Integrity decodeIntegrityToken method with server-held credentials, never credentials shipped in the app.
  3. In requestDetails, check requestPackageName, recompute the digest from the request you actually received and compare it with requestHash, and check timestampMillis against your freshness window.
  4. Read appIntegrity.appRecognitionVerdict (PLAY_RECOGNIZED for your Play build), deviceIntegrity.deviceRecognitionVerdict and, if relevant, accountDetails.appLicensingVerdict. An UNEVALUATED verdict is not a pass.
  5. Apply account authorisation and your own duplicate-submission control before committing.

Standard requests include Google’s replay protection for the token. Your business operation still needs its own idempotency handling.

Quotas

Play Integrity has a default daily request quota per app, shared across standard and classic requests. Estimate your peak volume, request only on sensitive actions rather than on every screen, and ask Google for a higher quota before launch if you need it. Monitor quota errors and route them to your unavailable-evidence policy.

iOS: App Attest

Check availability first

Read DCAppAttestService.shared.isSupported before generating a key. It returns false in the Simulator, on devices that cannot attest and in some app extensions. Treat that as “no evidence”, not as an attack, and handle it under the same policy as an outage. Apple also advises rolling App Attest out gradually to a large existing user base, because key attestation calls go to Apple’s servers.

Attest the key once

  1. Call generateKey() and keep the returned key identifier on the device.
  2. Ask your server for a random, single-use challenge. Hash it and pass it to attestKey(_:clientDataHash:).
  3. On the server, validate the attestation object as Apple documents: certificate chain to the Apple App Attestation root, nonce derived from the challenge, key identifier, RP ID hash of your Team ID and bundle ID, a counter of zero, the aaguid for the expected environment and the credential ID.
  4. Store the verified public key with the app instance and user association, keeping development and production keys apart.

Verify an assertion on each sensitive request

  1. Issue a new challenge and include it in the client data that describes the action.
  2. The app calls generateAssertion(_:clientDataHash:) with the hash of that client data.
  3. The server verifies the signature with the stored key, the RP ID hash and the embedded challenge.
  4. Require the counter to be higher than the stored value, then store the new value. Reject a reused challenge. Coordinate counter updates across server instances so concurrent requests cannot slip through.

Keys do not move to a new device through backup and restore, and reinstalling the app loses them. When the server sees an unknown key or the app receives an invalid-key error, run attestation again rather than locking the user out.

Test rejection, retry and outage paths

Use authorised test accounts and non-production operations. Start with an accepted request, then change one property at a time so each rejection has a clear cause.

CaseExpected handling
Legitimate request, valid evidenceProceed to normal account authorisation
Changed action parametersBinding mismatch; the action does not run
Wrong package, Team ID or environmentEvidence from the wrong scope is rejected
Reused assertion or consumed challengeReplay rejected without repeating a transaction
Expired evidenceFresh evidence requested through a bounded retry
isSupported false, quota reached or service outageThe endpoint’s explicit unavailable-evidence policy applies
Duplicate legitimate submissionOriginal outcome returned

Log invalid evidence and unavailable evidence separately. Never let verification failure fall back to approval. A content screen and an account-recovery change need not share one outage policy. Keep support reachable and avoid retry loops that keep prompting the user.

Record app, OS, protection and verifier versions with expected and actual results. Repeat install, update, sign-in and the protected flow, plus concurrent requests and a server restart for replay state. Keep raw tokens and assertions out of general logs.

  • Trust configuration is owned and versioned.
  • Android and iOS evidence is tested separately.
  • Token providers are prepared ahead of the protected action.
  • Challenge consumption and counter updates survive concurrency.
  • Missing evidence never becomes a successful check.
  • Account authorisation stays independent of attestation.

For product scope, see mobile app attestation and iOS app attestation. For the in-app side of the same decision, see how to handle runtime threat signals.

Further reading

Common questions

  • Can the app verify its own attestation result?

    No. Verify attestation on your backend, never in the app. On Android, the server decodes the Play Integrity token through Google's server API with server-held credentials. On iOS, the server validates the App Attest key once, then verifies a signed assertion with that key on every sensitive request.

  • Does a passing attestation authorise the user's action?

    No. Attestation is evidence about the app and device; it does not authenticate the user or prove the account may perform the action. Apply account authorisation and your own duplicate-submission control separately, and keep account authorisation independent of attestation on both Android and iOS.

  • Which Play Integrity fields should the server check?

    In requestDetails, check requestPackageName, recompute the digest of the request you received and compare it with requestHash, and check timestampMillis against your freshness window. Then read appIntegrity.appRecognitionVerdict, deviceIntegrity.deviceRecognitionVerdict and, if relevant, accountDetails.appLicensingVerdict. An UNEVALUATED verdict is not a pass.

  • What should happen when DCAppAttestService.shared.isSupported returns false?

    Treat it as no evidence, not as an attack, and handle it under the same policy as an outage. It returns false in the Simulator, on devices that cannot attest and in some app extensions. Decide each endpoint's unavailable-evidence policy in advance, and never let verification failure fall back to approval.

  • What happens to App Attest keys after a reinstall or device restore?

    They are lost. Keys do not move to a new device through backup and restore, and reinstalling the app loses them. When the server sees an unknown key or the app receives an invalid-key error, run attestation again rather than locking the user out.

Verify evidence before authorising requests

Tell us your platforms, framework and the backend operation you need to protect. We never request binaries, private keys, credentials or production data.

Or request a technical demo