Alamofire(八)-- 安全策略ServerTrustPolicy

引言

在網(wǎng)絡(luò)請求、通訊過程中白筹,最重要的就是安全了智末,稍有不慎,被別人截取徒河、攻擊吹害,都有可能對自己或者公司帶來不可估量的損失,所以虚青,網(wǎng)絡(luò)安全是尤為重大的它呀。
這篇文章,我們就來講講棒厘,Alamofire作為一個如此重要的三方庫纵穿,它的安全策略是怎么設(shè)計和使用的。

HTTPS

在說到Alamofire的安全策略之前奢人,我們先來了解一下HTTPS谓媒,畢竟Alamofire也需要通過HTTPS進行網(wǎng)絡(luò)請求通訊的。

幾種協(xié)議的介紹與關(guān)系

  • HTTPHTTP協(xié)議傳輸?shù)臄?shù)據(jù)都是未加密的(明文)何乎,因此使用HTTP協(xié)議傳輸隱私信息非常不安全句惯。
  • HTTPS:為了保證隱私數(shù)據(jù)能加密傳輸,采用SSL/TLS協(xié)議用于對HTTP協(xié)議傳輸?shù)臄?shù)據(jù)進行加密支救,也就是HTTPS抢野。
  • SSLSSL(Secure Sockets Layer)協(xié)議是由網(wǎng)景公司設(shè)計,后被IETF定義在RFC 6101中各墨。
  • TLSTLS可以說是SSL的改進版指孤,實際上我們現(xiàn)在的HTTPS都是用的TLS協(xié)議。

特點

  • HTTPS在傳輸數(shù)據(jù)之前需要客戶端(瀏覽器)與服務(wù)端(網(wǎng)站)之間進行一次握手,在握手過程中將確立雙方加密傳輸數(shù)據(jù)的密碼信息恃轩。
  • TLS/SSL中使用了非對稱加密结洼,對稱加密以及HASH算法。其中非對稱加密算法用于在握手過程中加密生成的密碼叉跛,對稱加密算法用于對真正傳輸?shù)臄?shù)據(jù)進行加密松忍,而HASH算法用于驗證數(shù)據(jù)的完整性。
  • TLS握手過程中如果有任何錯誤筷厘,都會使加密連接斷開鸣峭,從而阻止了隱私信息的傳輸。

請求過程

我們先來看一下這張圖(圖片來自網(wǎng)絡(luò)):


