Files
web-terminal/ios/Packages/ClientTLS/Tests/ClientTLSTests/PKCS12ImporterTests.swift
Yaojia Wang e38e6d1689 feat(ios): device client-cert mTLS — ClientTLS package, transport wiring, install UX (C-iOS)
- ios/Packages/ClientTLS: SecIdentity wrapper, PKCS12 importer (typed errors), keychain store
  (AfterFirstUnlockThisDeviceOnly), pure MutualTLSChallengeResponder truth table, cross-platform
  X.509 DER summary. 14/14 tests.
- Both transports (SessionCore URLSessionTermTransport, App URLSessionHTTPTransport) + SessionThumbnail
  take a lazy @Sendable ()->ClientIdentity? provider: WS resolves per-connect, HTTP per client-cert
  challenge, so a freshly-imported cert applies without an app relaunch. AppEnvironment injects
  { store.loadedIdentityOrNil() }.
- ClientCertScreen (.fileImporter([.pkcs12]) + passphrase -> import -> keychain), reachable via a
  设备证书 entry in SessionListScreen.hostMenu. PairingViewModel gates tunnel-host probes on cert
  presence and re-maps mTLS-reject to a clientCertRejected message.
Verified: ClientTLS 14/14, SessionCore 93/93, xcodegen + xcodebuild BUILD SUCCEEDED.
2026-07-07 09:42:12 +02:00

70 lines
2.4 KiB
Swift

import Foundation
import Security
import Testing
@testable import ClientTLS
// C-iOS-1 · PKCS#12 import against a real OpenSSL-generated fixture `.p12`,
// including the wrong-passphrase and corrupt-file error mappings.
@Test("import with the correct passphrase yields an identity whose leaf CN matches")
func importSucceedsAndExposesLeaf() throws {
// Arrange
let data = ClientTLSFixtures.deviceP12Data
// Act
let identity = try PKCS12Importer.importIdentity(
data: data, passphrase: ClientTLSFixtures.passphrase
)
// Assert
let summary = try #require(identity.summary())
#expect(summary.subjectCommonName == ClientTLSFixtures.leafCommonName)
#expect(summary.issuerCommonName == ClientTLSFixtures.issuerCommonName)
// 825-day leaf minted at authoring time not yet expired.
#expect(summary.isExpired() == false)
}
@Test("import drops the leaf from the issuer chain (credential must not repeat it)")
func importIssuerChainExcludesLeaf() throws {
// Arrange / Act
let identity = try PKCS12Importer.importIdentity(
data: ClientTLSFixtures.deviceP12Data, passphrase: ClientTLSFixtures.passphrase
)
// Assert fixture chain is [leaf, CA]; issuerCertificates keeps only the CA.
let leaf = try #require(identity.leafCertificate())
let leafData = SecCertificateCopyData(leaf) as Data
#expect(identity.issuerCertificates.count == 1)
for issuer in identity.issuerCertificates {
#expect((SecCertificateCopyData(issuer) as Data) != leafData)
}
}
@Test("wrong passphrase maps errSecAuthFailed → .wrongPassphrase")
func importWrongPassphrase() {
// Arrange / Act / Assert
#expect(throws: PKCS12ImportError.wrongPassphrase) {
_ = try PKCS12Importer.importIdentity(
data: ClientTLSFixtures.deviceP12Data, passphrase: "not-the-passphrase"
)
}
}
@Test("garbage bytes map errSecDecode → .corruptFile")
func importCorruptFile() {
// Arrange
let garbage = Data([0x00, 0x01, 0x02, 0x03, 0x99, 0xAB, 0xCD, 0xEF])
// Act / Assert
#expect(throws: PKCS12ImportError.corruptFile) {
_ = try PKCS12Importer.importIdentity(data: garbage, passphrase: "x")
}
}
@Test("empty data is treated as a corrupt file, not a crash")
func importEmptyData() {
#expect(throws: PKCS12ImportError.corruptFile) {
_ = try PKCS12Importer.importIdentity(data: Data(), passphrase: "x")
}
}