# Device Integrity Patterns

Server verification, backend layout, error recovery and rollout for DeviceCheck
and App Attest. The client basics are in the [skill overview](../SKILL.md).

## Verifying an attestation on the server

Run every step; reject on the first failure.

1. Decode the attestation object and confirm the CBOR structure is well formed.
2. Extract the certificates and confirm they chain up to Apple's App Attest root.
3. Hash the challenge you issued (`clientDataHash = SHA256(challenge)`), concatenate
   `authData` and that hash, then hash again: `nonce = SHA256(authData || clientDataHash)`.
4. The leaf certificate carries an extension, OID `1.2.840.113635.100.8.2`,
   whose octet string has to be byte-for-byte equal to `nonce`.
5. Hash the credential certificate's public key. It must match the `keyId` the app sent.
6. The RP ID hash in `authData` must equal `SHA256(teamID + "." + bundleID)`.
7. The counter must be `0`, `aaguid` must name the environment you expect
   (development or production), and `credentialId` must equal `keyId`.
8. Persist the public key you just verified plus the receipt. Keep those, not the raw attestation object.
9. Burn the challenge only once steps 1 to 8 all pass, ideally in the same
   database transaction that writes the key record.

## Verifying an assertion on the server

1. Decode the CBOR assertion into `authenticatorData` and `signature`.
2. Compute `clientDataHash = SHA256(clientData)`. The client data carries the
   one-time challenge and the request context.
3. Verify `signature` with the stored public key over
   `SHA256(authenticatorData || clientDataHash)`.
4. Check the RP ID hash, and require the counter to be greater than the stored
   counter; on a key's first assertion anything above zero passes.
5. Confirm the embedded challenge is the one you issued and that the request
   context (method, path, body digest) matches the request that arrived.
6. Consume the challenge and store the new counter only after everything passes,
   preferably atomically.

## Server architecture

### Attestation versus assertion

| | Attestation | Assertion |
|---|---|---|
| When | Right after key generation | On every sensitive request |
| Proves | Real hardware, real app, holds this key | This request came from that attested instance |
| Frequency | Once per key | Once per request |

### Suggested services

- **Challenge endpoint** - hands out random nonces (16 or more bytes of
  entropy). Store it with a short time to live (about five minutes), its purpose,
  and the key or request it is meant for.
- **Attestation endpoint** - runs the attestation steps and stores the public
  key and receipt indexed by `keyId`.
- **Assertion middleware** - sits in front of purchases, account changes and
  other sensitive routes and runs the assertion steps.

Reject a challenge that is missing, expired, already used, or issued for a
different context. Consume it only after full verification; burning it the
moment it arrives turns a transient failure into a lost challenge and blocks a
safe retry.

### Risk assessment

Pair App Attest with Apple's fraud risk assessment for defense in depth. App
Attest confirms the binary is genuine; it cannot tell you whether the person
using it is abusing your service. It is also not user authentication, session
management, entitlement or subscription validation, TLS or pinning. Protected
endpoints still require all of those. Route sign-in work to `authentication`,
transport security to `ios-networking`, and Keychain or pinning policy to
`swift-security`.

## Handling `DCError`

```swift
import DeviceCheck

enum AttestRecovery {
    case retryLater, useDeviceCheck, inspectKeyState, fixInput, giveUp
}

func recovery(for error: DCError) -> AttestRecovery {
    switch error.code {
    case .unknownSystemFailure:
        return .retryLater          // transient system problem, back off
    case .featureUnsupported:
        return .useDeviceCheck      // no App Attest on this device or OS
    case .invalidKey:
        return .inspectKeyState     // regenerate only if the key is confirmed bad
    case .invalidInput:
        return .fixInput            // malformed clientDataHash or keyId
    case .serverUnavailable:
        return .retryLater          // same keyId and same hash on retry
    @unknown default:
        return .giveUp
    }
}
```

## Retry strategy

```swift
func withAttestRetry<T>(
    maxAttempts: Int = 3,
    _ operation: () async throws -> T
) async throws -> T {
    var lastError: Error?
    for attempt in 0..<maxAttempts {
        do {
            return try await operation()
        } catch let failure as DCError {
            guard failure.code == .serverUnavailable else { throw failure }
            lastError = failure
            let backoff = Duration.seconds(1 << (attempt + 1))
            try await Task.sleep(for: backoff)
        }
    }
    throw lastError ?? IntegrityError.attestationFailed
}
```

Only `.serverUnavailable` loops; every other error leaves immediately. Across
those retries nothing changes: one challenge, one `keyId`, one `clientDataHash`. Ask for a
new challenge only when you begin a brand-new attestation attempt.

## Rejected keys

`invalidKey` has three causes: `attestKey` on a key that is already attested,
`generateAssertion` with a key that was never attested, or the service
rejecting the key. Check local and server state first. When it confirms the key
is unusable, remove it from the Keychain with the **same account-scoped query
used to save it**, clear the cached value, and enroll again:

```swift
extension AttestKeyStore {
    func discardKeyAndReenroll() async throws -> String {
        let status = SecItemDelete(baseQuery as CFDictionary)
        if status != errSecSuccess && status != errSecItemNotFound {
            throw IntegrityError.keyNotGenerated
        }
        keyID = nil
        return try await generateKeyIfNeeded()
    }
}
```

`baseQuery` is the actor property from the skill overview and already
includes `"attest-key.\(accountID)"` as `kSecAttrAccount`. Building a separate
delete query with a bare account string would miss the stored item, leave the
stale `keyId` behind, and the next launch would load it again.

## Integration outline

1. Check `DCAppAttestService.shared.isSupported`; when false, fall back to `DCDevice`.
2. Call `generateKeyIfNeeded()` for the signed-in account. A new key is justified
   only for a new account, device or install enrollment, or for confirmed
   bad-key recovery.
3. Attest once per key. On `.serverUnavailable`, retry with the same challenge,
   key and hash.
4. For each sensitive request, fetch a one-time challenge and sign it together
   with the request context.
5. On `invalidKey`, establish whether the key is already attested, not attested,
   or rejected before replacing it.

## Gradual rollout

Apple recommends enabling App Attest gradually. Put it behind a remote feature
flag, keep the `DCDevice` fallback for unsupported devices, and ramp large
audiences in steps. Be ready to switch attestation traffic off if
`.serverUnavailable` errors or rate limiting climb.

## Environment entitlement

The environment entitlement (`...devicecheck.appattest-environment`) is
`development` or `production`. Development builds without it use the sandbox; TestFlight, App
Store and Enterprise builds always use production. Keys and receipts from one
environment fail in the other, so route server verification to match. App
Clips and extensions that call App Attest need the capability on their own
target as well.

### Error type used in the examples

```swift
enum IntegrityError: Error {
    case deviceCheckUnsupported
    case keyNotGenerated
    case attestationFailed
    case attestationVerificationFailed
    case assertionFailed
    case serverVerificationFailed
}
```

## Apple documentation

- [DeviceCheck framework](https://developer.apple.com/documentation/devicecheck), whose article list includes
  the app integrity overview, validating apps that talk to your server, the
  attestation object guide, and preparation steps for the App Attest service
- [Fraud risk assessment](https://developer.apple.com/documentation/devicecheck/assessing-fraud-risk)
- [DCAppAttestService](https://developer.apple.com/documentation/devicecheck/dcappattestservice)
- [DCDevice](https://developer.apple.com/documentation/devicecheck/dcdevice)
- The App Attest environment page under Bundle Resources > Entitlements
