From 3a6f4f1e197a26347fee72b691535ab7ce998d43 Mon Sep 17 00:00:00 2001 From: Loay Ashraf Date: Sat, 4 May 2024 23:10:02 +0300 Subject: [PATCH 1/3] feat: add TLS pinning types and extensions Resolves: none. --- RxNetworkKit.xcodeproj/project.pbxproj | 74 ++++++++++++ .../Reactive+URLSessionDownloadResponse.swift | 4 +- .../Reactive+URLSessionUploadResponse.swift | 4 +- .../Request/Logger/HTTPRequestLogger.swift | 60 ++++++---- .../Extensions/Reactive+RESTResponse.swift | 2 +- Source/Session/Types/Session.swift | 10 +- .../Session/Types/SessionConfiguration.swift | 9 ++ .../TLS/Extensions/Data+SecCertificate.swift | 20 ++++ .../Extensions/SecCertificate+Bundle.swift | 21 ++++ .../Extensions/SecCertificate+PublicKey.swift | 16 +++ Source/TLS/Extensions/SecKey+Bundle.swift | 22 ++++ .../SecKey+ExternalRepresentation.swift | 16 +++ .../TLS/Extensions/Set+SecCertificate.swift | 62 ++++++++++ Source/TLS/Extensions/Set+SecKey.swift | 30 +++++ Source/TLS/Types/TLSTrustChainLevel.swift | 14 +++ .../TLS/Types/TLSTrustEvaluationPolicy.swift | 14 +++ Source/TLS/Types/TLSTrustEvaluator.swift | 112 ++++++++++++++++++ .../TLSTrustEvaluatorConfiguration.swift | 19 +++ 18 files changed, 480 insertions(+), 29 deletions(-) create mode 100644 Source/TLS/Extensions/Data+SecCertificate.swift create mode 100644 Source/TLS/Extensions/SecCertificate+Bundle.swift create mode 100644 Source/TLS/Extensions/SecCertificate+PublicKey.swift create mode 100644 Source/TLS/Extensions/SecKey+Bundle.swift create mode 100644 Source/TLS/Extensions/SecKey+ExternalRepresentation.swift create mode 100644 Source/TLS/Extensions/Set+SecCertificate.swift create mode 100644 Source/TLS/Extensions/Set+SecKey.swift create mode 100644 Source/TLS/Types/TLSTrustChainLevel.swift create mode 100644 Source/TLS/Types/TLSTrustEvaluationPolicy.swift create mode 100644 Source/TLS/Types/TLSTrustEvaluator.swift create mode 100644 Source/TLS/Types/TLSTrustEvaluatorConfiguration.swift diff --git a/RxNetworkKit.xcodeproj/project.pbxproj b/RxNetworkKit.xcodeproj/project.pbxproj index ca07bfb..edf0808 100644 --- a/RxNetworkKit.xcodeproj/project.pbxproj +++ b/RxNetworkKit.xcodeproj/project.pbxproj @@ -65,6 +65,17 @@ C6BDFFF02ACDF4AB0022F675 /* WebSocketCloseHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6BDFFEF2ACDF4AB0022F675 /* WebSocketCloseHandler.swift */; }; C6BDFFF32ACDF5100022F675 /* WebSocketMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6BDFFF22ACDF50F0022F675 /* WebSocketMessage.swift */; }; C6BDFFF52ACDF5250022F675 /* WebSocketCloseCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6BDFFF42ACDF5250022F675 /* WebSocketCloseCode.swift */; }; + C6C643012BE6C7B30071C2CC /* TLSTrustEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6C643002BE6C7B30071C2CC /* TLSTrustEvaluator.swift */; }; + C6C643032BE6C8580071C2CC /* TLSTrustEvaluatorConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6C643022BE6C8580071C2CC /* TLSTrustEvaluatorConfiguration.swift */; }; + C6C643052BE6C8770071C2CC /* TLSTrustEvaluationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6C643042BE6C8770071C2CC /* TLSTrustEvaluationPolicy.swift */; }; + C6C643072BE6C88A0071C2CC /* TLSTrustChainLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6C643062BE6C88A0071C2CC /* TLSTrustChainLevel.swift */; }; + C6C643092BE6C9340071C2CC /* SecCertificate+Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6C643082BE6C9340071C2CC /* SecCertificate+Bundle.swift */; }; + C6C6430B2BE6C9510071C2CC /* SecCertificate+PublicKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6C6430A2BE6C9510071C2CC /* SecCertificate+PublicKey.swift */; }; + C6C6430D2BE6C98B0071C2CC /* SecKey+ExternalRepresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6C6430C2BE6C98B0071C2CC /* SecKey+ExternalRepresentation.swift */; }; + C6C6430F2BE6C9A40071C2CC /* SecKey+Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6C6430E2BE6C9A40071C2CC /* SecKey+Bundle.swift */; }; + C6C643112BE6C9EC0071C2CC /* Set+SecCertificate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6C643102BE6C9EC0071C2CC /* Set+SecCertificate.swift */; }; + C6C643132BE6CA080071C2CC /* Set+SecKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6C643122BE6CA080071C2CC /* Set+SecKey.swift */; }; + C6C643152BE6CA830071C2CC /* Data+SecCertificate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6C643142BE6CA830071C2CC /* Data+SecCertificate.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -123,6 +134,17 @@ C6BDFFEF2ACDF4AB0022F675 /* WebSocketCloseHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocketCloseHandler.swift; sourceTree = ""; }; C6BDFFF22ACDF50F0022F675 /* WebSocketMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocketMessage.swift; sourceTree = ""; }; C6BDFFF42ACDF5250022F675 /* WebSocketCloseCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocketCloseCode.swift; sourceTree = ""; }; + C6C643002BE6C7B30071C2CC /* TLSTrustEvaluator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TLSTrustEvaluator.swift; sourceTree = ""; }; + C6C643022BE6C8580071C2CC /* TLSTrustEvaluatorConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TLSTrustEvaluatorConfiguration.swift; sourceTree = ""; }; + C6C643042BE6C8770071C2CC /* TLSTrustEvaluationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TLSTrustEvaluationPolicy.swift; sourceTree = ""; }; + C6C643062BE6C88A0071C2CC /* TLSTrustChainLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TLSTrustChainLevel.swift; sourceTree = ""; }; + C6C643082BE6C9340071C2CC /* SecCertificate+Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SecCertificate+Bundle.swift"; sourceTree = ""; }; + C6C6430A2BE6C9510071C2CC /* SecCertificate+PublicKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SecCertificate+PublicKey.swift"; sourceTree = ""; }; + C6C6430C2BE6C98B0071C2CC /* SecKey+ExternalRepresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SecKey+ExternalRepresentation.swift"; sourceTree = ""; }; + C6C6430E2BE6C9A40071C2CC /* SecKey+Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SecKey+Bundle.swift"; sourceTree = ""; }; + C6C643102BE6C9EC0071C2CC /* Set+SecCertificate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Set+SecCertificate.swift"; sourceTree = ""; }; + C6C643122BE6CA080071C2CC /* Set+SecKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Set+SecKey.swift"; sourceTree = ""; }; + C6C643142BE6CA830071C2CC /* Data+SecCertificate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+SecCertificate.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -163,6 +185,7 @@ isa = PBXGroup; children = ( C61CB5622AF2CF31006A203A /* Session */, + C6C642FF2BE6C78E0071C2CC /* TLS */, C6554A292AD5B5600090DD3A /* REST */, 0B77E06129D965D30077FBC0 /* HTTP */, 0B77E06729D965D30077FBC0 /* Reachability */, @@ -362,6 +385,40 @@ path = "Web Socket"; sourceTree = ""; }; + C6C642FF2BE6C78E0071C2CC /* TLS */ = { + isa = PBXGroup; + children = ( + C6C643162BE6CACF0071C2CC /* Extensions */, + C6C643172BE6CAFE0071C2CC /* Types */, + ); + path = TLS; + sourceTree = ""; + }; + C6C643162BE6CACF0071C2CC /* Extensions */ = { + isa = PBXGroup; + children = ( + C6C643142BE6CA830071C2CC /* Data+SecCertificate.swift */, + C6C643082BE6C9340071C2CC /* SecCertificate+Bundle.swift */, + C6C6430A2BE6C9510071C2CC /* SecCertificate+PublicKey.swift */, + C6C6430E2BE6C9A40071C2CC /* SecKey+Bundle.swift */, + C6C6430C2BE6C98B0071C2CC /* SecKey+ExternalRepresentation.swift */, + C6C643102BE6C9EC0071C2CC /* Set+SecCertificate.swift */, + C6C643122BE6CA080071C2CC /* Set+SecKey.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + C6C643172BE6CAFE0071C2CC /* Types */ = { + isa = PBXGroup; + children = ( + C6C643062BE6C88A0071C2CC /* TLSTrustChainLevel.swift */, + C6C643042BE6C8770071C2CC /* TLSTrustEvaluationPolicy.swift */, + C6C643002BE6C7B30071C2CC /* TLSTrustEvaluator.swift */, + C6C643022BE6C8580071C2CC /* TLSTrustEvaluatorConfiguration.swift */, + ); + path = Types; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -460,10 +517,14 @@ 0B77E09F29D965D30077FBC0 /* NWPath+InterfaceType.swift in Sources */, 0B77E0A029D965D30077FBC0 /* NetworkReachabilityStatus.swift in Sources */, 0B77E0AE29D965D30077FBC0 /* Single+VerifyResponse.swift in Sources */, + C6C643092BE6C9340071C2CC /* SecCertificate+Bundle.swift in Sources */, 0B77E0B429D965D30077FBC0 /* HTTPDownloadRequestRouter.swift in Sources */, + C6C643132BE6CA080071C2CC /* Set+SecKey.swift in Sources */, 0B77E09A29D965D30077FBC0 /* HTTPMIMEType.swift in Sources */, C658CF652AD5F648009E561D /* RxNetworkKit.swift in Sources */, C6A9BEFF2A93FB1D00459E32 /* ProcessInfo+operatingSystemName.swift in Sources */, + C6C643072BE6C88A0071C2CC /* TLSTrustChainLevel.swift in Sources */, + C6C643032BE6C8580071C2CC /* TLSTrustEvaluatorConfiguration.swift in Sources */, 0B77E08E29D965D30077FBC0 /* HTTPUploadRequestFile.swift in Sources */, 0B77E08D29D965D30077FBC0 /* HTTPUploadRequestFormData.swift in Sources */, C61A7E262B62782D00407C38 /* Reactive+RESTResponse.swift in Sources */, @@ -471,13 +532,18 @@ C69A78562ACF001400ECF092 /* Docs.docc in Sources */, 0B77E0A129D965D30077FBC0 /* NWInterfaceType+RawRepresentable.swift in Sources */, 0B77E0B129D965D30077FBC0 /* Observable+Decodable.swift in Sources */, + C6C6430F2BE6C9A40071C2CC /* SecKey+Bundle.swift in Sources */, + C6C643152BE6CA830071C2CC /* Data+SecCertificate.swift in Sources */, 0B77E08A29D965D30077FBC0 /* HTTPDownloadRequestEvent.swift in Sources */, C6A9BEFD2A93FAF100459E32 /* URLSessionConfiguration+setUserAgentHTTPHeader.swift in Sources */, C6554A2B2AD5BBB60090DD3A /* RESTClient.swift in Sources */, + C6C643052BE6C8770071C2CC /* TLSTrustEvaluationPolicy.swift in Sources */, 0B77E09129D965D30077FBC0 /* Reactive+URLSessionAdaptedUploadResponse.swift in Sources */, + C6C643012BE6C7B30071C2CC /* TLSTrustEvaluator.swift in Sources */, C6A9BEFA2A93F16200459E32 /* URLSessionConfiguration+setAdditionalHTTPHeader.swift in Sources */, C6BDFFF32ACDF5100022F675 /* WebSocketMessage.swift in Sources */, C61A7E242B6276F800407C38 /* HTTPRequestLogger.swift in Sources */, + C6C6430B2BE6C9510071C2CC /* SecCertificate+PublicKey.swift in Sources */, C61A7E302B627B3000407C38 /* SessionConfiguration.swift in Sources */, C6B4B4C42AD47A2F009073ED /* WebSocketError.swift in Sources */, 0B77E0A429D965D30077FBC0 /* NetworkReachability.swift in Sources */, @@ -487,6 +553,7 @@ C6BDFFF52ACDF5250022F675 /* WebSocketCloseCode.swift in Sources */, 0B77E0B629D965D30077FBC0 /* HTTPUploadRequestRouter.swift in Sources */, 0B77E0AB29D965D30077FBC0 /* Single+Retry.swift in Sources */, + C6C643112BE6C9EC0071C2CC /* Set+SecCertificate.swift in Sources */, C61A7E2C2B62798800407C38 /* URLRequest+CURLCommand.swift in Sources */, 0B77E08F29D965D30077FBC0 /* URLSession+UploadTask.swift in Sources */, 0B77E08C29D965D30077FBC0 /* Reactive+URLSessionAdaptedDownloadResponse.swift in Sources */, @@ -497,6 +564,7 @@ 0B77E0AC29D965D30077FBC0 /* Single+Decode.swift in Sources */, C61A7E2E2B6279C700407C38 /* Data+JSON.swift in Sources */, 0B77E0A329D965D30077FBC0 /* NetworkInterfaceType.swift in Sources */, + C6C6430D2BE6C98B0071C2CC /* SecKey+ExternalRepresentation.swift in Sources */, 0B77E09229D965D30077FBC0 /* HTTPUploadRequestEvent.swift in Sources */, 0B77E09029D965D30077FBC0 /* Data+AppendString.swift in Sources */, C6BDFFE82ACDF3830022F675 /* Reactive+WebSocketReceive.swift in Sources */, @@ -570,8 +638,10 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TVOS_DEPLOYMENT_TARGET = 14.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; + WATCHOS_DEPLOYMENT_TARGET = 7.0; }; name = Debug; }; @@ -627,9 +697,11 @@ SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; + TVOS_DEPLOYMENT_TARGET = 14.0; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; + WATCHOS_DEPLOYMENT_TARGET = 7.0; }; name = Release; }; @@ -666,6 +738,7 @@ SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,3,4"; @@ -707,6 +780,7 @@ SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,3,4"; diff --git a/Source/HTTP/Extensions/Reactive+URLSessionDownloadResponse.swift b/Source/HTTP/Extensions/Reactive+URLSessionDownloadResponse.swift index b8bf369..89a664f 100644 --- a/Source/HTTP/Extensions/Reactive+URLSessionDownloadResponse.swift +++ b/Source/HTTP/Extensions/Reactive+URLSessionDownloadResponse.swift @@ -25,7 +25,7 @@ extension Reactive where Base: URLSession { let task = self.base.fileDownloadTask(with: request) { data, response, error in #if DEBUG if URLSession.logRequests { - HTTPRequestLogger.shared.log(response: (response, data, error), bodyPlaceholder: "[File Body]") + HTTPRequestLogger.shared.log(response: (request.url, response, data, error), bodyPlaceholder: "[File Body]") } #endif guard let response = response, let data = data else { @@ -66,7 +66,7 @@ extension Reactive where Base: URLSession { let task = self.base.fileDownloadTask(with: request, saveTo: url) { data, response, error in #if DEBUG if URLSession.logRequests { - HTTPRequestLogger.shared.log(response: (response, data, error), bodyPlaceholder: "[File Body]") + HTTPRequestLogger.shared.log(response: (request.url, response, data, error), bodyPlaceholder: "[File Body]") } #endif guard let response = response, let data = data else { diff --git a/Source/HTTP/Extensions/Reactive+URLSessionUploadResponse.swift b/Source/HTTP/Extensions/Reactive+URLSessionUploadResponse.swift index 44497b9..faa4410 100644 --- a/Source/HTTP/Extensions/Reactive+URLSessionUploadResponse.swift +++ b/Source/HTTP/Extensions/Reactive+URLSessionUploadResponse.swift @@ -26,7 +26,7 @@ extension Reactive where Base: URLSession { let task = self.base.fileUploadTask(with: request, from: file) { data, response, error in #if DEBUG if URLSession.logRequests { - HTTPRequestLogger.shared.log(response: (response, data, error)) + HTTPRequestLogger.shared.log(response: (request.url, response, data, error)) } #endif guard let response = response, let data = data else { @@ -67,7 +67,7 @@ extension Reactive where Base: URLSession { let task = self.base.formDataUploadTask(with: request, from: formData) { data, response, error in #if DEBUG if URLSession.logRequests { - HTTPRequestLogger.shared.log(response: (response, data, error)) + HTTPRequestLogger.shared.log(response: (request.url, response, data, error)) } #endif guard let response = response, let data = data else { diff --git a/Source/HTTP/Types/Request/Logger/HTTPRequestLogger.swift b/Source/HTTP/Types/Request/Logger/HTTPRequestLogger.swift index 4c63dab..c18d721 100644 --- a/Source/HTTP/Types/Request/Logger/HTTPRequestLogger.swift +++ b/Source/HTTP/Types/Request/Logger/HTTPRequestLogger.swift @@ -29,9 +29,9 @@ class HTTPRequestLogger { /// Prints incoming response to console. /// /// - Parameters: - /// - response: `(URLResponse?, Data?, Error?)` to be printed to console. + /// - response: `(URL?, URLResponse?, Data?, Error?)` to be printed to console. /// - bodyPlaceholder: `String?` placeholder to be printed in place of actual body. - func log(response: (URLResponse?, Data?, Error?), bodyPlaceholder: String? = nil) { + func log(response: (URL?, URLResponse?, Data?, Error?), bodyPlaceholder: String? = nil) { let logMessage = makeLogMessage(for: response, bodyPlaceholder: bodyPlaceholder) print(logMessage) } @@ -48,17 +48,19 @@ class HTTPRequestLogger { logMessage += "* * * * * * * * * * OUTGOING REQUEST * * * * * * * * * *\n" - let urlAsString = request.url?.absoluteString ?? "" - let urlComponents = URLComponents(string: urlAsString) + let urlString = request.url?.absoluteString ?? "" + let urlComponents = URLComponents(string: urlString) let method = request.httpMethod != nil ? "\(request.httpMethod ?? "")" : "" let path = "\(urlComponents?.path ?? "")" let query = "\(urlComponents?.query ?? "")" let host = "\(urlComponents?.host ?? "")" + var requestDetails = """ - \n\(urlAsString) \n + \n\(urlString) \n \(method) \(path)?\(query) HTTP/1.1 \n HOST: \(host)\n """ + for (key,value) in request.allHTTPHeaderFields ?? [:] { requestDetails += "\(key): \(value) \n" } @@ -87,38 +89,43 @@ class HTTPRequestLogger { /// Make console message for incoming response. /// /// - Parameters: - /// - response: `(URLResponse?, Data?, Error?)` to be included in message. + /// - response: `(URL?, URLResponse?, Data?, Error?)` to be included in message. /// - bodyPlaceholder: `String?` placeholder to be included in message in place of actual body. /// /// - Returns: `String` of incoming response message. - private func makeLogMessage(for response: (URLResponse?, Data?, Error?), bodyPlaceholder: String?) -> String { + private func makeLogMessage(for response: (URL?, URLResponse?, Data?, Error?), bodyPlaceholder: String?) -> String { var logMessage: String = "" logMessage += "* * * * * * * * * * INCOMING RESPONSE * * * * * * * * * *\n" - let httpResponse = response.0 as? HTTPURLResponse - let responseBody = response.1 - let responseError = response.2 + let url = response.0 + let httpResponse = response.1 as? HTTPURLResponse + let responseBody = response.2 + let responseError = response.3 + + + let urlString = url?.absoluteString ?? "" + let urlComponents = URLComponents(string: urlString) + let host = "\(urlComponents?.host ?? "")" + let path = "\(urlComponents?.path ?? "")" + let query = "\(urlComponents?.query ?? "")" - let urlString = httpResponse?.url?.absoluteString - let components = URLComponents(string: urlString ?? "") - let path = "\(components?.path ?? "")" - let query = "\(components?.query ?? "")" var responseDetails = "" - if let urlString = urlString { - responseDetails += "\n\(urlString)" - responseDetails += "\n\n" - } + + responseDetails += "\n\(urlString)" + responseDetails += "\n\n" + if let statusCode = httpResponse?.statusCode { responseDetails += "HTTP \(statusCode) \(path)?\(query)\n" } - if let host = components?.host { - responseDetails += "Host: \(host)\n" - } + + responseDetails += "Host: \(host)\n" + for (key, value) in httpResponse?.allHeaderFields ?? [:] { responseDetails += "\(key): \(value)\n" } - if let bodyPlaceholder = bodyPlaceholder { + if let bodyPlaceholder = bodyPlaceholder, + responseError == nil { responseDetails += "\n\(bodyPlaceholder)\n" } else if let body = responseBody { if let jsonString = body.json { @@ -128,7 +135,14 @@ class HTTPRequestLogger { } } if let responseError = responseError { - responseDetails += "\nError: \(responseError.localizedDescription)\n" + let errorCode = (responseError as NSError).code + if errorCode == -999, + TLSTrustEvaluator.getBlockedHosts().contains(host) { + responseDetails += "\nError: TLS trust evaluation failed for the specified host, you may need to update the pinned certificates or public keys.\n" + } else { + responseDetails += "\nError: \(responseError.localizedDescription)\n" + } + } logMessage += responseDetails diff --git a/Source/REST/Extensions/Reactive+RESTResponse.swift b/Source/REST/Extensions/Reactive+RESTResponse.swift index b4800fb..4da068e 100644 --- a/Source/REST/Extensions/Reactive+RESTResponse.swift +++ b/Source/REST/Extensions/Reactive+RESTResponse.swift @@ -35,7 +35,7 @@ extension Reactive where Base: URLSession { let task = self.base.dataTask(with: request) { data, response, error in #if DEBUG if URLSession.logRequests { - HTTPRequestLogger.shared.log(response: (response, data, error)) + HTTPRequestLogger.shared.log(response: (request.url, response, data, error)) } #endif diff --git a/Source/Session/Types/Session.swift b/Source/Session/Types/Session.swift index 8396574..7d149e8 100644 --- a/Source/Session/Types/Session.swift +++ b/Source/Session/Types/Session.swift @@ -12,6 +12,8 @@ public class Session { /// Principal `URLSession`object used to create request tasks. let urlSession: URLSession + private let tlsTrustEvaluator: TLSTrustEvaluator + private let tlsTrustEvaluatorQueue: OperationQueue /// Creates a `Session` instance. /// @@ -24,8 +26,14 @@ public class Session { urlSessionConfiguration.setUserAgentHTTPHeader() } URLSession.logRequests = configuration.logRequests + tlsTrustEvaluator = .init(configuration: configuration.tlsTrustEvaluatorConfiguration) + tlsTrustEvaluatorQueue = .init() + tlsTrustEvaluatorQueue.name = "RxNetworkKit - TLSTrustEvaluator" + tlsTrustEvaluatorQueue.qualityOfService = .utility // Initialize `URLSession`. - urlSession = .init(configuration: urlSessionConfiguration) + urlSession = .init(configuration: urlSessionConfiguration, + delegate: tlsTrustEvaluator, + delegateQueue: tlsTrustEvaluatorQueue) } } diff --git a/Source/Session/Types/SessionConfiguration.swift b/Source/Session/Types/SessionConfiguration.swift index cd77118..86b3da1 100644 --- a/Source/Session/Types/SessionConfiguration.swift +++ b/Source/Session/Types/SessionConfiguration.swift @@ -15,12 +15,17 @@ public class SessionConfiguration { .init(urlSessionConfiguration: .default) } + public static var ephemeral: SessionConfiguration { + .init(urlSessionConfiguration: .ephemeral) + } + /// `URLSessionConfiguration` object used to create `URLSession` object. let urlSessionConfiguration: URLSessionConfiguration /// `Bool` flag that indicates wether a `URLSession` should add `User-Agent` header to outgoing requests. public var setUserAgentHeader: Bool = true /// `Bool` flag that indicates wether a `URLSession` should print outgoing requests to the console. public var logRequests: Bool = true + public var tlsTrustEvaluatorConfiguration: TLSTrustEvaluatorConfiguration = .default /// Creates a `SessionConfiguration` instance. /// @@ -30,4 +35,8 @@ public class SessionConfiguration { self.urlSessionConfiguration = urlSessionConfiguration } + public static func background(withIdentifier identifier: String) -> SessionConfiguration { + .init(urlSessionConfiguration: .background(withIdentifier: identifier)) + } + } diff --git a/Source/TLS/Extensions/Data+SecCertificate.swift b/Source/TLS/Extensions/Data+SecCertificate.swift new file mode 100644 index 0000000..19c215c --- /dev/null +++ b/Source/TLS/Extensions/Data+SecCertificate.swift @@ -0,0 +1,20 @@ +// +// Data+SecCertificate.swift +// RxNetworkKit +// +// Created by Loay Ashraf on 04/05/2024. +// + +import Foundation + +extension Data { + + var certificate: SecCertificate? { + guard let base64String = String(data: self, encoding: .utf8)?.replacingOccurrences(of: "-----BEGIN CERTIFICATE-----", with: "").replacingOccurrences(of: "-----END CERTIFICATE-----", with: ""), + let base64DecodedData = Data(base64Encoded: base64String, options: .ignoreUnknownCharacters) else { + return SecCertificateCreateWithData(nil, self as CFData) + } + return SecCertificateCreateWithData(nil, base64DecodedData as CFData) + } + +} diff --git a/Source/TLS/Extensions/SecCertificate+Bundle.swift b/Source/TLS/Extensions/SecCertificate+Bundle.swift new file mode 100644 index 0000000..98cb7d4 --- /dev/null +++ b/Source/TLS/Extensions/SecCertificate+Bundle.swift @@ -0,0 +1,21 @@ +// +// SecCertificate+Bundle.swift +// RxNetworkKit +// +// Created by Loay Ashraf on 04/05/2024. +// + +import Foundation + +public extension SecCertificate { + + static func fromBundle(_ name: String, _ `extension`: String) -> SecCertificate? { + guard let fileURL = Bundle.main.url(forResource: name, withExtension: `extension`), + let certificateData = try? Data(contentsOf: fileURL), + let certificate = certificateData.certificate else { + return nil + } + return certificate + } + +} diff --git a/Source/TLS/Extensions/SecCertificate+PublicKey.swift b/Source/TLS/Extensions/SecCertificate+PublicKey.swift new file mode 100644 index 0000000..0cc5c40 --- /dev/null +++ b/Source/TLS/Extensions/SecCertificate+PublicKey.swift @@ -0,0 +1,16 @@ +// +// SecCertificate+PublicKey.swift +// RxNetworkKit +// +// Created by Loay Ashraf on 04/05/2024. +// + +import Foundation + +public extension SecCertificate { + + var publicKey: SecKey? { + SecCertificateCopyKey(self) + } + +} diff --git a/Source/TLS/Extensions/SecKey+Bundle.swift b/Source/TLS/Extensions/SecKey+Bundle.swift new file mode 100644 index 0000000..7c54fea --- /dev/null +++ b/Source/TLS/Extensions/SecKey+Bundle.swift @@ -0,0 +1,22 @@ +// +// SecKey+Bundle.swift +// RxNetworkKit +// +// Created by Loay Ashraf on 04/05/2024. +// + +import Foundation + +public extension SecKey { + + static func fromBundle(_ name: String, _ `extension`: String) -> SecKey? { + guard let fileURL = Bundle.main.url(forResource: name, withExtension: `extension`), + let certificateData = try? Data(contentsOf: fileURL), + let certificate = certificateData.certificate, + let publicKey = certificate.publicKey else { + return nil + } + return publicKey + } + +} diff --git a/Source/TLS/Extensions/SecKey+ExternalRepresentation.swift b/Source/TLS/Extensions/SecKey+ExternalRepresentation.swift new file mode 100644 index 0000000..0e4435b --- /dev/null +++ b/Source/TLS/Extensions/SecKey+ExternalRepresentation.swift @@ -0,0 +1,16 @@ +// +// SecKey+ExternalRepresentation.swift +// RxNetworkKit +// +// Created by Loay Ashraf on 04/05/2024. +// + +import Foundation + +public extension SecKey { + + var externalRepresentation: CFData? { + SecKeyCopyExternalRepresentation(self, nil) + } + +} diff --git a/Source/TLS/Extensions/Set+SecCertificate.swift b/Source/TLS/Extensions/Set+SecCertificate.swift new file mode 100644 index 0000000..6e24693 --- /dev/null +++ b/Source/TLS/Extensions/Set+SecCertificate.swift @@ -0,0 +1,62 @@ +// +// Set+SecCertificate.swift +// RxNetworkKit +// +// Created by Loay Ashraf on 04/05/2024. +// + +import Foundation + +public extension Set where Element == SecCertificate { + + static var `default`: Set { + var certificates: Set = [] + + var cerURLs = Bundle.main.urls(forResourcesWithExtension: "cer", subdirectory: nil) ?? [] + cerURLs.append(contentsOf: Bundle.main.urls(forResourcesWithExtension: "CER", subdirectory: nil) ?? []) + + var derURLs = Bundle.main.urls(forResourcesWithExtension: "der", subdirectory: nil) ?? [] + derURLs.append(contentsOf: Bundle.main.urls(forResourcesWithExtension: "DER", subdirectory: nil) ?? []) + + var crtURLs = Bundle.main.urls(forResourcesWithExtension: "crt", subdirectory: nil) ?? [] + crtURLs.append(contentsOf: Bundle.main.urls(forResourcesWithExtension: "CRT", subdirectory: nil) ?? []) + + var pemURLs = Bundle.main.urls(forResourcesWithExtension: "pem", subdirectory: nil) ?? [] + pemURLs.append(contentsOf: Bundle.main.urls(forResourcesWithExtension: "PEM", subdirectory: nil) ?? []) + + for cerURL in cerURLs { + if let certificateData = try? Data(contentsOf: cerURL), + let certificate = certificateData.certificate { + certificates.insert(certificate) + } + } + + for derURL in derURLs { + if let certificateData = try? Data(contentsOf: derURL), + let certificate = certificateData.certificate { + certificates.insert(certificate) + } + } + + for crtURL in crtURLs { + if let certificateData = try? Data(contentsOf: crtURL), + let certificate = certificateData.certificate { + certificates.insert(certificate) + } + } + + for pemURL in pemURLs { + if let certificateData = try? Data(contentsOf: pemURL), + let certificate = certificateData.certificate { + certificates.insert(certificate) + } + } + + return certificates + } + + static func singleFromBundle(_ name: String, _ `extension`: String) -> Set { + Set([.fromBundle(name, `extension`)!]) + } + +} diff --git a/Source/TLS/Extensions/Set+SecKey.swift b/Source/TLS/Extensions/Set+SecKey.swift new file mode 100644 index 0000000..0e5ba01 --- /dev/null +++ b/Source/TLS/Extensions/Set+SecKey.swift @@ -0,0 +1,30 @@ +// +// Set+SecKey.swift +// RxNetworkKit +// +// Created by Loay Ashraf on 04/05/2024. +// + +import Foundation + +public extension Set where Element == SecKey { + + static var `default`: Set { + var publicKeys: Set = [] + + let certificates: Set = .default + + for certificate in certificates { + if let publicKey = SecCertificateCopyKey(certificate) { + publicKeys.insert(publicKey) + } + } + + return publicKeys + } + + static func singleFromBundle(_ name: String, _ `extension`: String) -> Set { + Set([.fromBundle(name, `extension`)!]) + } + +} diff --git a/Source/TLS/Types/TLSTrustChainLevel.swift b/Source/TLS/Types/TLSTrustChainLevel.swift new file mode 100644 index 0000000..913ebc0 --- /dev/null +++ b/Source/TLS/Types/TLSTrustChainLevel.swift @@ -0,0 +1,14 @@ +// +// TLSTrustChainLevel.swift +// RxNetworkKit +// +// Created by Loay Ashraf on 04/05/2024. +// + +import Foundation + +public enum TLSTrustChainLevel: Int { + case leaf + case intermediate + case root +} diff --git a/Source/TLS/Types/TLSTrustEvaluationPolicy.swift b/Source/TLS/Types/TLSTrustEvaluationPolicy.swift new file mode 100644 index 0000000..37dd302 --- /dev/null +++ b/Source/TLS/Types/TLSTrustEvaluationPolicy.swift @@ -0,0 +1,14 @@ +// +// TLSTrustEvaluationPolicy.swift +// RxNetworkKit +// +// Created by Loay Ashraf on 04/05/2024. +// + +import Foundation + +public enum TLSTrustEvaluationPolicy { + case certificates(_ certificates: Set = .default) + case publicKeys(_ publicKeys: Set = .default) + case mixed(_ certificates: Set = .default, _ publicKeys: Set = .default) +} diff --git a/Source/TLS/Types/TLSTrustEvaluator.swift b/Source/TLS/Types/TLSTrustEvaluator.swift new file mode 100644 index 0000000..72c7b81 --- /dev/null +++ b/Source/TLS/Types/TLSTrustEvaluator.swift @@ -0,0 +1,112 @@ +// +// TLSTrustEvaluator.swift +// RxNetworkKit +// +// Created by Loay Ashraf on 04/05/2024. +// + +import Foundation + +final class TLSTrustEvaluator: NSObject { + + fileprivate static var blockedHosts: Set = [] + fileprivate let configuration: TLSTrustEvaluatorConfiguration + + init(configuration: TLSTrustEvaluatorConfiguration) { + self.configuration = configuration + } + + static func getBlockedHosts() -> Set { + return blockedHosts + } + + fileprivate static func insertBlockedHost(host: String) { + blockedHosts.insert(host) + } + +} + +extension TLSTrustEvaluator: URLSessionDelegate { + + func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + + guard !configuration.evaluationPolicies.isEmpty else { + if configuration.evaluateAllHosts { + completionHandler(.performDefaultHandling, nil) + } else { + TLSTrustEvaluator.insertBlockedHost(host: challenge.protectionSpace.host) + completionHandler(.cancelAuthenticationChallenge, nil) + } + return + } + + guard let trust = challenge.protectionSpace.serverTrust else { + TLSTrustEvaluator.insertBlockedHost(host: challenge.protectionSpace.host) + completionHandler(.cancelAuthenticationChallenge, nil) + return + } + + if let evaluation = configuration.evaluationPolicies[challenge.protectionSpace.host] { + + switch evaluation { + + case .certificates(let certificates): + + guard evaluateCertificate(trust: trust, pinnedCertificates: certificates) else { + TLSTrustEvaluator.insertBlockedHost(host: challenge.protectionSpace.host) + completionHandler(.cancelAuthenticationChallenge, nil) + return + } + + case .publicKeys(let publicKeys): + + guard evaluatePublicKey(trust: trust, pinnedPublicKeys: publicKeys) else { + TLSTrustEvaluator.insertBlockedHost(host: challenge.protectionSpace.host) + completionHandler(.cancelAuthenticationChallenge, nil) + return + } + + case .mixed(let certificates, let publicKeys): + + if !evaluateCertificate(trust: trust, pinnedCertificates: certificates) { + guard evaluatePublicKey(trust: trust, pinnedPublicKeys: publicKeys) else { + TLSTrustEvaluator.insertBlockedHost(host: challenge.protectionSpace.host) + completionHandler(.cancelAuthenticationChallenge, nil) + return + } + } + + } + + completionHandler(.useCredential, .init(trust: trust)) + + } else { + + if configuration.evaluateAllHosts { + completionHandler(.performDefaultHandling, nil) + } else { + TLSTrustEvaluator.insertBlockedHost(host: challenge.protectionSpace.host) + completionHandler(.cancelAuthenticationChallenge, nil) + } + + } + + } + + private func evaluateCertificate(trust: SecTrust, pinnedCertificates: Set) -> Bool { + SecTrustSetAnchorCertificates(trust, Array(pinnedCertificates) as CFArray) + return SecTrustEvaluateWithError(trust, nil) + } + + private func evaluatePublicKey(trust: SecTrust, pinnedPublicKeys: Set) -> Bool { + if SecTrustGetCertificateCount(trust) > 0, + let certificate = SecTrustGetCertificateAtIndex(trust, 0), + let publicKey = certificate.publicKey, + let publicKeyData = publicKey.externalRepresentation { + let pinnedPublicKeysData = pinnedPublicKeys.compactMap({ $0.externalRepresentation }) + return pinnedPublicKeysData.contains(publicKeyData) + } + return false + } + +} diff --git a/Source/TLS/Types/TLSTrustEvaluatorConfiguration.swift b/Source/TLS/Types/TLSTrustEvaluatorConfiguration.swift new file mode 100644 index 0000000..46c963f --- /dev/null +++ b/Source/TLS/Types/TLSTrustEvaluatorConfiguration.swift @@ -0,0 +1,19 @@ +// +// TLSTrustEvaluatorConfiguration.swift +// RxNetworkKit +// +// Created by Loay Ashraf on 04/05/2024. +// + +import Foundation + +public class TLSTrustEvaluatorConfiguration { + + public static var `default`: TLSTrustEvaluatorConfiguration { + .init() + } + + public var evaluationPolicies: [String: TLSTrustEvaluationPolicy] = [:] + public var evaluateAllHosts: Bool = true + +} From fdb750a249268f18401352e868d5e92f097c2ed0 Mon Sep 17 00:00:00 2001 From: Loay Ashraf Date: Fri, 17 May 2024 12:25:27 +0300 Subject: [PATCH 2/3] feat: update documentation Resolves: none. --- RxNetworkKit.xcodeproj/project.pbxproj | 4 ---- Source/Session/Types/Session.swift | 3 +++ Source/Session/Types/SessionConfiguration.swift | 11 +++++++++-- Source/TLS/Extensions/Data+SecCertificate.swift | 1 + Source/TLS/Extensions/SecCertificate+Bundle.swift | 7 +++++++ .../TLS/Extensions/SecCertificate+PublicKey.swift | 1 + Source/TLS/Extensions/SecKey+Bundle.swift | 7 +++++++ .../Extensions/SecKey+ExternalRepresentation.swift | 1 + Source/TLS/Extensions/Set+SecCertificate.swift | 9 +++++++++ Source/TLS/Extensions/Set+SecKey.swift | 9 +++++++++ Source/TLS/Types/TLSTrustChainLevel.swift | 14 -------------- Source/TLS/Types/TLSTrustEvaluationPolicy.swift | 4 ++++ Source/TLS/Types/TLSTrustEvaluator.swift | 12 ++++++++++++ .../TLS/Types/TLSTrustEvaluatorConfiguration.swift | 4 ++++ 14 files changed, 67 insertions(+), 20 deletions(-) delete mode 100644 Source/TLS/Types/TLSTrustChainLevel.swift diff --git a/RxNetworkKit.xcodeproj/project.pbxproj b/RxNetworkKit.xcodeproj/project.pbxproj index edf0808..8f0c5cd 100644 --- a/RxNetworkKit.xcodeproj/project.pbxproj +++ b/RxNetworkKit.xcodeproj/project.pbxproj @@ -68,7 +68,6 @@ C6C643012BE6C7B30071C2CC /* TLSTrustEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6C643002BE6C7B30071C2CC /* TLSTrustEvaluator.swift */; }; C6C643032BE6C8580071C2CC /* TLSTrustEvaluatorConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6C643022BE6C8580071C2CC /* TLSTrustEvaluatorConfiguration.swift */; }; C6C643052BE6C8770071C2CC /* TLSTrustEvaluationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6C643042BE6C8770071C2CC /* TLSTrustEvaluationPolicy.swift */; }; - C6C643072BE6C88A0071C2CC /* TLSTrustChainLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6C643062BE6C88A0071C2CC /* TLSTrustChainLevel.swift */; }; C6C643092BE6C9340071C2CC /* SecCertificate+Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6C643082BE6C9340071C2CC /* SecCertificate+Bundle.swift */; }; C6C6430B2BE6C9510071C2CC /* SecCertificate+PublicKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6C6430A2BE6C9510071C2CC /* SecCertificate+PublicKey.swift */; }; C6C6430D2BE6C98B0071C2CC /* SecKey+ExternalRepresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6C6430C2BE6C98B0071C2CC /* SecKey+ExternalRepresentation.swift */; }; @@ -137,7 +136,6 @@ C6C643002BE6C7B30071C2CC /* TLSTrustEvaluator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TLSTrustEvaluator.swift; sourceTree = ""; }; C6C643022BE6C8580071C2CC /* TLSTrustEvaluatorConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TLSTrustEvaluatorConfiguration.swift; sourceTree = ""; }; C6C643042BE6C8770071C2CC /* TLSTrustEvaluationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TLSTrustEvaluationPolicy.swift; sourceTree = ""; }; - C6C643062BE6C88A0071C2CC /* TLSTrustChainLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TLSTrustChainLevel.swift; sourceTree = ""; }; C6C643082BE6C9340071C2CC /* SecCertificate+Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SecCertificate+Bundle.swift"; sourceTree = ""; }; C6C6430A2BE6C9510071C2CC /* SecCertificate+PublicKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SecCertificate+PublicKey.swift"; sourceTree = ""; }; C6C6430C2BE6C98B0071C2CC /* SecKey+ExternalRepresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SecKey+ExternalRepresentation.swift"; sourceTree = ""; }; @@ -411,7 +409,6 @@ C6C643172BE6CAFE0071C2CC /* Types */ = { isa = PBXGroup; children = ( - C6C643062BE6C88A0071C2CC /* TLSTrustChainLevel.swift */, C6C643042BE6C8770071C2CC /* TLSTrustEvaluationPolicy.swift */, C6C643002BE6C7B30071C2CC /* TLSTrustEvaluator.swift */, C6C643022BE6C8580071C2CC /* TLSTrustEvaluatorConfiguration.swift */, @@ -523,7 +520,6 @@ 0B77E09A29D965D30077FBC0 /* HTTPMIMEType.swift in Sources */, C658CF652AD5F648009E561D /* RxNetworkKit.swift in Sources */, C6A9BEFF2A93FB1D00459E32 /* ProcessInfo+operatingSystemName.swift in Sources */, - C6C643072BE6C88A0071C2CC /* TLSTrustChainLevel.swift in Sources */, C6C643032BE6C8580071C2CC /* TLSTrustEvaluatorConfiguration.swift in Sources */, 0B77E08E29D965D30077FBC0 /* HTTPUploadRequestFile.swift in Sources */, 0B77E08D29D965D30077FBC0 /* HTTPUploadRequestFormData.swift in Sources */, diff --git a/Source/Session/Types/Session.swift b/Source/Session/Types/Session.swift index 7d149e8..70c5706 100644 --- a/Source/Session/Types/Session.swift +++ b/Source/Session/Types/Session.swift @@ -12,7 +12,10 @@ public class Session { /// Principal `URLSession`object used to create request tasks. let urlSession: URLSession + + /// Principal `TLSTrustEvaluator` object used to evaluate TLS server trust. private let tlsTrustEvaluator: TLSTrustEvaluator + /// `OperationQueue` used by the TLS trust evaluator object. private let tlsTrustEvaluatorQueue: OperationQueue /// Creates a `Session` instance. diff --git a/Source/Session/Types/SessionConfiguration.swift b/Source/Session/Types/SessionConfiguration.swift index 86b3da1..3ffafe0 100644 --- a/Source/Session/Types/SessionConfiguration.swift +++ b/Source/Session/Types/SessionConfiguration.swift @@ -14,18 +14,19 @@ public class SessionConfiguration { public static var `default`: SessionConfiguration { .init(urlSessionConfiguration: .default) } - + /// Ephemeral `SessionConfiguration` object. public static var ephemeral: SessionConfiguration { .init(urlSessionConfiguration: .ephemeral) } /// `URLSessionConfiguration` object used to create `URLSession` object. let urlSessionConfiguration: URLSessionConfiguration + /// `TLSTrustEvaluatorConfiguration` object used to create `TLSTrustEvaluator` object. + public var tlsTrustEvaluatorConfiguration: TLSTrustEvaluatorConfiguration = .default /// `Bool` flag that indicates wether a `URLSession` should add `User-Agent` header to outgoing requests. public var setUserAgentHeader: Bool = true /// `Bool` flag that indicates wether a `URLSession` should print outgoing requests to the console. public var logRequests: Bool = true - public var tlsTrustEvaluatorConfiguration: TLSTrustEvaluatorConfiguration = .default /// Creates a `SessionConfiguration` instance. /// @@ -35,6 +36,12 @@ public class SessionConfiguration { self.urlSessionConfiguration = urlSessionConfiguration } + + /// Creates a background `SessionConfiguration` object using the given identifier. + /// + /// - Parameter identifier: `String` background session identifier. + /// + /// - Returns: `SessionConfiguration` background session configuration. public static func background(withIdentifier identifier: String) -> SessionConfiguration { .init(urlSessionConfiguration: .background(withIdentifier: identifier)) } diff --git a/Source/TLS/Extensions/Data+SecCertificate.swift b/Source/TLS/Extensions/Data+SecCertificate.swift index 19c215c..a2d3218 100644 --- a/Source/TLS/Extensions/Data+SecCertificate.swift +++ b/Source/TLS/Extensions/Data+SecCertificate.swift @@ -9,6 +9,7 @@ import Foundation extension Data { + /// Certificate for this `Data` object. var certificate: SecCertificate? { guard let base64String = String(data: self, encoding: .utf8)?.replacingOccurrences(of: "-----BEGIN CERTIFICATE-----", with: "").replacingOccurrences(of: "-----END CERTIFICATE-----", with: ""), let base64DecodedData = Data(base64Encoded: base64String, options: .ignoreUnknownCharacters) else { diff --git a/Source/TLS/Extensions/SecCertificate+Bundle.swift b/Source/TLS/Extensions/SecCertificate+Bundle.swift index 98cb7d4..5509f3f 100644 --- a/Source/TLS/Extensions/SecCertificate+Bundle.swift +++ b/Source/TLS/Extensions/SecCertificate+Bundle.swift @@ -9,6 +9,13 @@ import Foundation public extension SecCertificate { + /// Reads certificate from the main bundle. + /// + /// - Parameters: + /// - name: name of the certificate file. + /// - `extension`: extension of the certificate file. + /// + /// - Returns: `SecCertificate` object read from the main bundle using the provided name and extension. static func fromBundle(_ name: String, _ `extension`: String) -> SecCertificate? { guard let fileURL = Bundle.main.url(forResource: name, withExtension: `extension`), let certificateData = try? Data(contentsOf: fileURL), diff --git a/Source/TLS/Extensions/SecCertificate+PublicKey.swift b/Source/TLS/Extensions/SecCertificate+PublicKey.swift index 0cc5c40..03cb346 100644 --- a/Source/TLS/Extensions/SecCertificate+PublicKey.swift +++ b/Source/TLS/Extensions/SecCertificate+PublicKey.swift @@ -9,6 +9,7 @@ import Foundation public extension SecCertificate { + /// Public key for this `SecCertificate` object. var publicKey: SecKey? { SecCertificateCopyKey(self) } diff --git a/Source/TLS/Extensions/SecKey+Bundle.swift b/Source/TLS/Extensions/SecKey+Bundle.swift index 7c54fea..52ca1d3 100644 --- a/Source/TLS/Extensions/SecKey+Bundle.swift +++ b/Source/TLS/Extensions/SecKey+Bundle.swift @@ -9,6 +9,13 @@ import Foundation public extension SecKey { + /// Reads public key of a certificate from the main bundle. + /// + /// - Parameters: + /// - name: name of the certificate file. + /// - `extension`: extension of the certificate file. + /// + /// - Returns: `SecKey` object read from the main bundle using the provided name and extension. static func fromBundle(_ name: String, _ `extension`: String) -> SecKey? { guard let fileURL = Bundle.main.url(forResource: name, withExtension: `extension`), let certificateData = try? Data(contentsOf: fileURL), diff --git a/Source/TLS/Extensions/SecKey+ExternalRepresentation.swift b/Source/TLS/Extensions/SecKey+ExternalRepresentation.swift index 0e4435b..923c01b 100644 --- a/Source/TLS/Extensions/SecKey+ExternalRepresentation.swift +++ b/Source/TLS/Extensions/SecKey+ExternalRepresentation.swift @@ -9,6 +9,7 @@ import Foundation public extension SecKey { + /// External representation for this `SecKey` object. var externalRepresentation: CFData? { SecKeyCopyExternalRepresentation(self, nil) } diff --git a/Source/TLS/Extensions/Set+SecCertificate.swift b/Source/TLS/Extensions/Set+SecCertificate.swift index 6e24693..2277e06 100644 --- a/Source/TLS/Extensions/Set+SecCertificate.swift +++ b/Source/TLS/Extensions/Set+SecCertificate.swift @@ -9,6 +9,8 @@ import Foundation public extension Set where Element == SecCertificate { + /// A set of all the certificates that can be read from the main bundle + /// `cer`, `der`, `crt` and `pem` file extensions are supported. static var `default`: Set { var certificates: Set = [] @@ -55,6 +57,13 @@ public extension Set where Element == SecCertificate { return certificates } + /// Reads a single certificate from the main bundle. + /// + /// - Parameters: + /// - name: name of the certificate file. + /// - `extension`: extension of the certificate file. + /// + /// - Returns: a set containing a single certificate object read from the main bundle using the provided name and extension. static func singleFromBundle(_ name: String, _ `extension`: String) -> Set { Set([.fromBundle(name, `extension`)!]) } diff --git a/Source/TLS/Extensions/Set+SecKey.swift b/Source/TLS/Extensions/Set+SecKey.swift index 0e5ba01..f6712f2 100644 --- a/Source/TLS/Extensions/Set+SecKey.swift +++ b/Source/TLS/Extensions/Set+SecKey.swift @@ -9,6 +9,8 @@ import Foundation public extension Set where Element == SecKey { + /// A set of all the certificate public keys that can be read from the main bundle + /// `cer`, `der`, `crt` and `pem` file extensions are supported. static var `default`: Set { var publicKeys: Set = [] @@ -23,6 +25,13 @@ public extension Set where Element == SecKey { return publicKeys } + /// Reads the public key of a single certificate from the main bundle. + /// + /// - Parameters: + /// - name: name of the certificate file. + /// - `extension`: extension of the certificate file. + /// + /// - Returns: a set containing a single certificate object read from the main bundle using the provided name and extension. static func singleFromBundle(_ name: String, _ `extension`: String) -> Set { Set([.fromBundle(name, `extension`)!]) } diff --git a/Source/TLS/Types/TLSTrustChainLevel.swift b/Source/TLS/Types/TLSTrustChainLevel.swift deleted file mode 100644 index 913ebc0..0000000 --- a/Source/TLS/Types/TLSTrustChainLevel.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// TLSTrustChainLevel.swift -// RxNetworkKit -// -// Created by Loay Ashraf on 04/05/2024. -// - -import Foundation - -public enum TLSTrustChainLevel: Int { - case leaf - case intermediate - case root -} diff --git a/Source/TLS/Types/TLSTrustEvaluationPolicy.swift b/Source/TLS/Types/TLSTrustEvaluationPolicy.swift index 37dd302..037e38b 100644 --- a/Source/TLS/Types/TLSTrustEvaluationPolicy.swift +++ b/Source/TLS/Types/TLSTrustEvaluationPolicy.swift @@ -7,8 +7,12 @@ import Foundation +/// Policy for TLS server trust evaluation. public enum TLSTrustEvaluationPolicy { + /// Use the given certificates to evaluate the TLS server trust. case certificates(_ certificates: Set = .default) + /// Use the given public keys to evaluate the TLS server trust. case publicKeys(_ publicKeys: Set = .default) + /// Use the given certificates or the public keys to evaluate the TLS server trust. case mixed(_ certificates: Set = .default, _ publicKeys: Set = .default) } diff --git a/Source/TLS/Types/TLSTrustEvaluator.swift b/Source/TLS/Types/TLSTrustEvaluator.swift index 72c7b81..10bcfe4 100644 --- a/Source/TLS/Types/TLSTrustEvaluator.swift +++ b/Source/TLS/Types/TLSTrustEvaluator.swift @@ -7,19 +7,31 @@ import Foundation +/// The object responsible for evaluating TLS server trust for hosts using the provided policies. final class TLSTrustEvaluator: NSObject { + /// Set of hosts that did not pass the TLS server trust evaluation. fileprivate static var blockedHosts: Set = [] + /// Configuration for TLS server trust evaluation. fileprivate let configuration: TLSTrustEvaluatorConfiguration + /// Creates a `TLSTrustEvaluator` instance. + /// + /// - Parameter configuration: `TLSTrustEvaluatorConfiguration` object used to configure and create tthe `TLSTrustEvaluator` instance. init(configuration: TLSTrustEvaluatorConfiguration) { self.configuration = configuration } + /// Returns the blocked hosts. + /// + /// - Returns: `Set` hosts that did not pass the TLS server trust evaluation. static func getBlockedHosts() -> Set { return blockedHosts } + /// Adds a new blocked host. + /// + /// - Parameter host: `String` host that did not pass the TLS server trust evaluation. fileprivate static func insertBlockedHost(host: String) { blockedHosts.insert(host) } diff --git a/Source/TLS/Types/TLSTrustEvaluatorConfiguration.swift b/Source/TLS/Types/TLSTrustEvaluatorConfiguration.swift index 46c963f..a06f8b1 100644 --- a/Source/TLS/Types/TLSTrustEvaluatorConfiguration.swift +++ b/Source/TLS/Types/TLSTrustEvaluatorConfiguration.swift @@ -7,13 +7,17 @@ import Foundation +/// Configuration object for TLS server trust evaluation. public class TLSTrustEvaluatorConfiguration { + /// Default `TLSTrustEvaluatorConfiguration` object. public static var `default`: TLSTrustEvaluatorConfiguration { .init() } + /// `[String: TLSTrustEvaluationPolicy]` dictionary of TLS server trust evaluation policy for each host. public var evaluationPolicies: [String: TLSTrustEvaluationPolicy] = [:] + /// `Bool` flag that indicates whether a `TLSTrustEvaluator` should evaluate all hosts even the ones that do not have a policy specified. public var evaluateAllHosts: Bool = true } From 17701387364b4a77cb22c0f9c7cc75c9ddbef84a Mon Sep 17 00:00:00 2001 From: Loay Ashraf Date: Fri, 17 May 2024 12:40:16 +0300 Subject: [PATCH 3/3] feat: update documentation Resolves: none. --- Docs.docc/Pages/RxNetworkKit.md | 2 ++ Source/Session/Types/Session.swift | 2 +- Source/TLS/Types/TLSTrustEvaluationPolicy.swift | 2 +- Source/TLS/Types/TLSTrustEvaluatorConfiguration.swift | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Docs.docc/Pages/RxNetworkKit.md b/Docs.docc/Pages/RxNetworkKit.md index 1991849..cd63ca2 100644 --- a/Docs.docc/Pages/RxNetworkKit.md +++ b/Docs.docc/Pages/RxNetworkKit.md @@ -36,6 +36,8 @@ RxNetworkKit is a generic reactive networking framework that leverages the stabi - ``Session`` - ``SessionConfiguration`` +- ``TLSTrustEvaluatorConfiguration`` +- ``TLSTrustEvaluationPolicy`` - ``RESTClient`` - ``HTTPClient`` diff --git a/Source/Session/Types/Session.swift b/Source/Session/Types/Session.swift index 70c5706..ff1d2b0 100644 --- a/Source/Session/Types/Session.swift +++ b/Source/Session/Types/Session.swift @@ -8,7 +8,7 @@ import Foundation /// Wrapper object for `URLSession` that can be shared between multiple clients. -public class Session { +public final class Session { /// Principal `URLSession`object used to create request tasks. let urlSession: URLSession diff --git a/Source/TLS/Types/TLSTrustEvaluationPolicy.swift b/Source/TLS/Types/TLSTrustEvaluationPolicy.swift index 037e38b..a3f622b 100644 --- a/Source/TLS/Types/TLSTrustEvaluationPolicy.swift +++ b/Source/TLS/Types/TLSTrustEvaluationPolicy.swift @@ -7,7 +7,7 @@ import Foundation -/// Policy for TLS server trust evaluation. +/// Policy for TLS server trust evaluation for a specific host. public enum TLSTrustEvaluationPolicy { /// Use the given certificates to evaluate the TLS server trust. case certificates(_ certificates: Set = .default) diff --git a/Source/TLS/Types/TLSTrustEvaluatorConfiguration.swift b/Source/TLS/Types/TLSTrustEvaluatorConfiguration.swift index a06f8b1..b7e212a 100644 --- a/Source/TLS/Types/TLSTrustEvaluatorConfiguration.swift +++ b/Source/TLS/Types/TLSTrustEvaluatorConfiguration.swift @@ -7,7 +7,7 @@ import Foundation -/// Configuration object for TLS server trust evaluation. +/// Configuration object for TLS server trust evaluator. public class TLSTrustEvaluatorConfiguration { /// Default `TLSTrustEvaluatorConfiguration` object.