@@ -29,6 +29,7 @@ import Foundation
2929 private let sessionHeaders : HTTPHeaders
3030 private let interceptor : any Interceptor
3131 private let logger : any NetworkLogger
32+ private let certificate : any Certificate
3233
3334 private let configuration : URLSessionConfiguration
3435 private let delegateQueue : OperationQueue
@@ -49,6 +50,7 @@ import Foundation
4950 baseHeaders: HTTPHeaders = [ ] ,
5051 interceptor: any Interceptor = DefaultInterceptor ( ) ,
5152 logger: any NetworkLogger = PrintNetworkLogger ( ) ,
53+ certificate: any Certificate = NoPinnedCertificate ( ) ,
5254 name: String ? = nil
5355 ) {
5456 self . name = name ?? " NetworkSession "
@@ -57,6 +59,7 @@ import Foundation
5759 self . sessionHeaders = baseHeaders
5860 self . interceptor = interceptor
5961 self . logger = logger
62+ self . certificate = certificate
6063
6164 let operationQueue = OperationQueue ( )
6265 operationQueue. name = " NetworkActorSerialExecutorOperationQueue "
@@ -90,6 +93,14 @@ internal extension NetworkSession {
9093 self . activeTasks. removeValue ( forKey: task. taskIdentifier)
9194 }
9295
96+ func getPinnedCertificate( ) -> any Certificate {
97+ self . certificate
98+ }
99+
100+ func getLogger( ) -> any NetworkLogger {
101+ self . logger
102+ }
103+
93104}
94105
95106// MARK: - Network session delegate
@@ -106,37 +117,89 @@ final class NetworkSessionDelegate: NSObject {
106117
107118extension NetworkSessionDelegate : URLSessionDelegate {
108119
120+ /// SSL Pinning: Compare server certificate with local (pinned) certificate
121+ /// https://developer.apple.com/documentation/foundation/performing-manual-server-trust-authentication
109122 public func urlSession(
110123 _ session: URLSession ,
111124 didReceive challenge: URLAuthenticationChallenge ,
112125 completionHandler: @escaping ( URLSession . AuthChallengeDisposition , URLCredential ? ) -> Void
113126 ) {
114- // // SSL Pinning: Compare server certificate with local (pinned) certificate
115- // guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
116- // let serverTrust = challenge.protectionSpace.serverTrust,
117- // let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0),
118- // let pinnedCertificateData = Self.pinnedCertificateData()
119- // else {
120- // completionHandler(.cancelAuthenticationChallenge, nil)
121- // return
122- // }
123- //
124- // // Extract server certificate data
125- // let serverCertificateData = SecCertificateCopyData(serverCertificate) as Data
126- //
127- // if serverCertificateData == pinnedCertificateData {
128- // let credential = URLCredential(trust: serverTrust)
129- // completionHandler(.useCredential, credential)
130- // } else {
131- // completionHandler(.cancelAuthenticationChallenge, nil)
132- // }
133-
134- #warning("TODO: Enable SSL pinning")
135- // https://developer.apple.com/documentation/foundation/performing-manual-server-trust-authentication
136-
137- // remove
138- completionHandler ( . performDefaultHandling, nil )
127+ /// Mark completionHandler as isolated to ``NetworkActor``.
128+ ///
129+ /// This delegate function is always called on NetworkActor's operation queue, but is marked as `nonisolated`. Completion handler
130+ /// would usually be called on the same thread.
131+ ///
132+ /// NetworkSessionDelegate cannot conform to `URLSessionDelegate` on `@NetworkActor` because of erroneous
133+ /// `Sendable Self` requirement preventing marking the conformance .
134+ typealias YesActor = @NetworkActor ( URLSession . AuthChallengeDisposition , URLCredential ? ) -> Void
135+ typealias NoActor = ( URLSession . AuthChallengeDisposition , URLCredential ? ) -> Void
136+ let completionHandler = unsafeBitCast ( completionHandler as NoActor , to: YesActor . self)
137+
138+ Task { @NetworkActor in
139+ // Evaluate only SSL/TLS certificates
140+ guard challenge. protectionSpace. authenticationMethod == NSURLAuthenticationMethodServerTrust else {
141+ completionHandler ( . performDefaultHandling, nil )
142+ return
143+ }
144+
145+ guard let serverTrust = challenge. protectionSpace. serverTrust else {
146+ completionHandler ( . performDefaultHandling, nil )
147+ return
148+ }
149+
150+ // Load and validate local certificate
151+ let error : ( any Error ) ?
152+ let certificateDisposition : CertificateDisposition ?
153+ do {
154+ certificateDisposition = try await networkSession. getPinnedCertificate ( )
155+ . certificateDisposition ( using: serverTrust)
156+ error = nil
157+ } catch let certificateError {
158+ certificateDisposition = nil
159+ error = certificateError
160+ }
161+
162+ switch certificateDisposition {
163+ case . evaluate( let certificates) :
164+ // Set local certificate as serverTrust anchor
165+ SecTrustSetAnchorCertificates ( serverTrust, certificates as CFArray )
166+ SecTrustSetAnchorCertificatesOnly ( serverTrust, true )
167+
168+ // Evaluate certificate chain manually
169+ var error : CFError ?
170+ let trusted = SecTrustEvaluateWithError ( serverTrust, & error)
171+ if trusted {
172+ let credential = URLCredential ( trust: serverTrust)
173+ completionHandler ( . useCredential, credential)
174+ } else {
175+ completionHandler ( . cancelAuthenticationChallenge, nil )
176+ }
177+
178+ case . useSystemTrustEvaluation:
179+ completionHandler ( . performDefaultHandling, nil )
180+
181+ case . deny( let reason) :
182+ networkSession. getLogger ( ) . logNetworkEvent (
183+ message: reason,
184+ level: . error,
185+ file: #file,
186+ line: #line
187+ )
188+
189+ completionHandler ( . cancelAuthenticationChallenge, nil )
190+
191+ // call throws
192+ case . none:
193+ networkSession. getLogger ( ) . logNetworkEvent (
194+ message: error? . localizedDescription ?? " nil " ,
195+ level: . error,
196+ file: #file,
197+ line: #line
198+ )
199+ }
200+ }
139201 }
202+
140203}
141204
142205extension NetworkSessionDelegate : URLSessionTaskDelegate {
0 commit comments