在網絡請求、通信過程當中,最重要的就是安全了,稍有不慎,被別人截取、攻擊,都有可能對本身或者公司帶來不可估量的損失,因此,網絡安全是尤其重大的。 這篇文章,咱們就來說講,
Alamofire
做爲一個如此重要的三方庫,它的安全策略是怎麼設計和使用的。算法
在說到Alamofire
的安全策略以前,咱們先來了解一下HTTPS
,畢竟Alamofire
也須要經過HTTPS
進行網絡請求通信的。swift
HTTP
:HTTP
協議傳輸的數據都是未加密的(明文),所以使用HTTP
協議傳輸隱私信息很是不安全。HTTPS
:爲了保證隱私數據能加密傳輸,採用SSL/TLS
協議用於對HTTP
協議傳輸的數據進行加密,也就是HTTPS
。SSL
:SSL(Secure Sockets Layer)
協議是由網景公司設計,後被IETF
定義在RFC 6101
中。TLS
:TLS
能夠說是SSL
的改進版,實際上咱們如今的HTTPS
都是用的TLS
協議。
HTTPS
在傳輸數據以前須要客戶端(瀏覽器)與服務端(網站)之間進行一次握手,在握手過程當中將確立雙方加密傳輸數據的密碼信息。TLS/SSL
中使用了非對稱加密,對稱加密以及HASH
算法。其中非對稱加密算法用於在握手過程當中加密生成的密碼,對稱加密算法用於對真正傳輸的數據進行加密,而HASH
算法用於驗證數據的完整性。TLS
握手過程當中若是有任何錯誤,都會使加密鏈接斷開,從而阻止了隱私信息的傳輸。
咱們先來看一下這張圖(圖片來自網絡): 數組
看着這張圖,接下來咱們來簡單分析一下:瀏覽器
- 客戶端的
HTTPS
請求首先向服務器發送一條請求,注意,HTTPS
請求均是以https
開頭。- 這時候,服務器端就須要一個證書,這個證書既能夠是本身經過某些工具生成,也能夠是從某些機構獲取。若是是經過某些合法機構生成的證書,是不須要進行驗證的,同時,這些請求不會觸發
@objc(URLSession:task:didReceiveChallenge:completionHandler:)
代理方法。若是是本身生成的證書,須要在客戶端進行驗證,且證書中應該包含公鑰、私鑰。(公鑰:公開的,任何人均可以使用該公鑰加密數據,只有知道了私鑰才能解密數據。私鑰:要求高度保密的,只有知道了私鑰才能解密用公鑰加密的數據。)- 服務器端把公鑰發送給客戶端
- 此時,客戶端拿到公鑰,這裏要注意,拿到公鑰後,並不會直接用於加密數據發送,僅僅是客戶端給服務器端發送加密數據,還須要服務器端給客戶端發送加密數據,所以,咱們須要在客戶端與服務器端創建一個安全的通信通道,開啓這條通道的密碼只有客戶端和服務器端知道。而後,客戶端會本身生成一個隨機數密碼,由於這個隨機數密碼目前只有客戶端知道,因此,這個隨機數密碼是絕對安全的。
- 再來,客戶端用這個隨機數密碼再經過公鑰加密後發送給服務器端,若是被中間人攻擊截獲了,沒有私鑰的狀況下,他也是沒法解密的。
- 服務器端收到客戶端發送的加密數據後,使用私鑰把數據解密後,就獲取到了這個隨機數。
- 此時此刻,客戶端與服務器端的安全通道就已經鏈接好了,主要目的就是交換隨機數,便於服務器使用這個隨機數把數據加密後發送到客戶端,此間,使用的是對稱加密技術(備註:關於對稱加密、非對稱加密的詳細知識網上或者書籍有不少,內容太多,這裏就不詳細解釋了,也解釋不完的😅)。
- 最後,客戶端拿到了服務器端的加密數據後,再使用隨機數解密,這樣,客戶端與服務器端就能經過隨機數加密發送數據,進行安全的通信了。
HTTPS
每次握手其實都是須要時間開銷的,因此,不能每次鏈接都這樣走一次,所以,咱們須要使用對稱加密數據的方式。 在Alamofire
中,主要的工做是對服務器的驗證,其自定義的安全策略驗證,我猜,也是模仿的上邊的這個過程。 另外,在對服務器的驗證下,還應該加上域名驗證,這樣才能更加的安全安全
OK,前戲都已經說完了,接下來,進入主題。服務器
在查看
ServerTrustPolicy.swift
文件的時候,咱們發現,最核心的2個類ServerTrustPolicyManager
和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
方式。
先看一下函數方法:
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
}
複製代碼
很簡單的,沒有什麼好說的,都是固定的寫法。
先看一下函數方法:
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
證書格式的詳細說明看這裏百度百科。
咱們先把函數看一下:
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
:建立策略,是否驗證hostSecTrustSetPolicies
:爲待驗證的對象設置策略trustIsValid
:進行驗證
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] {
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] {
return certificates.map { SecCertificateCopyData($0) as Data }
}
複製代碼
該函數把服務器的
SecCertificate
處理成證書二進制數組。
在下邊的驗證選項中,咱們能夠根據本身的需求進行驗證,最安全的是證書鏈加host
雙重驗證:
performDefaultEvaluation
:默認的策略,只有合法證書才能經過驗證。performRevokedEvaluation
:對註銷證書作的一種額外設置pinCertificates
:驗證指定的證書,這裏邊有一個參數:是否驗證證書鏈,關於證書鏈的相關內容能夠去查一查其餘更爲詳細的資料,驗證證書鏈算是比較嚴格的驗證了。若是不驗證證書鏈的話,只要對比指定的證書有沒有和服務器信任的證書匹配項,只要有一個能匹配上,就驗證經過pinPublicKeys
:這個和上邊的那個差很少disableEvaluation
:該選項下,驗證一直都是經過的,也就是說無條件信任customEvaluation
:自定義驗證,須要返回一個布爾類型的結果
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
使用了一個字典屬性,用來存放有key
、value
對應關係的數據。- 因爲須要根據
host
來讀取策略,所以,該類增長了serverTrustPolicy
方法。
先看一下擴展代碼:
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