device-integrity

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Device Integrity

设备完整性

Verify that requests to your server come from a genuine Apple device running your unmodified app. DeviceCheck provides per-device bits for simple flags (e.g., "claimed promo offer"). App Attest uses Secure Enclave keys and Apple attestation to cryptographically prove app legitimacy on each request.
验证发送到您服务器的请求来自运行未修改版应用的正版Apple设备。DeviceCheck提供每设备标识位,用于设置简单标记(例如"已领取促销优惠")。App Attest使用Secure Enclave密钥和Apple证明机制,通过加密方式在每次请求中证明应用的合法性。

Contents

目录

DCDevice (DeviceCheck Tokens)

DCDevice(DeviceCheck令牌)

DCDevice
generates a unique, ephemeral token that identifies a device. The token is sent to your server, which then communicates with Apple's servers to read or set two per-device bits. Available on iOS 11+.
DCDevice
生成唯一的临时令牌,用于标识设备。将该令牌发送到您的服务器,服务器再与Apple服务器通信,读取或设置两个每设备标识位。支持iOS 11及以上版本。

Token Generation

令牌生成

swift
import DeviceCheck

func generateDeviceToken() async throws -> Data {
    guard DCDevice.current.isSupported else {
        throw DeviceIntegrityError.deviceCheckUnsupported
    }

    return try await DCDevice.current.generateToken()
}
swift
import DeviceCheck

func generateDeviceToken() async throws -> Data {
    guard DCDevice.current.isSupported else {
        throw DeviceIntegrityError.deviceCheckUnsupported
    }

    return try await DCDevice.current.generateToken()
}

Sending the Token to Your Server

将令牌发送到服务器

swift
func sendTokenToServer(_ token: Data) async throws {
    let tokenString = token.base64EncodedString()

    var request = URLRequest(url: serverURL.appending(path: "verify-device"))
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = try JSONEncoder().encode(["device_token": tokenString])

    let (_, response) = try await URLSession.shared.data(for: request)
    guard let httpResponse = response as? HTTPURLResponse,
          httpResponse.statusCode == 200 else {
        throw DeviceIntegrityError.serverVerificationFailed
    }
}
swift
func sendTokenToServer(_ token: Data) async throws {
    let tokenString = token.base64EncodedString()

    var request = URLRequest(url: serverURL.appending(path: "verify-device"))
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = try JSONEncoder().encode(["device_token": tokenString])

    let (_, response) = try await URLSession.shared.data(for: request)
    guard let httpResponse = response as? HTTPURLResponse,
          httpResponse.statusCode == 200 else {
        throw DeviceIntegrityError.serverVerificationFailed
    }
}

Server-Side Overview

服务器端概述

Your server uses the device token to call Apple's DeviceCheck API endpoints:
EndpointPurpose
https://api.devicecheck.apple.com/v1/query_two_bits
Read the two bits for a device
https://api.devicecheck.apple.com/v1/update_two_bits
Set the two bits for a device
https://api.devicecheck.apple.com/v1/validate_device_token
Validate a device token without reading bits
The server authenticates with a DeviceCheck private key from the Apple Developer portal, creating a signed JWT for each request.
您的服务器使用设备令牌调用Apple的DeviceCheck API端点:
端点用途
https://api.devicecheck.apple.com/v1/query_two_bits
读取设备的两个标识位
https://api.devicecheck.apple.com/v1/update_two_bits
设置设备的两个标识位
https://api.devicecheck.apple.com/v1/validate_device_token
验证设备令牌,无需读取标识位
服务器使用Apple开发者门户提供的DeviceCheck私钥进行身份验证,为每个请求创建签名的JWT。

What the Two Bits Are For

两个标识位的用途

Apple stores two Boolean values per device per developer team. You decide what they mean. Common uses:
  • Bit 0: Device has claimed a promotional offer.
  • Bit 1: Device has been flagged for fraud.
Bits persist across app reinstall; device reset does not clear them. You control when to reset them via the server API.
Apple为每个开发者团队的每台设备存储两个布尔值。您可以自定义它们的含义,常见用途包括:
  • 位0: 设备已领取促销优惠。
  • 位1: 设备已被标记为欺诈风险。
