You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
310 lines
14 KiB
310 lines
14 KiB
// |
|
// ServerTrustPolicy.swift |
|
// |
|
// Copyright (c) 2014 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 |
|
|
|
/// Responsible for managing the mapping of `ServerTrustPolicy` objects to a given host. |
|
open class ServerTrustPolicyManager { |
|
/// The dictionary of policies mapped to a particular host. |
|
public let policies: [String: ServerTrustPolicy] |
|
|
|
/// Initializes the `ServerTrustPolicyManager` instance with the given policies. |
|
/// |
|
/// Since different servers and web services can have different leaf certificates, intermediate and even root |
|
/// certficates, it is important to have the flexibility to specify evaluation policies on a per host basis. This |
|
/// allows for scenarios such as using default evaluation for host1, certificate pinning for host2, public key |
|
/// pinning for host3 and disabling evaluation for host4. |
|
/// |
|
/// - parameter policies: A dictionary of all policies mapped to a particular host. |
|
/// |
|
/// - returns: The new `ServerTrustPolicyManager` instance. |
|
public init(policies: [String: ServerTrustPolicy]) { |
|
self.policies = policies |
|
} |
|
|
|
/// Returns the `ServerTrustPolicy` for the given host if applicable. |
|
/// |
|
/// By default, this method will return the policy that perfectly matches the given host. Subclasses could override |
|
/// this method and implement more complex mapping implementations such as wildcards. |
|
/// |
|
/// - parameter host: The host to use when searching for a matching policy. |
|
/// |
|
/// - returns: The server trust policy for the given host if found. |
|
open func serverTrustPolicy(forHost host: String) -> ServerTrustPolicy? { |
|
return policies[host] |
|
} |
|
} |
|
|
|
// MARK: - |
|
|
|
extension URLSession { |
|
private struct AssociatedKeys { |
|
static var managerKey = "URLSession.ServerTrustPolicyManager" |
|
} |
|
|
|
var serverTrustPolicyManager: ServerTrustPolicyManager? { |
|
get { |
|
return objc_getAssociatedObject(self, &AssociatedKeys.managerKey) as? ServerTrustPolicyManager |
|
} |
|
set (manager) { |
|
objc_setAssociatedObject(self, &AssociatedKeys.managerKey, manager, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) |
|
} |
|
} |
|
} |
|
|
|
// MARK: - ServerTrustPolicy |
|
|
|
/// 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. |
|
/// |
|
/// - performRevokedEvaluation: Uses the default and revoked server trust evaluations allowing you to control whether to |
|
/// validate the host provided by the challenge as well as specify the revocation flags for |
|
/// testing for revoked certificates. Apple platforms did not start testing for revoked |
|
/// certificates automatically until iOS 10.1, macOS 10.12 and tvOS 10.1 which is |
|
/// demonstrated in our TLS tests. 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 performRevokedEvaluation(validateHost: Bool, revocationFlags: CFOptionFlags) |
|
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 .performRevokedEvaluation(validateHost, revocationFlags): |
|
let defaultPolicy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil) |
|
let revokedPolicy = SecPolicyCreateRevocation(revocationFlags) |
|
SecTrustSetPolicies(serverTrust, [defaultPolicy, revokedPolicy] as CFTypeRef) |
|
|
|
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 |
|
|
|
if #available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, *) { |
|
isValid = SecTrustEvaluateWithError(trust, nil) |
|
} else { |
|
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..<SecTrustGetCertificateCount(trust) { |
|
if let certificate = SecTrustGetCertificateAtIndex(trust, index) { |
|
certificates.append(certificate) |
|
} |
|
} |
|
|
|
return certificateData(for: certificates) |
|
} |
|
|
|
private func certificateData(for certificates: [SecCertificate]) -> [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..<SecTrustGetCertificateCount(trust) { |
|
if |
|
let certificate = SecTrustGetCertificateAtIndex(trust, index), |
|
let publicKey = publicKey(for: certificate) |
|
{ |
|
publicKeys.append(publicKey) |
|
} |
|
} |
|
|
|
return publicKeys |
|
} |
|
|
|
private static func publicKey(for certificate: SecCertificate) -> 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 |
|
} |
|
}
|
|
|