Alamofire(八)-- 安全策略ServerTrustPolicy

引言

在网络请求、通讯过程中,最重要的就是安全了,稍有不慎,被别人截取、攻击,都有可能对自己或者公司带来不可估量的损失,所以,网络安全是尤为重大的。
这篇文章,我们就来讲讲,Alamofire作为一个如此重要的三方库,它的安全策略是怎么设计和使用的。

HTTPS

在说到Alamofire的安全策略之前,我们先来了解一下HTTPS,毕竟Alamofire也需要通过HTTPS进行网络请求通讯的。

几种协议的介绍与关系

  • HTTPHTTP协议传输的数据都是未加密的(明文),因此使用HTTP协议传输隐私信息非常不安全。
  • HTTPS:为了保证隐私数据能加密传输,采用SSL/TLS协议用于对HTTP协议传输的数据进行加密,也就是HTTPS
  • SSLSSL(Secure Sockets Layer)协议是由网景公司设计,后被IETF定义在RFC 6101中。
  • TLSTLS可以说是SSL的改进版,实际上我们现在的HTTPS都是用的TLS协议。

特点

  • HTTPS在传输数据之前需要客户端(浏览器)与服务端(网站)之间进行一次握手,在握手过程中将确立双方加密传输数据的密码信息。
  • TLS/SSL中使用了非对称加密,对称加密以及HASH算法。其中非对称加密算法用于在握手过程中加密生成的密码,对称加密算法用于对真正传输的数据进行加密,而HASH算法用于验证数据的完整性。
  • TLS握手过程中如果有任何错误,都会使加密连接断开,从而阻止了隐私信息的传输。

请求过程

我们先来看一下这张图(图片来自网络):


看着这张图,接下来我们来简单分析一下:

  • 客户端的HTTPS请求首先向服务器发送一条请求,注意,HTTPS请求均是以https开头。
  • 这时候,服务器端就需要一个证书,这个证书既可以是自己通过某些工具生成,也可以是从某些机构获取。如果是通过某些合法机构生成的证书,是不需要进行验证的,同时,这些请求不会触发@objc(URLSession:task:didReceiveChallenge:completionHandler:)代理方法。如果是自己生成的证书,需要在客户端进行验证,且证书中应该包含公钥、私钥。(公钥:公开的,任何人都可以使用该公钥加密数据,只有知道了私钥才能解密数据。私钥:要求高度保密的,只有知道了私钥才能解密用公钥加密的数据。
  • 服务器端把公钥发送给客户端
  • 此时,客户端拿到公钥,这里要注意,拿到公钥后,并不会直接用于加密数据发送,仅仅是客户端给服务器端发送加密数据,还需要服务器端给客户端发送加密数据,因此,我们需要在客户端与服务器端建立一个安全的通讯通道,开启这条通道的密码只有客户端和服务器端知道。然后,客户端会自己生成一个随机数密码,因为这个随机数密码目前只有客户端知道,所以,这个随机数密码是绝对安全的。
  • 再来,客户端用这个随机数密码再通过公钥加密后发送给服务器端,如果被中间人攻击截获了,没有私钥的情况下,他也是无法解密的。
  • 服务器端收到客户端发送的加密数据后,使用私钥把数据解密后,就获取到了这个随机数。
  • 此时此刻,客户端与服务器端的安全通道就已经连接好了,主要目的就是交换随机数,便于服务器使用这个随机数把数据加密后发送到客户端,此间,使用的是对称加密技术(备注:关于对称加密、非对称加密的详细知识网上或者书籍有很多,内容太多,这里就不详细解释了,也解释不完的😅)。
  • 最后,客户端拿到了服务器端的加密数据后,再使用随机数解密,这样,客户端与服务器端就能通过随机数加密发送数据,进行安全的通讯了。

总结

HTTPS每次握手其实都是需要时间开销的,所以,不能每次连接都这样走一次,因此,我们需要使用对称加密数据的方式。
Alamofire中,主要的工作是对服务器的验证,其自定义的安全策略验证,我猜,也是模仿的上边的这个过程。
另外,在对服务器的验证下,还应该加上域名验证,这样才能更加的安全

OK,前戏都已经说完了,接下来,进入主题。

ServerTrustPolicy

在查看ServerTrustPolicy.swift文件的时候,我们发现,最核心的2个类ServerTrustPolicyManagerServerTrustPolicy。因此,接下来,我们就分别来说一说。

ServerTrustPolicy

简述

Alamofire中,ServerTrustPolicy是一个枚举类型:

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

注意: 这些选项并不是函数,只是不同的类型加上了关联值而已。

函数说明

获取证书

首先,看下获取证书的函数方法:

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
    }
  • 如果在和服务器的安全连接中,需要对服务器进行验证,一个好的方法就是在本地工程保存一些证书,得到服务器传过来的证书后进行对比,如果有匹配,则表示可以信任该服务器。其中包括带有这些后缀的证书:".cer", ".CER", ".crt", ".CRT", ".der", ".DER"
  • 函数中,paths保存的是这些证书的路径,再通过map函数转换为路径,最后,根据这些路径获取证书数据。

