import Foundation import Security import Testing @testable import ClientTLS // B4 · The persistence half of enrollment: what `storeEnrolledLeaf` actually // leaves in the keychain. Three cases, all reachable off-device because the // server's answer is a REAL certificate here (`EnrolledLeafFixtures`): // // 1. first enroll → leaf installed + enrollment record ADDED // 2. rotation (new leaf) → distinct leaf added, record UPDATED in place // 3. retry (same leaf) → errSecDuplicateItem tolerated, record still correct // // Case 2/3 matter because the record write and the leaf write are separate // keychain operations: a device whose record says "dev-B" while the installed // leaf is "dev-A" cannot renew (the server checks the PoP against the enrolled // key for THAT deviceId). // // These tests install certificates into the real keychain, so each one sweeps the // fixture leaves before AND after itself — see `EnrolledLeafFixtures`. private let controlPlaneURL = URL(string: "https://cp.terminal.yaojia.wang")! /// Run `body` with the fixture leaves swept on both sides. Actor-isolated like /// its callers so nothing crosses a concurrency boundary. private func withCleanLeafKeychain( _ body: (StoreKeychainKeys, KeychainClientIdentityStore) async throws -> Void ) async throws { let keys = StoreKeychainKeys.unique() EnrolledLeafFixtures.purgeFixtureLeaves() defer { EnrolledLeafFixtures.purgeFixtureLeaves() KeychainProbe.purge(keys) } let store = KeychainClientIdentityStore(service: keys.service, account: keys.account) try await body(keys, store) } private func enrollmentClient( _ transport: RecordingEnrollmentTransport ) -> DeviceEnrollmentClient { DeviceEnrollmentClient( baseURL: controlPlaneURL, bearerToken: "enroll-bearer", transport: transport ) } @Test("a first enroll installs the leaf and records the server's rotation timing", .keychainSerialized) func enrollInstallsLeafAndRecord() async throws { try await withCleanLeafKeychain { keys, store in // Arrange #expect(try store.renewalState() == nil) let transport = RecordingEnrollmentTransport( status: 201, body: enrollmentResponseJSON( deviceId: "dev-first", certDER: EnrolledLeafFixtures.leaf, caChain: [EnrolledLeafFixtures.issuer], notAfter: "2026-10-28T00:00:00.000Z", renewAfter: "2026-09-27T00:00:00.000Z" ) ) // Act — the returned summary is NOT asserted: reading it goes through // `SecItemCopyMatching(kSecClassIdentity)`, which on macOS ignores the // key tag and can hand back an unrelated login-keychain identity (see // `defaultKeychainYieldsForeignIdentity`). What is persisted IS asserted. _ = try await store.enroll( using: enrollmentClient(transport), subdomain: "yaojia", deviceName: "Yaojia iPhone", keyProvider: { try SecureEnclaveKeyFactory.generateSoftware() } ) // Assert — exactly one leaf installed… #expect(EnrolledLeafFixtures.installedCount() == 1) // …and the record carries what the server said, so a later renew can // address the right device. let state = try #require(try store.renewalState()) #expect(state.deviceId == "dev-first") #expect(state.notAfter != nil) #expect(state.renewAfter != nil) #expect(state.isRenewalDue(asOf: try #require(state.renewAfter))) } } @Test("rotation installs the new leaf and updates the record in place", .keychainSerialized) func rotationUpdatesRecordInPlace() async throws { try await withCleanLeafKeychain { keys, store in // Arrange — already enrolled. let first = RecordingEnrollmentTransport( status: 201, body: enrollmentResponseJSON( deviceId: "dev-old", certDER: EnrolledLeafFixtures.leaf ) ) _ = try await store.enroll( using: enrollmentClient(first), subdomain: "yaojia", deviceName: "iPhone", keyProvider: { try SecureEnclaveKeyFactory.generateSoftware() } ) #expect(try store.renewalState()?.deviceId == "dev-old") // Act — a rotation returns a DISTINCT leaf. let second = RecordingEnrollmentTransport( status: 201, body: enrollmentResponseJSON( deviceId: "dev-new", certDER: EnrolledLeafFixtures.rotatedLeaf ) ) _ = try await store.enroll( using: enrollmentClient(second), subdomain: "yaojia", deviceName: "iPhone", keyProvider: { try SecureEnclaveKeyFactory.generateSoftware() } ) // Assert — the record is UPDATED (one item, new value), never duplicated: // two records at the same (service, account) would make renew pick one at // random. #expect(KeychainProbe.count(service: keys.service, account: keys.enrollmentAccount) == 1) #expect(try store.renewalState()?.deviceId == "dev-new") // …and a leaf is installed at every step — the anti-lockout invariant // (ordering itself is pinned by KeychainItemReplaceTests). #expect(EnrolledLeafFixtures.installedCount() >= 1) } } @Test("re-storing a byte-identical leaf is tolerated and keeps the record correct", .keychainSerialized) func reEnrollingSameLeafIsIdempotent() async throws { try await withCleanLeafKeychain { keys, store in // Arrange — an interrupted enroll that is retried: the server re-issues // the SAME certificate. let body = enrollmentResponseJSON( deviceId: "dev-retry", certDER: EnrolledLeafFixtures.leaf ) let first = RecordingEnrollmentTransport(status: 201, body: body) _ = try await store.enroll( using: enrollmentClient(first), subdomain: "yaojia", deviceName: "iPhone", keyProvider: { try SecureEnclaveKeyFactory.generateSoftware() } ) // Act — `SecItemAdd` now answers errSecDuplicateItem, which must NOT be // treated as a failure (the leaf we wanted installed IS installed). let retry = RecordingEnrollmentTransport(status: 201, body: body) await #expect(throws: Never.self) { _ = try await store.enroll( using: enrollmentClient(retry), subdomain: "yaojia", deviceName: "iPhone", keyProvider: { try SecureEnclaveKeyFactory.generateSoftware() } ) } // Assert — still one leaf, and the record is intact. #expect(EnrolledLeafFixtures.installedCount() == 1) #expect(try store.renewalState()?.deviceId == "dev-retry") } }