Alamofire源码解读系列(八)之安全策略(ServerTrustPolicy)

本篇主要讲解Alamofire中安全验证代码html

前言

做为开发人员,理解HTTPS的原理和应用算是一项基本技能。HTTPS目前来讲是很是安全的,但仍然有大量的公司还在使用HTTP。其实HTTPS也并非很贵啊。ios

在网上能够找到大把的介绍HTTTPS的文章,在阅读ServerTrustPolicy.swfit代码前,咱们先简单的讲一下HTTPS请求的过程:web

上边的图片已经标出了步骤,咱们逐步的来分析:swift

  1. HTTPS请求以https开头,咱们首先向服务器发送一条请求。
  2. 服务器须要一个证书,这个证书能够从某些机构得到,也能够本身经过工具生成,经过某些合法机构生成的证书客户端不须要进行验证,这样的请求不会触发Apple的@objc(URLSession:task:didReceiveChallenge:completionHandler:)代理方法,本身生成的证书则须要客户端进行验证。证书中包含公钥和私钥:api

    • 公钥是公开的,任何人均可以使用该公钥加密数据,只有知道了私钥才能解密数据
    • 私钥是要求高度保密的,只有知道了私钥才能解密用公钥加密的数据
    • 关于非对称加密的知识,你们能够在网上找到
  3. 服务器会把公钥发送给客户端
  4. 客户端此刻就拿到了公钥。注意,这里不是直接就拿公钥加密数据发送了,由于这仅仅能知足客户端给服务器发加密数据,那么服务器怎么给客户端发送加密数据呢?所以须要在客户端和服务器间创建一条通道,通道的密码只有客户端和服务器知道。只能让客户端本身生成一个密码,这个密码就是一个随机数,这个随机数绝对是安全的,由于目前只有客户端本身知道
  5. 客户端把这个随机数经过公钥加密后发送给服务器,就算被别人截获了加密后的数据,在没有私钥的状况下,是根本没法解密的
  6. 服务器用私钥把数据解密后,就得到了这个随机数
  7. 到这里客户端和服务器的安全链接就已经创建了,最主要的目的是交换随机数,而后服务器就用这个随机数把数据加密后发给客户端,使用的是对称加密技术。
  8. 客户端得到了服务器的加密数据,使用随机数解密,到此,客户端和服务器就能经过随机数发送数据了数组

HTTPS前边的几回握手是须要时间开销的,所以,不能每次链接都走一遍,这就是后边使用对称加密数据的缘由。Alamofire中主要作的是对服务器的验证,关于自定义的安全验证应该也是模仿了上边的整个过程。相对于Apple来讲,隐藏了发送随机数这一过程。安全

对于服务器的验证除了证书验证以外必定要加上域名验证,这样才能更安全。服务器若要验证客户端则会使用签名技术。若是假装成客户端来获取服务器的数据最大的问题就是不知道某个请求的参数是什么,这样也就没法获取数据。服务器

ServerTrustPolicyManager

ServerTrustPolicyManager是对ServerTrustPolicy的管理,咱们能够暂时把ServerTrustPolicy当作是一个安全策略,就是指对一个服务器采起的策略。然而在真实的开发中,一个APP可能会用到不少不一样的主机地址(host)。所以就产生了这样的需求,为不一样的host绑定一个特定的安全策略。网络

所以ServerTrustPolicyManager须要一个字典来存放这些有key,value对应关系的数据。咱们看下边的代码:session

/// Responsible for managing the mapping of `ServerTrustPolicy` objects to a given host.
open class ServerTrustPolicyManager {
    /// The dictionary of policies mapped to a particular host.
    open 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]
    }
}

出于优秀代码的设计问题,在后续的使用中确定会有根据host读取策略的要求,所以,在上边的类中设计了最后一个函数。

咱们是这么使用的:

let serverTrustPolicies: [String: ServerTrustPolicy] = [
    "test.example.com": .pinCertificates(
        certificates: ServerTrustPolicy.certificates(),
        validateCertificateChain: true,
        validateHost: true
    ),
    "insecure.expired-apis.com": .disableEvaluation
]

