Loading...
Loading...
Compare original and translation side by side
DCDeviceDCDeviceimport DeviceCheck
func generateDeviceToken() async throws -> Data {
guard DCDevice.current.isSupported else {
throw DeviceIntegrityError.deviceCheckUnsupported
}
return try await DCDevice.current.generateToken()
}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
}
}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
}
}| Endpoint | Purpose |
|---|---|
| Read the two bits for a device |
| Set the two bits for a device |
| Validate a device token without reading bits |
| 端点 | 用途 |
|---|---|
| 读取设备的两个标识位 |
| 设置设备的两个标识位 |
| 验证设备令牌,无需读取标识位 |
DCAppAttestServiceDCAppAttestServiceimport 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
}import DeviceCheck
let attestService = DCAppAttestService.shared
guard attestService.isSupported else {
// 回退到DCDevice令牌或其他风险评估方案。
// App Attest在模拟器或部分设备型号上不可用。
return
}keyIdimport 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)
}
}keyIdkeyIdimport 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)
}
}keyIdimport 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
}
}
}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
}
}
}nonceSHA256(challenge)nonceSHA256(challenge)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)
}
}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 {
/// 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)
}
}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)
}
}clientDataHashclientDataHash| 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设备上 | 每个密钥一次 |
| 断言 | 每次敏感请求时 | 请求来自已验证的应用实例 | 每次请求 |
keyIdkeyIdimport 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
}
}
}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 // Non-retryable errors propagate immediately
}
}
throw lastError ?? DeviceIntegrityError.attestationFailed
}
}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
}
}attestKeyDCError.invalidKeykeyIdextension 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)
}
}attestKeyDCError.invalidKeykeyIdextension 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)
}
}actorisSupportedDCDevicegenerateKeyIfNeeded()attestKeyWithRetry()generateAssertion(for:)DCError.invalidKeyactorisSupportedDCDevicegenerateKeyIfNeeded()attestKeyWithRetry()generateAssertion(for:)DCError.invalidKeyDCDeviceDCDevicedevelopmentproduction<key>com.apple.developer.devicecheck.appattest-environment</key>
<string>production</string>developmentproductiondevelopmentproduction<key>com.apple.developer.devicecheck.appattest-environment</key>
<string>production</string>developmentproductionenum DeviceIntegrityError: Error {
case deviceCheckUnsupported
case keyNotGenerated
case attestationFailed
case attestationVerificationFailed
case assertionFailed
case serverVerificationFailed
}enum DeviceIntegrityError: Error {
case deviceCheckUnsupported
case keyNotGenerated
case attestationFailed
case attestationVerificationFailed
case assertionFailed
case serverVerificationFailed
}keyIdDCDevicedevelopmentproductionDCError.invalidKeykeyIdDCDevicedevelopmentproductionDCError.invalidKeyDCAppAttestService.isSupportedDCDevicekeyIdDCError.serverUnavailable.invalidKeyDCAppAttestService.isSupportedDCDevicekeyIdDCError.serverUnavailable.invalidKey