diff --git a/Source/Supporting Files/HTTP.swift b/Source/Supporting Files/HTTP.swift index e1178b3..f6a8830 100644 --- a/Source/Supporting Files/HTTP.swift +++ b/Source/Supporting Files/HTTP.swift @@ -24,6 +24,38 @@ typealias JSON = [String: Any] +class URLSessionDelegateHandler : NSObject, URLSessionDelegate { + + func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + + if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { + let trust = challenge.protectionSpace.serverTrust! + let host = challenge.protectionSpace.host + guard session.serverTrustPolicy.evaluate(trust, forHost: host) else { + completionHandler(URLSession.AuthChallengeDisposition.rejectProtectionSpace, nil) + return + } + + let credential = URLCredential(trust: trust) + challenge.sender?.use(credential, for: challenge) + completionHandler(URLSession.AuthChallengeDisposition.useCredential, credential) + } + + completionHandler(URLSession.AuthChallengeDisposition.performDefaultHandling, nil) + } +} + +public struct HTTPConfig { + public static var serverTrustPolicy = ServerTrustPolicy.performDefaultEvaluation(validateHost: true) +} + +extension URLSession { + + var serverTrustPolicy : ServerTrustPolicy { + return HTTPConfig.serverTrustPolicy + } +} + struct HTTP { static func POST(_ URL: Foundation.URL, parameters: [String: String], completion: @escaping (Result) -> Void) { var request = URLRequest(url: URL) @@ -35,8 +67,10 @@ struct HTTP { request.httpBody = parameters.map { "\($0)=\($1)" } .joined(separator: "&") .data(using: String.Encoding.utf8) - - let session = URLSession.shared + + let session = URLSession(configuration: URLSessionConfiguration.default, + delegate: URLSessionDelegateHandler(), + delegateQueue: nil) let task = session.dataTask(with: request) { data, response, error in if let error = error { diff --git a/Source/Supporting Files/ServerTrustPolicy.swift b/Source/Supporting Files/ServerTrustPolicy.swift new file mode 100644 index 0000000..c326bbc --- /dev/null +++ b/Source/Supporting Files/ServerTrustPolicy.swift @@ -0,0 +1,242 @@ +// +// ServerTrustPolicy.swift +// +// Copyright (c) 2014-2016 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// The `ServerTrustPolicy` evaluates the server trust generally provided by an `NSURLAuthenticationChallenge` when +/// connecting to a server over a secure HTTPS connection. The policy configuration then evaluates the server trust +/// with a given set of criteria to determine whether the server trust is valid and the connection should be made. +/// +/// Using pinned certificates or public keys for evaluation helps prevent man-in-the-middle (MITM) attacks and other +/// vulnerabilities. Applications dealing with sensitive customer data or financial information are strongly encouraged +/// to route all communication over an HTTPS connection with pinning enabled. +/// +/// - performDefaultEvaluation: Uses the default server trust evaluation while allowing you to control whether to +/// validate the host provided by the challenge. Applications are encouraged to always +/// validate the host in production environments to guarantee the validity of the server's +/// certificate chain. +/// +/// - pinCertificates: Uses the pinned certificates to validate the server trust. The server trust is +/// considered valid if one of the pinned certificates match one of the server certificates. +/// By validating both the certificate chain and host, certificate pinning provides a very +/// secure form of server trust validation mitigating most, if not all, MITM attacks. +/// Applications are encouraged to always validate the host and require a valid certificate +/// chain in production environments. +/// +/// - pinPublicKeys: Uses the pinned public keys to validate the server trust. The server trust is considered +/// valid if one of the pinned public keys match one of the server certificate public keys. +/// By validating both the certificate chain and host, public key pinning provides a very +/// secure form of server trust validation mitigating most, if not all, MITM attacks. +/// Applications are encouraged to always validate the host and require a valid certificate +/// chain in production environments. +/// +/// - disableEvaluation: Disables all evaluation which in turn will always consider any server trust as valid. +/// +/// - customEvaluation: Uses the associated closure to evaluate the validity of the server trust. +public enum ServerTrustPolicy { + case performDefaultEvaluation(validateHost: Bool) + case pinCertificates(certificates: [SecCertificate], validateCertificateChain: Bool, validateHost: Bool) + case pinPublicKeys(publicKeys: [SecKey], validateCertificateChain: Bool, validateHost: Bool) + case disableEvaluation + case customEvaluation((_ serverTrust: SecTrust, _ host: String) -> Bool) + + // MARK: - Bundle Location + + /// Returns all certificates within the given bundle with a `.cer` file extension. + /// + /// - parameter bundle: The bundle to search for all `.cer` files. + /// + /// - returns: All certificates within the given bundle. + public static func certificates(in bundle: Bundle = Bundle.main) -> [SecCertificate] { + var certificates: [SecCertificate] = [] + + let paths = Set([".cer", ".CER", ".crt", ".CRT", ".der", ".DER"].map { fileExtension in + bundle.paths(forResourcesOfType: fileExtension, inDirectory: nil) + }.joined()) + + for path in paths { + if + let certificateData = try? Data(contentsOf: URL(fileURLWithPath: path)) as CFData, + let certificate = SecCertificateCreateWithData(nil, certificateData) + { + certificates.append(certificate) + } + } + + return certificates + } + + /// Returns all public keys within the given bundle with a `.cer` file extension. + /// + /// - parameter bundle: The bundle to search for all `*.cer` files. + /// + /// - returns: All public keys within the given bundle. + public static func publicKeys(in bundle: Bundle = Bundle.main) -> [SecKey] { + var publicKeys: [SecKey] = [] + + for certificate in certificates(in: bundle) { + if let publicKey = publicKey(for: certificate) { + publicKeys.append(publicKey) + } + } + + return publicKeys + } + + // MARK: - Evaluation + + /// Evaluates whether the server trust is valid for the given host. + /// + /// - parameter serverTrust: The server trust to evaluate. + /// - parameter host: The host of the challenge protection space. + /// + /// - returns: Whether the server trust is valid. + public func evaluate(_ serverTrust: SecTrust, forHost host: String) -> Bool { + var serverTrustIsValid = false + + switch self { + case let .performDefaultEvaluation(validateHost): + let policy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil) + SecTrustSetPolicies(serverTrust, policy) + + serverTrustIsValid = trustIsValid(serverTrust) + case let .pinCertificates(pinnedCertificates, validateCertificateChain, validateHost): + if validateCertificateChain { + let policy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil) + SecTrustSetPolicies(serverTrust, policy) + + SecTrustSetAnchorCertificates(serverTrust, pinnedCertificates as CFArray) + SecTrustSetAnchorCertificatesOnly(serverTrust, true) + + serverTrustIsValid = trustIsValid(serverTrust) + } else { + let serverCertificatesDataArray = certificateData(for: serverTrust) + let pinnedCertificatesDataArray = certificateData(for: pinnedCertificates) + + outerLoop: for serverCertificateData in serverCertificatesDataArray { + for pinnedCertificateData in pinnedCertificatesDataArray { + if serverCertificateData == pinnedCertificateData { + serverTrustIsValid = true + break outerLoop + } + } + } + } + case let .pinPublicKeys(pinnedPublicKeys, validateCertificateChain, validateHost): + var certificateChainEvaluationPassed = true + + if validateCertificateChain { + let policy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil) + SecTrustSetPolicies(serverTrust, policy) + + certificateChainEvaluationPassed = trustIsValid(serverTrust) + } + + if certificateChainEvaluationPassed { + outerLoop: for serverPublicKey in ServerTrustPolicy.publicKeys(for: serverTrust) as [AnyObject] { + for pinnedPublicKey in pinnedPublicKeys as [AnyObject] { + if serverPublicKey.isEqual(pinnedPublicKey) { + serverTrustIsValid = true + break outerLoop + } + } + } + } + case .disableEvaluation: + serverTrustIsValid = true + case let .customEvaluation(closure): + serverTrustIsValid = closure(serverTrust, host) + } + + return serverTrustIsValid + } + + // MARK: - Private - Trust Validation + + private func trustIsValid(_ trust: SecTrust) -> Bool { + var isValid = false + + var result = SecTrustResultType.invalid + let status = SecTrustEvaluate(trust, &result) + + if status == errSecSuccess { + let unspecified = SecTrustResultType.unspecified + let proceed = SecTrustResultType.proceed + + + isValid = result == unspecified || result == proceed + } + + return isValid + } + + // MARK: - Private - Certificate Data + + private func certificateData(for trust: SecTrust) -> [Data] { + var certificates: [SecCertificate] = [] + + for index in 0.. [Data] { + return certificates.map { SecCertificateCopyData($0) as Data } + } + + // MARK: - Private - Public Key Extraction + + private static func publicKeys(for trust: SecTrust) -> [SecKey] { + var publicKeys: [SecKey] = [] + + for index in 0.. SecKey? { + var publicKey: SecKey? + + let policy = SecPolicyCreateBasicX509() + var trust: SecTrust? + let trustCreationStatus = SecTrustCreateWithCertificates(certificate, policy, &trust) + + if let trust = trust, trustCreationStatus == errSecSuccess { + publicKey = SecTrustCopyPublicKey(trust) + } + + return publicKey + } +} diff --git a/SwiftyOAuth.xcodeproj/project.pbxproj b/SwiftyOAuth.xcodeproj/project.pbxproj index 38ab3cd..bc69c1b 100644 --- a/SwiftyOAuth.xcodeproj/project.pbxproj +++ b/SwiftyOAuth.xcodeproj/project.pbxproj @@ -42,6 +42,7 @@ 6DF7DF8F1CD53F300099C320 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DF7DF8E1CD53F300099C320 /* Result.swift */; }; 6DF7DF911CD53F6E0099C320 /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DF7DF901CD53F6E0099C320 /* Token.swift */; }; 6DF7DF961CD547C30099C320 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DF7DF951CD547C30099C320 /* Utilities.swift */; }; + D087CC231DCA04C400F2EE9D /* ServerTrustPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D087CC221DCA04C400F2EE9D /* ServerTrustPolicy.swift */; }; E5BE044F1D0A01E1004C5969 /* TokenStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5BE044E1D0A01E1004C5969 /* TokenStore.swift */; }; E5BE04521D0A0397004C5969 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5BE04511D0A0397004C5969 /* UserDefaults.swift */; }; E5BE04541D0A0659004C5969 /* UbiquitousKeyValueStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5BE04531D0A0659004C5969 /* UbiquitousKeyValueStore.swift */; }; @@ -97,6 +98,7 @@ 6DF7DF8E1CD53F300099C320 /* Result.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = ""; }; 6DF7DF901CD53F6E0099C320 /* Token.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Token.swift; sourceTree = ""; }; 6DF7DF951CD547C30099C320 /* Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; }; + D087CC221DCA04C400F2EE9D /* ServerTrustPolicy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerTrustPolicy.swift; sourceTree = ""; }; E5BE044E1D0A01E1004C5969 /* TokenStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenStore.swift; sourceTree = ""; }; E5BE04511D0A0397004C5969 /* UserDefaults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = ""; }; E5BE04531D0A0659004C5969 /* UbiquitousKeyValueStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UbiquitousKeyValueStore.swift; sourceTree = ""; }; @@ -215,6 +217,7 @@ isa = PBXGroup; children = ( 6DE48D191CE21FB500FB2DC9 /* HTTP.swift */, + D087CC221DCA04C400F2EE9D /* ServerTrustPolicy.swift */, 6DF7DF721CD5393F0099C320 /* Info.plist */, 6DF7DF8E1CD53F300099C320 /* Result.swift */, 6DF7DF701CD5393E0099C320 /* SwiftyOAuth.h */, @@ -356,6 +359,7 @@ 6D1C3C4A1D0469230096C7C9 /* Stripe.swift in Sources */, 6D1C3C501D046C9F0096C7C9 /* Slack.swift in Sources */, 6D1C3C4C1D0469A60096C7C9 /* Reddit.swift in Sources */, + D087CC231DCA04C400F2EE9D /* ServerTrustPolicy.swift in Sources */, 6DF7DF881CD539780099C320 /* Provider.swift in Sources */, 6D5351361CFB7097001BEF41 /* Medium.swift in Sources */, 6D1C3C541D046E8F0096C7C9 /* Basecamp.swift in Sources */,