看著這張圖敞掘,接下來我們來簡單分析一下:

  • 客戶端的HTTPS請求首先向服務(wù)器發(fā)送一條請求,注意楣铁,HTTPS請求均是以https開頭玖雁。
  • 這時候,服務(wù)器端就需要一個證書盖腕,這個證書既可以是自己通過某些工具生成赫冬,也可以是從某些機構(gòu)獲取。如果是通過某些合法機構(gòu)生成的證書溃列,是不需要進行驗證的劲厌,同時,這些請求不會觸發(fā)@objc(URLSession:task:didReceiveChallenge:completionHandler:)代理方法听隐。如果是自己生成的證書补鼻,需要在客戶端進行驗證,且證書中應該包含公鑰雅任、私鑰风范。(公鑰:公開的,任何人都可以使用該公鑰加密數(shù)據(jù)沪么,只有知道了私鑰才能解密數(shù)據(jù)硼婿。私鑰:要求高度保密的,只有知道了私鑰才能解密用公鑰加密的數(shù)據(jù)禽车。
  • 服務(wù)器端把公鑰發(fā)送給客戶端
  • 此時寇漫,客戶端拿到公鑰,這里要注意殉摔,拿到公鑰后州胳,并不會直接用于加密數(shù)據(jù)發(fā)送,僅僅是客戶端給服務(wù)器端發(fā)送加密數(shù)據(jù)逸月,還需要服務(wù)器端給客戶端發(fā)送加密數(shù)據(jù)陋葡,因此,我們需要在客戶端與服務(wù)器端建立一個安全的通訊通道彻采,開啟這條通道的密碼只有客戶端和服務(wù)器端知道腐缤。然后捌归,客戶端會自己生成一個隨機數(shù)密碼,因為這個隨機數(shù)密碼目前只有客戶端知道岭粤,所以惜索,這個隨機數(shù)密碼是絕對安全的。
  • 再來剃浇,客戶端用這個隨機數(shù)密碼再通過公鑰加密后發(fā)送給服務(wù)器端巾兆,如果被中間人攻擊截獲了,沒有私鑰的情況下虎囚,他也是無法解密的角塑。
  • 服務(wù)器端收到客戶端發(fā)送的加密數(shù)據(jù)后,使用私鑰把數(shù)據(jù)解密后淘讥,就獲取到了這個隨機數(shù)圃伶。
  • 此時此刻,客戶端與服務(wù)器端的安全通道就已經(jīng)連接好了蒲列,主要目的就是交換隨機數(shù)窒朋,便于服務(wù)器使用這個隨機數(shù)把數(shù)據(jù)加密后發(fā)送到客戶端,此間蝗岖,使用的是對稱加密技術(shù)(備注:關(guān)于對稱加密侥猩、非對稱加密的詳細知識網(wǎng)上或者書籍有很多,內(nèi)容太多抵赢,這里就不詳細解釋了欺劳,也解釋不完的??)。
  • 最后铅鲤,客戶端拿到了服務(wù)器端的加密數(shù)據(jù)后杰标,再使用隨機數(shù)解密,這樣彩匕,客戶端與服務(wù)器端就能通過隨機數(shù)加密發(fā)送數(shù)據(jù)腔剂,進行安全的通訊了。

總結(jié)

HTTPS每次握手其實都是需要時間開銷的驼仪,所以掸犬,不能每次連接都這樣走一次,因此绪爸,我們需要使用對稱加密數(shù)據(jù)的方式湾碎。
Alamofire中,主要的工作是對服務(wù)器的驗證奠货,其自定義的安全策略驗證介褥,我猜,也是模仿的上邊的這個過程。
另外柔滔,在對服務(wù)器的驗證下溢陪,還應該加上域名驗證,這樣才能更加的安全

OK睛廊,前戲都已經(jīng)說完了形真,接下來,進入主題超全。

ServerTrustPolicy

在查看ServerTrustPolicy.swift文件的時候咆霜,我們發(fā)現(xiàn),最核心的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)
}

注意: 這些選項并不是函數(shù),只是不同的類型加上了關(guān)聯(lián)值而已改览。

函數(shù)說明

獲取證書

首先下翎,看下獲取證書的函數(shù)方法:

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
    }
  • 如果在和服務(wù)器的安全連接中缤言,需要對服務(wù)器進行驗證宝当,一個好的方法就是在本地工程保存一些證書,得到服務(wù)器傳過來的證書后進行對比胆萧,如果有匹配庆揩,則表示可以信任該服務(wù)器。其中包括帶有這些后綴的證書:".cer", ".CER", ".crt", ".CRT", ".der", ".DER"跌穗。
  • 函數(shù)中订晌,paths保存的是這些證書的路徑,再通過map函數(shù)轉(zhuǎn)換為路徑蚌吸,最后锈拨,根據(jù)這些路徑獲取證書數(shù)據(jù)。

獲取公鑰

獲取公鑰的函數(shù)方法:

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
    }

在本地證書中取出公鑰羹唠,其中又調(diào)用了另外一個函數(shù)方法publicKey(for: certificate)奕枢,注意到,獲取SecKey可以通過SecCertificate方式佩微,也可以通過SecTrust方式缝彬。

通過SecTrust獲取SecKey

先看一下函數(shù)方法:

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

先看一下函數(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
    }
  • 一樣的,固定寫法,只是要特別注意一下SecPolicyCreateBasicX509()一疯,默認是按照X509證書格式來解析的撼玄,所以,在生成證書的時候违施,最好用這個格式來互纯,不然有可能無法獲得publicKey
  • 有關(guān)X509證書格式的詳細說明看這里百度百科磕蒲。

核心方法evaluate

我們先把函數(shù)看一下:

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
    }
  • 這個函數(shù)很長留潦,一看switch語句,就知道辣往,它的總體思想就是需要根據(jù)不同的策略做出不同操作兔院。
  • evaluate函數(shù)需要接收2個參數(shù),一個是服務(wù)器的證書站削,還有一個是host坊萝,返回值是一個bool類型。
  • 因為evaluate函數(shù)被定義在枚舉中许起,因此十偶,它肯定是依賴枚舉的子選項,只有初始化枚舉后园细,才能調(diào)用這個函數(shù)惦积。
驗證步驟說明

