device-integrity by dpearson2699/swift-ios-skills
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill device-integrity验证发往你服务器的请求是否来自运行你未经修改应用的真实 Apple 设备。DeviceCheck 提供每设备的比特位用于简单标志(例如,“已领取促销优惠”)。App Attest 使用安全隔离区密钥和 Apple 认证,在每次请求时以加密方式证明应用的合法性。
DCDevice 生成一个唯一的、临时的令牌来标识设备。该令牌被发送到你的服务器,然后你的服务器与 Apple 的服务器通信以读取或设置两个每设备比特位。适用于 iOS 11+。
import DeviceCheck
func generateDeviceToken() async throws -> Data {
guard DCDevice.current.isSupported else {
throw DeviceIntegrityError.deviceCheckUnsupported
}
return try await DCDevice.current.generateToken()
}
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
}
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
你的服务器使用设备令牌调用 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。
Apple 为每个开发者团队的每台设备存储两个布尔值。你决定它们的含义。常见用途:
比特位在应用重新安装后仍然存在;设备重置不会清除它们。你可以通过服务器 API 控制何时重置它们。
DCAppAttestService 验证你的应用在特定设备上的特定实例是合法的。它使用安全隔离区中硬件支持的密钥来创建加密认证和断言。适用于 iOS 14+。
流程分为三个阶段:
import DeviceCheck
let attestService = DCAppAttestService.shared
guard attestService.isSupported else {
// 回退到 DCDevice 令牌或其他风险评估。
// App Attest 在模拟器或所有设备型号上不可用。
return
}
生成存储在安全隔离区中的加密密钥对。返回的 keyId 是一个字符串标识符,你需要持久化它(例如,在钥匙串中),以便后续的认证和断言调用。
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: - 钥匙串辅助函数(简化版)
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。生成新密钥会使之前的所有认证失效。
认证证明密钥是在运行你未经修改应用的真实 Apple 设备上生成的。每个密钥执行一次认证,然后将认证对象存储在你的服务器上。
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
}
}
}
你的服务器必须:
nonce 是否匹配 SHA256(challenge)。完整的服务器验证算法请参阅验证连接到你的服务器的应用。
认证之后,使用断言来对单个请求进行签名。每个断言都证明请求来自已认证的应用实例。
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)
}
}
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)
}
}
你的服务器必须:
clientDataHash 与接收到的请求主体的 SHA-256 匹配。| 阶段 | 时机 | 证明内容 | 频率 |
|---|---|---|---|
| 认证 | 密钥生成后 | 密钥存在于运行你未经修改应用的真实 Apple 设备上 | 每个密钥一次 |
| 断言 | 每次敏感请求时 | 请求来自已认证的应用实例 | 每次请求 |
keyId 存储公钥和收据。将 App Attest 与欺诈风险评估结合使用,以实现深度防御。仅凭 App Attest 并不能保证用户没有滥用应用——它只确认应用是真实的。
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
}
}
}
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
}
}
如果 attestKey 返回 DCError.invalidKey,则表示安全隔离区密钥已失效(例如,操作系统更新、安全隔离区重置)。从钥匙串中删除存储的 keyId 并生成新密钥:
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)
}
}
将上述模式组合到一个管理完整生命周期的单一 actor 中:
isSupported,在不支持的设备上回退到 DCDevice 令牌。generateKeyIfNeeded() 以创建或加载持久化的密钥。attestKeyWithRetry()。generateAssertion(for:)。DCError.invalidKey。Apple 建议渐进式推出。通过远程功能开关控制 App Attest,并在不支持的设备上回退到 DCDevice 令牌。
在你的授权文件中设置 App Attest 环境。在测试期间使用 development,为 App Store 构建使用 production:
<key>com.apple.developer.devicecheck.appattest-environment</key>
<string>production</string>
当缺少此授权时,系统在调试构建中使用 development,在 App Store 和 TestFlight 构建中使用 production。
enum DeviceIntegrityError: Error {
case deviceCheckUnsupported
case keyNotGenerated
case attestationFailed
case attestationVerificationFailed
case assertionFailed
case serverVerificationFailed
}
keyId 持久化在钥匙串中。DCDevice 令牌作为回退。development,App Store 使用 production。不匹配会导致认证失败。DCError.invalidKey。 密钥可能因操作系统更新而失效。检测并重新生成。DCAppAttestService.isSupported;在不支持时回退到 DCDevicekeyId 持久化在钥匙串中DCError 情况:.serverUnavailable 重试,.invalidKey 重新生成密钥每周安装数
331
代码库
GitHub 星标数
269
首次出现
2026年3月8日
安全审计
安装于
codex328
opencode325
github-copilot325
amp325
cline325
kimi-cli325
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.
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+.
import DeviceCheck
func generateDeviceToken() async throws -> Data {
guard DCDevice.current.isSupported else {
throw DeviceIntegrityError.deviceCheckUnsupported
}
return try await DCDevice.current.generateToken()
}
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
}
}
Your server uses the device token to call Apple's DeviceCheck API endpoints:
| Endpoint | Purpose |
|---|---|
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 stores two Boolean values per device per developer team. You decide what they mean. Common uses:
Bits persist across app reinstall; device reset does not clear them. You control when to reset them via the server API.
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:
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
}
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.
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.
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.
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
}
}
}
Your server must:
nonce in the attestation matches SHA256(challenge).See Validating apps that connect to your server for the full server verification algorithm.
After attestation, use assertions to sign individual requests. Each assertion proves the request came from the attested app instance.
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)
}
}
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)
}
}
Your server must:
clientDataHash matches the SHA-256 of the received request body.| 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 |
keyId.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.
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
}
}
}
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
}
}
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:
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)
}
}
Combine the patterns above into a single actor that manages the full lifecycle:
isSupported and fall back to DCDevice tokens on unsupported devices.generateKeyIfNeeded() on launch to create or load the persisted key.attestKeyWithRetry() once after key generation.generateAssertion(for:) on each sensitive server request.DCError.invalidKey by regenerating and re-attesting.Apple recommends a gradual rollout. Gate App Attest behind a remote feature flag and fall back to DCDevice tokens on unsupported devices.
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 missing, the system uses development in debug builds and production for App Store and TestFlight builds.
enum DeviceIntegrityError: Error {
case deviceCheckUnsupported
case keyNotGenerated
case attestationFailed
case attestationVerificationFailed
case assertionFailed
case serverVerificationFailed
}
keyId in Keychain.DCDevice tokens as fallback.development and App Store uses production. Mismatches cause attestation failures.DCError.invalidKey. Keys can be invalidated by OS updates. Detect and regenerate.DCAppAttestService.isSupported checked before use; fallback to DCDevice when unsupportedkeyId persisted in KeychainDCError cases handled: .serverUnavailable with retry, .invalidKey with key regenerationWeekly Installs
331
Repository
GitHub Stars
269
First Seen
Mar 8, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex328
opencode325
github-copilot325
amp325
cline325
kimi-cli325
OpenClaw技能安全审计指南:skill-vetter工具详解与安装前安全检查
12,200 周安装
Nx Cloud CI 监控与自愈修复工具 - monitor-ci 自动化流水线协调器
323 周安装
GCP Cloud Run 部署指南:容器化Web服务与事件驱动函数实战
495 周安装
WordPress项目分类工具 - 快速检测代码库类型与工具配置 | wp-project-triage
530 周安装
CEO顾问工具包:战略规划、财务建模与领导力框架,助力CEO高效决策与管理
494 周安装
Airflow数据血缘关系追踪指南:如何追溯上游数据源与DAG依赖
469 周安装
AI PR 分类工具 - 自动化代码审查分析与成本优化 | pr-triage
591 周安装