- 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.
37 lines
1.6 KiB
Swift
37 lines
1.6 KiB
Swift
import Foundation
|
|
|
|
/// C-iOS-2 · Reusable `NSObject` URLSession delegate that answers
|
|
/// **session-level** auth challenges by delegating to `MutualTLSChallengeResponder`.
|
|
///
|
|
/// This is the seam for the HTTP transport (`session.data(for:)` surfaces
|
|
/// challenges through `urlSession(_:didReceive:completionHandler:)`). The WS
|
|
/// transport can't reuse it — its connection object is already the session's
|
|
/// delegate and needs the **task-level** callback — so that one implements the
|
|
/// task-level method itself against the same responder.
|
|
///
|
|
/// `@unchecked Sendable`: URLSession retains its delegate and invokes it from
|
|
/// arbitrary queues; every stored field is an immutable `let` over thread-safe
|
|
/// values (`ClientIdentity` is `@unchecked Sendable`, the responder is stateless).
|
|
public final class ClientTLSSessionDelegate: NSObject, URLSessionDelegate, @unchecked Sendable {
|
|
private let identity: ClientIdentity?
|
|
private let responder: MutualTLSChallengeResponder
|
|
|
|
public init(
|
|
identity: ClientIdentity?,
|
|
responder: MutualTLSChallengeResponder = MutualTLSChallengeResponder()
|
|
) {
|
|
self.identity = identity
|
|
self.responder = responder
|
|
super.init()
|
|
}
|
|
|
|
public func urlSession(
|
|
_ session: URLSession,
|
|
didReceive challenge: URLAuthenticationChallenge,
|
|
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
|
|
) {
|
|
let resolution = responder.resolve(challenge, identity: identity)
|
|
completionHandler(resolution.disposition, resolution.credential)
|
|
}
|
|
}
|