美文网首页Swift开发
Alamofire(八)-- 安全策略ServerTrustPo

Alamofire(八)-- 安全策略ServerTrustPo

作者: Daniel_Harvey | 来源:发表于2019-08-27 11:35 被阅读0次

    引言

    在网络请求、通讯过程中,最重要的就是安全了,稍有不慎,被别人截取、攻击,都有可能对自己或者公司带来不可估量的损失,所以,网络安全是尤为重大的。
    这篇文章,我们就来讲讲,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

    相关文章

      网友评论

        本文标题:Alamofire(八)-- 安全策略ServerTrustPo

        本文链接:https://www.haomeiwen.com/subject/ldosectx.html