AFSecurityPolicy 之 evaluateServerTrust

App Transport Security(ATS)

iOS9 引入了新特性 App Transport Security (ATS),要求 App 內訪問的網絡必須使用 HTTPs 協議。

在 WWDC 2016 開發者大會上,蘋果宣佈了一個最後期限:到 2017年1月1日 App Store 中的所有應用都必須啓用 App Transport Security 安全功能

ATS 要求後臺服務器必須支持最新的 TLS_V1.2 協議和 ECDH 加密算法。鑑於 HTTP 全面改造升級 HTTPs 的業務鋪設尚需時日,蘋果延遲了 App 強制接入 ATS 的 deadline。目前允許開發者設置 NSAllowsArbitraryLoads=YES 來暫時關閉 ATS,從而繼續使用 HTTP 連接。

  1. Info.plist 中添加 App Transport Security Settings (或 NSAppTransportSecurity), 類型爲 Dictionary。
  2. 在 App Transport Security Settings 下添加 Allow Arbitrary Loads (或 NSAllowsArbitraryLoads),類型爲 Boolean,值設爲 YES。

NSAllowsArbitraryLoads

How to migrate to HTTPS using App Transport Security when developing iOS apps
Transport security has blocked a cleartext HTTP
ATS 對 HTTP 協議屏蔽引起的問題
iOS9 & iOS10 HTTP 不能正常使用的解決辦法
IOS-關於App Transport Security相關說明及適配
關於iOS9中的App Transport Security相關說明及適配
Xcode7.2與iOS9之坑 iOS 9之適配ATS

使用更安全的 HTTPs 替代 HTTP 是大勢所趨,作爲程序開發者必須瞭解 HTTPs 原理機制和 ATS 適配開發要點。
本篇續接 TLS Handshake Flow(extracts from RFCs)TLS握手協商流程解析,介紹實際訪問 HTTPs 服務器的 TLS(SSL) 握手階段需要客戶端處理的 NSURLAuthenticationChallenge。

NSURLAuthenticationChallenge.NSURLProtectionSpace

iOS 中的 NSURLAuthenticationChallenge 是認證挑戰類,也就是要求客戶端進行挑戰。
而服務器要求的挑戰方式則由 NSURLAuthenticationChallenge 的屬性 NSURLProtectionSpace *protectionSpace 來描述。
那麼 NSURLProtectionSpace(保護空間)裏面有哪些信息呢?可以肯定的是包括挑戰的方式(401授權、客戶端證書、服務端要求信任等),服務器的 URL 地址,端口號,協議等等。

以下爲@interface NSURLProtectionSpace 向外暴露的主要屬性:

@interface NSURLProtectionSpace : NSObject <NSSecureCoding, NSCopying>

@property (nullable, readonly, copy) NSString *realm;

@property (readonly) BOOL receivesCredentialSecurely;

@property (readonly) BOOL isProxy;

@property (readonly, copy) NSString *host;

@property (readonly) NSInteger port;

@property (nullable, readonly, copy) NSString *proxyType;

@property (nullable, readonly, copy) NSString *protocol;

@property (readonly, copy) NSString *authenticationMethod;

@end

對於 SSL(TLS HandShake),authenticationMethod 對應有2種類型:

  • NSURLAuthenticationMethodClientCertificate:SSL Client certificate,表示服務器要求客戶端提供證書(收到 CertificateRequest)。
  • NSURLAuthenticationMethodServerTrust:SecTrustRef validation required,表示需要評估是否信任服務器證書。

對於 NSURLAuthenticationMethodServerTrust,NSURLProtectionSpace 提供了一個 SecTrustRef 屬性。

@interface NSURLProtectionSpace(NSServerTrustValidationSpace)

/*!
    @method serverTrust
    @abstract Returns a SecTrustRef which represents the state of the servers SSL transaction state
    @result A SecTrustRef from Security.framework.  (Nil if the authenticationMethod is not NSURLAuthenticationMethodServerTrust)
 */
@property (nullable, readonly) SecTrustRef serverTrust NS_AVAILABLE(10_6, 3_0);

@end

NSURLCredential

客戶端若要接受挑戰,則需提供挑戰的憑證(用戶和密碼,或者提供客戶端證書,或者信任服務器證書,或者代理)。
iOS 提供了一個 NSURLCredential 的類來表示挑戰憑證。

針對 HTTP 401 錯誤 - 未授權: (Unauthorized),需要通過用戶賬號密碼建立憑證:

// Initialize a NSURLCredential with a user and password
+[NSURLCredential(NSInternetPassword) credentialWithUser:password:persistence:]

針對 TLS HandShake 中服務器下發的 Certificate,macOS/iOS 的 Security.framework 的 SecPolicy.h/SecTrust.h 中定義了證書校驗信任評估的接口。

// Evaluates a trust reference synchronously.
OSStatus SecTrustEvaluate(SecTrustRef trust, SecTrustResultType * __nullable result);

