import Foundation import Security import Testing @testable import ClientTLS // B4 · The device key factory. Two things must hold for the enrollment path to // work at all: (1) a PERMANENT tagged key survives and is found again by tag — // that is what lets the enrolled leaf bind to it and `kSecClassIdentity` // assemble; (2) when the Secure Enclave is not usable (Simulator, missing // entitlement) the failure is CLASSIFIED as `.secureEnclaveUnavailable`, because // that is the signal callers use to fall back to a software key. A // `.keyGenerationFailed` there would look like a bug instead of a platform // limit and the fallback would never happen. // // Runs against the REAL keychain (no shim exists in this package): each test // uses a UUID-scoped tag and deletes it again, so nothing leaks between runs. /// A per-test keychain tag — never collides with another test or another run. private func uniqueTag() -> Data { Data("com.yaojia.webterm.clienttls.test.key-\(UUID().uuidString)".utf8) } @Test("a software key exposes a 65-byte uncompressed X9.63 point and signs verifiably") func softwareKeySignsVerifiably() throws { // Arrange let key = try SecureEnclaveKeyFactory.generateSoftware() // Act let point = try key.publicKeyX963() let message = Data("certificationRequestInfo".utf8) let signature = try key.sign(message) // Assert — X9.63 uncompressed form, and an X9.62 DER ECDSA signature that // verifies under the same algorithm the server's PoP check uses. #expect(point.count == 65) #expect(point.first == 0x04) let publicKey = try #require( SecKeyCreateWithData( point as CFData, [ kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, kSecAttrKeyClass as String: kSecAttrKeyClassPublic, kSecAttrKeySizeInBits as String: 256, ] as CFDictionary, nil ) ) #expect( SecKeyVerifySignature( publicKey, .ecdsaSignatureMessageX962SHA256, message as CFData, signature as CFData, nil ) ) #expect(signature.first == 0x30) // SEQUENCE { r, s } } @Test("a non-permanent key is NOT stored in the keychain", .keychainSerialized) func nonPermanentKeyIsNotPersisted() async throws { // Arrange let tag = uniqueTag() defer { try? SecureEnclaveKeyFactory.delete(tag: tag) } // Act — tagged but permanent: false (the unit-test / Simulator shape). _ = try SecureEnclaveKeyFactory.generateSoftware(tag: tag, permanent: false) // Assert #expect(try SecureEnclaveKeyFactory.load(tag: tag) == nil) } @Test("a permanent tagged key is found again by tag and deleted idempotently", .keychainSerialized) func permanentKeyRoundtripsByTag() async throws { // Arrange let tag = uniqueTag() defer { try? SecureEnclaveKeyFactory.delete(tag: tag) } #expect(try SecureEnclaveKeyFactory.load(tag: tag) == nil) // pre-enroll state // Act let generated = try SecureEnclaveKeyFactory.generateSoftware(tag: tag, permanent: true) let loaded = try SecureEnclaveKeyFactory.load(tag: tag) // Assert — the SAME key comes back (rotation must re-sign with it, never // mint a new one). let reloaded = try #require(loaded) #expect(try reloaded.publicKeyX963() == (try generated.publicKeyX963())) // Act — delete, then delete again. try SecureEnclaveKeyFactory.delete(tag: tag) // Assert — gone, and a second delete is a no-op (not a throw). #expect(try SecureEnclaveKeyFactory.load(tag: tag) == nil) #expect(throws: Never.self) { try SecureEnclaveKeyFactory.delete(tag: tag) } } @Test("two keys under different tags stay independent", .keychainSerialized) func tagsScopeKeysIndependently() async throws { // Arrange let tagA = uniqueTag() let tagB = uniqueTag() defer { try? SecureEnclaveKeyFactory.delete(tag: tagA) try? SecureEnclaveKeyFactory.delete(tag: tagB) } // Act let keyA = try SecureEnclaveKeyFactory.generateSoftware(tag: tagA, permanent: true) let keyB = try SecureEnclaveKeyFactory.generateSoftware(tag: tagB, permanent: true) // Assert #expect(try keyA.publicKeyX963() != (try keyB.publicKeyX963())) #expect(try SecureEnclaveKeyFactory.load(tag: tagA)?.publicKeyX963() == (try keyA.publicKeyX963())) #expect(try SecureEnclaveKeyFactory.load(tag: tagB)?.publicKeyX963() == (try keyB.publicKeyX963())) // Deleting one leaves the other installed. try SecureEnclaveKeyFactory.delete(tag: tagA) #expect(try SecureEnclaveKeyFactory.load(tag: tagA) == nil) #expect(try SecureEnclaveKeyFactory.load(tag: tagB) != nil) } @Test("Secure-Enclave keygen without the entitlement fails as .secureEnclaveUnavailable", .keychainSerialized) func secureEnclaveKeygenClassifiesUnavailability() async throws { // Arrange let tag = uniqueTag() defer { try? SecureEnclaveKeyFactory.delete(tag: tag) } // Act / Assert — an UNSIGNED test binary has no // `com.apple.developer.kernel...`/keychain-access-group entitlement, so // SecKeyCreateRandomKey over the SE token fails with -34018. What is under // test is the CLASSIFICATION: it must be `.secureEnclaveUnavailable` // (callers' fallback signal), never `.keyGenerationFailed`. // // On a host that CAN mint an SE key (entitled build on real hardware) the // call legitimately succeeds; then the key must be a usable signer. Both // outcomes are asserted so this test is honest on every machine. do { let key = try SecureEnclaveKeyFactory.generateSecureEnclave(tag: tag) let signature = try key.sign(Data("probe".utf8)) #expect(signature.first == 0x30) } catch let error as SecureEnclaveKeyError { guard case let .secureEnclaveUnavailable(description) = error else { Issue.record("SE keygen failure must classify as .secureEnclaveUnavailable: \(error)") return } #expect(description.isEmpty == false) // the underlying CFError is kept for logs } } @Test("signing with a public-only key surfaces .signatureFailed instead of crashing") func signingWithPublicKeyFails() throws { // Arrange — wrap a PUBLIC key in the signer (the shape a mis-wired // composition root could produce). let point = try SecureEnclaveKeyFactory.generateSoftware().publicKeyX963() let publicKey = try #require( SecKeyCreateWithData( point as CFData, [ kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, kSecAttrKeyClass as String: kSecAttrKeyClassPublic, kSecAttrKeySizeInBits as String: 256, ] as CFDictionary, nil ) ) let key = SecureEnclaveKey(privateKey: publicKey) // Act / Assert do { _ = try key.sign(Data("m".utf8)) Issue.record("signing with a public key must throw") } catch let error as SecureEnclaveKeyError { guard case let .signatureFailed(description) = error else { Issue.record("expected .signatureFailed, got \(error)") return } #expect(description.isEmpty == false) } } @Test("a nil CFError is described as 'unknown' rather than crashing the error path") func describeNilError() { #expect(SecureEnclaveKey.describe(nil) == "unknown") }