标识位在应用重新安装后仍会保留;设备重置不会清除它们。您可以通过服务器API控制何时重置这些标识位。

DCAppAttestService (App Attest)

DCAppAttestService(App Attest)

DCAppAttestService
validates that a specific instance of your app on a specific device is legitimate. It uses a hardware-backed key in the Secure Enclave to create cryptographic attestations and assertions. Available on iOS 14+.
The flow has three phases:
  1. Key generation -- create a key pair in the Secure Enclave.
  2. Attestation -- Apple certifies the key belongs to a genuine Apple device running your app.
  3. Assertion -- sign server requests with the attested key to prove ongoing legitimacy.
DCAppAttestService
验证特定设备上的应用实例是否合法。它使用Secure Enclave中的硬件备份密钥创建加密证明和断言。支持iOS 14及以上版本。
流程分为三个阶段:
  1. 密钥生成 —— 在Secure Enclave中创建密钥对。
  2. 证明 —— Apple认证该密钥属于运行您应用的正版Apple设备。
  3. 断言 —— 使用已证明的密钥对服务器请求进行签名,以持续证明合法性。

Checking Support

检查支持情况

swift
import DeviceCheck

let attestService = DCAppAttestService.shared

guard attestService.isSupported else {
    // Fall back to DCDevice token or other risk assessment.
    // App Attest is not available on simulators or all device models.
    return
}
swift
import DeviceCheck

let attestService = DCAppAttestService.shared

guard attestService.isSupported else {
    // 回退到DCDevice令牌或其他风险评估方案。
    // App Attest在模拟器或部分设备型号上不可用。
    return
}

App Attest Key Generation

App Attest密钥生成

Generate a cryptographic key pair stored in the Secure Enclave. The returned
keyId
is a string identifier you persist (e.g., in Keychain) for later attestation and assertion calls.
swift
import DeviceCheck

actor AppAttestManager {
    private let service = DCAppAttestService.shared
    private var keyId: String?

    /// Generate and persist a key pair for App Attest.
    func generateKeyIfNeeded() async throws -> String {
        if let existingKeyId = loadKeyIdFromKeychain() {
            self.keyId = existingKeyId
            return existingKeyId
        }

        let newKeyId = try await service.generateKey()
        saveKeyIdToKeychain(newKeyId)
        self.keyId = newKeyId
        return newKeyId
    }

    // MARK: - Keychain helpers (simplified)

    private func saveKeyIdToKeychain(_ keyId: String) {
        let data = Data(keyId.utf8)
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: "app-attest-key-id",
            kSecAttrService as String: Bundle.main.bundleIdentifier ?? "",
            kSecValueData as String: data,
            kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
        ]
        SecItemDelete(query as CFDictionary) // Remove old if exists
        SecItemAdd(query as CFDictionary, nil)
    }

    private func loadKeyIdFromKeychain() -> String? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: "app-attest-key-id",
            kSecAttrService as String: Bundle.main.bundleIdentifier ?? "",
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]
        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)
        guard status == errSecSuccess, let data = result as? Data else { return nil }
        return String(data: data, encoding: .utf8)
    }
}
Important: Generate the key once and persist the
keyId
. Generating a new key invalidates any previous attestation.
生成存储在Secure Enclave中的加密密钥对。返回的
keyId
是一个字符串标识符,您需要持久化存储(例如在Keychain中),以便后续调用证明和断言接口。
swift
import DeviceCheck

actor AppAttestManager {
    private let service = DCAppAttestService.shared
    private var keyId: String?

    /// 为App Attest生成并持久化密钥对。
    func generateKeyIfNeeded() async throws -> String {
        if let existingKeyId = loadKeyIdFromKeychain() {
            self.keyId = existingKeyId
            return existingKeyId
        }

        let newKeyId = try await service.generateKey()
        saveKeyIdToKeychain(newKeyId)
        self.keyId = newKeyId
        return newKeyId
    }

    // MARK: - Keychain辅助方法(简化版)

    private func saveKeyIdToKeychain(_ keyId: String) {
        let data = Data(keyId.utf8)
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: "app-attest-key-id",
            kSecAttrService as String: Bundle.main.bundleIdentifier ?? "",
            kSecValueData as String: data,
            kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
        ]
        SecItemDelete(query as CFDictionary) // 若存在则删除旧条目
        SecItemAdd(query as CFDictionary, nil)
    }

    private func loadKeyIdFromKeychain() -> String? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: "app-attest-key-id",
            kSecAttrService as String: Bundle.main.bundleIdentifier ?? "",
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]
        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)
        guard status == errSecSuccess, let data = result as? Data else { return nil }
        return String(data: data, encoding: .utf8)
    }
}
重要提示: 仅生成一次密钥并持久化
keyId
。生成新密钥会使之前的任何证明失效。