調用 SecTrustEvaluate 對證書校驗通過後,客戶端需要基於對服務器的信任來建立憑證。

// Create a new NSURLCredential which specifies that a handshake has been trusted.
+[NSURLCredential(NSServerTrust) credentialForTrust:];

Apple Developer Guide for Security

Security Starting Point
Secure Coding Guide
Certificate, Key, and Trust Services

Cryptographic Services Guide
Authentication, Authorization, and Permissions Guide
Certificate, Key, and Trust Services Programming Guide

Security.SecureTransport
Using the Secure Socket Layer for Network Communication

HTTPS Server Trust Evaluation
Overriding TLS Chain Validation Correctly
Authentication Challenges and TLS Chain Validation

Sample Code

AdvancedURLConnections

Networking, Internet, & Web | Protocol Streams | Sample Code

This sample demonstrates various advanced networking techniques with NSURLConnection. Specifically, it demonstrates how to respond to authentication challenges, how to modify the default server trust evaluation (for example, to support a server with a self-signed certificate), and how to provide client identities.

CustomHTTPProtocol

Core Services Layer | Foundation | Sample Code

CustomHTTPProtocol shows how to use an NSURLProtocol subclass to intercept the NSURLConnections made by a high-level subsystem that does not otherwise expose its network connections. In this specific case, it intercepts the HTTPS requests made by a web view and overrides server trust evaluation, allowing you to browse a site whose certificate is not trusted by default.

AFSecurityPolicy

當基於 NSURLConnection 或 NSURLSession 訪問 HTTPs 網站時,NSURLConnection 或 NSURLSession 並沒有驗證證書是否合法,無法避免中間人攻擊。
要做到真正的安全通信,需要客戶端校驗確認是否信任服務器證書(鏈)並提供憑證。否則會返回以下錯誤:

NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9843)

AFNetworking 的 AFSecurityPolicy 接口封裝了證書驗證的過程。

收到 NSURLAuthenticationChallenge

在收到 Certificate 後,相關回調 NSURLConnectionDelegate/NSURLSessionDelegate 中調用 AFSecurityPolicy 對 NSURLAuthenticationChallenge 進行處理。

AFNetworking 2.x 的 AFURLConnectionOperation/NSURLConnection
回調 NSURLConnectionDelegate
-[AFURLConnectionOperation connection:willSendRequestForAuthenticationChallenge:]

AFNetworking 3.x 的 AFURLSessionManager 的回調 NSURLSessionDelegate
-[AFURLSessionManager URLSession:didReceiveChallenge:completionHandler:]
NSURLSessionTaskDelegate
-[AFURLSessionManager URLSession:task:didReceiveChallenge:completionHandler:]

接受 NSURLAuthenticationChallenge

willSendRequestForAuthenticationChallengedidReceiveChallenge 中均需要處理如何接受挑戰(NSURLAuthenticationChallenge)。

    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        // 校驗評估是否信任證書(鏈)
        if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
            // 創建 NSURLCredential 對象
            NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
            // NSURLConnectionDelegate/willSendRequestForAuthenticationChallenge:爲 challenge 的發送方提供 credential
            [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
            // NSURLSessionDelegate/didReceiveChallenge:回調 completionHandler
            // completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
        } else {
            // NSURLConnectionDelegate/willSendRequestForAuthenticationChallenge
            [[challenge sender] cancelAuthenticationChallenge:challenge];
            // NSURLSessionDelegate/didReceiveChallenge:回調 completionHandler
            // completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
        }
    }

evaluateServerTrust forDomain

-[AFSecurityPolicy evaluateServerTrust:forDomain:] 主要調用了 SecPolicyCreateSSLSecTrustSetPoliciesSecTrustEvaluate 這3個函數。

-[AFSecurityPolicy evaluateServerTrust:forDomain:]
{
    // 1. 爲服務器 domain 創建 SecPolicyRef
    [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];

    // 2. 爲 SecTrustRef 設置 SecPolicyRef[]
    SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);

    // 3.調用 SecTrustEvaluate 校驗證書(鏈)——SecTrustRef
    if (self.SSLPinningMode == AFSSLPinningModeNone) {
        return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
    }

}

challenge.protectionSpace.serverTrust 爲證書,challenge.protectionSpace.host(或調用 [[connection currentRequest] valueForHTTPHeaderField:@”host”] 獲取)爲服務器域名。
以上展示的是 AFSSLPinningModeNone 模式完全信任服務器(domain)證書的基本處理流程。

SSL Pinning

除了去系統信任 CA 機構列表驗證的 AFSSLPinningModeNone 模式,AFNetworking 還提供了 SSL Pinning 方式的驗證。

