iOS Authentication Challenge

概述

在 iOS 中進行網絡通訊時,爲了安全,可能會產生認證質詢(Authentication Challenge),例如: HTTP Basic AuthenticationHTTPS Server Trust Authentication 。本文介紹的是使用 URLSession 發送網絡請求時,應該如何處理這些認證質詢,最後會對 iOS 最著名的兩個網絡框架 -- AFNetworkingAlamofire 中處理認證質詢部分的代碼進行閱讀、分析。本文中使用的開發語言是 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

肯定質詢類型

URLAuthenticationChallenge

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 屬性。數組

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 AuthenticationHTTPS Server Trust Authentication )。使用此值來肯定是否能夠處理該質詢和怎麼處理質詢。服務器

NSURLAuthenticationMethod 常量

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.AuthChallengeDispositionURLCredential? ,須要根據 challenge.protectionSpace.authenticationMethod 的值,肯定如何響應質詢,而且提供對應的 URLCredential 實例。

URLSession.AuthChallengeDisposition

它是一個枚舉類型,表示有如下幾種方式來響應質詢:

public enum AuthChallengeDisposition : Int {

    // 使用指定的憑據(credential)
    case useCredential 

    // 默認的質詢處理,若是有提供憑據也會被忽略,若是沒有實現 URLSessionDelegate 處理質詢的方法則會使用這種方式
    case performDefaultHandling 
	
    // 取消認證質詢,若是有提供憑據也會被忽略,會取消當前的 URLSessionTask 請求
    case cancelAuthenticationChallenge 

    // 拒絕質詢,而且進行下一個認證質詢,若是有提供憑據也會被忽略;大多數狀況不會使用這種方式,沒法爲某個質詢提供憑據,則一般應返回 performDefaultHandling
    case rejectProtectionSpace
}
複製代碼

URLCredential

要成功響應質詢,還須要提供對應的憑據。有三種初始化方式,分別用於不一樣類型的質詢類型。

// 使用給定的持久性設置、用戶名和密碼建立 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.Persistence

用於代表 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
}
複製代碼

URLCredentialStorage

用於管理 URLCredential 的持久化。

基於用戶名/密碼的認證

HTTP BasicHTTP DigestNTLM 都是基於用戶名/密碼的認證,處理這種認證質詢的方式以下:

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 Server Trust Authentication

當發送一個 HTTPS 請求時, URLSessionDelegate 將收到一個類型爲 NSURLAuthenticationMethodServerTrust 的認證質詢。其餘類型的認證質詢都是服務器對 App 進行認證,而這種類型則是 App 對服務器進行認證。

大多數狀況下,對於這種類型的認證質詢能夠不實現 URLSessionDelegate 處理認證質詢的方法, URLSessionTask 會使用默認的處理方式( performDefaultHandling )進行處理。可是若是是如下的狀況,則須要手動進行處理:

  • 與使用自簽名證書的服務器進行 HTTPS 鏈接。
  • 進行更嚴格的服務器信任評估來增強安全性,如:經過使用 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)
    }
}
複製代碼

Trust Services

對服務器的信任認證中最核心的步驟是對服務器信任實例 serverTrust 進行評估,也就是以上代碼中的 evaluate(trust, forHost: challenge.protectionSpace.host)

這須要涉及到蘋果的 Security 框架,它是一個比較底層的框架,用於保護 App 管理的數據,並控制對 App 的訪問,通常開發不多會接觸到。這裏面的類都不能跟普通的類同樣直接進行操做,例如:沒有 gettersetter 方法,而是使用相似 C 語言風格的函數進行操做,這些函數的名字都是以對應的類名開頭,例如:對 SecTrust 實例進行評估的函數 SecTrustEvaluateWithError(_:_:)

對服務器信任實例 serverTrust 進行評估須要用到的是 Certificate, Key, and Trust Services 部分

SecTrust

class SecTrust 複製代碼

用於評估信任的類,主要包含了:

  • 一個須要評估的證書,可能還有它所在的證書鏈中的中間證書和根證書(錨點證書)
  • 一個或者多個評估策略

注意:能夠從 SecTrust 實例中獲取證書和公鑰,但前提是已經對它進行了評估而且評估經過。評估經過後,SecTrust 實例中會存在一條正確的證書鏈。

SecPolicy

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?
複製代碼

SecCertificate

class SecCertificate 複製代碼

X.509 標準證書類

SecIdentity

私鑰和證書的組合

證書概念

還須要瞭解一些跟評估相關的證書概念

根證書

數字證書是由數字證書認證機構(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 包含了錯誤信息
    }
}
複製代碼

