Greetings, traveler!
Apple App Attest lets your server verify that a request comes from a legitimate instance of your app running on Apple hardware.
This matters when the server exposes something that should not be available to arbitrary clients. A modified copy of the app, a script that reproduces your API calls, or a client that changes request data should not automatically receive the same access as the original app.
App Attest provides cryptographic evidence that the server can verify instead of relying on a secret embedded in the application binary.
The problem App Attest solves
Suppose an iOS application requests a private resource:
POST /api/private-content
Authorization: Bearer <token>
HTTPS protects the connection, but it does not prove that the client making the request is your original application.
An attacker who understands the API could reproduce the request from another client. Putting a static API key inside the application does not solve the problem either. The application eventually needs access to that key, which means an attacker analyzing the binary or runtime may be able to recover it.
App Attest changes the trust model.
Instead of asking the application to present a shared secret, your server asks it to provide cryptographic proof created using a key associated with that app instance.
Apple describes App Attest as a way for the server to verify three properties: the request comes from a genuine Apple device, it comes from your genuine application, and the protected payload has not been modified.
The App Attest flow
App Attest is part of the DeviceCheck framework and is exposed through DCAppAttestService.
The basic flow has two phases:
- Attest a newly generated key.
- Use that key to create assertions for later requests.
The application starts by checking whether App Attest is available:
import DeviceCheck
let service = DCAppAttestService.shared
guard service.isSupported else {
// Apply your fallback policy.
return
}Apple explicitly requires checking isSupported because App Attest is not available in every execution environment.
The app then generates a cryptographic key:
let keyID = try await service.generateKey()The private key is bound to the Secure Enclave and is not exposed directly to your application. The returned keyID identifies the key and is later used for attestation and assertions.
Generating the key alone proves nothing to your server. The next step is attestation.
Attesting the key
Before starting attestation, the app should request a unique challenge from your server.
For example:
GET /api/app-attest/challenge
The server generates random data and remembers it temporarily. The application hashes data containing that challenge:
import CryptoKit
let clientDataHash = Data(
SHA256.hash(data: challenge)
)It then asks Apple to attest the generated key:
let attestation = try await service.attestKey(
keyID,
clientDataHash: clientDataHash
)The app sends the resulting attestation object, the keyID, and the data required to reconstruct the expected hash to your server.
Apple recommends a unique, one-time server challenge because it makes replaying an old attestation significantly harder.
You should never treat successful attestKey execution inside the application as proof that the client is trusted. A modified application controls its own code and could simply pretend that verification succeeded. Apple explicitly requires verification of the attestation object on your server.
What the server verifies
The attestation object follows the Web Authentication data format and contains information that allows the server to validate the key and application identity.
Apple’s validation procedure includes checking the Apple certificate chain, nonce, app identity, credential identifier, environment, and other authenticator data.
The RP ID is derived from your App ID. The server compares it with the value expected for your application.
The initial attestation also contains an assertion counter. For a newly attested key, the server expects this counter to be 0.
After verification succeeds, the server extracts and stores the public key associated with the keyID. That public key is later used to validate assertions created by the application.
Implementing this validation correctly matters. Apple provides a dedicated Attestation Object Validation Guide with expected values that can be used to test a server implementation.
Assertions protect later requests
You normally do not call attestKey every time the app communicates with your API. Attestation establishes trust in the key. After that, the same key can generate assertions for later requests.
Consider an endpoint that returns premium content:
POST /api/premium-content
The server first provides another unique challenge. The application can combine this challenge with the request data:
struct ProtectedRequest: Codable {
let challenge: Data
let resourceID: String
}After encoding the request, calculate its SHA256 hash:
let requestData = try JSONEncoder().encode(request)
let clientDataHash = Data(
SHA256.hash(data: requestData)
)Then create an assertion:
let assertion = try await DCAppAttestService.shared.generateAssertion(
keyID,
clientDataHash: clientDataHash
)The request sent to your backend can include the original client data, keyID, and assertion.
Your server already has the public key obtained during attestation. It uses that key to verify the assertion signature and checks that the assertion corresponds to the request data it received.
This is useful because an attacker cannot simply capture a valid assertion and attach it to a different request. The assertion is tied to the hash supplied as clientDataHash.
The assertion counter
Assertions contain a counter associated with the attested key. The server stores the counter from the last accepted assertion and requires the next one to contain a greater value. The first assertion must have a counter greater than 0.
This gives the server another replay protection mechanism. A previously accepted assertion should not become valid again simply because somebody sends the same bytes to the API later.
Your server therefore needs to maintain state for each attested key:
keyID
publicKey
lastCounter
userID
environment
Apple also recommends ensuring that a public key is not unexpectedly associated with multiple users.
When to use App Attest
App Attest makes sense when your backend needs to distinguish the real application from clients that reproduce or modify its behavior.
One example is access to premium content:
iOS app
|
| assertion
v
API
|
| verified
v
premium resource
Another case is an operation where manipulating request parameters would have business consequences. A game backend, for example, may want stronger evidence around requests that submit results, redeem rewards, or unlock server-controlled resources.
It can also protect an endpoint that returns configuration or sensitive application data that should only be available to legitimate app installations.
Apple itself recommends using assertions at sensitive points in the application lifecycle, such as when downloading premium content. There is no fixed limit on how many assertions you can create with an attested key, but that does not mean every ordinary API request needs one.
App Attest is not user authentication
App Attest answers a different question from Sign in with Apple, OAuth, access tokens, or your own login system.
Authentication asks:
Who is this user?
App Attest helps your server answer:
Can I trust the application instance making this request?
A banking application, for example, could still authenticate the customer with its normal authentication system while separately using App Attest as another signal before allowing sensitive operations.
The two mechanisms solve different problems and can be used together.
App Attest does not replace HTTPS
Assertions can prove that particular client data was signed with an attested key, but App Attest is not a replacement for TLS.
Your API should still use HTTPS for transport confidentiality and server authentication. App Attest adds client integrity information on top of the existing secure transport.
Handle unsupported environments
DCAppAttestService.isSupported must be part of the integration rather than an assertion that always succeeds:
guard DCAppAttestService.shared.isSupported else {
return
}Your backend needs a policy for these cases. That policy depends on what the API protects. You might allow access with additional restrictions, require another verification mechanism, or reject particularly sensitive operations.
App Attest should usually contribute to a risk decision rather than cause the application to crash or enter an unusable state when the API is unavailable. Apple also notes that App Attest cannot provide absolute certainty about a device whose operating system itself has been compromised.
Keep and reuse the generated key
Generating fresh keys for every launch or request is the wrong model. Keys survive normal application updates. They do not survive application reinstallation, device migration, or restoration from backup. When one of those events occurs, the application needs to generate and attest a new key.
Apple recommends limiting unnecessary key generation because the number of attestations associated with a device is also useful as a fraud signal.
If attestKey fails because the App Attest server is temporarily unavailable, Apple recommends retrying later with the same key and the same client data hash instead of immediately generating another key.
Development and production environments
App Attest separates development and production keys. During development, Apple provides a sandbox environment so repeated testing does not affect production device metrics. Development and production attestations have different identifiers inside the attestation data, and your server must validate them against the expected environment.
Do not mix development and production records on the server. Apple also recommends rolling App Attest out gradually for applications with very large user bases because attesting many existing installations simultaneously can trigger service throttling.
Fraud assessment
A successful attestation contains a receipt that the server can store. That receipt can later be sent to Apple’s servers to obtain a fraud assessment metric. One of the signals is an approximate count of App Attest keys created for your application on a particular device.
An unexpectedly large number of attestations can indicate that a compromised device is generating assertions for many modified application instances.
Apple does not present this metric as proof of fraud. It is another signal that your backend can include in its risk model.
App Attest in iOS 27
Apple expanded the information available through App Attest in iOS 27.
Attestation and assertion data can expose the app’s launch validation category and bundle version. The validation category lets the server distinguish between categories such as App Store, TestFlight, development-signed, Enterprise or ad hoc, and other launch conditions.
The server can compare these values with what it expects for the version and distribution method of the application. For example, a production service could reject an unexpected development-signed build trying to access an endpoint intended only for the App Store version.
A practical architecture
A complete integration usually looks like this:
First launch or new installation
App
|
| request challenge
v
Server
|
| challenge
v
App
|
| generateKey()
| attestKey()
|
| keyID + attestation
v
Server
|
| verify attestation
| store public key
v
Trusted app instance
Later, when the application performs a sensitive operation:
App
|
| request challenge
v
Server
|
| challenge
v
App
|
| hash challenge + request
| generateAssertion()
|
| request + keyID + assertion
v
Server
|
| verify signature
| verify App ID
| verify challenge
| verify counter
| verify expected app metadata
v
Execute protected operation
The trust decision remains on the server. The application only produces evidence.
That distinction is the most important part of an App Attest implementation. If the client decides whether its own attestation is valid, an attacker controlling that client can change the decision. When the server verifies the evidence independently, modifying the application no longer provides the same shortcut.