SSL Pinning 方式(AFSSLPinningModeCertificateAFSSLPinningModePublicKey)把服務端下發的證書預先打包到 APP 的 bundle 中,然後通過比較服務端下發的證書和本地證書是否相同來校驗證書。
使用該方式的原因是CA機構頒發的證書比較昂貴,一些企業或者個人不申請CA頒發的證書,而是自己手動創建證書。用 SSL Pinning 的方式只要比較證書內容一樣,無需驗證證書的權威性。

  • AFSSLPinningModeNone

    這個模式表示不做 SSL pinning,只跟瀏覽器一樣在系統的信任機構列表裏驗證服務端返回的證書(鏈)。
    若證書是信任機構簽發的就會通過;若是自己服務器生成的證書,這裏是不會通過的。

  • AFSSLPinningModeCertificate

    這個模式表示用證書綁定方式驗證證書,需要客戶端保存服務端的證書拷貝。
    適用於非瀏覽器應用,因爲瀏覽器跟很多未知服務端打交道,無法把每個服務端的證書都保存到本地。但CS架構的APP應用一般事先知道要進行通信的服務端(例如 QQ 文件後臺服務器:*.ftn.qq.com),可以直接在客戶端保存這些固定服務端的證書用於校驗。

    驗證分兩步:第一步驗證證書的域名/有效期等信息;第二步是對比服務端返回的證書跟客戶端返回的是否一致。
    從代碼上看,和去系統信任機構列表裏驗證一樣調用 SecTrustEvaluate,只是這裏的列表換成了客戶端預先保存的證書列表(鏈)。

  • AFSSLPinningModePublicKey

    這個模式同樣是用證書綁定方式驗證,客戶端要有服務端的證書拷貝,只是驗證時只驗證證書裏的公鑰,不驗證證書的有效期等信息。
    只要公鑰是正確的,就能保證通信不會被竊聽,因爲中間人沒有私鑰,無法解開通過公鑰加密的數據。

AFSSLPinningModeCertificate

AFCertificateTrustChainForServerTrust 中調用 SecTrustGetCertificateCount(serverTrust) 取得 SecTrustRef 證書鏈的長度;通過 SecTrustGetCertificateAtIndex 獲取到證書鏈(trustChain)上的每一個證書(SecCertificateRef)。
然後對 trustChain(serverCertificates) 進行驗證,具體參考源碼註釋。

AFSSLPinningModePublicKey

AFPublicKeyTrustChainForServerTrust 中獲取證書鏈(trustChain)上的每一個證書(SecCertificateRef)後, 調用 SecTrustCreateWithCertificates 獲取每個證書(SecCertificateRef)對應的 SecTrustRef,進而調用 SecTrustEvaluate 校驗。
校驗通過後,調用 SecTrustCopyPublicKey 獲取每個證書 SecTrustRef 對應的公鑰(SecKeyRef)。
然後對 trustChain(publicKeys)進行校驗,具體參考源碼註釋。

源代碼

以下摘自最新 AFNetworking 3.x 源碼,稍作註釋:

-[AFSecurityPolicy evaluateServerTrust:forDomain:]
{

    // 4.從 SecTrustRef 獲取 trustChain,進行 SSL Pinning 驗證。

    switch (self.SSLPinningMode) {
        case AFSSLPinningModeNone: // 預期上面已經處理,不應走到這裏
        default:
            return NO;
        case AFSSLPinningModeCertificate: {
            NSMutableArray *pinnedCertificates = [NSMutableArray array];
            for (NSData *certificateData in self.pinnedCertificates) {
                [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
            }

            // The SecTrustEvaluate function looks for an anchor certificate in the array of certificates specified by the SecTrustSetAnchorCertificates function
            SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
            // 指定在本地證書列表(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);
            // 反向遍歷服務器下發的證書鏈(serverCertificates),一般如果本地預存證書包含服務器返回的證書(或證書鏈的根證書),則校驗通過。
            for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
                if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
                    return YES;
                }
            }

            return NO;
        }
        case AFSSLPinningModePublicKey: {
            NSUInteger trustedPublicKeyCount = 0;
            // 獲取服務器下發的證書鏈(serverCertificates)對應的公鑰
            NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);

            // 遍歷對比服務器下發證書公鑰和本地預存證書公鑰(pinnedPublicKeys)
            for (id trustChainPublicKey in publicKeys) {
                for (id pinnedPublicKey in self.pinnedPublicKeys) {
                    if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
                        trustedPublicKeyCount += 1;
                    }
                }
            }

            // AFPublicKeyTrustChainForServerTrust 並沒有嚴格要求證書鏈都通過 SecTrustEvaluate,這裏匹配上一個即算驗證通過。
            return trustedPublicKeyCount > 0;
        }
    }

}

參考

HTTP協議授權訪問
https信任證書的三種方案

AFNetworking 2.3.1 源碼解析之 三. AFSecurityPolicy
AFNetworking 2.6.2 源碼解析之 AFSecurityPolicy
通讀AFN ③–HTTPS訪問控制(AFSecurityPolicy)
AFNetworking 源碼分析之 (五)驗證 HTTPS 請求的證書
AFNetworking 3.0 源碼解讀(二)之 AFSecurityPolicy

iOS https自建證書 請求服務器 和 WKWebView

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章