获取公钥

获取公钥的函数方法:

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),注意到,获取SecKey可以通过SecCertificate方式,也可以通过SecTrust方式。

通过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
    }

很简单的,没有什么好说的,都是固定的写法。

通过SecCertificate获取SecKey

先看一下函数方法:

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
    }
  • 一样的,固定写法,只是要特别注意一下SecPolicyCreateBasicX509(),默认是按照X509证书格式来解析的,所以,在生成证书的时候,最好用这个格式来,不然有可能无法获得publicKey
  • 有关X509证书格式的详细说明看这里百度百科

核心方法evaluate

我们先把函数看一下:

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
    }
  • 这个函数很长,一看switch语句,就知道,它的总体思想就是需要根据不同的策略做出不同操作。
  • evaluate函数需要接收2个参数,一个是服务器的证书,还有一个是host,返回值是一个bool类型。
  • 因为evaluate函数被定义在枚举中,因此,它肯定是依赖枚举的子选项,只有初始化枚举后,才能调用这个函数。
验证步骤说明

从上面的函数可以看到,不论我们使用哪一种策略,要完成验证,都需要以下步骤:

  • SecPolicyCreateSSL:创建策略,是否验证host
  • SecTrustSetPolicies:为待验证的对象设置策略
  • trustIsValid:进行验证
辅助函数
private 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
    }

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

private func certificateData(for trust: SecTrust) -> [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)
    }

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

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

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

策略用法

在下边的验证选项中,我们可以根据自己的需求进行验证,最安全的是证书链加host双重验证:

  • performDefaultEvaluation:默认的策略,只有合法证书才能通过验证。
  • performRevokedEvaluation:对注销证书做的一种额外设置
  • pinCertificates:验证指定的证书,这里边有一个参数:是否验证证书链,关于证书链的相关内容可以去查一查其他更为详细的资料,验证证书链算是比较严格的验证了。如果不验证证书链的话,只要对比指定的证书有没有和服务器信任的证书匹配项,只要有一个能匹配上,就验证通过
  • pinPublicKeys:这个和上边的那个差不多
  • disableEvaluation:该选项下,验证一直都是通过的,也就是说无条件信任
  • customEvaluation:自定义验证,需要返回一个布尔类型的结果

ServerTrustPolicyManager

简述

ServerTrustPolicyManager这个类是对ServerTrustPolicy的管理类,因为在实际项目开发中,项目中可能会使用不同的主机地址host,因此,我们需要为不同的host绑定一个特定安全策略。

我们先来看一下ServerTrustPolicyManager类怎么定义的:

open class ServerTrustPolicyManager {
            
            public let policies: [String: ServerTrustPolicy]
            
            public init(policies: [String: ServerTrustPolicy]) {
                self.policies = policies
            }
            
            open func serverTrustPolicy(forHost host: String) -> ServerTrustPolicy? {
                return policies[host]
            }
        }
  • ServerTrustPolicyManager使用了一个字典属性,用来存放有keyvalue对应关系的数据。
  • 由于需要根据host来读取策略,因此,该类增加了serverTrustPolicy方法。

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

可以看到,ServerTrustPolicyManager作为URLSession的一个属性,是通过运行时的手段来实现。

总结

这篇文章,也只是简单的解析了一下Alamofire中,它的安全策略设计方法,当然,在实际项目开发中,大可以不必要关心这些实现细节,但是作为一个敬业的、喜欢iOS开发的开发者来说,还是很有必要知晓其中的设计方法、使用方法,很多细节的东西,还需要做很多的功课才行。


常规打广告系列:
简书:Alamofire(八)-- 安全策略ServerTrustPolicy
掘金:Alamofire(八)-- 安全策略ServerTrustPolicy
小专栏:Alamofire(八)-- 安全策略ServerTrustPolicy

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

推荐阅读更多精彩内容