Serialize sign calls that require auth prompt (#776)

This commit is contained in:
Sergei Razmetov 2025-12-17 17:50:47 +03:00
parent afb48529c7
commit 825824d5cb
No known key found for this signature in database
2 changed files with 42 additions and 1 deletions

View File

@ -18,6 +18,7 @@ extension SecureEnclave {
public let id = UUID()
public let name = String(localized: .secureEnclave)
private let persistentAuthenticationHandler = PersistentAuthenticationHandler<Secret>()
private let signingSerializer = SigningSerializer()
/// Initializes a Store.
@MainActor public init() {
@ -38,6 +39,16 @@ extension SecureEnclave {
// MARK: SecretStore
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) async throws -> Data {
if secret.attributes.authentication.required {
return try await signingSerializer.serialize { [self] in
try await self.performSign(data: data, with: secret, for: provenance)
}
} else {
return try await performSign(data: data, with: secret, for: provenance)
}
}
private func performSign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) async throws -> Data {
var context: LAContext
if let existing = await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret) {
context = unsafe existing.context
@ -85,7 +96,6 @@ extension SecureEnclave {
default:
throw UnsupportedAlgorithmError()
}
}
public func existingPersistedAuthenticationContext(secret: Secret) async -> PersistedAuthenticationContext? {

View File

@ -0,0 +1,31 @@
import Foundation
/// Serializes signing operations for protected keys to prevent LAContext conflicts.
/// macOS only allows one biometric authentication prompt at a time, so concurrent
/// requests for protected Secure Enclave keys must be queued.
actor SigningSerializer {
private var isProcessing = false
private var waiters: [CheckedContinuation<Void, Never>] = []
func serialize<T: Sendable>(operation: @escaping @Sendable () async throws -> T) async throws -> T {
// If someone is already processing, wait in line
if isProcessing {
await withCheckedContinuation { continuation in
waiters.append(continuation)
}
}
isProcessing = true
defer {
if let next = waiters.first {
waiters.removeFirst()
next.resume()
} else {
isProcessing = false
}
}
return try await operation()
}
}