device-integrity
dpearson2699/swift-ios-skills
使用 DeviceCheck(DCDevice 的每台裝置位元)和 App Attest(DCAppAttestService 的金鑰產生、驗證及斷言流程)來驗證裝置的合法性與應用程式的完整性。 適用於實作防詐騙機制、偵測遭入侵的裝置、透過 Apple 伺服器驗證應用程式真實性、使用經認證的請求保護敏感的 API 端點,或為後端架構新增裝置驗證功能。
...展開全部關於device-integrity
「device-integrity 」技能提供全面指引,協助您在 iOS 應用程式中實作 Apple 的 DeviceCheck 與 App Attest 框架,以驗證裝置的合法性與應用程式的真實性。此技能針對一項關鍵的安全挑戰提供解決方案:確保 API 請求源自運行未經修改版應用程式的正版 Apple 裝置,藉此防範詐欺、越獄攻擊,並防止未經授權存取敏感的後端端點。
此技能涵蓋兩個主要的 Apple 框架:DCDevice 透過暫存令牌進行簡單的單一裝置標記管理,以及 DCAppAttestService 透過 Secure Enclave 支援的金鑰進行加密驗證。 本文件包含代幣生成、伺服器通訊、驗證流程及斷言驗證的完整實作範例,並提供客戶端 Swift 程式碼以及伺服器端架構指引,以協助您與 Apple 的驗證端點進行整合。
本技能非常適合開發具有防詐騙需求、支付系統、促銷優惠兌換,或任何需要強大裝置與應用程式完整性保證的 iOS 開發者。 本課程涵蓋錯誤處理模式、應避免的常見實作錯誤、伺服器驗證工作流程,以及生產環境部署的安全最佳實務。課程對象為需要保護後端 API 免受遭入侵裝置或遭篡改應用程式威脅的中階至高階 iOS 開發者。
常見問題
DeviceCheck 與 App Attest 之間有何差異?
DeviceCheck (DCDevice) 提供簡單的「每台裝置專屬」憑證,以及兩個用於基本裝置追蹤(例如兌換促銷優惠)的持久位元。App Attest (DCAppAttestService) 則利用 Secure Enclave 金鑰提供加密證明,以驗證特定應用程式執行個體是否合法且未經竄改,為敏感操作提供更強大的安全性。
這些框架需要哪些 iOS 版本?
DCDevice 適用於 iOS 11 及後續版本。DCAppAttestService 則需 iOS 14 或後續版本。在嘗試使用任一框架之前,請務必先檢查 isSupported。
我可以在多個請求中重複使用 DeviceCheck 憑證嗎?
不可以。DeviceCheck 憑證是暫存且僅限單次使用的。您必須針對每次伺服器操作生成新的憑證,而非將憑證儲存於快取或重複使用。
DeviceCheck 中的兩個位元用於什麼用途?
Apple 會針對每個開發團隊的每台裝置儲存兩個布林值。您可以根據使用情境定義這些位元的意義。常見的例子包括追蹤裝置是否已領取促銷優惠(位元 0),或將裝置標記為詐欺裝置(位元 1)。這些位元在重新安裝應用程式後仍會保留。
我需要伺服器才能實作裝置完整性嗎?
是的。DeviceCheck 和 App Attest 均需要伺服器端的驗證。您的應用程式會產生憑證或驗證結果,將其傳送至您的伺服器,而您的伺服器則會使用從 Apple 開發者入口網站取得的 DeviceCheck 私密金鑰,與 Apple 的驗證端點進行通訊。
Device Integrity
Verify that requests to your server come from a genuine Apple device running alegitimate instance of your app. DeviceCheck provides per-device bits forsimple flags (e.g., "claimed promo offer"). App Attest uses Secure Enclave keysand Apple attestation to cryptographically prove app legitimacy on sensitiverequests.
Contents
- DCDevice (DeviceCheck Tokens)
- DCAppAttestService (App Attest)
- App Attest Key Generation
- App Attest Attestation Flow
- App Attest Assertion Flow
- Server Verification Guidance
- Error Handling
- Common Patterns
- Common Mistakes
- Review Checklist
- References
DCDevice (DeviceCheck Tokens)
DCDevice generates aunique, ephemeral token that identifies a device. Treat each token assingle-use: generate a new token for each server operation instead of caching orreusing one. The token is sent to your server, which then communicates withApple's servers to read or set two per-device bits. Available on iOS 11+.
Token Generation
import DeviceCheckfunc generateDeviceToken() async throws -> Data { guard DCDevice.current.isSupported else { throw DeviceIntegrityError.deviceCheckUnsupported } return try await DCDevice.current.generateToken()}
Sending the Token to Your Server
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 |
|---|---|
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 Developerportal, creating a signed JWT for each request.
Use https://api.development.devicecheck.apple.com only while testing; usehttps://api.devicecheck.apple.com for production.
What the Two Bits Are For
Apple stores two Boolean values per device per developer team. You decide whatthey mean. Common uses:
- Bit 0: Device has claimed a promotional offer.
- Bit 1: Device has been flagged for fraud.
Bits persist across app reinstall. You control when to reset them via theserver API.
DCAppAttestService (App Attest)
DCAppAttestServicevalidates that a specific instance of your app on a specific device islegitimate. It uses a hardware-backed key in the Secure Enclave to createcryptographic attestations and assertions. Available on iOS 14+.
The 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.
Checking Support
import DeviceChecklet attestService = DCAppAttestService.sharedguard attestService.isSupported else { // Fall back to DCDevice token or other risk assessment. // App Attest is not available on simulators or all device models. return}
For app extensions, App Attest is supported only in Action, extensible SSO, andwatchOS extensions. Treat other extension types as unsupported even ifisSupported returns true.
App Attest Key Generation
Generate one cryptographic key pair per user account on each device. Theprivate key stays in the Secure Enclave. The returned keyId is the onlyidentifier your app can later use to access the key, so record and reuse theaccount/device-scoped keyId; do not share one key across users. Avoidunnecessary regeneration because each new key affects App Attest key-count riskmetrics. Only treat the keyId as usable after your server verifiesattestation. If server verification fails, discard the keyId and generate anew key before retrying.
import DeviceCheckactor AppAttestManager { private let service = DCAppAttestService.shared private var keyId: String? /// Generate and record 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-\(currentAccountID)", 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-\(currentAccountID)", 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 per user account on a device, persist thataccount/device keyId, and keep the key count low. Generating unnecessary keyspollutes App Attest risk metrics.
App Attest Attestation Flow
Attestation proves that the key was generated on a genuine Apple device runninga legitimate instance of your app. You perform attestation once per key, thenstore the verified public key and receipt on your server. The app stores thekeyId for future assertions after the server accepts the attestation.
Client-Side Attestation
import DeviceCheckimport CryptoKitextension 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 } }}
Server-Side Attestation Verification
Your server validates the attestation object (CBOR), verifies the certificatechain against Apple's App Attest root CA, checks Apple's nonce calculation, andstores the verified public key and receipt for future assertion verification.The attestation nonce is not SHA256(challenge) alone; it isSHA256(authData || SHA256(challenge)) and is compared with the credentialcertificate extension 1.2.840.113635.100.8.2. Seereferences/device-integrity-patterns.mdfor the full server verification flow.
App Attest Assertion Flow
After attestation, use assertions to sign sensitive requests. Each assertionproves the request came from the attested app instance and includes aserver-issued, one-time challenge to prevent replay.
Client-Side Assertion
import DeviceCheckimport CryptoKitextension AppAttestManager { /// Generate an assertion for encoded client data. /// Client data should include a one-time server challenge and request context. func generateAssertion(for clientData: Data) async throws -> Data { guard let keyId else { throw DeviceIntegrityError.keyNotGenerated } let clientDataHash = Data(SHA256.hash(data: clientData)) return try await service.generateAssertion(keyId, clientDataHash: clientDataHash) }}
Using Assertions in Network Requests
struct AppAttestClientData: Encodable { let challenge: String let method: String let path: String let bodySHA256: String}extension AppAttestManager { /// Perform an attested API request. func makeAttestedRequest( to url: URL, method: String = "POST", body: Data ) async throws -> (Data, URLResponse) { let challenge = try await fetchAssertionChallenge() let bodyHash = Data(SHA256.hash(data: body)).base64EncodedString() let clientData = try JSONEncoder().encode( AppAttestClientData( challenge: challenge, method: method, path: url.path, bodySHA256: bodyHash ) ) let assertion = try await generateAssertion(for: clientData) var request = URLRequest(url: url) request.httpMethod = method request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue(assertion.base64EncodedString(), forHTTPHeaderField: "X-App-Attest-Assertion") request.setValue(clientData.base64EncodedString(), forHTTPHeaderField: "X-App-Attest-Client-Data") request.httpBody = body return try await URLSession.shared.data(for: request) } private func fetchAssertionChallenge() async throws -> String { let url = serverURL.appending(path: "assert/challenge") let (data, _) = try await URLSession.shared.data(from: url) return String(decoding: data, as: UTF8.self) }}
Server-Side Assertion Verification
Your server decodes the assertion (CBOR), verifies the authenticator data andcounter, recomputes clientDataHash from the submitted client data, verifiesthe signature over SHA256(authenticatorData || clientDataHash) with thestored public key, and confirms the embedded challenge and request context. Seereferences/device-integrity-patterns.mdfor step-by-step server verification.
Server Verification Guidance
See references/device-integrity-patterns.md for full server architecture guidance including attestation vs. assertion comparison, recommended endpoint design, and risk assessment.
Security Boundaries
App Attest proves app-instance integrity for selected requests. It does notreplace user authentication, OAuth/JWT/session handling, API token design,entitlement or subscription authorization, TLS, certificate pinning, or generalnetworking security. Treat those as handoffs to authentication, networking, orbroader security guidance, and still enforce normal authentication andauthorization after App Attest passes.
Error Handling
Handle DCError codes from DeviceCheck operations. Key cases:
.serverUnavailable— retry with exponential backoff.invalidKey— the key was already attested, assertion used an unattested key, or the service rejected the key.featureUnsupported— fall back toDCDevicetokens.invalidInput— malformedclientDataHashorkeyId
For attestKey, retry .serverUnavailable later with the same keyId and thesame clientDataHash. For other attestation errors, discard the key identifierand create a new key before retrying. Seereferences/device-integrity-patterns.mdfor full error handling code, retry strategy, and rejected-key recovery.
Common Patterns
Environment Entitlement
Set the App Attest environment in your entitlements file. Use developmentduring 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 Attestsandbox by default. After distribution through TestFlight, the App Store, or theApple Developer Enterprise Program, the app ignores the entitlement value anduses production.
See references/device-integrity-patterns.md for the full integration manager pattern, gradual rollout guidance, and error type definition.
Common Mistakes
- Generating a new key on every launch. Generate once per user account on a device, persist the
keyId, and keep key counts low. - Reusing
DCDevicetokens. Treat generated tokens as single-use. Generate a new token for each server operation. - Skipping the fallback for unsupported devices or extensions. Not all devices and extension types support App Attest. Use
DCDevicetokens or other risk assessment as fallback. - Trusting attestation client-side. All verification must happen on your server.
- Signing only the raw request body. Assertion client data must include a one-time server challenge and enough request context for the server to bind the assertion to the request.
- Verifying the wrong attestation nonce. Compare the certificate extension with
SHA256(authData || SHA256(challenge)), notSHA256(challenge)alone. - Not implementing replay protection. The server must validate one-time challenges and track the assertion counter.
- Mixing development and production environments. Sandbox keys and receipts do not work in production, and production keys and receipts do not work in sandbox.
- Not handling
DCError.invalidKey. Check for repeated attestation, unattested assertion keys, or service rejection; regenerate only after the state is known bad.
Review Checklist
-
DCDevicetokens generated per server operation and never cached for reuse -
DCAppAttestService.isSupportedchecked before use; unsupported devices and extension types have a fallback - Key generated once per user account on each device and
keyIdpersisted only for that app account/device - Attestation performed once per key; server stores verified public key and receipt
- Server validates attestation certificate chain, App ID hash, environment
aaguid, credential ID, and nonceSHA256(authData || SHA256(challenge)) - Assertions include one-time challenge plus request context; server verifies signature, RP ID, counter, challenge, and request binding
- Protected endpoints still enforce normal user authentication and entitlement authorization after App Attest passes
-
DCErrorcases handled:.serverUnavailableretries attestation with the same key/hash; bad keys are discarded and regenerated - App Attest environment entitlement and sandbox/production server routing are consistent
- Gradual rollout considered; feature flag in place for enabling/disabling
References
- Extended patterns: references/device-integrity-patterns.md
- DeviceCheck framework
- DCDevice
- DCAppAttestService
- Establishing your app's integrity
- Validating apps that connect to your server
- Attestation Object Validation Guide
- App Attest Environment





首頁
