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

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

前言

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

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

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

  1. HTTPS请求以https开头,我们首先向服务器发送一条请求。

  2. 服务器需要一个证书,这个证书可以从某些机构获得,也可以自己通过工具生成,通过某些合法机构生成的证书客户端不需要进行验证,这样的请求不会触发Apple的@objc(URLSession:task:didReceiveChallenge:completionHandler:)代理方法,自己生成的证书则需要客户端进行验证。证书中包含公钥和私钥:

    • 公钥是公开的,任何人都可以使用该公钥加密数据,只有知道了私钥才能解密数据
    • 私钥是要求高度保密的,只有知道了私钥才能解密用公钥加密的数据
    • 关于非对称加密的知识,大家可以在网上找到
  1. 服务器会把公钥发送给客户端
  2. 客户端此刻就拿到了公钥。注意,这里不是直接就拿公钥加密数据发送了,因为这仅仅能满足客户端给服务器发加密数据,那么服务器怎么给客户端发送加密数据呢?因此需要在客户端和服务器间建立一条通道,通道的密码只有客户端和服务器知道。只能让客户端自己生成一个密码,这个密码就是一个随机数,这个随机数绝对是安全的,因为目前只有客户端自己知道
  3. 客户端把这个随机数通过公钥加密后发送给服务器,就算被别人截获了加密后的数据,在没有私钥的情况下,是根本无法解密的
  4. 服务器用私钥把数据解密后,就获得了这个随机数
  5. 到这里客户端和服务器的安全连接就已经建立了,最主要的目的是交换随机数,然后服务器就用这个随机数把数据加密后发给客户端,使用的是对称加密技术。
  6. 客户端获得了服务器的加密数据,使用随机数解密,到此,客户端和服务器就能通过随机数发送数据了

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

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

ServerTrustPolicyManager

ServerTrustPolicyManager是对ServerTrustPolicy的管理,我们可以暂时把ServerTrustPolicy当做是一个安全策略,就是指对一个服务器采取的策略。然而在真实的开发中,一个APP可能会用到很多不同的主机地址(host)。因此就产生了这样的需求,为不同的host绑定一个特定的安全策略。

因此ServerTrustPolicyManager需要一个字典来存放这些有key,value对应关系的数据。我们看下边的代码:

/// 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) 简书-----博客园

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容