從上面的函數(shù)可以看到,不論我們使用哪一種策略猛频,要完成驗證狮崩,都需要以下步驟:

  • SecPolicyCreateSSL:創(chuàng)建策略,是否驗證host
  • SecTrustSetPolicies:為待驗證的對象設(shè)置策略
  • trustIsValid:進行驗證
輔助函數(shù)
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
    }

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

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

該函數(shù)把服務(wù)器的SecTrust處理成證書二進制數(shù)組睦柴。

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

該函數(shù)把服務(wù)器的SecCertificate處理成證書二進制數(shù)組。

策略用法

在下邊的驗證選項中毡熏,我們可以根據(jù)自己的需求進行驗證坦敌,最安全的是證書鏈加host雙重驗證:

  • performDefaultEvaluation:默認的策略,只有合法證書才能通過驗證痢法。
  • performRevokedEvaluation:對注銷證書做的一種額外設(shè)置
  • pinCertificates:驗證指定的證書狱窘,這里邊有一個參數(shù):是否驗證證書鏈,關(guān)于證書鏈的相關(guān)內(nèi)容可以去查一查其他更為詳細的資料疯暑,驗證證書鏈算是比較嚴格的驗證了训柴。如果不驗證證書鏈的話,只要對比指定的證書有沒有和服務(wù)器信任的證書匹配項妇拯,只要有一個能匹配上幻馁,就驗證通過
  • pinPublicKeys:這個和上邊的那個差不多
  • disableEvaluation:該選項下洗鸵,驗證一直都是通過的,也就是說無條件信任
  • customEvaluation:自定義驗證仗嗦,需要返回一個布爾類型的結(jié)果

ServerTrustPolicyManager

簡述

ServerTrustPolicyManager這個類是對ServerTrustPolicy的管理類膘滨,因為在實際項目開發(fā)中,項目中可能會使用不同的主機地址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使用了一個字典屬性德撬,用來存放有key铲咨、value對應關(guān)系的數(shù)據(jù)。
  • 由于需要根據(jù)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的一個屬性摇天,是通過運行時的手段來實現(xiàn)。

總結(jié)

這篇文章恐仑,也只是簡單的解析了一下Alamofire中泉坐,它的安全策略設(shè)計方法,當然裳仆,在實際項目開發(fā)中腕让,大可以不必要關(guān)心這些實現(xiàn)細節(jié),但是作為一個敬業(yè)的鉴逞、喜歡iOS開發(fā)的開發(fā)者來說记某,還是很有必要知曉其中的設(shè)計方法司训、使用方法构捡,很多細節(jié)的東西,還需要做很多的功課才行壳猜。


常規(guī)打廣告系列:
簡書:Alamofire(八)-- 安全策略ServerTrustPolicy
掘金:Alamofire(八)-- 安全策略ServerTrustPolicy
小專欄:Alamofire(八)-- 安全策略ServerTrustPolicy

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末勾徽,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子统扳,更是在濱河造成了極大的恐慌喘帚,老刑警劉巖该抒,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件烫幕,死亡現(xiàn)場離奇詭異,居然都是意外死亡才顿,警方通過查閱死者的電腦和手機朱嘴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門倾鲫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來粗合,“玉大人,你說我怎么就攤上這事乌昔∠毒危” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵磕道,是天一觀的道長供屉。 經(jīng)常有香客問我,道長溺蕉,這世上最難降的妖魔是什么伶丐? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮疯特,結(jié)果婚禮上撵割,老公的妹妹穿的比我還像新娘。我一直安慰自己辙芍,他們只是感情好啡彬,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著故硅,像睡著了一般庶灿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上吃衅,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天往踢,我揣著相機與錄音,去河邊找鬼徘层。 笑死峻呕,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的趣效。 我是一名探鬼主播瘦癌,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼跷敬!你這毒婦竟也來了讯私?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤西傀,失蹤者是張志新(化名)和其女友劉穎斤寇,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拥褂,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡娘锁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了饺鹃。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片莫秆。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡碎税,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出馏锡,到底是詐尸還是另有隱情雷蹂,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布杯道,位于F島的核電站匪煌,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏党巾。R本人自食惡果不足惜萎庭,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望齿拂。 院中可真熱鬧驳规,春花似錦、人聲如沸署海。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽砸狞。三九已至捻勉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間刀森,已是汗流浹背踱启。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留研底,地道東北人埠偿。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像榜晦,于是被迫代替她去往敵國和親冠蒋。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容