Alamofire源碼解讀系列(八)之安全策略(ServerTrustPolicy)

本篇主要講解Alamofire中安全驗證代碼

前言

作為開發(fā)人員,理解HTTPS的原理和應(yīng)用算是一項基本技能。HTTPS目前來說是非常安全的,但仍然有大量的公司還在使用HTTP。其實HTTPS也并不是很貴啊。

在網(wǎng)上可以找到大把的介紹HTTTPS的文章,在閱讀ServerTrustPolicy.swfit代碼前,我們先簡單的講一下HTTPS請求的過程:

上邊的圖片已經(jīng)標(biāo)出了步驟,我們逐步的來分析:

  1. HTTPS請求以https開頭,我們首先向服務(wù)器發(fā)送一條請求。

  2. 服務(wù)器需要一個證書,這個證書可以從某些機(jī)構(gòu)獲得,也可以自己通過工具生成,通過某些合法機(jī)構(gòu)生成的證書客戶端不需要進(jìn)行驗證,這樣的請求不會觸發(fā)Apple的@objc(URLSession:task:didReceiveChallenge:completionHandler:)代理方法,自己生成的證書則需要客戶端進(jìn)行驗證。證書中包含公鑰和私鑰:

    • 公鑰是公開的,任何人都可以使用該公鑰加密數(shù)據(jù),只有知道了私鑰才能解密數(shù)據(jù)
    • 私鑰是要求高度保密的,只有知道了私鑰才能解密用公鑰加密的數(shù)據(jù)
    • 關(guān)于非對稱加密的知識,大家可以在網(wǎng)上找到
  1. 服務(wù)器會把公鑰發(fā)送給客戶端
  2. 客戶端此刻就拿到了公鑰。注意,這里不是直接就拿公鑰加密數(shù)據(jù)發(fā)送了,因為這僅僅能滿足客戶端給服務(wù)器發(fā)加密數(shù)據(jù),那么服務(wù)器怎么給客戶端發(fā)送加密數(shù)據(jù)呢?因此需要在客戶端和服務(wù)器間建立一條通道,通道的密碼只有客戶端和服務(wù)器知道。只能讓客戶端自己生成一個密碼,這個密碼就是一個隨機(jī)數(shù),這個隨機(jī)數(shù)絕對是安全的,因為目前只有客戶端自己知道
  3. 客戶端把這個隨機(jī)數(shù)通過公鑰加密后發(fā)送給服務(wù)器,就算被別人截獲了加密后的數(shù)據(jù),在沒有私鑰的情況下,是根本無法解密的
  4. 服務(wù)器用私鑰把數(shù)據(jù)解密后,就獲得了這個隨機(jī)數(shù)
  5. 到這里客戶端和服務(wù)器的安全連接就已經(jīng)建立了,最主要的目的是交換隨機(jī)數(shù),然后服務(wù)器就用這個隨機(jī)數(shù)把數(shù)據(jù)加密后發(fā)給客戶端,使用的是對稱加密技術(shù)。
  6. 客戶端獲得了服務(wù)器的加密數(shù)據(jù),使用隨機(jī)數(shù)解密,到此,客戶端和服務(wù)器就能通過隨機(jī)數(shù)發(fā)送數(shù)據(jù)了

HTTPS前邊的幾次握手是需要時間開銷的,因此,不能每次連接都走一遍,這就是后邊使用對稱加密數(shù)據(jù)的原因。Alamofire中主要做的是對服務(wù)器的驗證,關(guān)于自定義的安全驗證應(yīng)該也是模仿了上邊的整個過程。相對于Apple來說,隱藏了發(fā)送隨機(jī)數(shù)這一過程。

對于服務(wù)器的驗證除了證書驗證之外一定要加上域名驗證,這樣才能更安全。服務(wù)器若要驗證客戶端則會使用簽名技術(shù)。如果偽裝成客戶端來獲取服務(wù)器的數(shù)據(jù)最大的問題就是不知道某個請求的參數(shù)是什么,這樣也就無法獲取數(shù)據(jù)。

ServerTrustPolicyManager

ServerTrustPolicyManager是對ServerTrustPolicy的管理,我們可以暫時把ServerTrustPolicy當(dāng)做是一個安全策略,就是指對一個服務(wù)器采取的策略。然而在真實的開發(fā)中,一個APP可能會用到很多不同的主機(jī)地址(host)。因此就產(chǎn)生了這樣的需求,為不同的host綁定一個特定的安全策略。

因此ServerTrustPolicyManager需要一個字典來存放這些有key,value對應(yīng)關(guān)系的數(shù)據(jù)。我們看下邊的代碼:

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

