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.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import ClientTLS
|
||||
|
||||
// C-iOS-1 · Store roundtrip via the InMemory store (same import/summary code
|
||||
// path as the keychain store, minus the entitlement — the keychain variant is
|
||||
// covered by on-device/simulator tests, mirroring KeychainHostStoreLiveTests).
|
||||
|
||||
@Test("save → loadIdentity roundtrips and hasInstalledIdentity flips")
|
||||
func storeRoundtrip() throws {
|
||||
// Arrange
|
||||
let store = InMemoryClientIdentityStore()
|
||||
#expect(store.hasInstalledIdentity() == false)
|
||||
#expect(try store.loadIdentity() == nil)
|
||||
|
||||
// Act
|
||||
try store.save(
|
||||
p12Data: ClientTLSFixtures.deviceP12Data, passphrase: ClientTLSFixtures.passphrase
|
||||
)
|
||||
|
||||
// Assert
|
||||
#expect(store.hasInstalledIdentity() == true)
|
||||
let identity = try #require(try store.loadIdentity())
|
||||
#expect(identity.summary()?.subjectCommonName == ClientTLSFixtures.leafCommonName)
|
||||
let summary = try #require(try store.loadSummary())
|
||||
#expect(summary.issuerCommonName == ClientTLSFixtures.issuerCommonName)
|
||||
}
|
||||
|
||||
@Test("save validates the passphrase and leaves prior state untouched on failure")
|
||||
func storeSaveValidatesBeforePersisting() {
|
||||
// Arrange
|
||||
let store = InMemoryClientIdentityStore()
|
||||
|
||||
// Act / Assert — a wrong passphrase must surface, not persist.
|
||||
#expect(throws: PKCS12ImportError.wrongPassphrase) {
|
||||
try store.save(p12Data: ClientTLSFixtures.deviceP12Data, passphrase: "wrong")
|
||||
}
|
||||
#expect(store.hasInstalledIdentity() == false)
|
||||
}
|
||||
|
||||
@Test("remove clears the stored identity")
|
||||
func storeRemove() throws {
|
||||
// Arrange
|
||||
let store = InMemoryClientIdentityStore()
|
||||
try store.save(
|
||||
p12Data: ClientTLSFixtures.deviceP12Data, passphrase: ClientTLSFixtures.passphrase
|
||||
)
|
||||
#expect(store.hasInstalledIdentity() == true)
|
||||
|
||||
// Act
|
||||
try store.remove()
|
||||
|
||||
// Assert
|
||||
#expect(store.hasInstalledIdentity() == false)
|
||||
#expect(try store.loadIdentity() == nil)
|
||||
}
|
||||
|
||||
@Test("loadedIdentityOrNil returns nil (not a throw) when nothing is installed")
|
||||
func loadedIdentityOrNilEmpty() {
|
||||
let store = InMemoryClientIdentityStore()
|
||||
#expect(store.loadedIdentityOrNil() == nil)
|
||||
}
|
||||
Reference in New Issue
Block a user