本篇主要講解Alamofire中安全驗證代碼
前言
作為開發(fā)人員,理解HTTPS的原理和應用算是一項基本技能断医。HTTPS目前來說是非常安全的,但仍然有大量的公司還在使用HTTP妄讯。其實HTTPS也并不是很貴啊孩锡。
在網(wǎng)上可以找到大把的介紹HTTTPS的文章,在閱讀ServerTrustPolicy.swfit
代碼前亥贸,我們先簡單的講一下HTTPS請求的過程:
上邊的圖片已經(jīng)標出了步驟躬窜,我們逐步的來分析:
HTTPS請求以
https
開頭,我們首先向服務器發(fā)送一條請求炕置。-
服務器需要一個證書荣挨,這個證書可以從某些機構(gòu)獲得,也可以自己通過工具生成朴摊,通過某些合法機構(gòu)生成的證書客戶端不需要進行驗證默垄,這樣的請求不會觸發(fā)Apple的
@objc(URLSession:task:didReceiveChallenge:completionHandler:)
代理方法,自己生成的證書則需要客戶端進行驗證甚纲。證書中包含公鑰和私鑰:- 公鑰是公開的口锭,任何人都可以使用該公鑰加密數(shù)據(jù),只有知道了私鑰才能解密數(shù)據(jù)
- 私鑰是要求高度保密的介杆,只有知道了私鑰才能解密用公鑰加密的數(shù)據(jù)
- 關(guān)于
非對稱加密
的知識鹃操,大家可以在網(wǎng)上找到
- 服務器會把公鑰發(fā)送給客戶端
- 客戶端此刻就拿到了公鑰。注意春哨,這里不是直接就拿公鑰加密數(shù)據(jù)發(fā)送了荆隘,因為這僅僅能滿足客戶端給服務器發(fā)加密數(shù)據(jù),那么服務器怎么給客戶端發(fā)送加密數(shù)據(jù)呢赴背?因此需要在客戶端和服務器間建立一條通道椰拒,通道的密碼只有客戶端和服務器知道。只能讓客戶端自己生成一個密碼凰荚,這個密碼就是一個隨機數(shù)潦匈,這個隨機數(shù)絕對是安全的势似,因為目前只有客戶端自己知道
- 客戶端把這個隨機數(shù)通過公鑰加密后發(fā)送給服務器覆致,就算被別人截獲了加密后的數(shù)據(jù)卤橄,在沒有私鑰的情況下裕膀,是根本無法解密的
- 服務器用私鑰把數(shù)據(jù)解密后勿锅,就獲得了這個隨機數(shù)
- 到這里客戶端和服務器的安全連接就已經(jīng)建立了,最主要的目的是交換隨機數(shù)灭美,然后服務器就用這個隨機數(shù)把數(shù)據(jù)加密后發(fā)給客戶端,使用的是對稱加密技術(shù)养盗。
- 客戶端獲得了服務器的加密數(shù)據(jù)缚陷,使用隨機數(shù)解密,到此往核,客戶端和服務器就能通過隨機數(shù)發(fā)送數(shù)據(jù)了
HTTPS前邊的幾次握手是需要時間開銷的箫爷,因此,不能每次連接都走一遍聂儒,這就是后邊使用對稱加密數(shù)據(jù)的原因虎锚。Alamofire中主要做的是對服務器的驗證,關(guān)于自定義的安全驗證應該也是模仿了上邊的整個過程衩婚。相對于Apple來說窜护,隱藏了發(fā)送隨機數(shù)這一過程。
對于服務器的驗證除了證書驗證之外一定要加上域名驗證非春,這樣才能更安全柱徙。服務器若要驗證客戶端則會使用簽名技術(shù)。如果偽裝成客戶端來獲取服務器的數(shù)據(jù)最大的問題就是不知道某個請求的參數(shù)是什么奇昙,這樣也就無法獲取數(shù)據(jù)护侮。
ServerTrustPolicyManager
ServerTrustPolicyManager
是對ServerTrustPolicy
的管理,我們可以暫時把ServerTrustPolicy
當做是一個安全策略储耐,就是指對一個服務器采取的策略羊初。然而在真實的開發(fā)中,一個APP可能會用到很多不同的主機地址(host)什湘。因此就產(chǎn)生了這樣的需求长赞,為不同的host綁定一個特定的安全策略。
因此ServerTrustPolicyManager
需要一個字典來存放這些有key禽炬,value對應關(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的收到服務器要求驗證的方法中會出現(xiàn)热幔,這個會在后續(xù)的文章中給出說明乐设。
把ServerTrustPolicyManager綁定到URLSession
ServerTrustPolicyManager作為URLSession的一個屬性,通過運行時的手段來實現(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)
}
}
}
上邊的代碼用到了運行時近尚,尤其是OBJC_ASSOCIATION_RETAIN_NONATOMIC
這個選項,其中包含了強引用和若引用的問題场勤,我想在這里簡單的解釋一下引用問題戈锻。
我們可以這么理解歼跟,不管是類還是對象,或者是對象的屬性格遭,我們都稱之為一個object哈街。我們把這個object比作一個鐵盒子,當有其它的對象對他強引用的時候拒迅,就像給這個鐵盒子綁了一個繩子骚秦,弱引用就像一條虛幻的激光一樣連接這個盒子。當然璧微,在oc中作箍,很多對象默認的情況下就是strong的。
我們可以想象這個盒子是被繩子拉住了前硫,才能漂浮在空中胞得,如果沒有繩子就會掉到無底深淵,然后銷毀屹电。這里最重要的概念就是懒震,只要一個對象沒有了強引用,那么就會立刻銷毀嗤详。
我們舉個例子:
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語言的強大,ServerTrustPolicy被設(shè)計成enum
枚舉材失。既然本質(zhì)上只是個枚舉痕鳍,那么我們先不關(guān)心枚舉中的函數(shù),先單獨看看有哪些枚舉子選項:
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)值而已笼呆。我們先不對這些選項做不解釋熊响,因為在下邊的方法中會根據(jù)這些選項做出不同的操作,到那時在說明這些選項的作用更好诗赌。
還有一點要明白耘眨,在swift中是像下邊代碼這樣初始化枚舉的:
ServerTrustPolicy.performDefaultEvaluation(validateHost: true)
我們用上帝視角來看作者的代碼,接下來就應該看看那些帶有static的函數(shù)了境肾,因為這些函數(shù)都是靜態(tài)函數(shù),可以直接用ServerTrustPolicy調(diào)用胆屿,雖然歸屬于ServerTrustPolicy奥喻,但相對比較獨立。
獲取證書
/// 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ā)中非迹,如果和服務器的安全連接需要對服務器進行驗證环鲤,最好的辦法就是在本地保存一些證書,拿到服務器傳過來的證書憎兽,然后進行對比冷离,如果有匹配的,就表示可以信任該服務器纯命。從上邊的函數(shù)中可以看出西剥,Alamofire會在Bundle(默認為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
}
上邊的過程沒什么好說的枕荞,基本上這是固定寫法柜候,值得注意的是上邊默認是按照X509證書格式來解析的搞动,因此在生成證書的時候最好使用這個格式。否則可能無法獲取到publicKey渣刷。
最核心的方法evaluate
從函數(shù)設(shè)計的角度考慮鹦肿,evaluate應該接受兩個參數(shù),一個是服務器的證書辅柴,一個是host箩溃。返回一個布爾類型。
evaluate函數(shù)是枚舉中的一個函數(shù)碌嘀,因此它必然依賴枚舉的子選項涣旨。這就說明只有初始化枚舉才能使用這個函數(shù)。
舉一個現(xiàn)實生活中的一個小例子股冗。有一個管理員霹陡,他手下管理這3個員工,分別是廚師止状,前臺烹棉,行政,現(xiàn)在有一個任務需要想辦法弄明白這3個人會不會喊麥怯疤,有兩種方法可以得出結(jié)果浆洗,一種是管理員一個一個的去問,也就是得出結(jié)果的方法掌握在管理員手中集峦,只有通過管理員才能知道答案辅髓。有一個老板想知道廚師會不會喊麥。他必須要去問管理員才行少梁。這就造成了邏輯上的問題洛口。另一種方法,讓每一個人當場喊一個凯沪,任何人在任何場合都能得出結(jié)果第焰。
最近重新看了代碼大全這本書,對子程序的設(shè)計有了全新的認識妨马。重點還在于抽象類型是什么挺举?這個就不多說了,有興趣的朋友可以去看看那本書烘跺。
這個函數(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步:
-
SecPolicyCreateSSL
創(chuàng)建策略梧喷,是否驗證host -
SecTrustSetPolicies
為待驗證的對象設(shè)置策略 -
trustIsValid
進行驗證
到了這里就有必要介紹一下幾種策略的用法了:
-
performDefaultEvaluation
默認的策略,只有合法證書才能通過驗證 -
performRevokedEvaluation
對注銷證書做的一種額外設(shè)置,關(guān)于注銷證書驗證超過了本篇文章的范圍铺敌,有興趣的朋友可以查看官方文檔汇歹。 -
pinCertificates
驗證指定的證書,這里邊有一個參數(shù):是否驗證證書鏈偿凭,關(guān)于證書鏈的相關(guān)內(nèi)容可以看這篇文章iOS 中對 HTTPS 證書鏈的驗證.驗證證書鏈算是比較嚴格的驗證了产弹。這里邊設(shè)置錨點等等,這里就不做解釋了弯囊。如果不驗證證書鏈的話痰哨,只要對比指定的證書有沒有和服務器信任的證書匹配項,只要有一個能匹配上匾嘱,就驗證通過 -
pinPublicKeys
這個更上邊的那個差不多斤斧,就不做介紹了 -
disableEvaluation
該選項下,驗證一直都是通過的奄毡,也就是說無條件信任 -
customEvaluation
自定義驗證,需要返回一個布爾類型的結(jié)果
上邊的這些驗證選項中贝或,我們可能根據(jù)自己的需求進行驗證吼过,其中最安全的是證書鏈加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ù)把服務器的SecTrust處理成證書二進制數(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)細節(jié),要想弄明白這些策略的詳情羊赵,還需要做很多的功課才行趟佃。
由于知識水平有限,如有錯誤昧捷,還望指出
鏈接
Alamofire源碼解讀系列(一)之概述和使用 簡書-----博客園
Alamofire源碼解讀系列(二)之錯誤處理(AFError) 簡書-----博客園
Alamofire源碼解讀系列(三)之通知處理(Notification) 簡書-----博客園
Alamofire源碼解讀系列(四)之參數(shù)編碼(ParameterEncoding) 簡書-----博客園
Alamofire源碼解讀系列(五)之結(jié)果封裝(Result) 簡書-----博客園
Alamofire源碼解讀系列(六)之Task代理(TaskDelegate) 簡書-----博客園
Alamofire源碼解讀系列(七)之網(wǎng)絡(luò)監(jiān)控(NetworkReachabilityManager) 簡書-----博客園