HTTPS目前來說是非常安全的矾芙,但仍然有大量的公司還在使用HTTP。其實(shí)HTTPS也并不是很貴啊近上。
在網(wǎng)上可以找到大把的介紹HTTTPS的文章剔宪,在閱讀ServerTrustPolicy.swfit代碼前,我們先簡(jiǎn)單的講一下HTTPS請(qǐng)求的過程:
上邊的圖片已經(jīng)標(biāo)出了步驟,我們逐步的來分析:
1.HTTPS請(qǐng)求以https開頭葱绒,我們首先向服務(wù)器發(fā)送一條請(qǐng)求感帅。
服務(wù)器需要一個(gè)證書,這個(gè)證書可以從某些機(jī)構(gòu)獲得地淀,也可以自己通過工具生成失球,通過某些合法機(jī)構(gòu)生成的證書客戶端不需要進(jìn)行驗(yàn)證,這樣的請(qǐng)求不會(huì)觸發(fā)Apple的@objc(URLSession:task:didReceiveChallenge:completionHandler:)代理方法帮毁,自己生成的證書則需要客戶端進(jìn)行驗(yàn)證实苞。證書中包含公鑰和私鑰:
公鑰是公開的,任何人都可以使用該公鑰加密數(shù)據(jù)烈疚,只有知道了私鑰才能解密數(shù)據(jù)
私鑰是要求高度保密的黔牵,只有知道了私鑰才能解密用公鑰加密的數(shù)據(jù)
關(guān)于非對(duì)稱加密的知識(shí),大家可以在網(wǎng)上找到
3.服務(wù)器會(huì)把公鑰發(fā)送給客戶端
4.客戶端此刻就拿到了公鑰爷肝。注意猾浦,這里不是直接就拿公鑰加密數(shù)據(jù)發(fā)送了,因?yàn)檫@僅僅能滿足客戶端給服務(wù)器發(fā)加密數(shù)據(jù)灯抛,那么服務(wù)器怎么給客戶端發(fā)送加密數(shù)據(jù)呢金赦?因此需要在客戶端和服務(wù)器間建立一條通道,通道的密碼只有客戶端和服務(wù)器知道牧愁。只能讓客戶端自己生成一個(gè)密碼素邪,這個(gè)密碼就是一個(gè)隨機(jī)數(shù),這個(gè)隨機(jī)數(shù)絕對(duì)是安全的猪半,因?yàn)槟壳爸挥锌蛻舳俗约褐?/p>
5.客戶端把這個(gè)隨機(jī)數(shù)通過公鑰加密后發(fā)送給服務(wù)器兔朦,就算被別人截獲了加密后的數(shù)據(jù),在沒有私鑰的情況下磨确,是根本無法解密的
6.服務(wù)器用私鑰把數(shù)據(jù)解密后沽甥,就獲得了這個(gè)隨機(jī)數(shù)
7.到這里客戶端和服務(wù)器的安全連接就已經(jīng)建立了,最主要的目的是交換隨機(jī)數(shù)乏奥,然后服務(wù)器就用這個(gè)隨機(jī)數(shù)把數(shù)據(jù)加密后發(fā)給客戶端摆舟,使用的是對(duì)稱加密技術(shù)。
8.客戶端獲得了服務(wù)器的加密數(shù)據(jù)邓了,使用隨機(jī)數(shù)解密恨诱,到此,客戶端和服務(wù)器就能通過隨機(jī)數(shù)發(fā)送數(shù)據(jù)了
HTTPS前邊的幾次握手是需要時(shí)間開銷的骗炉,因此照宝,不能每次連接都走一遍,這就是后邊使用對(duì)稱加密數(shù)據(jù)的原因句葵。Alamofire中主要做的是對(duì)服務(wù)器的驗(yàn)證厕鹃,關(guān)于自定義的安全驗(yàn)證應(yīng)該也是模仿了上邊的整個(gè)過程兢仰。相對(duì)于Apple來說,隱藏了發(fā)送隨機(jī)數(shù)這一過程剂碴。
對(duì)于服務(wù)器的驗(yàn)證除了證書驗(yàn)證之外一定要加上域名驗(yàn)證把将,這樣才能更安全。服務(wù)器若要驗(yàn)證客戶端則會(huì)使用簽名技術(shù)忆矛。如果偽裝成客戶端來獲取服務(wù)器的數(shù)據(jù)最大的問題就是不知道某個(gè)請(qǐng)求的參數(shù)是什么察蹲,這樣也就無法獲取數(shù)據(jù)。
ServerTrustPolicyManager
ServerTrustPolicyManager是對(duì)ServerTrustPolicy的管理洪碳,我們可以暫時(shí)把ServerTrustPolicy當(dāng)做是一個(gè)安全策略递览,就是指對(duì)一個(gè)服務(wù)器采取的策略。然而在真實(shí)的開發(fā)中瞳腌,一個(gè)APP可能會(huì)用到很多不同的主機(jī)地址(host)。因此就產(chǎn)生了這樣的需求镜雨,為不同的host綁定一個(gè)特定的安全策略嫂侍。
因此ServerTrustPolicyManager需要一個(gè)字典來存放這些有key,value對(duì)應(yīng)關(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è)計(jì)問題挑宠,在后續(xù)的使用中肯定會(huì)有根據(jù)host讀取策略的要求,因此颓影,在上邊的類中設(shè)計(jì)了最后一個(gè)函數(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中這個(gè)ServerTrustPolicyManager會(huì)在SessionDelegate的收到服務(wù)器要求驗(yàn)證的方法中會(huì)出現(xiàn)
把ServerTrustPolicyManager綁定到URLSession
ServerTrustPolicyManager作為URLSession的一個(gè)屬性,通過運(yùn)行時(shí)的手段來實(shí)現(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)
}
}
}
上邊的代碼用到了運(yùn)行時(shí)碎浇,尤其是OBJC_ASSOCIATION_RETAIN_NONATOMIC這個(gè)選項(xiàng),其中包含了強(qiáng)引用和若引用的問題璃俗,我想在這里簡(jiǎn)單的解釋一下引用問題奴璃。
我們可以這么理解,不管是類還是對(duì)象城豁,或者是對(duì)象的屬性苟穆,我們都稱之為一個(gè)object。我們把這個(gè)object比作一個(gè)鐵盒子唱星,當(dāng)有其它的對(duì)象對(duì)他強(qiáng)引用的時(shí)候雳旅,就像給這個(gè)鐵盒子綁了一個(gè)繩子,弱引用就像一條虛幻的激光一樣連接這個(gè)盒子间聊。當(dāng)然攒盈,在oc中,很多對(duì)象默認(rèn)的情況下就是strong的甸饱。
我們可以想象這個(gè)盒子是被繩子拉住了沦童,才能漂浮在空中仑濒,如果沒有繩子就會(huì)掉到無底深淵,然后銷毀偷遗。這里最重要的概念就是墩瞳,只要一個(gè)對(duì)象沒有了強(qiáng)引用,那么就會(huì)立刻銷毀
我們舉個(gè)例子:
MyViewController *myController = [[MyViewController alloc] init…];
上邊的代碼是再平常不過的一段代碼氏豌,創(chuàng)建了一個(gè)MyViewController實(shí)例喉酌,然后使用myController指向了這個(gè)實(shí)例,因此這個(gè)實(shí)例就有了一個(gè)繩子泵喘,他就不會(huì)立刻銷毀泪电,如果我們把代碼改成這樣:
MyViewController * __weak myController = [[MyViewController alloc] init…];
把myController指向?qū)嵗O(shè)置為弱引用,那么即使在下一行代碼打印這個(gè)myController纪铺,也會(huì)是nil相速。因?yàn)閷?shí)例并沒有一個(gè)繩子讓他能不不銷毀。
所謂道理都是相通的鲜锚,只要理解了這個(gè)概念就能明白引用循環(huán)的問題突诬,需要注意的是作用域的問題,如果上邊的myController在一個(gè)函數(shù)中芜繁,那么出了函數(shù)的作用域旺隙,也會(huì)銷毀。
ServerTrustPolicy
接下來將是本篇文章最核心的內(nèi)容骏令,得益于swift語言的強(qiáng)大蔬捷,ServerTrustPolicy被設(shè)計(jì)成enum枚舉。既然本質(zhì)上只是個(gè)枚舉榔袋,那么我們先不關(guān)心枚舉中的函數(shù)周拐,先單獨(dú)看看有哪些枚舉子選項(xiàng)
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)
千萬別認(rèn)為上邊的某些選項(xiàng)是個(gè)函數(shù),其實(shí)他們只是不同的類型加上關(guān)聯(lián)值而已摘昌。我們先不對(duì)這些選項(xiàng)做不解釋速妖,因?yàn)樵谙逻叺姆椒ㄖ袝?huì)根據(jù)這些選項(xiàng)做出不同的操作,到那時(shí)在說明這些選項(xiàng)的作用更好聪黎。
還有一點(diǎn)要明白罕容,在swift中是像下邊代碼這樣初始化枚舉的:
ServerTrustPolicy.performDefaultEvaluation(validateHost: true)
我們用上帝視角來看作者的代碼,接下來就應(yīng)該看看那些帶有static的函數(shù)了稿饰,因?yàn)檫@些函數(shù)都是靜態(tài)函數(shù)锦秒,可以直接用ServerTrustPolicy調(diào)用,雖然歸屬于ServerTrustPolicy喉镰,但相對(duì)比較獨(dú)立旅择。
獲取證書
/// 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ā)中,如果和服務(wù)器的安全連接需要對(duì)服務(wù)器進(jìn)行驗(yàn)證侣姆,最好的辦法就是在本地保存一些證書生真,拿到服務(wù)器傳過來的證書沉噩,然后進(jìn)行對(duì)比,如果有匹配的柱蟀,就表示可以信任該服務(wù)器川蒙。從上邊的函數(shù)中可以看出,Alamofire會(huì)在Bundle(默認(rèn)為main)中查找?guī)в衃".cer", ".CER", ".crt", ".CRT", ".der", ".DER"]后綴的證書长已。
注意畜眨,上邊函數(shù)中的paths保存的是這些證書的路徑,map把這些后綴轉(zhuǎn)換成路徑术瓮,我們以.cer為例康聂。通過map后,原來的".cer"就變成了一個(gè)數(shù)組胞四,也就是說通過map后恬汁,原來的數(shù)組變成了二維數(shù)組了,然后再通過joined()函數(shù)撬讽,把二維數(shù)組轉(zhuǎn)換成一維數(shù)組蕊连。
然后要做的就是根據(jù)這些路徑獲取證書數(shù)據(jù)了,就不多做解釋了游昼。
獲取公鑰
這個(gè)比較好理解,就是在本地證書中取出公鑰尝蠕,至于證書是由什么組成的烘豌,大家可以網(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ù)很簡(jiǎn)單,但是他用到了另外一個(gè)函數(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
}
上邊的過程沒什么好說的廊佩,基本上這是固定寫法,值得注意的是上邊默認(rèn)是按照X509證書格式來解析的靖榕,因此在生成證書的時(shí)候最好使用這個(gè)格式标锄。否則可能無法獲取到publicKey。
最核心的方法evaluate
從函數(shù)設(shè)計(jì)的角度考慮茁计,evaluate應(yīng)該接受兩個(gè)參數(shù)料皇,一個(gè)是服務(wù)器的證書,一個(gè)是host星压。返回一個(gè)布爾類型践剂。
evaluate函數(shù)是枚舉中的一個(gè)函數(shù),因此它必然依賴枚舉的子選項(xiàng)娜膘。這就說明只有初始化枚舉才能使用這個(gè)函數(shù)逊脯。
舉一個(gè)現(xiàn)實(shí)生活中的一個(gè)小例子。有一個(gè)管理員竣贪,他手下管理這3個(gè)員工军洼,分別是廚師巩螃,前臺(tái),行政匕争,現(xiàn)在有一個(gè)任務(wù)需要想辦法弄明白這3個(gè)人會(huì)不會(huì)喊麥避乏,有兩種方法可以得出結(jié)果,一種是管理員一個(gè)一個(gè)的去問汗捡,也就是得出結(jié)果的方法掌握在管理員手中淑际,只有通過管理員才能知道答案。有一個(gè)老板想知道廚師會(huì)不會(huì)喊麥扇住。他必須要去問管理員才行春缕。這就造成了邏輯上的問題。另一種方法艘蹋,讓每一個(gè)人當(dāng)場(chǎng)喊一個(gè)锄贼,任何人在任何場(chǎng)合都能得出結(jié)果。
最近重新看了代碼大全這本書女阀,對(duì)子程序的設(shè)計(jì)有了全新的認(rèn)識(shí)宅荤。重點(diǎn)還在于抽象類型是什么?這個(gè)就不多說了浸策,有興趣的朋友可以去看看那本書冯键。
這個(gè)函數(shù)很長(zhǎng),但總體的思想是根據(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
}
(1)SecPolicyCreateSSL 創(chuàng)建策略,是否驗(yàn)證host
(2)SecTrustSetPolicies 為待驗(yàn)證的對(duì)象設(shè)置策略
(3)trustIsValid 進(jìn)行驗(yàn)證
到了這里就有必要介紹一下幾種策略的用法了:
(1)performDefaultEvaluation 默認(rèn)的策略蚯舱,只有合法證書才能通過驗(yàn)證
(2)performRevokedEvaluation 對(duì)注銷證書做的一種額外設(shè)置改化,關(guān)于注銷證書驗(yàn)證超過了本篇文章的范圍,有興趣的朋友可以查看官方文檔
(3)pinCertificates
驗(yàn)證指定的證書枉昏,這里邊有一個(gè)參數(shù):是否驗(yàn)證證書鏈陈肛,關(guān)于證書鏈的相關(guān)內(nèi)容可以看這篇文章iOS 中對(duì) HTTPS 證書鏈的驗(yàn)證.驗(yàn)證證書鏈算是比較嚴(yán)格的驗(yàn)證了。這里邊設(shè)置錨點(diǎn)等等兄裂,這里就不做解釋了句旱。如果不驗(yàn)證證書鏈的話,只要對(duì)比指定的證書有沒有和服務(wù)器信任的證書匹配項(xiàng)懦窘,只要有一個(gè)能匹配上前翎,就驗(yàn)證通過
(4)pinPublicKeys 這個(gè)更上邊的那個(gè)差不多,就不做介紹了
(5)disableEvaluation 該選項(xiàng)下畅涂,驗(yàn)證一直都是通過的港华,也就是說無條件信任
(6)customEvaluation 自定義驗(yàn)證,需要返回一個(gè)布爾類型的結(jié)果
上邊的這些驗(yàn)證選項(xiàng)中午衰,我們可能根據(jù)自己的需求進(jìn)行驗(yàn)證立宜,其中最安全的是證書鏈加host雙重驗(yàn)證冒萄。而且在上邊的evaluate函數(shù)中用到了4個(gè)輔助函數(shù),我們來看看
func trustIsValid(_ trust: SecTrust) -> Bool
該函數(shù)用于判斷是否驗(yàn)證成功
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ù)把服務(wù)器的SecTrust處理成證書二進(jìn)制數(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
}
其實(shí)在開發(fā)中橙数,可以不必關(guān)心這些實(shí)現(xiàn)細(xì)節(jié)尊流,要想弄明白這些策略的詳情,還需要做很多的功課才行