App Attest Attestation Flow

App Attest证明流程

Attestation proves that the key was generated on a genuine Apple device running your unmodified app. You perform attestation once per key, then store the attestation object on your server.
证明用于验证密钥是在运行未修改版应用的正版Apple设备上生成的。您需要为每个密钥执行一次证明,然后将证明对象存储在服务器上。

Client-Side Attestation

客户端证明

swift
import DeviceCheck
import CryptoKit

extension AppAttestManager {
    /// Attest the key with Apple. Send the attestation object to your server.
    func attestKey() async throws -> Data {
        guard let keyId else {
            throw DeviceIntegrityError.keyNotGenerated
        }

        // 1. Request a one-time challenge from your server
        let challenge = try await fetchServerChallenge()

        // 2. Hash the challenge (Apple requires a SHA-256 hash)
        let challengeHash = Data(SHA256.hash(data: challenge))

        // 3. Ask Apple to attest the key
        let attestation = try await service.attestKey(keyId, clientDataHash: challengeHash)

        // 4. Send the attestation object to your server for verification
        try await sendAttestationToServer(
            keyId: keyId,
            attestation: attestation,
            challenge: challenge
        )

        return attestation
    }

    private func fetchServerChallenge() async throws -> Data {
        let url = serverURL.appending(path: "attest/challenge")
        let (data, _) = try await URLSession.shared.data(from: url)
        return data
    }

    private func sendAttestationToServer(
        keyId: String,
        attestation: Data,
        challenge: Data
    ) async throws {
        var request = URLRequest(url: serverURL.appending(path: "attest/verify"))
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")

        let payload: [String: String] = [
            "key_id": keyId,
            "attestation": attestation.base64EncodedString(),
            "challenge": challenge.base64EncodedString()
        ]
        request.httpBody = try JSONEncoder().encode(payload)

        let (_, response) = try await URLSession.shared.data(for: request)
        guard let httpResponse = response as? HTTPURLResponse,
              httpResponse.statusCode == 200 else {
            throw DeviceIntegrityError.attestationVerificationFailed
        }
    }
}
swift
import DeviceCheck
import CryptoKit

extension AppAttestManager {
    /// 通过Apple证明密钥。将证明对象发送到服务器。
    func attestKey() async throws -> Data {
        guard let keyId else {
            throw DeviceIntegrityError.keyNotGenerated
        }

        // 1. 从服务器请求一次性挑战
        let challenge = try await fetchServerChallenge()

        // 2. 对挑战进行哈希(Apple要求使用SHA-256哈希)
        let challengeHash = Data(SHA256.hash(data: challenge))

        // 3. 请求Apple证明该密钥
        let attestation = try await service.attestKey(keyId, clientDataHash: challengeHash)

        // 4. 将证明对象发送到服务器进行验证
        try await sendAttestationToServer(
            keyId: keyId,
            attestation: attestation,
            challenge: challenge
        )

        return attestation
    }

    private func fetchServerChallenge() async throws -> Data {
        let url = serverURL.appending(path: "attest/challenge")
        let (data, _) = try await URLSession.shared.data(from: url)
        return data
    }

    private func sendAttestationToServer(
        keyId: String,
        attestation: Data,
        challenge: Data
    ) async throws {
        var request = URLRequest(url: serverURL.appending(path: "attest/verify"))
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")

        let payload: [String: String] = [
            "key_id": keyId,
            "attestation": attestation.base64EncodedString(),
            "challenge": challenge.base64EncodedString()
        ]
        request.httpBody = try JSONEncoder().encode(payload)

        let (_, response) = try await URLSession.shared.data(for: request)
        guard let httpResponse = response as? HTTPURLResponse,
              httpResponse.statusCode == 200 else {
            throw DeviceIntegrityError.attestationVerificationFailed
        }
    }
}

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. Verify the
    nonce
    in the attestation matches
    SHA256(challenge)
    .
  4. Extract and store the public key and receipt for future assertion verification.
