CtrlK
BlogDocsLog inGet started
Tessl Logo

dpearson2699/swift-ios-skills

Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.

90

Quality

90%

Does it follow best practices?

Impact

Average score across 248 eval scenarios

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

device-integrity-patterns.mdskills/device-integrity/references/

Device Integrity Extended Patterns

Overflow reference for the device-integrity skill. Contains server verification details, advanced error handling, and integration patterns.

Contents

  • Server-Side Attestation Verification
  • Server-Side Assertion Verification
  • Server Architecture
  • Error Handling
  • Retry Strategy
  • Handling Rejected Keys
  • Full Integration Manager
  • Gradual Rollout
  • Environment Entitlement

Server-Side Attestation Verification

Your server must:

  1. Verify the attestation object is a valid CBOR-encoded structure.
  2. Extract the certificate chain and validate it against Apple's App Attest root CA.
  3. Compute clientDataHash = SHA256(challenge), append it to the decoded authData, then compute nonce = SHA256(authData || clientDataHash).
  4. Extract the credential certificate extension with OID 1.2.840.113635.100.8.2 and verify its octet string equals nonce.
  5. Verify the public-key hash matches the app-provided keyId.
  6. Verify the RP ID hash matches SHA256(teamID + "." + bundleID).
  7. Verify the initial counter is 0, the aaguid matches the expected development or production environment, and credentialId equals keyId.
  8. Store the verified public key and receipt for future assertion verification.
  9. Mark the challenge consumed only after every verification step succeeds, ideally in the same transaction that stores the key state.

See Validating apps that connect to your server for the full server verification algorithm.

Server-Side Assertion Verification

Your server must:

  1. Decode the assertion (CBOR).
  2. Recompute clientDataHash = SHA256(clientData), where clientData includes a one-time server challenge and request context.
  3. Verify the signature using the stored public key over SHA256(authenticatorData || clientDataHash).
  4. Verify the RP ID hash and the counter (greater than the stored counter, or greater than 0 for the first assertion).
  5. Confirm the embedded challenge matches the issued challenge and the request context binds the assertion to the received request.
  6. Mark the challenge consumed and update the stored counter only after every verification step succeeds, ideally atomically.

Server Architecture

Attestation vs. Assertion

PhaseWhenWhat It ProvesFrequency
AttestationAfter key generationThe key lives on a genuine Apple device running a legitimate instance of your appOnce per key
AssertionWith each sensitive requestThe request came from the attested app instancePer request

Recommended Server Architecture

  1. Challenge endpoint -- generate a random nonce with at least 16 bytes of entropy, store it server-side with a short TTL (e.g., 5 minutes), purpose, and expected request/key context.
  2. Attestation verification endpoint -- validate the attestation object, store the public key and receipt keyed by keyId.
  3. Assertion verification middleware -- verify assertions on sensitive endpoints (purchases, account changes).

Reject expired, missing, mismatched, or already-consumed challenges. Consume a challenge only after the corresponding attestation or assertion is fully verified; consuming on receipt can block safe retries after transient failures.

Risk Assessment

Combine App Attest with fraud risk assessment for defense in depth. App Attest alone does not guarantee the user is not abusing the app -- it confirms the app is genuine.

App Attest is not a user authentication, session, entitlement, TLS, certificate pinning, or subscription validation system. Keep those controls in the appropriate authentication, networking, or broader security layer, and require them in addition to App Attest on protected endpoints.

Error Handling

DCError Codes

import DeviceCheck

func handleAttestError(_ error: Error) {
    if let dcError = error as? DCError {
        switch dcError.code {
        case .unknownSystemFailure:
            // Transient system error -- retry with exponential backoff
            break
        case .featureUnsupported:
            // Device or OS does not support this feature
            // Fall back to alternative verification
            break
        case .invalidKey:
            // Already-attested key, unattested assertion key, or service rejection
            // Inspect local/server state; discard and regenerate only when bad
            break
        case .invalidInput:
            // The clientDataHash or keyId was malformed
            break
        case .serverUnavailable:
            // Retry attestation later with the same keyId and clientDataHash
            break
        @unknown default:
            break
        }
    }
}