出于優(yōu)秀代碼的設(shè)計問題,在后續(xù)的使用中肯定會有根據(jù)host讀取策略的要求,因此,在上邊的類中設(shè)計了最后一個函數(shù)。

我們是這么使用的:

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的收到服務(wù)器要求驗證的方法中會出現(xiàn),這個會在后續(xù)的文章中給出說明。

把ServerTrustPolicyManager綁定到URLSession

ServerTrustPolicyManager作為URLSession的一個屬性,通過運(yùn)行時的手段來實現(xiàn)。

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

上邊的代碼用到了運(yùn)行時,尤其是OBJC_ASSOCIATION_RETAIN_NONATOMIC這個選項,其中包含了強(qiáng)引用和若引用的問題,我想在這里簡單的解釋一下引用問題。

我們可以這么理解,不管是類還是對象,或者是對象的屬性,我們都稱之為一個object。我們把這個object比作一個鐵盒子,當(dāng)有其它的對象對他強(qiáng)引用的時候,就像給這個鐵盒子綁了一個繩子,弱引用就像一條虛幻的激光一樣連接這個盒子。當(dāng)然,在oc中,很多對象默認(rèn)的情況下就是strong的。

我們可以想象這個盒子是被繩子拉住了,才能漂浮在空中,如果沒有繩子就會掉到無底深淵,然后銷毀。這里最重要的概念就是,只要一個對象沒有了強(qiáng)引用,那么就會立刻銷毀。

我們舉個例子:

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

上邊的代碼是再平常不過的一段代碼,創(chuàng)建了一個MyViewController實例,然后使用myController指向了這個實例,因此這個實例就有了一個繩子,他就不會立刻銷毀,如果我們把代碼改成這樣:

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

把myController指向?qū)嵗O(shè)置為弱引用,那么即使在下一行代碼打印這個myController,也會是nil。因為實例并沒有一個繩子讓他能不不銷毀。

所謂道理都是相通的,只要理解了這個概念就能明白引用循環(huán)的問題,需要注意的是作用域的問題,如果上邊的myController在一個函數(shù)中,那么出了函數(shù)的作用域,也會銷毀。

ServerTrustPolicy

接下來將是本篇文章最核心的內(nèi)容,得益于swift語言的強(qiáng)大,ServerTrustPolicy被設(shè)計成enum枚舉。既然本質(zhì)上只是個枚舉,那么我們先不關(guān)心枚舉中的函數(shù),先單獨(dú)看看有哪些枚舉子選項:

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)

千萬別認(rèn)為上邊的某些選項是個函數(shù),其實他們只是不同的類型加上關(guān)聯(lián)值而已。我們先不對這些選項做不解釋,因為在下邊的方法中會根據(jù)這些選項做出不同的操作,到那時在說明這些選項的作用更好。

還有一點(diǎn)要明白,在swift中是像下邊代碼這樣初始化枚舉的:

ServerTrustPolicy.performDefaultEvaluation(validateHost: true)

我們用上帝視角來看作者的代碼,接下來就應(yīng)該看看那些帶有static的函數(shù)了,因為這些函數(shù)都是靜態(tài)函數(shù),可以直接用ServerTrustPolicy調(diào)用,雖然歸屬于ServerTrustPolicy,但相對比較獨(dú)立。

獲取證書

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

在開發(fā)中,如果和服務(wù)器的安全連接需要對服務(wù)器進(jìn)行驗證,最好的辦法就是在本地保存一些證書,拿到服務(wù)器傳過來的證書,然后進(jìn)行對比,如果有匹配的,就表示可以信任該服務(wù)器。從上邊的函數(shù)中可以看出,Alamofire會在Bundle(默認(rèn)為main)中查找?guī)в?code>[".cer", ".CER", ".crt", ".CRT", ".der", ".DER"]后綴的證書。

注意,上邊函數(shù)中的paths保存的是這些證書的路徑,map把這些后綴轉(zhuǎn)換成路徑,我們以.cer為例。通過map后,原來的".cer"就變成了一個數(shù)組,也就是說通過map后,原來的數(shù)組變成了二維數(shù)組了,然后再通過joined()函數(shù),把二維數(shù)組轉(zhuǎn)換成一維數(shù)組。

然后要做的就是根據(jù)這些路徑獲取證書數(shù)據(jù)了,就不多做解釋了。

獲取公鑰

這個比較好理解,就是在本地證書中取出公鑰,至于證書是由什么組成的,大家可以網(wǎng)上自己查找相關(guān)內(nèi)容,

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

上邊的函數(shù)很簡單,但是他用到了另外一個函數(shù)publicKey(for: certificate)

