在 iOS 中進行網絡通訊時,爲了安全,可能會產生認證質詢(Authentication Challenge),例如: HTTP Basic Authentication
、 HTTPS Server Trust Authentication
。本文介紹的是使用 URLSession
發送網絡請求時,應該如何處理這些認證質詢,最後會對 iOS 最著名的兩個網絡框架 -- AFNetworking
和 Alamofire
中處理認證質詢部分的代碼進行閱讀、分析。本文中使用的開發語言是 Swift。html
當發送一個 URLSessionTask
請求時,服務器可能會發出一個或者多個認證質詢。URLSessionTask
會嘗試處理,若是不能處理,則會調用 URLSessionDelegate
的方法來處理。git
URLSessionDelegate
處理認證質詢的方法:github
// 這個方法用於處理 session 範圍內的質詢,如:HTTPS Server Trust Authentication。一旦成功地處理了此類質詢,從該 URLSession 建立的全部任務保持有效。
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
}
// 這個方法用於處理任務特定的質詢,例如:基於用戶名/密碼的質詢。每一個任務均可能發出本身的質詢。當須要處理 session 範圍內的質詢時,但上面那個方法又沒有實現的話,也會調用此方法來代替
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
}
複製代碼
若是須要 URLSessionDelegate
的方法來處理認證質詢,但是又沒有實現相應的方法,則服務器可能會拒絕該請求,而且返回一個值爲 401(禁止)的 HTTP 狀態代碼。swift
URLSessionDelegate
處理認證質詢的方法都會接受一個 challenge: URLAuthenticationChallenge
參數,它提供了在處理認證質詢時所需的信息。api
class URLAuthenticationChallenge: NSObject {
// 須要認證的區域
var protectionSpace: URLProtectionSpace
// 表示最後一次認證失敗的 URLResponse 實例
var failureResponse: URLResponse?
// 以前認證失敗的次數
var previousFailureCount: Int
// 建議的憑據,有多是質詢提供的默認憑據,也有多是上次認證失敗時使用的憑據
var proposedCredential: URLCredential?
// 上次認證失敗的 Error 實例
var error: Error?
// 質詢的發送者
var sender: URLAuthenticationChallengeSender?
}
複製代碼
其中,它的核心是 protectionSpace: URLProtectionSpace
屬性。數組
須要認證的區域,定義了認證質詢的一系列信息,這些信息肯定了應開發者應該如何響應質詢,提供怎樣的 URLCredential
,例如: host
、端口、質詢的類型等。安全
class URLProtectionSpace : NSObject {
// 質詢的類型
var authenticationMethod: String
// 進行客戶端證書認證時,可接受的證書頒發機構
var distinguishedNames: [Data]?
var host: String
var port: Int
var `protocol`: String? var proxyType: String? var realm: String? var receivesCredentialSecurely: Bool // 表示服務器的SSL事務狀態 var serverTrust: SecTrust? } 複製代碼
其中,它的 authenticationMethod
屬性代表了正在發出的質詢的類型(例如: HTTP Basic Authentication
、 HTTPS Server Trust Authentication
)。使用此值來肯定是否能夠處理該質詢和怎麼處理質詢。服務器
authenticationMethod
屬性的值爲如下常量之一,這些就是認證質詢的類型。網絡
/* session 範圍內的認證質詢 */
// 客戶端證書認證
let NSURLAuthenticationMethodClientCertificate: String
// 協商使用 Kerberos 仍是 NTLM 認證
let NSURLAuthenticationMethodNegotiate: String
// NTLM 認證
let NSURLAuthenticationMethodNTLM: String
// 服務器信任認證(證書驗證)
let NSURLAuthenticationMethodServerTrust: String
/* 任務特定的認證質詢 */
// 使用某種協議的默認認證方法
let NSURLAuthenticationMethodDefault: String
// HTML Form 認證,使用 URLSession 發送請求時不會發出此類型認證質詢
let NSURLAuthenticationMethodHTMLForm: String
// HTTP Basic 認證
let NSURLAuthenticationMethodHTTPBasic: String
// HTTP Digest 認證
let NSURLAuthenticationMethodHTTPDigest: String
複製代碼
URLSessionDelegate
處理認證質詢的方法都接受一個 completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
閉包參數,最終須要調用此閉包來響應質詢,不然 URLSessionTask
請求會一直處於等待狀態。session
這個閉包接受兩個參數,它們的類型分別爲 URLSession.AuthChallengeDisposition
、 URLCredential?
,須要根據 challenge.protectionSpace.authenticationMethod
的值,肯定如何響應質詢,而且提供對應的 URLCredential
實例。
它是一個枚舉類型,表示有如下幾種方式來響應質詢:
public enum AuthChallengeDisposition : Int {
// 使用指定的憑據(credential)
case useCredential
// 默認的質詢處理,若是有提供憑據也會被忽略,若是沒有實現 URLSessionDelegate 處理質詢的方法則會使用這種方式
case performDefaultHandling
// 取消認證質詢,若是有提供憑據也會被忽略,會取消當前的 URLSessionTask 請求
case cancelAuthenticationChallenge
// 拒絕質詢,而且進行下一個認證質詢,若是有提供憑據也會被忽略;大多數狀況不會使用這種方式,沒法爲某個質詢提供憑據,則一般應返回 performDefaultHandling
case rejectProtectionSpace
}
複製代碼
要成功響應質詢,還須要提供對應的憑據。有三種初始化方式,分別用於不一樣類型的質詢類型。
// 使用給定的持久性設置、用戶名和密碼建立 URLCredential 實例。
public init(user: String, password: String, persistence: URLCredential.Persistence) {
}
// 用於客戶端證書認證質詢,當 challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate 時使用
// identity: 私鑰和和證書的組合
// certArray: 大多數狀況下傳 nil
// persistence: 該參數會被忽略,傳 .forSession 會比較合適
public init(identity: SecIdentity, certificates certArray: [Any]?, persistence: URLCredential.Persistence) {
}
// 用於服務器信任認證質詢,當 challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust 時使用
// 從 challenge.protectionSpace.serverTrust 中獲取 SecTrust 實例
// 使用該方法初始化 URLCredential 實例以前,須要對 SecTrust 實例進行評估
public init(trust: SecTrust) {
}
複製代碼
用於代表 URLCredential
實例的持久化方式,只有基於用戶名和密碼建立的 URLCredential
實例纔會被持久化到 keychain
裏面
public enum Persistence : UInt {
case none
case forSession
// 會存儲在 iOS 的 keychain 裏面
case permanent
// 會存儲在 iOS 的 keychain 裏面,而且會經過 iCloud 同步到其餘 iOS 設備
@available(iOS 6.0, *)
case synchronizable
}
複製代碼
用於管理 URLCredential
的持久化。
HTTP Basic
、 HTTP Digest
、 NTLM
都是基於用戶名/密碼的認證,處理這種認證質詢的方式以下:
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
switch challenge.protectionSpace.authenticationMethod {
case NSURLAuthenticationMethodHTTPBasic, NSURLAuthenticationMethodHTTPDigest, NSURLAuthenticationMethodNTLM:
let user = "user"
let password = "password"
let credential = URLCredential(user: user, password: password, persistence: .forSession)
completionHandler(.useCredential, credential)
default:
completionHandler(.performDefaultHandling, nil)
}
}
複製代碼
當發送一個 HTTPS 請求時, URLSessionDelegate
將收到一個類型爲 NSURLAuthenticationMethodServerTrust
的認證質詢。其餘類型的認證質詢都是服務器對 App 進行認證,而這種類型則是 App 對服務器進行認證。
大多數狀況下,對於這種類型的認證質詢能夠不實現 URLSessionDelegate
處理認證質詢的方法, URLSessionTask
會使用默認的處理方式( performDefaultHandling
)進行處理。可是若是是如下的狀況,則須要手動進行處理:
SSL Pinning
來防止中間人攻擊。對服務器的信任認證作法大體以下:
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
// 判斷認證質詢的類型,判斷是否存在服務器信任實例 serverTrust
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let serverTrust = challenge.protectionSpace.serverTrust else {
// 不然使用默認處理
completionHandler(.performDefaultHandling, nil)
return
}
// 自定義方法,對服務器信任實例 serverTrust 進行評估
if evaluate(trust, forHost: challenge.protectionSpace.host) {
// 評估經過則建立 URLCredential 實例,告訴系統接受服務器的憑據
let credential = URLCredential(trust: serverTrust)
completionHandler(.useCredential, credential)
} else {
// 不然取消此次認證,告訴系統拒絕服務器的憑據
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
複製代碼
對服務器的信任認證中最核心的步驟是對服務器信任實例 serverTrust
進行評估,也就是以上代碼中的 evaluate(trust, forHost: challenge.protectionSpace.host)
。
這須要涉及到蘋果的 Security
框架,它是一個比較底層的框架,用於保護 App 管理的數據,並控制對 App 的訪問,通常開發不多會接觸到。這裏面的類都不能跟普通的類同樣直接進行操做,例如:沒有 getter
、 setter
方法,而是使用相似 C 語言風格的函數進行操做,這些函數的名字都是以對應的類名開頭,例如:對 SecTrust
實例進行評估的函數 SecTrustEvaluateWithError(_:_:)
。
對服務器信任實例 serverTrust
進行評估須要用到的是 Certificate, Key, and Trust Services 部分
class SecTrust 複製代碼
用於評估信任的類,主要包含了:
注意:能夠從 SecTrust
實例中獲取證書和公鑰,但前提是已經對它進行了評估而且評估經過。評估經過後,SecTrust
實例中會存在一條正確的證書鏈。
class SecPolicy 複製代碼
評估策略,Security
框架提供瞭如下策略:
// 返回默認 X.509 策略的策略對象,只驗證證書是否符合 X.509 標準
func SecPolicyCreateBasicX509() -> SecPolicy
// 返回用於評估 SSL 證書鏈的策略對象
// 第一個參數:server,若是傳 true,則表明是在客戶端上驗證 SSL 服務器證書
// 第二個參數:hostname,若是傳非 nil,則表明會驗證 hostname
func SecPolicyCreateSSL(Bool, CFString?) -> SecPolicy
// 返回用於檢查證書吊銷的策略對象,一般不須要本身建立吊銷策略,除非但願重寫默認系統行爲,例如強制使用特定方法或徹底禁用吊銷檢查。
func SecPolicyCreateRevocation(CFOptionFlags) -> SecPolicy?
複製代碼
class SecCertificate 複製代碼
X.509 標準證書類
私鑰和證書的組合
還須要瞭解一些跟評估相關的證書概念
數字證書是由數字證書認證機構(Certificate authority,即 CA)來負責簽發和管理。首先,CA 組織結構中,最頂層的就是根 CA,根 CA 下能夠受權給多個二級 CA,而二級 CA 又能夠受權多個三級 CA,因此 CA 的組織結構是一個樹結構。根 CA 頒發的自簽名證書就是根證書,通常操做系統中都嵌入了一些默認受信任的根證書。
因爲證書是從根 CA 開始不斷向下級受權簽發的,因此證書鏈就是由某個證書和它各個上級證書組成的鏈條。一條完整的證書鏈是由最底層的證書開始,最終以根 CA 證書結束。但在這裏的證書鏈指的是從某個須要評估的證書開始,最終以錨點證書結束,例如:須要評估的證書 - 中間證書 - 中間證書 - 錨點證書。
錨點證書一般是操做系統中嵌入的固有受信任的根證書之一。但在這裏指的是評估 SecTrust
實例時,用於評估的證書鏈裏面最頂層的證書,它多是根證書,也多是證書鏈中的某一個。評估時會在 SecTrustSetAnchorCertificates(_:_:)
函數指定的證書數組中查找錨點證書,或者使用系統提供的默認集合。
從 challenge.protectionSpace.serverTrust
獲得 SecTrust
實例,經過如下函數來評估它是否有效
// iOS 12 如下的系統使用這個函數
func SecTrustEvaluate(_ trust: SecTrust, _ result: UnsafeMutablePointer<SecTrustResultType>) -> OSStatus
// iOS 12 及以上的系統推薦使用這個函數
func SecTrustEvaluateWithError(_ trust: SecTrust, _ error: UnsafeMutablePointer<CFError?>?) -> Bool
複製代碼
評估的步驟以下:
SecTrust
實例中存在一個須要評估的證書,評估函數會根據評估策略建立對應的證書鏈,而後從須要評估的證書開始,直到錨點證書,依次驗證證書鏈中各個證書的簽名,若是中途某個證書不經過驗證,或者某個證書已經設置了非默認信任設置(信任或者不信任),則會提早結束,返回一個成功或者失敗的結果。注意:
keychain
(或 iOS 中的應用程序 keychain
)中搜索中間證書,還有可能經過網絡來下載中間證書。SecTrustSetAnchorCertificates(_:_:)
函數指定的證書數組中查找錨點證書,或者使用系統提供的默認集合。SecTrust
實例中,例如調用如下函數
SecTrustCreateWithCertificates(_:_:_:)
SecTrustSetPolicies(_:_:)
評估結果
if #available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, *) {
var error: CFError?
let evaluationSucceeded = SecTrustEvaluateWithError(serverTrust, &error)
if evaluationSucceeded {
// 評估經過
} else {
// 評估不經過,error 包含了錯誤信息
}
} else {
var result = SecTrustResultType.invalid
let status = SecTrustEvaluate(serverTrust, &result)
if status == errSecSuccess && (result == .unspecified || result == .proceed) {
// 評估經過
} else {
// 評估不經過,result 和 status 包含了錯誤信息
}
}
複製代碼
使用某些抓包軟件能夠對網絡請求進行抓包,就算是 HTTPS
的請求均可以抓包成功。可是同時也會發現某些 App 發送的網絡請求會抓包失敗。由於這些 App 內使用了一項叫 SSL Pinning
的技術。抓包軟件對網絡請求進行抓包主要是利用「中間人攻擊」的技術,而 SSL Pinning
技術則是能夠防止「中間人攻擊」。
SSL Pinning
具體有兩種作法:
瞭解怎麼手動進行服務器信任評估後,就能夠輕鬆實現 SSL Pinning
關於 SSL Pinning
的選擇,蘋果的文檔上有提出
Create a Long-Term Server Authentication Strategy
If you determine that you need to evaluate server trust manually in some or all cases, plan for what your app will do if you need to change your server credentials. Keep the following guidelines in mind:
- Compare the server’s credentials against a public key, instead of storing a single certificate in your app bundle. This will allow you to reissue a certificate for the same key and update the server, rather than needing to update the app.
- Compare the issuing certificate authority’s (CA’s) keys, rather than using the leaf key. This way, you can deploy certificates containing new keys signed by the same CA.
- Use a set of keys or CAs, so you can rotate server credentials more gracefully.
簡單來講就是推薦使用 Public Key Pinning
,並且是把多個比較高級別的 CA 公鑰集成在 App 裏面,這樣服務器就能夠在部署的證書的時候有更多的選擇,更加的靈活。
以上已經詳細地介紹了 iOS Authentication Challenge 的處理,接下來結合 iOS 最著名的兩個網絡框架 AFNetworking
和 Alamofire
,瞭解實際場景中的應用,分析代碼實現的細節。其中因爲 Alamofire
是使用更先進的開發語言 -- Swift 實現的,處理會更加詳細和先進,因此是分析的重點。
Alamofire
中的認證質詢處理會更加具體詳細、完善,同時使用了新的 API ,如下是 Alamofire
5.0.0-rc.3 版本中的代碼。
主要涉及 SessionDelegate.swift
、ServerTrustManager.swift
兩個文件
typealias ChallengeEvaluation = (disposition: URLSession.AuthChallengeDisposition, credential: URLCredential?, error: AFError?)
// URLSessionDelegate 的方法
// 若是沒有實現用於處理 session 範圍內的認證質詢的方法,會調用這個方法做爲代替
// 因此爲了不重複,實際上只須要實現這個方法就能夠處理全部狀況
open func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
eventMonitor?.urlSession(session, task: task, didReceive: challenge)
let evaluation: ChallengeEvaluation
// 判斷認證質詢,主要分爲兩種狀況
switch challenge.protectionSpace.authenticationMethod {
case NSURLAuthenticationMethodServerTrust:
// 服務器信任認證質詢,也就是 HTTPS 證書認證
evaluation = attemptServerTrustAuthentication(with: challenge)
case NSURLAuthenticationMethodHTTPBasic, NSURLAuthenticationMethodHTTPDigest, NSURLAuthenticationMethodNTLM,
NSURLAuthenticationMethodNegotiate, NSURLAuthenticationMethodClientCertificate:
// 其餘類型認證質詢
evaluation = attemptCredentialAuthentication(for: challenge, belongingTo: task)
default:
evaluation = (.performDefaultHandling, nil, nil)
}
// 若是存在錯誤,則經過回調告訴外界
if let error = evaluation.error {
stateProvider?.request(for: task)?.didFailTask(task, earlyWithError: error)
}
// 響應質詢
completionHandler(evaluation.disposition, evaluation.credential)
}
複製代碼
// 處理服務器信任認證質詢的方法
func attemptServerTrustAuthentication(with challenge: URLAuthenticationChallenge) -> ChallengeEvaluation {
let host = challenge.protectionSpace.host
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let trust = challenge.protectionSpace.serverTrust
else {
return (.performDefaultHandling, nil, nil)
}
do {
// evaluator 是一個用於評估 serverTrust 的實例,它遵循了 ServerTrustEvaluating 協議
guard let evaluator = try stateProvider?.serverTrustManager?.serverTrustEvaluator(forHost: host) else {
return (.performDefaultHandling, nil, nil)
}
// 最終是調用 evaluator 的 valuate(_:forHost:) 方法來進行評估
try evaluator.evaluate(trust, forHost: host)
// 若是沒有拋出錯誤,則建立 URLCredential 實例,告訴系統接受服務器的憑據
return (.useCredential, URLCredential(trust: trust), nil)
} catch {
// 不然取消此次認證質詢,同時返回一個 error
return (.cancelAuthenticationChallenge, nil, error.asAFError(or: .serverTrustEvaluationFailed(reason: .customEvaluationFailed(error: error))))
}
}
複製代碼
Alamofire
內部提供了幾個遵循了 ServerTrustEvaluating
協議的類,用於不一樣的評估方式,方便開發者使用,分別是:
SecPolicyCreateSSL(_:_:)
策略進行評估,能夠選擇是否驗證host
DefaultTrustEvaluator
基礎上,增長用檢查證書撤銷的策略進行評估DefaultTrustEvaluator
基礎上,增長 Certificate Pinning
檢查DefaultTrustEvaluator
基礎上,增長 Public Key Pinning
檢查能夠看出這幾種處理方式都是在默認的基礎上,增長一些額外的評估,如下只分析PinnedCertificatesTrustEvaluator
的作法,對其餘類的作法有興趣的讀者能夠自行閱讀源碼
// PinnedCertificatesTrustEvaluator 中的評估方法
public func evaluate(_ trust: SecTrust, forHost host: String) throws {
guard !certificates.isEmpty else {
throw AFError.serverTrustEvaluationFailed(reason: .noCertificatesFound)
}
// 由於進行了 Certificate Pinning,因此首先須要設置錨點證書
if acceptSelfSignedCertificates {
try trust.af.setAnchorCertificates(certificates)
}
// 進行默認的評估
if performDefaultValidation {
try trust.af.performDefaultValidation(forHost: host)
}
// 驗證 host
if validateHost {
try trust.af.performValidation(forHost: host)
}
// 若是代碼能運行到這裏,表明評估經過;在手動設置了錨點證書後再使用評估函數而且經過了評估,此時從 SecTrust 實例取出的是一條包含了須要的評估證書直到錨點證書的證書鏈,將它們轉爲 Data 集合
let serverCertificatesData = Set(trust.af.certificateData)
// 將集成在 App 裏的證書轉爲 Data 集合
let pinnedCertificatesData = Set(certificates.af.data)
// 判斷兩個集合是否有交集,這是爲了進行一步增強安全性
let pinnedCertificatesInServerData = !serverCertificatesData.isDisjoint(with: pinnedCertificatesData)
if !pinnedCertificatesInServerData {
// 不然拋出錯誤
throw AFError.serverTrustEvaluationFailed(reason: .certificatePinningFailed(host: host, trust: trust, pinnedCertificates: certificates, serverCertificates: trust.af.certificates))
}
}
複製代碼
若是 Alamofire
內置的幾種評估方式不能知足開發者,則能夠自定義遵照 ServerTrustEvaluating
協議的類,自行處理。
// 處理其餘類型認證質詢的方法
func attemptCredentialAuthentication(for challenge: URLAuthenticationChallenge, belongingTo task: URLSessionTask) -> ChallengeEvaluation {
// 以前有過失敗,則返回
guard challenge.previousFailureCount == 0 else {
return (.rejectProtectionSpace, nil, nil)
}
// 這裏是直接取出外界事先準備好的 URLCredential 實例
guard let credential = stateProvider?.credential(for: task, in: challenge.protectionSpace) else {
// 若是沒有,則使用系統默認的處理
return (.performDefaultHandling, nil, nil)
}
return (.useCredential, credential, nil)
}
複製代碼
Alamofire
對其餘類型的認證質詢的處理比較簡單,由於這些類型的處理不肯定性比較大,因此 Alamofire
直接把認證質詢轉移給外界的調用者進行處理
AFNetworking
中的認證質詢處理相對於 Alamofire
會沒有那麼全面,代碼也簡單不少,可是足以處理大多數的狀況。如下是 AFNetworking
3.2.1 版本中的代碼。
主要涉及 AFURLSessionManager.m
、 AFSecurityPolicy.m
兩個文件。
它裏面實現了 URLSessionDelegate
兩個用於處理認證質詢的方法 ,兩個方法裏面的代碼幾乎是同樣的,因此下面只選擇其中一個來分析
// URLSessionDelegate 的方法
// 若是沒有實現用於處理 session 範圍內的認證質詢的方法,會調用這個方法做爲代替
// 因此爲了不重複,實際上只須要實現這個方法就能夠處理全部狀況
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__block NSURLCredential *credential = nil;
// taskDidReceiveAuthenticationChallenge 是一個外界傳入的 block,這裏的意思是若是外界有傳入,則把認證質詢轉移給外界的調用者進行處理
if (self.taskDidReceiveAuthenticationChallenge) {
disposition = self.taskDidReceiveAuthenticationChallenge(session, task, challenge, &credential);
} else {
// 不然判斷質詢類型
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
// 對服務器信任認證質詢進行處理,也就是 HTTPS 證書認證
if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
// 經過
disposition = NSURLSessionAuthChallengeUseCredential;
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
} else {
// 不然取消認證質詢
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
} else {
// 若是是其餘類型的認證質詢,則使用系統默認的處理
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
}
// 響應質詢
if (completionHandler) {
completionHandler(disposition, credential);
}
}
複製代碼
經過 AFSecurityPolicy
類裏面的如下方法處理服務器信任認證質詢
// 處理服務器信任認證質詢的方法
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
forDomain:(NSString *)domain
{
// 這裏的意思是若是須要驗證 DomainName,又容許自簽名的證書,但是又沒有使用 SSL Pinning 或者沒有提供證書,這樣就會矛盾,因此直接返回 NO
if (domain && self.allowInvalidCertificates && self.validatesDomainName && (self.SSLPinningMode == AFSSLPinningModeNone || [self.pinnedCertificates count] == 0)) {
// https://developer.apple.com/library/mac/documentation/NetworkingInternet/Conceptual/NetworkingTopics/Articles/OverridingSSLChainValidationCorrectly.html
// According to the docs, you should only trust your provided certs for evaluation.
// Pinned certificates are added to the trust. Without pinned certificates,
// there is nothing to evaluate against.
//
// From Apple Docs:
// "Do not implicitly trust self-signed certificates as anchors (kSecTrustOptionImplicitAnchors).
// Instead, add your own (self-signed) CA certificate to the list of trusted anchors."
NSLog(@"In order to validate a domain name for self signed certificates, you MUST use pinning.");
return NO;
}
NSMutableArray *policies = [NSMutableArray array];
// 根據是否須要驗證 DomainName(host) 來設置評估策略
if (self.validatesDomainName) {
[policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
} else {
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
}
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
if (self.SSLPinningMode == AFSSLPinningModeNone) {
// 在沒有使用 SSL Pinning 的狀況下,若是 allowInvalidCertificates 爲 YES,表示不對證書進行評估,能夠直接經過,不然須要對證書進行評估
return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
} else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
// 在使用 SSL Pinning 的狀況下,若是證書評估不經過,並且不容許自簽名證書,則直接返回 NO
return NO;
}
switch (self.SSLPinningMode) {
case AFSSLPinningModeCertificate: {
NSMutableArray *pinnedCertificates = [NSMutableArray array];
for (NSData *certificateData in self.pinnedCertificates) {
[pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
}
// 由於進行了 Certificate Pinning,因此首先須要設置錨點證書
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
// 進行評估
if (!AFServerTrustIsValid(serverTrust)) {
return NO;
}
// obtain the chain after being validated, which *should* contain the pinned certificate in the last position (if it's the Root CA)
NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
// 判斷兩個數組是否有交集,這是爲了進行一步增強安全性
for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
return YES;
}
}
return NO;
}
case AFSSLPinningModePublicKey: {
NSUInteger trustedPublicKeyCount = 0;
// 對 serverTrust 裏面證書鏈的證書逐個進行評估,而且返回評估經過的證書對應的公鑰數組
NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);
for (id trustChainPublicKey in publicKeys) {
for (id pinnedPublicKey in self.pinnedPublicKeys) {
// 判斷兩個數組是否有交集,這是爲了進行一步增強安全性
if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
trustedPublicKeyCount += 1;
}
}
}
return trustedPublicKeyCount > 0;
}
default:
return NO;
}
return NO;
}
複製代碼
Public Key Pinning
的評估方法
static NSArray * AFPublicKeyTrustChainForServerTrust(SecTrustRef serverTrust) {
// 使用 X.509 策略
SecPolicyRef policy = SecPolicyCreateBasicX509();
CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);
NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount];
for (CFIndex i = 0; i < certificateCount; i++) {
// 從 serverTrust 取出證書
SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i);
SecCertificateRef someCertificates[] = {certificate};
CFArrayRef certificates = CFArrayCreate(NULL, (const void **)someCertificates, 1, NULL);
SecTrustRef trust;
// 建立新的 SecTrustRef 實例
__Require_noErr_Quiet(SecTrustCreateWithCertificates(certificates, policy, &trust), _out);
SecTrustResultType result;
// 對新的 SecTrustRef 實例進行評估
__Require_noErr_Quiet(SecTrustEvaluate(trust, &result), _out);
// 若是評估經過,則取出公鑰,加入到 trustChain 數組,最後返回
[trustChain addObject:(__bridge_transfer id)SecTrustCopyPublicKey(trust)];
_out:
if (trust) {
CFRelease(trust);
}
if (certificates) {
CFRelease(certificates);
}
continue;
}
CFRelease(policy);
return [NSArray arrayWithArray:trustChain];
}
複製代碼
AFNetworking
和 Alamofire
對認證質詢的處理跟本文前面部分所介紹的內容基本一致。從它們的源碼來看,我我的以爲雖然 AFNetworking
的代碼比較簡單,可是邏輯有點混亂,而且因爲存在 allowInvalidCertificates
的判斷,因此邏輯就更復雜了,而 Alamofire
的代碼更加細緻,處理的方式更全面,邏輯也很清晰。形成這樣的結果多是由於 AFNetworking
過久沒有更新,而 Alamofire
卻一直在更新。因此若是有須要的話,我更推薦參考 Alamofire
的代碼。
認證質詢(Authentication Challenge)是進行安全的網絡通訊中重要的一環。在開發中,咱們通常會使用 AFNetworking
或者 Alamofire
搭建 App 的網絡層,大多數狀況下直接使用它們自帶的功能對認證質詢進行處理已經足夠。但若是存在特殊狀況,仍是須要咱們對這方面進行深刻的瞭解,進行自定義的處理。本文儘可能全面地對認證質詢相關的知識進行介紹,但因爲涉及到蘋果的 Security
框架,相關 API 的使用說明比較分散,也存在比較多的細節,因此我沒有對它們所有進行介紹,有須要的讀者能夠仔細閱讀蘋果官方文檔,參考 Alamofire
的代碼進行使用。
Handling an Authentication Challenge