Retry Strategy

import CryptoKit

extension AppAttestManager {
    func attestKeyWithRetry(challenge: Data, maxAttempts: Int = 3) async throws -> Data {
        guard let keyId else {
            throw DeviceIntegrityError.keyNotGenerated
        }

        let clientDataHash = Data(SHA256.hash(data: challenge))
        var lastError: Error?

        for attempt in 0..<maxAttempts {
            do {
                return try await service.attestKey(keyId, clientDataHash: clientDataHash)
            } catch let error as DCError where error.code == .serverUnavailable {
                lastError = error
                if attempt < maxAttempts - 1 {
                    try await Task.sleep(for: .seconds(pow(2.0, Double(attempt + 1))))
                }
            } catch {
                throw error // Non-retryable errors propagate immediately
            }
        }

        throw lastError ?? DeviceIntegrityError.attestationFailed
    }
}

Use the same challenge, keyId, and clientDataHash for each retry after .serverUnavailable. Do not fetch a fresh challenge for that retry loop unless you are also starting over with a new attestation attempt.

Handling Rejected Keys

DCError.invalidKey means the app called attestKey for an already-attested key, called generateAssertion with an unattested key, or the App Attest service rejected the key. If local/server state confirms the key cannot be used, delete the stored keyId and generate a new key:

extension AppAttestManager {
    func handleRejectedKey() async throws -> String {
        deleteKeyIdFromKeychain()
        keyId = nil
        return try await generateKeyIfNeeded()
    }

    private func deleteKeyIdFromKeychain() {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: "app-attest-key-id",
            kSecAttrService as String: Bundle.main.bundleIdentifier ?? ""
        ]
        SecItemDelete(query as CFDictionary)
    }
}

Full Integration Manager

Combine the patterns above into a single actor that manages the full lifecycle:

  1. Check isSupported and fall back to DCDevice tokens on unsupported devices.
  2. Call generateKeyIfNeeded() for each user account on each device, reuse the account/device-scoped keyId, and limit new key generation to new account/device/install enrollment or confirmed bad-key recovery.
  3. Attest once per key; if .serverUnavailable occurs, retry with the same challenge, key, and clientDataHash.
  4. For each sensitive request, obtain a one-time assertion challenge and sign client data that includes the challenge plus request context.
  5. Handle DCError.invalidKey by checking whether the key was already attested, not yet attested, or rejected before regenerating.

Gradual Rollout

Apple recommends a gradual rollout. Gate App Attest behind a remote feature flag and fall back to DCDevice tokens on unsupported devices. For large apps, ramp production adoption gradually and be prepared to reduce attestation traffic if .serverUnavailable or rate-limit behavior increases during rollout.

Environment Entitlement

Set the App Attest environment in your entitlements file. Use development during testing and production for App Store builds:

<key>com.apple.developer.devicecheck.appattest-environment</key>
<string>production</string>

When the entitlement is omitted during development, the app uses the App Attest sandbox by default. After distribution through TestFlight, the App Store, or the Apple Developer Enterprise Program, the app ignores the entitlement value and uses production. Sandbox keys and receipts do not work in production, and production keys and receipts do not work in sandbox.

If an App Clip or extension uses App Attest, configure the capability for that target too. App Attest is supported only in Action, extensible SSO, and watchOS extensions; other extension types are unsupported even if isSupported returns true.

Error Type

enum DeviceIntegrityError: Error {
    case deviceCheckUnsupported
    case keyNotGenerated
    case attestationFailed
    case attestationVerificationFailed
    case assertionFailed
    case serverVerificationFailed
}

Apple Documentation Links

skills

device-integrity

README.md

tile.json