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,73 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user