ServerTrustPolicy.swift 12.7 KB
Newer Older
1
// ServerTrustPolicy.swift
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
//
// Copyright (c) 2014–2015 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

25
/// Responsible for managing the mapping of `ServerTrustPolicy` objects to a given host.
26
public class ServerTrustPolicyManager {
27 28
    /// The dictionary of policies mapped to a particular host.
    public let policies: [String: ServerTrustPolicy]
29

30 31 32 33 34 35 36 37
    /**
        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.

38
        - parameter policies: A dictionary of all policies mapped to a particular host.
39

40
        - returns: The new `ServerTrustPolicyManager` instance.
41
    */
42 43 44 45
    public init(policies: [String: ServerTrustPolicy]) {
        self.policies = policies
    }

46 47 48 49 50 51 52 53 54 55
    /**
        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.
    */
56
    public func serverTrustPolicyForHost(host: String) -> ServerTrustPolicy? {
57
        return policies[host]
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
    }
}

// MARK: -

extension NSURLSession {
    private struct AssociatedKeys {
        static var ManagerKey = "NSURLSession.ServerTrustPolicyManager"
    }

    var serverTrustPolicyManager: ServerTrustPolicyManager? {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.ManagerKey) as? ServerTrustPolicyManager
        }
        set (manager) {
73
            objc_setAssociatedObject(self, &AssociatedKeys.ManagerKey, manager, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
74 75 76 77 78 79
        }
    }
}

// MARK: - ServerTrustPolicy

80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
/**
    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.
*/
112 113
public enum ServerTrustPolicy {
    case PerformDefaultEvaluation(validateHost: Bool)
114
    case PinCertificates(certificates: [SecCertificate], validateCertificateChain: Bool, validateHost: Bool)
115
    case PinPublicKeys(publicKeys: [SecKey], validateCertificateChain: Bool, validateHost: Bool)
116 117 118 119 120
    case DisableEvaluation
    case CustomEvaluation((serverTrust: SecTrust, host: String) -> Bool)

    // MARK: - Bundle Location

121 122 123
    /**
        Returns all certificates within the given bundle with a `.cer` file extension.

124
        - parameter bundle: The bundle to search for all `.cer` files.
125

126
        - returns: All certificates within the given bundle.
127
    */
128
    public static func certificatesInBundle(bundle: NSBundle = NSBundle.mainBundle()) -> [SecCertificate] {
129 130
        var certificates: [SecCertificate] = []

131 132 133 134 135
        let paths = Set([".cer", ".CER", ".crt", ".CRT", ".der", ".DER"].map { fileExtension in
            bundle.pathsForResourcesOfType(fileExtension, inDirectory: nil)
        }.flatten())

        for path in paths {
136 137
            if let
                certificateData = NSData(contentsOfFile: path),
138
                certificate = SecCertificateCreateWithData(nil, certificateData)
139 140 141 142 143 144 145 146
            {
                certificates.append(certificate)
            }
        }

        return certificates
    }

147 148 149
    /**
        Returns all public keys within the given bundle with a `.cer` file extension.

150
        - parameter bundle: The bundle to search for all `*.cer` files.
151

152
        - returns: All public keys within the given bundle.
153
    */
154
    public static func publicKeysInBundle(bundle: NSBundle = NSBundle.mainBundle()) -> [SecKey] {
155 156
        var publicKeys: [SecKey] = []

157
        for certificate in certificatesInBundle(bundle) {
158 159 160 161 162 163 164 165 166 167
            if let publicKey = publicKeyForCertificate(certificate) {
                publicKeys.append(publicKey)
            }
        }

        return publicKeys
    }