let sessionManager = SessionManager(
    serverTrustPolicyManager: ServerTrustPolicyManager(policies: serverTrustPolicies)
)

在Alamofire中这个ServerTrustPolicyManager会在SessionDelegate的收到服务器要求验证的方法中会出现,这个会在后续的文章中给出说明。

把ServerTrustPolicyManager绑定到URLSession

ServerTrustPolicyManager做为URLSession的一个属性,经过运行时的手段来实现。

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)
        }
    }
}

上边的代码用到了运行时,尤为是OBJC_ASSOCIATION_RETAIN_NONATOMIC这个选项,其中包含了强引用和若引用的问题,我想在这里简单的解释一下引用问题。

咱们能够这么理解,不论是类仍是对象,或者是对象的属性,咱们都称之为一个object。咱们把这个object比做一个铁盒子,当有其它的对象对他强引用的时候,就像给这个铁盒子绑了一个绳子,弱引用就像一条虚幻的激光同样链接这个盒子。固然,在oc中,不少对象默认的状况下就是strong的。

咱们能够想象这个盒子是被绳子拉住了,才能漂浮在空中,若是没有绳子就会掉到无底深渊,而后销毁。这里最重要的概念就是,只要一个对象没有了强引用,那么就会马上销毁。

咱们举个例子:

MyViewController *myController = [[MyViewController alloc] init…];

上边的代码是再日常不过的一段代码,建立了一个MyViewController实例,而后使用myController指向了这个实例,所以这个实例就有了一个绳子,他就不会马上销毁,若是咱们把代码改为这样:

MyViewController * __weak myController = [[MyViewController alloc] init…];

把myController指向实例设置为弱引用,那么即便在下一行代码打印这个myController,也会是nil。由于实例并无一个绳子让他能不不销毁。

所谓道理都是相通的,只要理解了这个概念就能明白引用循环的问题,须要注意的是做用域的问题,若是上边的myController在一个函数中,那么出了函数的做用域,也会销毁。

ServerTrustPolicy

接下来将是本篇文章最核心的内容,得益于swift语言的强大,ServerTrustPolicy被设计成enum枚举。既然本质上只是个枚举,那么咱们先不关心枚举中的函数,先单独看看有哪些枚举子选项:

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)

千万别认为上边的某些选项是个函数,其实他们只是不一样的类型加上关联值而已。咱们先不对这些选项作不解释,由于在下边的方法中会根据这些选项作出不一样的操做,到那时在说明这些选项的做用更好。

还有一点要明白,在swift中是像下边代码这样初始化枚举的:

ServerTrustPolicy.performDefaultEvaluation(validateHost: true)

咱们用上帝视角来看做者的代码,接下来就应该看看那些带有static的函数了,由于这些函数都是静态函数,能够直接用ServerTrustPolicy调用,虽然归属于ServerTrustPolicy,但相对比较独立。

获取证书

/// 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
    }

在开发中,若是和服务器的安全链接须要对服务器进行验证,最好的办法就是在本地保存一些证书,拿到服务器传过来的证书,而后进行对比,若是有匹配的,就表示能够信任该服务器。从上边的函数中能够看出,Alamofire会在Bundle(默认为main)中查找带有[".cer", ".CER", ".crt", ".CRT", ".der", ".DER"]后缀的证书。

注意,上边函数中的paths保存的是这些证书的路径,map把这些后缀转换成路径,咱们以.cer为例。经过map后,原来的".cer"就变成了一个数组,也就是说经过map后,原来的数组变成了二维数组了,而后再经过joined()函数,把二维数组转换成一维数组。

而后要作的就是根据这些路径获取证书数据了,就很少作解释了。

获取公钥

这个比较好理解,就是在本地证书中取出公钥,至于证书是由什么组成的,你们能够网上本身查找相关内容,

/// 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
    }

上边的函数很简单,可是他用到了另一个函数publicKey(for: certificate)

经过SecCertificate获取SecKey

获取SecKey能够经过SecCertificate也能够经过SecTrust,下边的函数是第一种状况:

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
    }

