Attestation (Driver Layer)#

At the Driver Layer, attestation is the protocol that lets the Curity Identity Server trust that an HAAPI request originates from a genuine, unmodified copy of a registered app. Both platforms wrap their native attestation services and exchange the resulting attestation data for a Client Attestation Token (CAT), which the client then uses to obtain an HAAPI access token. For the conceptual overview, see Attestation and Fallback.

Per-Platform Mechanisms#

The iOS Driver uses Apple’s DCAppAttestService (the DeviceCheck framework) to attest the cryptographic key that signs HAAPI requests. The attestation protocol is performed automatically when HaapiTokenManager first acquires a CAT.

Configuration on the server side carries the Apple-assigned app-id (team ID plus bundle ID):

<attestation>
    <ios>
        <app-id>01234abcde.app.curity.io</app-id>
    </ios>
</attestation>

Optionally configure an iOS attestation policy under facilities/client-attestation/ios-policy to switch between production and non-production modes or to override certificate-chain validation.

The Android Driver uses hardware-backed Key Attestation through the Android Keystore. The TEE or StrongBox produces a certificate chain rooted in Google’s hardware-attestation CA, which the server validates as part of issuing a CAT.

Configuration on the server side carries the Android package name and the SHA-256 hash of the signing certificate (Base64-encoded):

<attestation>
    <android>
        <package-name>com.example.client.app</package-name>
        <signature-digest>Z2DK...9oKQ=</signature-digest>
    </android>
</attestation>

To obtain the signature-digest, run ./gradlew signingReport, take the SHA-256 line, and convert it to Base64.

Custom Policies for Emulators and Development#

Hardware attestation does not work on emulators, simulators, or jailbroken devices. For development builds, define a custom attestation policy under facilities/client-attestation and reference it from the client.

Custom policies relax the security guarantees that attestation provides. Use them for development and emulator scenarios only — never in production.

<facilities xmlns="https://curity.se/ns/conf/base">
    <client-attestation>
        <ios-policy xmlns="https://curity.se/ns/conf/client-attestation">
            <id>non-prod-policy</id>
            <mode>non-production</mode>
            <override-certificate-chain-validation>
                <do-not-validate-certificate-chain/>
            </override-certificate-chain-validation>
        </ios-policy>
    </client-attestation>
</facilities>

Reference the policy from the client configuration via attestation/ios/ios-policy.

<facilities xmlns="https://curity.se/ns/conf/base">
    <client-attestation>
        <android-policy xmlns="https://curity.se/ns/conf/client-attestation">
            <id>emulator-policy</id>
            <verify-boot-state>false</verify-boot-state>
            <minimum-security-level>software</minimum-security-level>
            <override-certificate-chain-validation>
                <do-not-validate-certificate-chain/>
            </override-certificate-chain-validation>
        </android-policy>
    </client-attestation>
</facilities>

Reference the policy from the client configuration via attestation/android/android-policy.

DCR Fallback#

When attestation fails — older operating system versions, jailbroken devices, or rare hardware faults reported by Apple as Error Domain=com.apple.devicecheck.error Code=3 — the Driver Layer raises a non-attestation error. The next step depends on whether the client is configured for Dynamic Client Registration fallback:

  • With DCR configured: the client registers a per-device dynamic OAuth client and uses that for subsequent token requests. The fallback is wired up via the SDK Layer’s HaapiAccessor rather than at the Driver Layer directly.
  • Without DCR configured: the flow halts. The application surfaces the failure to the user, typically directing them through an alternative authentication path or to support.

For the conceptual model and security trade-offs, see Dynamic Client Registration (DCR).

App Attest Key Limit (iOS)#

Every new key the iOS Driver generates and attests counts against an app-wide limit: at most 6 new keys at once, then one more per hour, shared by every HaapiTokenManager in the app whichever identity server it talks to. Apple tracks how many keys each device attests and throttles attestation traffic per app, so the limit stops a misconfigured or misbehaving server — one that keeps rejecting the attestation state, omits it, or fails right after an attestation — from making devices attest keys without end. Reusing a key that is already attested is never limited, so a device with a working key is unaffected.