See Validating apps that connect to your server for the full server verification algorithm.
您的服务器必须:
  1. 验证证明对象是有效的CBOR编码结构。
  2. 提取证书链并根据Apple的App Attest根CA进行验证。
  3. 验证证明中的
    nonce
    SHA256(challenge)
    匹配。
  4. 提取并存储公钥和回执,用于后续的断言验证。
有关完整的服务器验证算法,请参阅验证连接到您服务器的应用

App Attest Assertion Flow

App Attest断言流程

After attestation, use assertions to sign individual requests. Each assertion proves the request came from the attested app instance.
完成证明后,使用断言对单个请求进行签名。每个断言都用于证明请求来自已验证的应用实例。

Client-Side Assertion

客户端断言

swift
import DeviceCheck
import CryptoKit

extension AppAttestManager {
    /// Generate an assertion to accompany a server request.
    /// - Parameter requestData: The request payload to sign (e.g., JSON body).
    /// - Returns: The assertion data to include with the request.
    func generateAssertion(for requestData: Data) async throws -> Data {
        guard let keyId else {
            throw DeviceIntegrityError.keyNotGenerated
        }

        // Hash the request data -- the server will verify this matches
        let clientDataHash = Data(SHA256.hash(data: requestData))

        return try await service.generateAssertion(keyId, clientDataHash: clientDataHash)
    }
}
swift
import DeviceCheck
import CryptoKit

extension AppAttestManager {
    /// 生成断言以伴随服务器请求。
    /// - Parameter requestData: 要签名的请求负载(例如JSON主体)。
    /// - Returns: 要包含在请求中的断言数据。
    func generateAssertion(for requestData: Data) async throws -> Data {
        guard let keyId else {
            throw DeviceIntegrityError.keyNotGenerated
        }

        // 对请求数据进行哈希——服务器将验证该哈希是否匹配
        let clientDataHash = Data(SHA256.hash(data: requestData))

        return try await service.generateAssertion(keyId, clientDataHash: clientDataHash)
    }
}

Using Assertions in Network Requests

在网络请求中使用断言

swift
extension AppAttestManager {
    /// Perform an attested API request.
    func makeAttestedRequest(
        to url: URL,
        method: String = "POST",
        body: Data
    ) async throws -> (Data, URLResponse) {
        let assertion = try await generateAssertion(for: body)

        var request = URLRequest(url: url)
        request.httpMethod = method
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue(assertion.base64EncodedString(), forHTTPHeaderField: "X-App-Assertion")
        request.httpBody = body

        return try await URLSession.shared.data(for: request)
    }
}
swift
extension AppAttestManager {
    /// 执行已证明的API请求。
    func makeAttestedRequest(
        to url: URL,
        method: String = "POST",
        body: Data
    ) async throws -> (Data, URLResponse) {
        let assertion = try await generateAssertion(for: body)

        var request = URLRequest(url: url)
        request.httpMethod = method
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue(assertion.base64EncodedString(), forHTTPHeaderField: "X-App-Assertion")
        request.httpBody = body

        return try await URLSession.shared.data(for: request)
    }
}

Server-Side Assertion Verification

服务器端断言验证

Your server must:
  1. Decode the assertion (CBOR).
  2. Verify the authenticator data, including the counter (must be greater than the stored counter).
  3. Verify the signature using the stored public key from attestation.
  4. Confirm the
    clientDataHash
    matches the SHA-256 of the received request body.
  5. Update the stored counter to prevent replay attacks.
您的服务器必须:
  1. 解码断言(CBOR格式)。
  2. 验证验证器数据,包括计数器(必须大于存储的计数器值)。
  3. 使用证明阶段存储的公钥验证签名。
  4. 确认
    clientDataHash
    与收到的请求主体的SHA-256哈希匹配。
  5. 更新存储的计数器以防止重放攻击。

