device-integrity
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseDevice 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令牌)
DCDeviceDCDeviceToken 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:
| Endpoint | Purpose |
|---|---|
| Read the two bits for a device |
| Set the two bits for a device |
| 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端点:
| 端点 | 用途 |
|---|---|
| 读取设备的两个标识位 |
| 设置设备的两个标识位 |
| 验证设备令牌,无需读取标识位 |
服务器使用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)
DCAppAttestServiceThe flow has three phases:
- Key generation -- create a key pair in the Secure Enclave.
- Attestation -- Apple certifies the key belongs to a genuine Apple device running your app.
- Assertion -- sign server requests with the attested key to prove ongoing legitimacy.
DCAppAttestService流程分为三个阶段:
- 密钥生成 —— 在Secure Enclave中创建密钥对。
- 证明 —— Apple认证该密钥属于运行您应用的正版Apple设备。
- 断言 —— 使用已证明的密钥对服务器请求进行签名,以持续证明合法性。
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
is a string identifier you persist (e.g., in Keychain) for later
attestation and assertion calls.
keyIdswift
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 . Generating a new
key invalidates any previous attestation.
keyId生成存储在Secure Enclave中的加密密钥对。返回的是一个字符串标识符,您需要持久化存储(例如在Keychain中),以便后续调用证明和断言接口。
keyIdswift
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)
}
}重要提示: 仅生成一次密钥并持久化。生成新密钥会使之前的任何证明失效。
keyIdApp 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:
- Verify the attestation object is a valid CBOR-encoded structure.
- Extract the certificate chain and validate it against Apple's App Attest root CA.
- Verify the in the attestation matches
nonce.SHA256(challenge) - 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.
您的服务器必须:
- 验证证明对象是有效的CBOR编码结构。
- 提取证书链并根据Apple的App Attest根CA进行验证。
- 验证证明中的与
nonce匹配。SHA256(challenge) - 提取并存储公钥和回执,用于后续的断言验证。
有关完整的服务器验证算法,请参阅验证连接到您服务器的应用。
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:
- Decode the assertion (CBOR).
- Verify the authenticator data, including the counter (must be greater than the stored counter).
- Verify the signature using the stored public key from attestation.
- Confirm the matches the SHA-256 of the received request body.
clientDataHash - Update the stored counter to prevent replay attacks.
您的服务器必须:
- 解码断言(CBOR格式)。
- 验证验证器数据,包括计数器(必须大于存储的计数器值)。
- 使用证明阶段存储的公钥验证签名。
- 确认与收到的请求主体的SHA-256哈希匹配。
clientDataHash - 更新存储的计数器以防止重放攻击。
Server Verification Guidance
服务器验证指南
Attestation vs. Assertion
证明 vs 断言
| Phase | When | What It Proves | Frequency |
|---|---|---|---|
| Attestation | After key generation | The key lives on a genuine Apple device running your unmodified app | Once per key |
| Assertion | With each sensitive request | The request came from the attested app instance | Per request |
| 阶段 | 时机 | 证明内容 | 频率 |
|---|---|---|---|
| 证明 | 密钥生成后 | 密钥存在于运行未修改版应用的正版Apple设备上 | 每个密钥一次 |
| 断言 | 每次敏感请求时 | 请求来自已验证的应用实例 | 每次请求 |
Recommended Server Architecture
推荐的服务器架构
- Challenge endpoint -- generate a random nonce, store it server-side with a short TTL (e.g., 5 minutes).
- Attestation verification endpoint -- validate the attestation object, store the public key and receipt keyed by .
keyId - Assertion verification middleware -- verify assertions on sensitive endpoints (purchases, account changes).
- 挑战端点 —— 生成随机一次性值,在服务器端存储并设置短TTL(例如5分钟)。
- 证明验证端点 —— 验证证明对象,存储公钥和回执,以为键。
keyId - 断言验证中间件 —— 在敏感端点(如购买、账户变更)验证断言。
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 returns , the Secure Enclave key has been
invalidated (e.g., OS update, Secure Enclave reset). Delete the stored
from Keychain and generate a new key:
attestKeyDCError.invalidKeykeyIdswift
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)
}
}如果返回,说明Secure Enclave中的密钥已失效(例如系统更新、Secure Enclave重置)。从Keychain中删除存储的并生成新密钥:
attestKeyDCError.invalidKeykeyIdswift
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 that manages the full lifecycle:
actor- Check and fall back to
isSupportedtokens on unsupported devices.DCDevice - Call on launch to create or load the persisted key.
generateKeyIfNeeded() - Call once after key generation.
attestKeyWithRetry() - Use on each sensitive server request.
generateAssertion(for:) - Handle by regenerating and re-attesting.
DCError.invalidKey
将上述模式整合到单个中,管理完整生命周期:
actor- 检查,在不支持的设备上回退到
isSupported令牌。DCDevice - 在启动时调用,创建或加载持久化的密钥。
generateKeyIfNeeded() - 密钥生成后调用一次。
attestKeyWithRetry() - 在每次敏感服务器请求时使用。
generateAssertion(for:) - 通过重新生成并重新证明来处理。
DCError.invalidKey
Gradual Rollout
逐步推广
Apple recommends a gradual rollout. Gate App Attest behind a remote feature
flag and fall back to tokens on unsupported devices.
DCDeviceApple建议逐步推广。通过远程功能标志控制App Attest的启用,在不支持的设备上回退到令牌。
DCDeviceEnvironment Entitlement
环境权限
Set the App Attest environment in your entitlements file. Use
during testing and for App Store builds:
developmentproductionxml
<key>com.apple.developer.devicecheck.appattest-environment</key>
<string>production</string>When the entitlement is missing, the system uses in debug builds
and for App Store and TestFlight builds.
developmentproduction在您的权限文件中设置App Attest环境。测试时使用,App Store构建时使用:
developmentproductionxml
<key>com.apple.developer.devicecheck.appattest-environment</key>
<string>production</string>当缺少该权限时,系统在调试构建中使用,在App Store和TestFlight构建中使用。
developmentproductionError 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
常见误区
- Generating a new key on every launch. Generate once, persist the in Keychain.
keyId - Skipping the fallback for unsupported devices. Not all devices support App Attest. Use tokens as fallback.
DCDevice - Trusting attestation client-side. All verification must happen on your server.
- Not implementing replay protection. The server must track and increment the assertion counter.
- Missing the environment entitlement. Without it, debug builds use and App Store uses
development. Mismatches cause attestation failures.production - Not handling . Keys can be invalidated by OS updates. Detect and regenerate.
DCError.invalidKey
- 每次启动都生成新密钥:仅生成一次,将持久化到Keychain中。
keyId - 不为不支持的设备提供回退方案:并非所有设备都支持App Attest,需使用令牌作为回退。
DCDevice - 在客户端信任证明结果:所有验证必须在服务器端进行。
- 未实现重放保护:服务器必须跟踪并递增断言计数器。
- 缺少环境权限:缺少该权限时,调试构建使用,App Store构建使用
development。不匹配会导致验证失败。production - 未处理:密钥可能因系统更新而失效,需检测并重新生成。
DCError.invalidKey
Review Checklist
检查清单
- checked before use; fallback to
DCAppAttestService.isSupportedwhen unsupportedDCDevice - Key generated once and persisted in Keychain
keyId - 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
- cases handled:
DCErrorwith retry,.serverUnavailablewith key regeneration.invalidKey - App Attest environment entitlement set correctly for debug vs. production
- Gradual rollout considered; feature flag in place for enabling/disabling
- 已检查;在不支持时回退到
DCAppAttestService.isSupportedDCDevice - 密钥仅生成一次,已持久化到Keychain中
keyId - 每个密钥执行一次证明;证明对象已发送到服务器
- 服务器根据Apple的App Attest根CA验证证明
- 为每个敏感请求生成断言;服务器验证签名和计数器
- 已处理场景:
DCError进行重试,.serverUnavailable重新生成密钥.invalidKey - 已为调试和生产环境正确设置App Attest环境权限
- 已考虑逐步推广;已设置功能标志用于启用/禁用