When a request needs a new key beyond the limit, the Driver throws HaapiError.attestationFailure without contacting Apple. Its error is attestation_limited and its cause is a HaapiAttestationLimitRefusal. The SDK Layer does not treat the refusal as a device that cannot attest: building a HaapiAccessor fails with that error instead of falling back to Dynamic Client Registration. A device that genuinely cannot attest still falls back on its first failing request.

Decide when to retry from the cause. The error’s recoverySuggestion, .retryable(condition: .whenAppInForeground), carries no time, and an attempt before the cause’s nextAttemptAfter fails the same way — at once and without contacting Apple, so it is harmless but pointless. Tell the user to try again later, or retry the first time the app is in the foreground after that time:

if let refusal = (error as? IdsvrError)?.cause as? HaapiAttestationLimitRefusal {
    if let next = refusal.nextAttemptAfter {
        // Don't retry before `next`; an earlier attempt fails the same way.
    } else {
        // The limit could not be read or saved; retry when the app is next in the foreground.
    }
}

A nil nextAttemptAfter means the limit could not be read from or saved to storage; no new key is allowed until it can be.

If the limit is reached while the Driver restarts after the server rejected the attestation state, or after generating or attesting a new key failed, that request fails with the error that caused the restart; the next request fails with attestation_limited.

The limit is fixed and cannot be configured. Its state lives in the app’s keychain and survives reinstalls, so automated tests on a real device count toward it. To run such tests without the limit, launch the app with the environment variable IDSVR_HAAPI_ATTESTATION_LIMIT set to disabled — in an Xcode scheme, in the environment variables of an .xctestplan, or in XCUIApplication.launchEnvironment. The SDK logs a warning while the variable is active. App Store and TestFlight launches never carry it; never set it from app code.

Keep New Keys to a Minimum#

Every key a device attests is counted outside your app, so treat a new key as a cost, never as a way to recover from an error:

  • Fraud signals. Apple’s risk metric is an approximate count of the keys your app attested on a device over the past 30 days, and a count higher than expected reads as a compromised device. Apple asks apps to generate a new key only after a reinstall, a device migration or restore, or for a new user; see Establishing your app’s integrity.
  • Throttling. Apple throttles attestation traffic per app and recommends fewer than 100 attestation requests per second across all installations, a threshold that can change. Installations that keep attesting new keys can slow attestation down for every user of the app, new installs included.
  • The SDK’s limit. Once a device has used its 6 new keys, it gets one more per hour. Until then, building a HaapiAccessor, or a request that needs a new key, fails with attestation_limited and does not fall back to Dynamic Client Registration.

An identity server accepts an attestation state only from the server and client it issued it to. Any other server, or another client on the same server, rejects it, and the SDK then generates and attests a new key. Keep keys apart per server and client:

  • Give each HaapiTokenManager a stable name per server and client. The default name is the client ID, so two servers that use the same client ID, such as staging and production, share one key unless you set a name with HaapiTokenManagerBuilder.setName(_:).
  • The capability check that runs when a HaapiAccessor is built uses one key for every configuration. An app that builds accessors for more than one server or client should give each its own name with HaapiAccessorBuilder.setAttestationValidationKeyName(keyName:).
  • Never derive a name from something that changes between launches, such as a random value or a timestamp: every new name is a new key.

An app that ran a release earlier than 4.1.0 keeps the attestation state saved by that release, and every manager uses it whatever its name.

Disabling Attestation for Testing#

For controlled testing scenarios, attestation validation can be disabled per-client on the server. The client still performs the attestation protocol, but the server does not validate the attestation data.

<attestation>
    <disable-attestation-validation/>
</attestation>

Do not use this in production. When attestation validation is disabled, any application can act as the configured client. Restrict this setting to non-production environments — for example, integration testing where the real attestation services are unavailable.

Was this helpful?