Files
web-terminal/ios/Packages/ClientTLS/Tests/ClientTLSTests/KeychainClientIdentityStoreLeafTests.swift
Yaojia Wang a5fa843f00 test(ios): ClientTLS coverage 55.76% -> 89.49%, gate it, fix the three dead CI legs
ClientTLS was the most security-sensitive package in the tree and the least
covered, and it was not in the coverage gate at all (the gate's 4-package set
predates it). 48 -> 84 tests against the real macOS keychain, serialized with a
custom Testing trait after a @globalActor proved insufficient (actors yield at
await, so cross-await critical sections got interleaved by other cases' cleanup).

CI: the app/ipad/ios17 legs ran a bundle containing LiveServerSmokeTests, which
spawns tsx, with no npm ci -- a hard failure, not a skip, on a bare checkout.
Adds the missing iPad UI-test leg, and makes a missing iOS 17 runtime fail loudly
instead of silently reporting green.
2026-07-30 12:45:26 +02:00

152 lines
6.7 KiB
Swift

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")
}
}