From 4b255dfdcca8a99317c5346cc13a1b71e156ccb9 Mon Sep 17 00:00:00 2001 From: ShihabMehboob Date: Wed, 21 Feb 2018 15:38:38 +0000 Subject: [PATCH 1/3] Added certificate pinning with validation --- Sources/SwiftyRequest/RestRequest.swift | 272 +++++++++++++----------- 1 file changed, 144 insertions(+), 128 deletions(-) diff --git a/Sources/SwiftyRequest/RestRequest.swift b/Sources/SwiftyRequest/RestRequest.swift index e39d61e..82b1b29 100644 --- a/Sources/SwiftyRequest/RestRequest.swift +++ b/Sources/SwiftyRequest/RestRequest.swift @@ -20,11 +20,11 @@ import LoggerAPI /// Object containing everything needed to build HTTP requests and execute them public class RestRequest: NSObject { - + // Check if there exists a self-signed certificate and whether it's a secure connection private let isSecure: Bool private let isSelfSigned: Bool - + /// A default `URLSession` instance private var session: URLSession { var session = URLSession(configuration: URLSessionConfiguration.default) @@ -35,13 +35,16 @@ public class RestRequest: NSObject { } return session } - + // The HTTP Request private var request: URLRequest - + + /// The pinned certificate being compared and validated + public var pinnedCertificateName: String? + /// `CircuitBreaker` instance for this `RestRequest` public var circuitBreaker: CircuitBreaker<(Data?, HTTPURLResponse?, Error?) -> Void, String>? - + /// Parameters for a `CircuitBreaker` instance. /// When set, a new circuitBreaker instance is created public var circuitParameters: CircuitParameters? = nil { @@ -58,14 +61,14 @@ public class RestRequest: NSObject { } } } - + // MARK: HTTP Request Paramters /// URL `String` used to store a url containing replacable template values private var urlTemplate: String? - + /// The string representation of HTTP request url private var url: String - + /// The HTTP request method: defaults to Get public var method: HTTPMethod { get { @@ -75,7 +78,7 @@ public class RestRequest: NSObject { request.httpMethod = newValue.rawValue } } - + /// HTTP Credentials public var credentials: Credentials? { didSet { @@ -93,7 +96,7 @@ public class RestRequest: NSObject { } } } - + /// HTTP Header Parameters public var headerParameters: [String: String] { get { @@ -109,7 +112,7 @@ public class RestRequest: NSObject { } } } - + /// HTTP Accept Type Header: defaults to application/json public var acceptType: String? { get { @@ -119,7 +122,7 @@ public class RestRequest: NSObject { request.setValue(newValue, forHTTPHeaderField: "Accept") } } - + /// HTTP Content Type Header: defaults to application/json public var contentType: String? { get { @@ -129,7 +132,7 @@ public class RestRequest: NSObject { request.setValue(newValue, forHTTPHeaderField: "Content-Type") } } - + /// HTTP User-Agent Header public var productInfo: String? { get { @@ -139,7 +142,7 @@ public class RestRequest: NSObject { request.setValue(newValue?.generateUserAgent(), forHTTPHeaderField: "User-Agent") } } - + /// HTTP Message Body public var messageBody: Data? { get { @@ -149,7 +152,7 @@ public class RestRequest: NSObject { request.httpBody = newValue } } - + /// HTTP Request Query Items public var queryItems: [URLQueryItem]? { set { @@ -168,36 +171,37 @@ public class RestRequest: NSObject { return nil } } - + /// Initialize a `RestRequest` instance /// /// - Parameters: /// - url: URL string to use for network request - public init(method: HTTPMethod = .get, url: String, containsSelfSignedCert: Bool? = false) { - + public init(method: HTTPMethod = .get, url: String, containsSelfSignedCert: Bool? = false, pinnedCertName: String? = nil) { + + self.pinnedCertificateName = pinnedCertName ?? nil self.isSecure = url.contains("https") self.isSelfSigned = containsSelfSignedCert ?? false - + // Instantiate basic mutable request let urlComponents = URLComponents(string: url) ?? URLComponents(string: "")! let urlObject = urlComponents.url ?? URL(string: "n/a")! self.request = URLRequest(url: urlObject) - + // Set inital fields self.url = url - + super.init() - + self.method = method self.acceptType = "application/json" self.contentType = "application/json" - + // We accept URLs with templated values which `URLComponents` does not treat as valid if URLComponents(string: url) == nil { self.urlTemplate = url } } - + // MARK: Response methods /// Request response method that either invokes `CircuitBreaker` or executes the HTTP request /// @@ -211,7 +215,7 @@ public class RestRequest: NSObject { completionHandler(nil, nil, error) return } - + let code = response.statusCode if code >= 200 && code < 300 { completionHandler(data, response, error) @@ -224,7 +228,7 @@ public class RestRequest: NSObject { task.resume() } } - + /// Request response method with the expected result of a `Data` object /// /// - Parameters: @@ -234,25 +238,25 @@ public class RestRequest: NSObject { public func responseData(templateParams: [String: String]? = nil, queryItems: [URLQueryItem]? = nil, completionHandler: @escaping (RestResponse) -> Void) { - + if let error = performSubstitutions(params: templateParams) { let result = Result.failure(error) let dataResponse = RestResponse(request: request, response: nil, data: nil, result: result) completionHandler(dataResponse) return } - + self.queryItems = queryItems - + response { data, response, error in - + if let error = error { let result = Result.failure(error) let dataResponse = RestResponse(request: self.request, response: response, data: nil, result: result) completionHandler(dataResponse) return } - + guard let data = data else { let result = Result.failure(RestError.noData) let dataResponse = RestResponse(request: self.request, response: response, data: nil, result: result) @@ -264,7 +268,7 @@ public class RestRequest: NSObject { completionHandler(dataResponse) } } - + /// Request response method with the expected result of the object, `T` specified /// /// - Parameters: @@ -279,25 +283,25 @@ public class RestRequest: NSObject { templateParams: [String: String]? = nil, queryItems: [URLQueryItem]? = nil, completionHandler: @escaping (RestResponse) -> Void) { - + if let error = performSubstitutions(params: templateParams) { let result = Result.failure(error) let dataResponse = RestResponse(request: request, response: nil, data: nil, result: result) completionHandler(dataResponse) return } - + self.queryItems = queryItems - + response { data, response, error in - + if let error = error { let result = Result.failure(error) let dataResponse = RestResponse(request: self.request, response: response, data: nil, result: result) completionHandler(dataResponse) return } - + if let responseToError = responseToError, let error = responseToError(response, data) { let result = Result.failure(error) @@ -305,7 +309,7 @@ public class RestRequest: NSObject { completionHandler(dataResponse) return } - + // ensure data is not nil guard let data = data else { let result = Result.failure(RestError.noData) @@ -313,7 +317,7 @@ public class RestRequest: NSObject { completionHandler(dataResponse) return } - + // parse json object let result: Result do { @@ -336,13 +340,13 @@ public class RestRequest: NSObject { } catch { result = .failure(error) } - + // execute callback let dataResponse = RestResponse(request: self.request, response: response, data: data, result: result) completionHandler(dataResponse) } } - + /// Request response method with the expected result of an array of type `T` specified /// /// - Parameters: @@ -356,23 +360,23 @@ public class RestRequest: NSObject { templateParams: [String: String]? = nil, queryItems: [URLQueryItem]? = nil, completionHandler: @escaping (RestResponse) -> Void) { - + if let error = performSubstitutions(params: templateParams) { let result = Result.failure(error) let dataResponse = RestResponse(request: request, response: nil, data: nil, result: result) completionHandler(dataResponse) return } - + response { data, response, error in - + if let error = error ?? responseToError?(response, data) { let result = Result.failure(error) let dataResponse = RestResponse(request: self.request, response: response, data: data, result: result) completionHandler(dataResponse) return } - + // ensure data is not nil guard let data = data else { let result = Result.failure(RestError.noData) @@ -380,7 +384,7 @@ public class RestRequest: NSObject { completionHandler(dataResponse) return } - + // parse json object let result: Result do { @@ -389,13 +393,13 @@ public class RestRequest: NSObject { } catch { result = .failure(error) } - + // execute callback let dataResponse = RestResponse(request: self.request, response: response, data: data, result: result) completionHandler(dataResponse) } } - + /// Request response method with the expected result of an array of type `T` specified /// /// - Parameters: @@ -410,25 +414,25 @@ public class RestRequest: NSObject { templateParams: [String: String]? = nil, queryItems: [URLQueryItem]? = nil, completionHandler: @escaping (RestResponse<[T]>) -> Void) { - + if let error = performSubstitutions(params: templateParams) { let result = Result<[T]>.failure(error) let dataResponse = RestResponse(request: request, response: nil, data: nil, result: result) completionHandler(dataResponse) return } - + self.queryItems = queryItems - + response { data, response, error in - + if let error = error { let result = Result<[T]>.failure(error) let dataResponse = RestResponse(request: self.request, response: response, data: nil, result: result) completionHandler(dataResponse) return } - + if let responseToError = responseToError, let error = responseToError(response, data) { let result = Result<[T]>.failure(error) @@ -436,7 +440,7 @@ public class RestRequest: NSObject { completionHandler(dataResponse) return } - + // ensure data is not nil guard let data = data else { let result = Result<[T]>.failure(RestError.noData) @@ -444,7 +448,7 @@ public class RestRequest: NSObject { completionHandler(dataResponse) return } - + // parse json object let result: Result<[T]> do { @@ -468,13 +472,13 @@ public class RestRequest: NSObject { } catch { result = .failure(error) } - + // execute callback let dataResponse = RestResponse(request: self.request, response: response, data: data, result: result) completionHandler(dataResponse) } } - + /// Request response method with the expected result of a `String` /// /// - Parameters: @@ -487,25 +491,25 @@ public class RestRequest: NSObject { templateParams: [String: String]? = nil, queryItems: [URLQueryItem]? = nil, completionHandler: @escaping (RestResponse) -> Void) { - + if let error = performSubstitutions(params: templateParams) { let result = Result.failure(error) let dataResponse = RestResponse(request: request, response: nil, data: nil, result: result) completionHandler(dataResponse) return } - + self.queryItems = queryItems - + response { data, response, error in - + if let error = error { let result = Result.failure(error) let dataResponse = RestResponse(request: self.request, response: response, data: nil, result: result) completionHandler(dataResponse) return } - + if let responseToError = responseToError, let error = responseToError(response, data) { let result = Result.failure(error) @@ -513,7 +517,7 @@ public class RestRequest: NSObject { completionHandler(dataResponse) return } - + // ensure data is not nil guard let data = data else { let result = Result.failure(RestError.noData) @@ -521,10 +525,10 @@ public class RestRequest: NSObject { completionHandler(dataResponse) return } - + // Retrieve string encoding type let encoding = self.getCharacterEncoding(from: response?.allHeaderFields["Content-Type"] as? String) - + // parse data as a string guard let string = String(data: data, encoding: encoding) else { let result = Result.failure(RestError.serializationError) @@ -532,14 +536,14 @@ public class RestRequest: NSObject { completionHandler(dataResponse) return } - + // execute callback let result = Result.success(string) let dataResponse = RestResponse(request: self.request, response: response, data: data, result: result) completionHandler(dataResponse) } } - + /// Request response method to use when there is no expected result /// /// - Parameters: @@ -552,39 +556,39 @@ public class RestRequest: NSObject { templateParams: [String: String]? = nil, queryItems: [URLQueryItem]? = nil, completionHandler: @escaping (RestResponse) -> Void) { - + if let error = performSubstitutions(params: templateParams) { let result = Result.failure(error) let dataResponse = RestResponse(request: request, response: nil, data: nil, result: result) completionHandler(dataResponse) return } - + self.queryItems = queryItems - + response { data, response, error in - + if let error = error { let result = Result.failure(error) let dataResponse = RestResponse(request: self.request, response: response, data: nil, result: result) completionHandler(dataResponse) return } - + if let responseToError = responseToError, let error = responseToError(response, data) { let result = Result.failure(error) let dataResponse = RestResponse(request: self.request, response: response, data: data, result: result) completionHandler(dataResponse) return } - + // execute callback let result = Result.success(()) let dataResponse = RestResponse(request: self.request, response: response, data: data, result: result) completionHandler(dataResponse) } } - + /// Utility method to download a file from a remote origin /// /// - Parameters: @@ -598,16 +602,16 @@ public class RestRequest: NSObject { } let fileManager = FileManager.default try fileManager.moveItem(at: source, to: destination) - + completionHandler(response as? HTTPURLResponse, error) - + } catch { completionHandler(nil, RestError.fileManagerError) } } task.resume() } - + /// Method used by `CircuitBreaker` as the contextCommand /// /// - Parameter invocation: `Invocation` contains a command argument, Void return type, and a String fallback arguement @@ -622,46 +626,46 @@ public class RestRequest: NSObject { callback(data, response as? HTTPURLResponse, error) } task.resume() - + } - + /// Method to perform substitution on `String` URL if it contains templated placeholders /// /// - Parameter params: dictionary of parameters to substitute in /// - Returns: returns either a `RestError` or nil if there were no problems setting new URL on our `URLRequest` object private func performSubstitutions(params: [String: String]?) -> RestError? { - + guard let params = params else { return nil } - + // Get urlTemplate if available, otherwise just use the request's url let urlString = urlTemplate ?? url - + guard let urlComponents = urlString.expand(params: params) else { return RestError.invalidSubstitution } - + self.request.url = urlComponents.url - + return nil } - + /// Method to identify the charset encoding defined by the Content-Type header /// - Defaults set to .utf8 /// - Parameter contentType: The content-type header string /// - Returns: returns the defined or default String.Encoding.Type private func getCharacterEncoding(from contentType: String? = nil) -> String.Encoding { guard let text = contentType, - let regex = try? NSRegularExpression(pattern: "(?<=charset=).*?(?=$|;|\\s)", options: [.caseInsensitive]), - let match = regex.matches(in: text, range: NSRange(text.startIndex..., in: text)).last, - let range = Range(match.range, in: text) else { - return .utf8 + let regex = try? NSRegularExpression(pattern: "(?<=charset=).*?(?=$|;|\\s)", options: [.caseInsensitive]), + let match = regex.matches(in: text, range: NSRange(text.startIndex..., in: text)).last, + let range = Range(match.range, in: text) else { + return .utf8 } - + /// Strip whitespace and quotes let charset = String(text[range]).trimmingCharacters(in: CharacterSet(charactersIn: "\"").union(.whitespaces)) - + switch String(charset).lowercased() { case "iso-8859-1": return .isoLatin1 default: return .utf8 @@ -672,28 +676,28 @@ public class RestRequest: NSObject { /// Encapsulates properties needed to initialize a `CircuitBreaker` object within the `RestRequest` init. /// `A` is the type of the fallback's parameter public struct CircuitParameters { - + /// The circuit name: defaults to '1000'test' let name: String - + /// The circuit timeout: defaults to 1000 public let timeout: Int - + /// The circuit timeout: defaults to 60000 public let resetTimeout: Int - + /// Max failures allowed: defaults to 5 public let maxFailures: Int - + /// Rolling Window: defaults to 10000 public let rollingWindow: Int - + /// Bulkhead: defaults to 0 public let bulkhead: Int - + /// The error fallback callback public let fallback: (BreakerError, A) -> Void - + /// Initialize a `CircuitPrameters` instance public init(name: String = "circuitName", timeout: Int = 2000, resetTimeout: Int = 60000, maxFailures: Int = 5, rollingWindow: Int = 10000, bulkhead: Int = 0, fallback: @escaping (BreakerError, A) -> Void) { self.name = name @@ -709,16 +713,16 @@ public struct CircuitParameters { /// Contains data associated with a finished network request. /// With `T` being the type of the response expected to be received public struct RestResponse { - + /// The rest request public let request: URLRequest? - + /// The response to the request public let response: HTTPURLResponse? - + /// The Response Data public let data: Data? - + /// The Reponse Result public let result: Result } @@ -727,7 +731,7 @@ public struct RestResponse { public enum Result { /// a success of generic type `T` case success(T) - + /// a failure with an `Error` object case failure(Error) } @@ -736,35 +740,35 @@ public enum Result { public enum Credentials { /// an API key is being used, no additional data needed case apiKey - + /// a basic username/password authentication is being used with said value, passed in case basicAuthentication(username: String, password: String) } /// Enum describing error types that can occur during a rest request and response public enum RestError: Error, CustomStringConvertible { - + /// no data was returned from the network case noData - + /// data couldn't be parsed correctly case serializationError - + /// failure to encode data into a certain format case encodingError - + /// failure in file manipulation case fileManagerError - + /// the file trying to be accessed is invalid case invalidFile - + /// the url substitution attempted could not be made case invalidSubstitution - + /// Error response status case erroredResponseStatus(Int) - + /// Error Description public var description: String { switch self { @@ -777,7 +781,7 @@ public enum RestError: Error, CustomStringConvertible { case .erroredResponseStatus(let s) : return "Error HTTP Response: `\(s)`" } } - + /// Computed Property to extract error code public var code: Int? { switch self { @@ -789,37 +793,49 @@ public enum RestError: Error, CustomStringConvertible { // URL Session extension extension RestRequest: URLSessionDelegate { - + /// URL session function to allow trusting certain URLs public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { let method = challenge.protectionSpace.authenticationMethod let host = challenge.protectionSpace.host - + guard let url = URLComponents(string: self.url), let baseHost = url.host else { completionHandler(.performDefaultHandling, nil) return } - + let warning = "Attempting to establish a secure connection; This is only supported by macOS 10.6 or higher. Resorting to default handling." - + switch (method, host) { case (NSURLAuthenticationMethodServerTrust, baseHost): #if !os(Linux) - guard #available(iOS 3.0, macOS 10.6, *), let trust = challenge.protectionSpace.serverTrust else { + guard #available(iOS 3.0, macOS 10.6, *), let trust = challenge.protectionSpace.serverTrust else { + Log.warning(warning) + fallthrough + } + if let certificateData = NSData(contentsOfFile: Bundle.main.path(forResource: self.pinnedCertificateName, ofType: "der") ?? "") { + if let serverCertificate = SecTrustGetCertificateAtIndex(trust, 0) { + let serverCertificateData = SecCertificateCopyData(serverCertificate) as NSData + if certificateData == serverCertificateData { + Log.verbose("Certificate validation successful") + completionHandler(.useCredential, URLCredential(trust: trust)) + return + } else { + completionHandler(.performDefaultHandling, nil) + } + } + } + + completionHandler(.useCredential, URLCredential(trust: trust)) + + #else Log.warning(warning) fallthrough - } - - let credential = URLCredential(trust: trust) - completionHandler(.useCredential, credential) - - #else - Log.warning(warning) - fallthrough #endif default: completionHandler(.performDefaultHandling, nil) } } - + } + From cba801100425d7bba52820ba2452dac8c0123f5a Mon Sep 17 00:00:00 2001 From: Shihab-Mehboob1 Date: Mon, 9 Jul 2018 15:48:43 +0100 Subject: [PATCH 2/3] minor adjustments --- Sources/SwiftyRequest/RestRequest.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftyRequest/RestRequest.swift b/Sources/SwiftyRequest/RestRequest.swift index 840e8e5..7d6ae09 100644 --- a/Sources/SwiftyRequest/RestRequest.swift +++ b/Sources/SwiftyRequest/RestRequest.swift @@ -809,11 +809,11 @@ extension RestRequest: URLSessionDelegate { switch (method, host) { case (NSURLAuthenticationMethodServerTrust, baseHost): #if !os(Linux) - guard #available(iOS 3.0, macOS 10.6, *), let trust = challenge.protectionSpace.serverTrust else { + guard #available(iOS 3.0, macOS 10.6, *), let trust = challenge.protectionSpace.serverTrust, let pinned = self.pinnedCertificateName else { Log.warning(warning) fallthrough } - if let certificateData = NSData(contentsOfFile: Bundle.main.path(forResource: self.pinnedCertificateName, ofType: "der") ?? "") { + if let certificateData = NSData(contentsOfFile: Bundle.main.path(forResource: pinned, ofType: "der") ?? "") { if let serverCertificate = SecTrustGetCertificateAtIndex(trust, 0) { let serverCertificateData = SecCertificateCopyData(serverCertificate) as NSData if certificateData == serverCertificateData { @@ -822,6 +822,7 @@ extension RestRequest: URLSessionDelegate { return } else { completionHandler(.performDefaultHandling, nil) + return } } } From e2ce699cf5316859d416a9325d714d973c3fb100 Mon Sep 17 00:00:00 2001 From: Shihab-Mehboob1 Date: Mon, 9 Jul 2018 16:16:01 +0100 Subject: [PATCH 3/3] minor adjustments --- Tests/SwiftyRequestTests/SwiftyRequestTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/SwiftyRequestTests/SwiftyRequestTests.swift b/Tests/SwiftyRequestTests/SwiftyRequestTests.swift index 89af343..bd17295 100644 --- a/Tests/SwiftyRequestTests/SwiftyRequestTests.swift +++ b/Tests/SwiftyRequestTests/SwiftyRequestTests.swift @@ -138,6 +138,7 @@ class SwiftyRequestTests: XCTestCase { let expectation = self.expectation(description: "Data Echoed Back") let request = RestRequest(method: .get, url: echoURLSecure, containsSelfSignedCert: true) + request.pinnedCertificateName = "" request.responseData { response in switch response.result {