Alamofire(八)-- 安全策略ServerTrustPolicy

引言

在網絡請求、通信過程當中,最重要的就是安全了,稍有不慎,被別人截取、攻擊,都有可能對本身或者公司帶來不可估量的損失,因此,網絡安全是尤其重大的。 這篇文章,咱們就來說講,Alamofire做爲一個如此重要的三方庫,它的安全策略是怎麼設計和使用的。算法

HTTPS

在說到Alamofire的安全策略以前,咱們先來了解一下HTTPS,畢竟Alamofire也須要經過HTTPS進行網絡請求通信的。swift

幾種協議的介紹與關係

  • 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是一個枚舉類型:app

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

相關文章
相關標籤/搜索