引言
在網(wǎng)絡(luò)請求、通訊過程中白筹,最重要的就是安全了智末,稍有不慎,被別人截取徒河、攻擊吹害,都有可能對自己或者公司帶來不可估量的損失,所以虚青,網(wǎng)絡(luò)安全是尤為重大的它呀。
這篇文章,我們就來講講棒厘,Alamofire
作為一個如此重要的三方庫纵穿,它的安全策略是怎么設(shè)計和使用的。
HTTPS
在說到Alamofire
的安全策略之前奢人,我們先來了解一下HTTPS
谓媒,畢竟Alamofire
也需要通過HTTPS
進行網(wǎng)絡(luò)請求通訊的。
幾種協(xié)議的介紹與關(guān)系
HTTP
:HTTP
協(xié)議傳輸?shù)臄?shù)據(jù)都是未加密的(明文)何乎,因此使用HTTP
協(xié)議傳輸隱私信息非常不安全句惯。HTTPS
:為了保證隱私數(shù)據(jù)能加密傳輸,采用SSL/TLS
協(xié)議用于對HTTP
協(xié)議傳輸?shù)臄?shù)據(jù)進行加密支救,也就是HTTPS
抢野。SSL
:SSL(Secure Sockets Layer)
協(xié)議是由網(wǎng)景公司設(shè)計,后被IETF
定義在RFC 6101
中各墨。TLS
:TLS
可以說是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個類ServerTrustPolicyManager
和ServerTrustPolicy
嘶朱。因此蛾坯,接下來,我們就分別來說一說疏遏。
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)建策略,是否驗證hostSecTrustSetPolicies
:為待驗證的對象設(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