Server Verification Guidance

服务器验证指南

Attestation vs. Assertion

证明 vs 断言

PhaseWhenWhat It ProvesFrequency
AttestationAfter key generationThe key lives on a genuine Apple device running your unmodified appOnce per key
AssertionWith each sensitive requestThe request came from the attested app instancePer request
阶段时机证明内容频率
证明密钥生成后密钥存在于运行未修改版应用的正版Apple设备上每个密钥一次
断言每次敏感请求时请求来自已验证的应用实例每次请求

Recommended Server Architecture

推荐的服务器架构

  1. Challenge endpoint -- generate a random nonce, store it server-side with a short TTL (e.g., 5 minutes).
  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).
  1. 挑战端点 —— 生成随机一次性值,在服务器端存储并设置短TTL(例如5分钟)。
  2. 证明验证端点 —— 验证证明对象,存储公钥和回执,以
    keyId
    为键。
  3. 断言验证中间件 —— 在敏感端点(如购买、账户变更)验证断言。

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与欺诈风险评估结合使用,实现深度防御。仅App Attest无法保证用户没有滥用应用——它只能确认应用是正版的。

Error Handling

错误处理

DCError Codes

DCError错误码

swift
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:
            // Key is corrupted or was invalidated
            // Generate a new key and re-attest
            break
        case .invalidInput:
            // The clientDataHash or keyId was malformed
            break
        case .serverUnavailable:
            // Apple's attestation server is unreachable -- retry later
            break
        @unknown default:
            break
        }
    }
}
swift
import DeviceCheck

func handleAttestError(_ error: Error) {
    if let dcError = error as? DCError {
        switch dcError.code {
        case .unknownSystemFailure:
            // 临时系统错误——使用指数退避重试
            break
        case .featureUnsupported:
            // 设备或操作系统不支持该功能
            // 回退到替代验证方案
            break
        case .invalidKey:
            // 密钥已损坏或失效
            // 生成新密钥并重新证明
            break
        case .invalidInput:
            // clientDataHash或keyId格式不正确
            break
        case .serverUnavailable:
            // Apple的证明服务器不可用——稍后重试
            break
        @unknown default:
            break
        }
    }
}

Retry Strategy

重试策略

swift
extension AppAttestManager {
    func attestKeyWithRetry(maxAttempts: Int = 3) async throws -> Data {
        var lastError: Error?

        for attempt in 0..<maxAttempts {
            do {
                return try await attestKey()
            } catch let error as DCError where error.code == .serverUnavailable {
                lastError = error
                let delay = UInt64(pow(2.0, Double(attempt))) * 1_000_000_000
                try await Task.sleep(nanoseconds: delay)
            } catch {
                throw error // Non-retryable errors propagate immediately
            }
        }

        throw lastError ?? DeviceIntegrityError.attestationFailed
    }
}
swift
extension AppAttestManager {
    func attestKeyWithRetry(maxAttempts: Int = 3) async throws -> Data {
        var lastError: Error?

        for attempt in 0..<maxAttempts {
            do {
                return try await attestKey()
            } catch let error as DCError where error.code == .serverUnavailable {
                lastError = error
                let delay = UInt64(pow(2.0, Double(attempt))) * 1_000_000_000
                try await Task.sleep(nanoseconds: delay)
            } catch {
                throw error // 不可重试的错误立即传播
            }
        }

        throw lastError ?? DeviceIntegrityError.attestationFailed
    }
}

Handling Invalidated Keys

处理失效的密钥

If
attestKey
returns
DCError.invalidKey
, the Secure Enclave key has been invalidated (e.g., OS update, Secure Enclave reset). Delete the stored
keyId
from Keychain and generate a new key:
swift
extension AppAttestManager {
    func handleInvalidKey() 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)
    }
}
如果
attestKey
返回
DCError.invalidKey
,说明Secure Enclave中的密钥已失效(例如系统更新、Secure Enclave重置)。从Keychain中删除存储的
keyId
并生成新密钥:
swift
extension AppAttestManager {
    func handleInvalidKey() 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)
    }
}

Common Patterns

