import Foundation import Security import Testing // B4 · Why every real-keychain test in this package runs one at a time. // // `KeychainClientIdentityStore` / `SecureEnclaveKeyFactory` call `SecItem*` and // `SecKeyCreateRandomKey` WITHOUT `kSecUseDataProtectionKeychain`, so on macOS // `swift test` reaches the FILE-BASED login keychain. Two problems follow from // Swift Testing's default parallel execution: // // 1. That backend is not safe for concurrent writes from one process: two // simultaneous permanent-key generations intermittently fail with // keyGenerationFailed("… Code=-25300 \"failed to generate CDSA key\" …") // (observed 2026-07-30 — green under `--no-parallel`, red in parallel). // 2. Certificate items are keyed by the cert's own subject on macOS, so the // fixture-leaf sweep is GLOBAL: one test's cleanup would delete a // concurrently-running test's freshly installed leaf. // // A global actor is NOT enough: an actor releases its executor at every `await`, // and these tests await network-stub calls in the middle of their critical // section (proved by exactly failure mode 2 appearing when a `@globalActor` was // used). What is needed is a mutex held ACROSS suspension points — applied as a // custom trait so the whole test body, setup and cleanup included, is inside it. /// A FIFO async mutex. actor KeychainTestMutex { static let shared = KeychainTestMutex() private var isHeld = false private var waiters: [CheckedContinuation] = [] func acquire() async { guard isHeld else { isHeld = true return } await withCheckedContinuation { waiters.append($0) } } func release() { guard waiters.isEmpty else { waiters.removeFirst().resume() // hand the lock straight to the next waiter return } isHeld = false } } /// Serializes a test against every other keychain-touching test. struct KeychainSerializedTrait: TestTrait, SuiteTrait, TestScoping { func provideScope( for test: Test, testCase: Test.Case?, performing function: @Sendable () async throws -> Void ) async throws { await KeychainTestMutex.shared.acquire() do { try await function() } catch { await KeychainTestMutex.shared.release() throw error } await KeychainTestMutex.shared.release() } } extension Trait where Self == KeychainSerializedTrait { /// Apply to every test that touches the real keychain. static var keychainSerialized: Self { Self() } } /// Does the default keychain hand back an identity for a tag that was never /// installed? /// /// On macOS's file-based keychain, `SecItemCopyMatching(kSecClass: /// kSecClassIdentity, kSecAttrApplicationTag: …)` IGNORES the tag and returns an /// arbitrary identity from the login keychain (verified 2026-07-30 with a random /// tag: `errSecSuccess` + an unrelated `localhost` identity). On iOS's /// data-protection keychain the tag IS honored, which is why the production code /// is correct on the shipping platform — but it means the `.p12` fallback branch /// of `loadIdentity()` is only reachable in a `swift test` run whose default /// keychain holds no identities (a clean CI runner). Tests that depend on that /// branch are gated on this probe instead of being silently wrong. func defaultKeychainYieldsForeignIdentity() -> Bool { let query: [String: Any] = [ kSecClass as String: kSecClassIdentity, kSecAttrApplicationTag as String: Data("clienttls-probe-\(UUID().uuidString)".utf8), kSecMatchLimit as String: kSecMatchLimitOne, ] return SecItemCopyMatching(query as CFDictionary, nil) == errSecSuccess }