    // MARK: - Evaluation

168 169 170
    /**
        Evaluates whether the server trust is valid for the given host.

171 172
        - parameter serverTrust: The server trust to evaluate.
        - parameter host:        The host of the challenge protection space.
173

174
        - returns: Whether the server trust is valid.
175
    */
176 177 178 179 180
    public func evaluateServerTrust(serverTrust: SecTrust, isValidForHost host: String) -> Bool {
        var serverTrustIsValid = false

        switch self {
        case let .PerformDefaultEvaluation(validateHost):
181
            let policy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil)
182
            SecTrustSetPolicies(serverTrust, [policy])
183 184

            serverTrustIsValid = trustIsValid(serverTrust)
185 186
        case let .PinCertificates(pinnedCertificates, validateCertificateChain, validateHost):
            if validateCertificateChain {
187
                let policy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil)
188
                SecTrustSetPolicies(serverTrust, [policy])
189

190
                SecTrustSetAnchorCertificates(serverTrust, pinnedCertificates)
191
                SecTrustSetAnchorCertificatesOnly(serverTrust, true)
192

193 194 195
                serverTrustIsValid = trustIsValid(serverTrust)
            } else {
                let serverCertificatesDataArray = certificateDataForTrust(serverTrust)
196
                let pinnedCertificatesDataArray = certificateDataForCertificates(pinnedCertificates)
197 198 199 200 201 202 203 204 205 206

                outerLoop: for serverCertificateData in serverCertificatesDataArray {
                    for pinnedCertificateData in pinnedCertificatesDataArray {
                        if serverCertificateData.isEqualToData(pinnedCertificateData) {
                            serverTrustIsValid = true
                            break outerLoop
                        }
                    }
                }
            }
207
        case let .PinPublicKeys(pinnedPublicKeys, validateCertificateChain, validateHost):
208 209
            var certificateChainEvaluationPassed = true

210
            if validateCertificateChain {
211
                let policy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil)
212
                SecTrustSetPolicies(serverTrust, [policy])
213 214 215 216 217

                certificateChainEvaluationPassed = trustIsValid(serverTrust)
            }

            if certificateChainEvaluationPassed {
218
                outerLoop: for serverPublicKey in ServerTrustPolicy.publicKeysForTrust(serverTrust) as [AnyObject] {
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253
                    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: serverTrust, host: host)
        }

        return serverTrustIsValid
    }

    // MARK: - Private - Trust Validation

    private func trustIsValid(trust: SecTrust) -> Bool {
        var isValid = false

        var result = SecTrustResultType(kSecTrustResultInvalid)
        let status = SecTrustEvaluate(trust, &result)

        if status == errSecSuccess {
            let unspecified = SecTrustResultType(kSecTrustResultUnspecified)
            let proceed = SecTrustResultType(kSecTrustResultProceed)

            isValid = result == unspecified || result == proceed
        }

        return isValid
    }

254 255 256 257 258 259
    // MARK: - Private - Certificate Data

    private func certificateDataForTrust(trust: SecTrust) -> [NSData] {
        var certificates: [SecCertificate] = []

        for index in 0..<SecTrustGetCertificateCount(trust) {
260 261 262
            if let certificate = SecTrustGetCertificateAtIndex(trust, index) {
                certificates.append(certificate)
            }
263 264 265 266 267 268
        }

        return certificateDataForCertificates(certificates)
    }

    private func certificateDataForCertificates(certificates: [SecCertificate]) -> [NSData] {
269
        return certificates.map { SecCertificateCopyData($0) as NSData }
270 271
    }

272 273
    // MARK: - Private - Public Key Extraction

274
    private static func publicKeysForTrust(trust: SecTrust) -> [SecKey] {
275 276 277
        var publicKeys: [SecKey] = []

        for index in 0..<SecTrustGetCertificateCount(trust) {
278 279 280 281
            if let
                certificate = SecTrustGetCertificateAtIndex(trust, index),
                publicKey = publicKeyForCertificate(certificate)
            {
282 283 284 285 286 287 288
                publicKeys.append(publicKey)
            }
        }

        return publicKeys
    }

289
    private static func publicKeyForCertificate(certificate: SecCertificate) -> SecKey? {
290 291
        var publicKey: SecKey?

292 293 294
        let policy = SecPolicyCreateBasicX509()
        var trust: SecTrust?
        let trustCreationStatus = SecTrustCreateWithCertificates(certificate, policy, &trust)
295

296 297
        if let trust = trust where trustCreationStatus == errSecSuccess {
            publicKey = SecTrustCopyPublicKey(trust)
298 299 300 301 302
        }

        return publicKey
    }
}