device-integrity
dpearson2699/swift-ios-skills
DeviceCheck(DCDevice 기기별 비트) 및 App Attest(DCAppAttestService 키 생성, 인증 및 확인 흐름)를 사용하여 기기의 진위 여부와 앱의 무결성을 확인합니다. 이 기능은 사기 방지 구현, 침해된 기기 탐지, Apple 서버를 통한 앱 진위성 검증, 인증된 요청을 통한 민감한 API 엔드포인트 보호, 또는 백엔드 아키텍처에 기기 검증 기능 추가 시에 사용합니다.
...모든 것을 확장하십시오소개 device-integrity
device-integrity 스킬은 iOS 애플리케이션에서 기기의 정품 여부와 앱의 진위성을 검증하기 위해 Apple의 DeviceCheck 및 App Attest 프레임워크를 구현하는 데 필요한 포괄적인 지침을 제공합니다. 이 스킬은 API 요청이 수정되지 않은 버전의 앱을 실행하는 정품 Apple 기기에서 비롯되었는지 확인함으로써, 사기, 탈옥(jailbreak) 악용 및 민감한 백엔드 엔드포인트에 대한 무단 접근을 방지하는 중요한 보안 과제를 해결합니다.
이 스킬은 두 가지 주요 Apple 프레임워크를 다룹니다. 일시적 토큰을 통해 기기별 플래그를 간편하게 관리하는 DCDevice와, Secure Enclave 기반 키를 사용한 암호화 검증을 수행하는 DCAppAttestService입니다. 이 문서에는 토큰 생성, 서버 통신, 인증 흐름 및 어설션 유효성 검증을 위한 완전한 구현 패턴이 포함되어 있습니다. 또한 Apple의 검증 엔드포인트와 통합하기 위한 클라이언트 측 Swift 코드와 서버 측 아키텍처 지침을 모두 제공합니다.
이 스킬은 사기 방지 기능이 필요한 애플리케이션, 결제 시스템, 프로모션 혜택 사용 또는 강력한 기기 및 앱 무결성 보장이 필요한 모든 시나리오를 개발하는 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은 개발자 팀당 기기별로 두 개의 부울(Boolean) 값을 저장합니다. 사용 사례에 따라 해당 값의 의미를 정의할 수 있습니다. 일반적인 예로는 기기가 프로모션 혜택을 수령했는지 추적하는 경우(비트 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
모든 파일
3개 파일device-integrity 설치
스킬 파일을 다운로드하여 .claude/skills/ 디렉터리에 압축을 풀어주세요.
ZIP 다운로드저장소를 클론하고 스킬 파일을 프로젝트에 복사하세요.
git clone https://github.com/dpearson2699/swift-ios-skills/blob/main/skills/device-integrity/SKILL.md # Copy SKILL.md to your .claude/skills/ directory
복사





집