通過SecCertificate獲取SecKey

獲取SecKey可以通過SecCertificate也可以通過SecTrust,下邊的函數(shù)是第一種情況:

  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
    }

上邊的過程沒什么好說的,基本上這是固定寫法,值得注意的是上邊默認(rèn)是按照X509證書格式來解析的,因此在生成證書的時候最好使用這個格式。否則可能無法獲取到publicKey。

最核心的方法evaluate

從函數(shù)設(shè)計的角度考慮,evaluate應(yīng)該接受兩個參數(shù),一個是服務(wù)器的證書,一個是host。返回一個布爾類型。

evaluate函數(shù)是枚舉中的一個函數(shù),因此它必然依賴枚舉的子選項。這就說明只有初始化枚舉才能使用這個函數(shù)。

舉一個現(xiàn)實生活中的一個小例子。有一個管理員,他手下管理這3個員工,分別是廚師,前臺,行政,現(xiàn)在有一個任務(wù)需要想辦法弄明白這3個人會不會喊麥,有兩種方法可以得出結(jié)果,一種是管理員一個一個的去問,也就是得出結(jié)果的方法掌握在管理員手中,只有通過管理員才能知道答案。有一個老板想知道廚師會不會喊麥。他必須要去問管理員才行。這就造成了邏輯上的問題。另一種方法,讓每一個人當(dāng)場喊一個,任何人在任何場合都能得出結(jié)果。

最近重新看了代碼大全這本書,對子程序的設(shè)計有了全新的認(rèn)識。重點(diǎn)還在于抽象類型是什么?這個就不多說了,有興趣的朋友可以去看看那本書。

這個函數(shù)很長,但總體的思想是根據(jù)不同的策略做出不同的操作。我們先把該函數(shù)弄上來:

 /// 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 創(chuàng)建策略,是否驗證host
  2. SecTrustSetPolicies 為待驗證的對象設(shè)置策略
  3. trustIsValid 進(jìn)行驗證

到了這里就有必要介紹一下幾種策略的用法了:

  • performDefaultEvaluation 默認(rèn)的策略,只有合法證書才能通過驗證
  • performRevokedEvaluation 對注銷證書做的一種額外設(shè)置,關(guān)于注銷證書驗證超過了本篇文章的范圍,有興趣的朋友可以查看官方文檔。
  • pinCertificates 驗證指定的證書,這里邊有一個參數(shù):是否驗證證書鏈,關(guān)于證書鏈的相關(guān)內(nèi)容可以看這篇文章iOS 中對 HTTPS 證書鏈的驗證.驗證證書鏈算是比較嚴(yán)格的驗證了。這里邊設(shè)置錨點(diǎn)等等,這里就不做解釋了。如果不驗證證書鏈的話,只要對比指定的證書有沒有和服務(wù)器信任的證書匹配項,只要有一個能匹配上,就驗證通過
  • pinPublicKeys 這個更上邊的那個差不多,就不做介紹了
  • disableEvaluation 該選項下,驗證一直都是通過的,也就是說無條件信任
  • customEvaluation 自定義驗證,需要返回一個布爾類型的結(jié)果

上邊的這些驗證選項中,我們可能根據(jù)自己的需求進(jìn)行驗證,其中最安全的是證書鏈加host雙重驗證。而且在上邊的evaluate函數(shù)中用到了4個輔助函數(shù),我們來看看:

func trustIsValid(_ trust: SecTrust) -> Bool

該函數(shù)用于判斷是否驗證成功

 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]

該函數(shù)把服務(wù)器的SecTrust處理成證書二進(jìn)制數(shù)組

 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
    }

總結(jié)

其實在開發(fā)中,可以不必關(guān)心這些實現(xiàn)細(xì)節(jié),要想弄明白這些策略的詳情,還需要做很多的功課才行。

由于知識水平有限,如有錯誤,還望指出

鏈接

Alamofire源碼解讀系列(一)之概述和使用 簡書-----博客園

Alamofire源碼解讀系列(二)之錯誤處理(AFError) 簡書-----博客園

Alamofire源碼解讀系列(三)之通知處理(Notification) 簡書-----博客園

Alamofire源碼解讀系列(四)之參數(shù)編碼(ParameterEncoding) 簡書-----博客園

Alamofire源碼解讀系列(五)之結(jié)果封裝(Result) 簡書-----博客園

Alamofire源碼解讀系列(六)之Task代理(TaskDelegate) 簡書-----博客園

Alamofire源碼解讀系列(七)之網(wǎng)絡(luò)監(jiān)控(NetworkReachabilityManager) 簡書-----博客園

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容