SSL Pinning

使用某些抓包軟件能夠對網絡請求進行抓包,就算是 HTTPS 的請求均可以抓包成功。可是同時也會發現某些 App 發送的網絡請求會抓包失敗。由於這些 App 內使用了一項叫 SSL Pinning 的技術。抓包軟件對網絡請求進行抓包主要是利用「中間人攻擊」的技術,而 SSL Pinning 技術則是能夠防止「中間人攻擊」。

SSL Pinning 具體有兩種作法:

  • Certificate Pinning:證書固定。將指定的證書集成在 App 裏面,在進行服務器信任評估前,使用該證書做爲錨點證書,再進行服務器信任評估,這樣就能夠限制了只有在指定的證書鏈上的證書才能經過評估,並且還能夠限制只能是某些域名的證書。缺點是集成在 App 裏面的證書會過時,若是證書過時,只能經過強制更新 App 才能保證正常進行網絡訪問。
  • Public Key Pinning:公鑰固定。將指定的公鑰集成在 App 裏面,在進行服務器信任評估後,還會提取服務器返回的證書內的公鑰,而後跟指定的公鑰進行匹配。優勢是公鑰不像證書同樣會過時。缺點是操做公鑰會相對麻煩,並且違反了密鑰輪換策略。

瞭解怎麼手動進行服務器信任評估後,就能夠輕鬆實現 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 最著名的兩個網絡框架 AFNetworkingAlamofire ,瞭解實際場景中的應用,分析代碼實現的細節。其中因爲 Alamofire 是使用更先進的開發語言 -- Swift 實現的,處理會更加詳細和先進,因此是分析的重點。

Alamofire

Alamofire 中的認證質詢處理會更加具體詳細、完善,同時使用了新的 API ,如下是 Alamofire 5.0.0-rc.3 版本中的代碼。

主要涉及 SessionDelegate.swiftServerTrustManager.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 協議的類,用於不一樣的評估方式,方便開發者使用,分別是:

  • DefaultTrustEvaluator:默認的評估方式,使用SecPolicyCreateSSL(_:_:)策略進行評估,能夠選擇是否驗證host
  • RevocationTrustEvaluator:在 DefaultTrustEvaluator 基礎上,增長用檢查證書撤銷的策略進行評估
  • PinnedCertificatesTrustEvaluator:在 DefaultTrustEvaluator 基礎上,增長 Certificate Pinning 檢查
  • PublicKeysTrustEvaluator:在 DefaultTrustEvaluator 基礎上,增長 Public Key Pinning 檢查
  • CompositeTrustEvaluator:組合評估,使用多種處理方式進行評估
  • DisabledEvaluator:只用於開發調試的類,使用它進行評估永遠不會經過

能夠看出這幾種處理方式都是在默認的基礎上,增長一些額外的評估,如下只分析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

AFNetworking 中的認證質詢處理相對於 Alamofire 會沒有那麼全面,代碼也簡單不少,可是足以處理大多數的狀況。如下是 AFNetworking 3.2.1 版本中的代碼。

主要涉及 AFURLSessionManager.mAFSecurityPolicy.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];
}
複製代碼

對比

AFNetworkingAlamofire 對認證質詢的處理跟本文前面部分所介紹的內容基本一致。從它們的源碼來看,我我的以爲雖然 AFNetworking 的代碼比較簡單,可是邏輯有點混亂,而且因爲存在 allowInvalidCertificates 的判斷,因此邏輯就更復雜了,而 Alamofire 的代碼更加細緻,處理的方式更全面,邏輯也很清晰。形成這樣的結果多是由於 AFNetworking 過久沒有更新,而 Alamofire 卻一直在更新。因此若是有須要的話,我更推薦參考 Alamofire 的代碼。

結語

認證質詢(Authentication Challenge)是進行安全的網絡通訊中重要的一環。在開發中,咱們通常會使用 AFNetworking 或者 Alamofire 搭建 App 的網絡層,大多數狀況下直接使用它們自帶的功能對認證質詢進行處理已經足夠。但若是存在特殊狀況,仍是須要咱們對這方面進行深刻的瞭解,進行自定義的處理。本文儘可能全面地對認證質詢相關的知識進行介紹,但因爲涉及到蘋果的 Security 框架,相關 API 的使用說明比較分散,也存在比較多的細節,因此我沒有對它們所有進行介紹,有須要的讀者能夠仔細閱讀蘋果官方文檔,參考 Alamofire 的代碼進行使用。

參考

Handling an Authentication Challenge

Certificate, Key, and Trust Services

HTTPS Server Trust Evaluation

相關文章
相關標籤/搜索