常见模式

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()
    on launch to create or load the persisted key.
  3. Call
    attestKeyWithRetry()
    once after key generation.
  4. Use
    generateAssertion(for:)
    on each sensitive server request.
  5. Handle
    DCError.invalidKey
    by regenerating and re-attesting.
将上述模式整合到单个
actor
中,管理完整生命周期:
  1. 检查
    isSupported
    ,在不支持的设备上回退到
    DCDevice
    令牌。
  2. 在启动时调用
    generateKeyIfNeeded()
    ,创建或加载持久化的密钥。
  3. 密钥生成后调用一次
    attestKeyWithRetry()
  4. 在每次敏感服务器请求时使用
    generateAssertion(for:)
  5. 通过重新生成并重新证明来处理
    DCError.invalidKey

Gradual Rollout

逐步推广

Apple recommends a gradual rollout. Gate App Attest behind a remote feature flag and fall back to
DCDevice
tokens on unsupported devices.
Apple建议逐步推广。通过远程功能标志控制App Attest的启用,在不支持的设备上回退到
DCDevice
令牌。

Environment Entitlement

环境权限

Set the App Attest environment in your entitlements file. Use
development
during testing and
production
for App Store builds:
xml
<key>com.apple.developer.devicecheck.appattest-environment</key>
<string>production</string>
When the entitlement is missing, the system uses
development
in debug builds and
production
for App Store and TestFlight builds.
在您的权限文件中设置App Attest环境。测试时使用
development
,App Store构建时使用
production
xml
<key>com.apple.developer.devicecheck.appattest-environment</key>
<string>production</string>
当缺少该权限时,系统在调试构建中使用
development
,在App Store和TestFlight构建中使用
production

Error Type

错误类型

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

Common Mistakes

常见误区

  1. Generating a new key on every launch. Generate once, persist the
    keyId
    in Keychain.
  2. Skipping the fallback for unsupported devices. Not all devices support App Attest. Use
    DCDevice
    tokens as fallback.
  3. Trusting attestation client-side. All verification must happen on your server.
  4. Not implementing replay protection. The server must track and increment the assertion counter.
  5. Missing the environment entitlement. Without it, debug builds use
    development
    and App Store uses
    production
    . Mismatches cause attestation failures.
  6. Not handling
    DCError.invalidKey
    .
    Keys can be invalidated by OS updates. Detect and regenerate.
  1. 每次启动都生成新密钥:仅生成一次,将
    keyId
    持久化到Keychain中。
  2. 不为不支持的设备提供回退方案:并非所有设备都支持App Attest,需使用
    DCDevice
    令牌作为回退。
  3. 在客户端信任证明结果:所有验证必须在服务器端进行。
  4. 未实现重放保护:服务器必须跟踪并递增断言计数器。
  5. 缺少环境权限:缺少该权限时,调试构建使用
    development
    ,App Store构建使用
    production
    。不匹配会导致验证失败。
  6. 未处理
    DCError.invalidKey
    :密钥可能因系统更新而失效,需检测并重新生成。

Review Checklist

检查清单

  • DCAppAttestService.isSupported
    checked before use; fallback to
    DCDevice
    when unsupported
  • Key generated once and
    keyId
    persisted in Keychain
  • Attestation performed once per key; attestation object sent to server
  • Server validates attestation against Apple's App Attest root CA
  • Assertions generated for each sensitive request; server verifies signature and counter
  • DCError
    cases handled:
    .serverUnavailable
    with retry,
    .invalidKey
    with key regeneration
  • App Attest environment entitlement set correctly for debug vs. production
  • Gradual rollout considered; feature flag in place for enabling/disabling
  • 已检查
    DCAppAttestService.isSupported
    ;在不支持时回退到
    DCDevice
  • 密钥仅生成一次,
    keyId
    已持久化到Keychain中
  • 每个密钥执行一次证明;证明对象已发送到服务器
  • 服务器根据Apple的App Attest根CA验证证明
  • 为每个敏感请求生成断言;服务器验证签名和计数器
  • 已处理
    DCError
    场景:
    .serverUnavailable
    进行重试,
    .invalidKey
    重新生成密钥
  • 已为调试和生产环境正确设置App Attest环境权限
  • 已考虑逐步推广;已设置功能标志用于启用/禁用