如何序列化访问并发调用会失效的Swift异步API?
Apple DCAppAttestService generateAssertion 并发缺陷及优化方案
问题描述
Apple 的 DCAppAttestService 中 generateAssertion(_:clientDataHash:completionHandler:) 方法存在并发安全缺陷:当多个异步任务同时调用该方法时,绝大多数情况下会返回 invalidKey(对应错误码3)的错误;但改为串行调用时,所有请求都能成功执行。
测试代码
func testConcurrentGenerateAssertionIsUnsafe() async throws { let service = DCAppAttestService.shared let attestationKey = try await service.generateKey() try await service.attestKey(attestationKey, clientDataHash: Data(SHA256.hash(data: Data()))) let calls = 5 let concurrentAssertResults = try await withThrowingTaskGroup(of: Data.self) { taskGroup -> [Data] in for i in 1...calls { taskGroup.addTask { do { let result = try await service.generateAssertion(attestationKey, clientDataHash: Data()) print("Assertion result \(i) \(result)") return result } catch { print("Throwing error \(i) \(error)") throw error } } } var assertResult = [Data]() for try await fetchKeyResult in taskGroup { assertResult.append(fetchKeyResult) } return assertResult } }
典型错误输出
Throwing error 5 Error Domain=com.apple.devicecheck.error Code=3 "(null)" Assertion result 4 140 bytes Assertion result 3 142 bytes Throwing error 1 Error Domain=com.apple.devicecheck.error Code=3 "(null)" Throwing error 2 Error Domain=com.apple.devicecheck.error Code=3 "(null)"
优化解决方案
方案1:用 Actor 封装并发安全调用
Swift 的 Actor 是原生并发安全原语,能自动保证内部方法的串行执行,完美适配这类非线程安全的API:
actor AppAttestManager { private let service = DCAppAttestService.shared // 封装 generateAssertion,确保串行调用 func generateSafeAssertion(_ keyId: String, clientDataHash: Data) async throws -> Data { return try await service.generateAssertion(keyId, clientDataHash: clientDataHash) } // 可按需封装其他相关方法 func generateKey() async throws -> String { return try await service.generateKey() } func attestKey(_ keyId: String, clientDataHash: Data) async throws -> Data { return try await service.attestKey(keyId, clientDataHash: clientDataHash) } }
使用示例:
func testSafeConcurrentCalls() async throws { let manager = AppAttestManager() let attestationKey = try await manager.generateKey() try await manager.attestKey(attestationKey, clientDataHash: Data(SHA256.hash(data: Data()))) let calls = 5 let results = try await withThrowingTaskGroup(of: Data.self) { group in for i in 1...calls { group.addTask { let result = try await manager.generateSafeAssertion(attestationKey, clientDataHash: Data()) print("Assertion result \(i) \(result)") return result } } var assertResults = [Data]() for try await result in group { assertResults.append(result) } return assertResults } print("All \(results.count) calls succeeded") }
方案2:用串行 DispatchQueue 包装调用
如果需要兼容更早的Swift版本,可使用GCD串行队列手动控制调用顺序:
class AppAttestQueueWrapper { private let service = DCAppAttestService.shared private let serialQueue = DispatchQueue(label: "com.yourdomain.appattest.serial") func generateAssertion(_ keyId: String, clientDataHash: Data) async throws -> Data { return try await withCheckedThrowingContinuation { continuation in serialQueue.async { Task { do { let result = try await self.service.generateAssertion(keyId, clientDataHash: clientDataHash) continuation.resume(returning: result) } catch { continuation.resume(throwing: error) } } } } } }
方案优势对比
- Actor方案:符合Swift现代并发编程范式,代码简洁、安全,无需手动管理队列调度,是最优选择。
- 串行队列方案:兼容Swift 5.5之前的版本,但代码相对繁琐,需要手动处理异步续体。
- 两种方案均比手动维护任务数组逐个执行更优雅,底层自动处理串行逻辑,减少出错概率。
内容的提问来源于stack exchange,提问作者Dracula
相关产品推荐
相关产品推荐

