- 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.
74 lines
3.2 KiB
Swift
74 lines
3.2 KiB
Swift
import Foundation
|
|
|
|
/// C-iOS-1 · The pure, synchronous decision at the heart of mutual TLS.
|
|
///
|
|
/// It maps a URLSession auth challenge (+ the optionally-installed device
|
|
/// identity) to a disposition and credential. Deliberately free of any
|
|
/// URLSession / socket state so the full truth table is unit-testable without a
|
|
/// live connection — the transports (WS task delegate, HTTP session delegate)
|
|
/// are thin adapters that call `resolve` and forward its result to the
|
|
/// completion handler.
|
|
///
|
|
/// Truth table (plan §C-iOS-1):
|
|
/// a. `ClientCertificate` + identity present → `.useCredential` with the
|
|
/// identity's `URLCredential`.
|
|
/// b. `ClientCertificate` + no identity → `.cancelAuthenticationChallenge`
|
|
/// (a clean, classifiable failure — never a silent no-cert continue).
|
|
/// c. `ServerTrust` → `.performDefaultHandling`
|
|
/// (the LE wildcard is validated by the system trust store).
|
|
/// d. anything else → `.performDefaultHandling`.
|
|
public struct MutualTLSChallengeResponder: Sendable {
|
|
/// The responder's decision. Not `Sendable` (it may carry a `URLCredential`,
|
|
/// which isn't) — it is produced and consumed synchronously on the delegate
|
|
/// queue, never sent across isolation domains.
|
|
public struct Resolution {
|
|
public let disposition: URLSession.AuthChallengeDisposition
|
|
public let credential: URLCredential?
|
|
|
|
public init(
|
|
disposition: URLSession.AuthChallengeDisposition,
|
|
credential: URLCredential? = nil
|
|
) {
|
|
self.disposition = disposition
|
|
self.credential = credential
|
|
}
|
|
}
|
|
|
|
public init() {}
|
|
|
|
/// Resolve a full `URLAuthenticationChallenge` by dispatching on its
|
|
/// authentication method.
|
|
public func resolve(
|
|
_ challenge: URLAuthenticationChallenge, identity: ClientIdentity?
|
|
) -> Resolution {
|
|
resolve(
|
|
authenticationMethod: challenge.protectionSpace.authenticationMethod,
|
|
identity: identity
|
|
)
|
|
}
|
|
|
|
/// Method-keyed core (challenge-free) — the directly-tested pure function.
|
|
public func resolve(
|
|
authenticationMethod: String, identity: ClientIdentity?
|
|
) -> Resolution {
|
|
switch authenticationMethod {
|
|
case NSURLAuthenticationMethodClientCertificate:
|
|
guard let identity else {
|
|
// (b) No installed identity: cancel so the failure surfaces as a
|
|
// classifiable client-cert error rather than a bare TLS reset.
|
|
return Resolution(disposition: .cancelAuthenticationChallenge)
|
|
}
|
|
// (a) Present the device certificate for this session.
|
|
return Resolution(
|
|
disposition: .useCredential, credential: identity.urlCredential()
|
|
)
|
|
case NSURLAuthenticationMethodServerTrust:
|
|
// (c) System-trusted LE wildcard — default evaluation.
|
|
return Resolution(disposition: .performDefaultHandling)
|
|
default:
|
|
// (d) Basic/Digest/NTLM/etc. — not used by this app; default.
|
|
return Resolution(disposition: .performDefaultHandling)
|
|
}
|
|
}
|
|
}
|