上边的过程没什么好说的,基本上这是固定写法,值得注意的是上边默认是按照X509证书格式来解析的,所以在生成证书的时候最好使用这个格式。不然可能没法获取到publicKey。

最核心的方法evaluate

从函数设计的角度考虑,evaluate应该接受两个参数,一个是服务器的证书,一个是host。返回一个布尔类型。

evaluate函数是枚举中的一个函数,所以它必然依赖枚举的子选项。这就说明只有初始化枚举才能使用这个函数。

举一个现实生活中的一个小例子。有一个管理员,他手下管理这3个员工,分别是厨师,前台,行政,如今有一个任务须要想办法弄明白这3我的会不会喊麦,有两种方法能够得出结果,一种是管理员一个一个的去问,也就是得出结果的方法掌握在管理员手中,只有经过管理员才能知道答案。有一个老板想知道厨师会不会喊麦。他必需要去问管理员才行。这就形成了逻辑上的问题。另外一种方法,让每个人当场喊一个,任何人在任何场合都能得出结果。

最近从新看了代码大全这本书,对子程序的设计有了全新的认识。重点还在于抽象类型是什么?这个就很少说了,有兴趣的朋友能够去看看那本书。

这个函数很长,但整体的思想是根据不一样的策略作出不一样的操做。咱们先把该函数弄上来:

/// 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
    }

无论选用那种策略,要完成验证都须要3步:

  1. SecPolicyCreateSSL 建立策略,是否验证host
  2. SecTrustSetPolicies 为待验证的对象设置策略
  3. trustIsValid 进行验证

到了这里就有必要介绍一下几种策略的用法了:

  • performDefaultEvaluation 默认的策略,只有合法证书才能经过验证
  • performRevokedEvaluation 对注销证书作的一种额外设置,关于注销证书验证超过了本篇文章的范围,有兴趣的朋友能够查看官方文档。
  • pinCertificates 验证指定的证书,这里边有一个参数:是否验证证书链,关于证书链的相关内容能够看这篇文章iOS 中对 HTTPS 证书链的验证.验证证书链算是比较严格的验证了。这里边设置锚点等等,这里就不作解释了。若是不验证证书链的话,只要对比指定的证书有没有和服务器信任的证书匹配项,只要有一个能匹配上,就验证经过
  • pinPublicKeys 这个更上边的那个差很少,就不作介绍了
  • disableEvaluation 该选项下,验证一直都是经过的,也就是说无条件信任
  • customEvaluation 自定义验证,须要返回一个布尔类型的结果

上边的这些验证选项中,咱们可能根据本身的需求进行验证,其中最安全的是证书链加host双重验证。并且在上边的evaluate函数中用到了4个辅助函数,咱们来看看:

func trustIsValid(_ trust: SecTrust) -> Bool

该函数用于判断是否验证成功

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
    }

func certificateData(for trust: SecTrust) -> [Data]

该函数把服务器的SecTrust处理成证书二进制数组

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)
    }

func certificateData(for certificates: [SecCertificate]) -> [Data]

private func certificateData(for certificates: [SecCertificate]) -> [Data] {
        return certificates.map { SecCertificateCopyData($0) as Data }
    }

func publicKeys(for trust: SecTrust) -> [SecKey]

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
    }

总结

其实在开发中,能够没必要关心这些实现细节,要想弄明白这些策略的详情,还须要作不少的功课才行。

因为知识水平有限,若有错误,还望指出

连接

Alamofire源码解读系列(一)之概述和使用 简书-----博客园

Alamofire源码解读系列(二)之错误处理(AFError) 简书-----博客园

Alamofire源码解读系列(三)之通知处理(Notification) 简书-----博客园

Alamofire源码解读系列(四)之参数编码(ParameterEncoding) 简书-----博客园

Alamofire源码解读系列(五)之结果封装(Result) 简书-----博客园

Alamofire源码解读系列(六)之Task代理(TaskDelegate) 简书-----博客园

Alamofire源码解读系列(七)之网络监控(NetworkReachabilityManager) 简书-----博客园

相关文章
相关标签/搜索