AFNetworking 3.0 源碼閱讀-HTTPS認證-AFSecurityPolicy

本想在這篇文章中單獨寫AFNetworking 3.0中AFSecurityPolicy的源碼閱讀筆記的。但隨著源碼閱讀的過程,發現關于有太多相關背景知識需要惡補..所以一邊學習一邊總結寫了這篇文章。如果有寫錯的地方,請及時指正。

1.HTTPS

HTTPS 是運行在 TLS/SSL 之上的 HTTP,是為了解決HTTP通信不安全的問題而設計的。

  • 對稱加密、非對稱加密
    對稱加密使用同一個密鑰進行加密解密,速度快。
    非對稱加密使用公鑰加密,私鑰解密,計算量大速度慢。非對稱加密又稱公鑰密碼技術

使用時兩者折中。在SSL/TLS中,用“對稱加密”來加解密通信信息,速度快;使用“非對稱加密”來加解密“對稱密鑰”。

  • SSL/TLS四次握手


    SSL/TLS協議運行機制的概述-阮一峰

    1.客戶端發出請求

  • ClientHello 向服務器傳遞隨機數1、協議版本、客戶端支持的加密套件(Cipher Suites)、壓縮方法、簽名算法等信息。

2.服務器回應

  • ** SeverHello** 服務器收到客戶端請求后,向客戶端發出回應。傳遞內容:確認使用的協議版本、從收到的客戶端加密套件中篩選出來的加密方法、壓縮算法和簽名算法,和服務器新生成的一個隨機數2等等返回給客戶端。
  • severCertificate 服務器發送數字證書(其實就是要拿到公鑰)。
  • CertificateRequest 如果服務器需要確認客戶端的身份(雙向認證),就會再包含一項請求,要求客戶端提供"客戶端證書"。比如,金融機構往往只允許認證客戶連入自己的網絡,就會向正式客戶提供USB密鑰,里面就包含了一張客戶端證書。

3.客戶端回應

  • Client Key Exchange 客戶端確認證書有效,則會生產最后一個隨機數3(pre-master key),并使用證書的公鑰加密這個隨機數,發回給服務端。(為了更高的安全性,會使用Diffie-Hellman算法;采用DH算法,最后一個隨機數是不需要傳遞的,客戶端和服務端交換參數之后就可以算出)
  • Change Cipher Spec 通知對方,編碼改變,接下來的所有消息都會使用雙方商定的加密方法和密鑰發送。
  • Finished 客戶端握手結束通知,表示客戶端的握手階段已經結束。該報文包含前面發送的所有報文的整體校驗值(hash),用來供服務器校驗。

4.服務器回應

  • Change Cipher Spec 服務端同樣發送Change Cipher Spec報文。
  • Finished 服務端同樣發送Finished報文

至此,整個握手階段全部結束。接下來,客戶端與服務器進入加密通信,就完全是使用普通的HTTP協議,只不過用"會話密鑰"加密內容
至于這個會話密鑰呢,就是通信兩端同時擁有的三個隨機數,用雙方事先商定的加密方法,各自生成之后通信所用的對稱密鑰。

  • 簡單總結一下,這四次握手過程主要交換了:
    1.證書,一般由服務器發給客戶端;驗證證書是不是可信機構頒發的,如果是自簽證書,一般在客戶端本地置入證書拷貝,然后兩份證書對比來判斷證書是否可信。
    如果是雙向認證的,客戶端也要給服務器發送證書。關于單向、雙向認證

2.三個隨機數,用來生成后續通信進行加解密的對稱密鑰。其中前兩個隨機數都是明文傳輸,只有第三個隨機數是加密的(公鑰足夠長,2048位,可保證不被破解)。
為什么是三個隨機數?SSL協議不相信每個主機都能產生完全隨機的隨機數,如果只有一個偽隨機數就容易被破解,如果3個偽隨機數就接近隨機了。

3.加密方式

  • 其他,session恢復
    由于新建立一個SSL/TLS Session的成本太高,所以之前有建立SSL/TLS連接Session的話,客戶端會保存Session ID。如果對話中斷,在下一次請求時在Client Hello中帶上session ID,服務端驗證有效之后,就會成功重用Sesssion。雙方就不再進行握手階段剩余的步驟,而直接用已有的對話密鑰進行加密通信

2.數字證書

這里先簡單講一些數字簽名它能確認消息的完整性,進行認證。公鑰密碼一樣也要用到一對公鑰、私鑰。但簽名是用私鑰加密(生成簽名),公鑰解密(驗證簽名)。私鑰加密只能由持有私鑰的人完成,而由于公鑰是對外公開的,因此任何人都可以用公鑰進行解密(驗證簽名)。

要確認公鑰是否合法,就需要使用證書。

公鑰證書一般會記有姓名、組織、郵箱地址等個人信息,以及屬于本人的公鑰,并由認證機構(CA)進行數字簽名。通過認證機構使用證書的過程如下圖所示:


《圖解密碼技術》

公鑰基礎設施(PKI)是為了能夠更有效地運用公鑰而制定的一系列規范和規格的總稱。使用最廣泛的 X.509 規范也是PKI的一種。

  • 證書鏈
    CA有層級的關系,處于最頂層的認證機構一般就稱為根CA。下層證書是經過上層簽名的。而根CA則會對自己的證書進行簽名,即自簽名

怎么驗證證書有沒有被篡改?
當客戶端走 HTTPS 訪問站點時,服務器會返回整個證書鏈。先從最底層的CA開始,用上層的公鑰對下層證書的數字簽名進行驗證。這樣逐層向上驗證,直到遇到了錨點證書。

  • 錨點證書
    嵌入到操作系統中的根證書(系統隱式信任的證書),通常是包括在系統中的 CA 根證書。不過你也可以在驗證證書鏈時,設置自定義的證書作為可信的錨點。

3.SSL Pinning

HTTPS挺安全的但也不是無懈可擊。本人在網絡安全方面也不是專業的,這里就簡單說一點。中間人攻擊。簡單來說,iPhone信任的證書包括一些預裝的證書和用戶自己安裝的證書,如果攻擊者手上擁有一個受信任的證書,那么就會發生中間人攻擊了。
這時候就需要SSL Pinning 了,原理是把server證書的拷貝捆綁在APP中,client通過對比server發來的證書檢測它有沒有被篡改。結合這篇文章中講的SSL中間人攻擊、模擬攻擊實例,對上面所講的有更好的理解。

4.證書校驗

4.1域名驗證

服務器證書上的域名和請求域名是否匹配。
使用NSURLSession獲取默認驗證策略:

- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler {
    SecTrustRef trust = challenge.protectionSpace.serverTrust;
    CFArrayRef defaultPolicies = NULL;//獲取默認的校驗策略
    SecTrustCopyPolicies(trust, &defaultPolicies);
    NSLog(@"Default Trust Policies: %@", (__bridge id)defaultPolicies);
}

默認的驗證策略是包含域名驗證的。如果想重置驗證策略,可以調用SecTrustSetPolicies。比如AFNetworking中就是這樣做的:

NSMutableArray *policies = [NSMutableArray array];
//BasicX509 就是不驗證域名,返回的服務器證書,只要是可信任CA機構簽發的,都會校驗通過
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];    
//設置評估中要使用的策略
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);

4.2校驗證書鏈

證書鏈的驗證,主要由三部分來保證證書的可信:葉子證書是對應HTTPS請求域名的證書,根證書是被系統信任的證書,以及這個證書鏈之間都是層層簽發可信任鏈;證書之所以能成立,本質是基于信任鏈,這樣任何一個節點證書加上域名校驗(CA機構不會為不同的對不同的用戶簽發相同域名的證書),就確定一條唯一可信證書鏈。

基于證書信任鏈進行校驗。如果該信任鏈只包含有效的證書并以已知的錨證書結束,那么證書被認為是有效的(當返回的服務器證書是錨點證書或者是基于該證書簽發的證書(可以是多個層級)都會被信任)。這里的錨證書也可以是自定義的證書,使用SecTrustSetAnchorCertificates函數設置錨點證書。比如AFNetworking中:

NSMutableArray *pinnedCertificates = [NSMutableArray array];
//把nsdata證書(der編碼的x.509證書)轉成SecCertificateRef類型的數據
for (NSData *certificateData in self.pinnedCertificates) {
  [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
}
//將本地證書設置成需要參與驗證的錨點證書
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
//驗證服務器證書是否可信(由系統默認可信或者由用戶選擇可信)。
if (!AFServerTrustIsValid(serverTrust)) {
  return NO;
}

ps:只使用SecTrustSetAnchorCertificates函數,沒使用SecTrustSetAnchorCertificatesOnly,就只會相信SecTrustSetAnchorCertificates由該錨點證書頒發的證書,哪怕是由系統其他錨點證書頒發的其他證書也不會通過驗證。
如果要想恢復系統中 CA 證書作為錨點的功能:

// true 代表僅被傳入的證書作為錨點,false 允許系統 CA 證書也作為錨點
SecTrustSetAnchorCertificatesOnly(trust, false);

4.3SSL Pinning把證書打包進app

如果用戶訪問不安全鏈接并且選擇信任了不該信任的證書,證書校驗依賴的源受污染,因此不能只依賴證書鏈來進行證書校驗。安全的做法是,把證書拷貝打包進app中并把它作為錨點證書,然后和服務器的證書鏈做匹配。
比如在AFNetworking中:

SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
//驗證服務器證書是否可信(由系統默認可信或者由用戶選擇可信)。
if (!AFServerTrustIsValid(serverTrust)) {
    return NO;
}
//從我們需要被驗證的服務端去拿證書鏈
//這里的證書鏈順序是從葉節點到根節點
NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
//逆序,從根節點開始匹配            
for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
     //如果本地證書中,有一個和它證書鏈中的證書匹配的,就返回YES
    if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
        return YES;
    }
}

5.AFNetworking3.0版本中的HTTPS認證

自 iOS 9 發布之后,由于新特性App Transport Security的引入,默認情況下是不能發送 HTTP 請求的。很多網站都在轉用 HTTPS,而 AFNetworking中的 AFSecurityPolicy就是用來滿足我們各種https認證需求的。
接下來從源碼入手分析AFSecurityPolicy內部是如何做https認證的。(AF默認做的僅僅是單向認證,如果要做雙向認證就只能自己寫block來實現了)

5.1AFSSLPinningMode和重要屬性

  1. AFSSLPinningMode共提供了三種驗證方式
typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {//三種驗證服務器的方式
    AFSSLPinningModeNone,//不使用固定證書(本地)驗證服務器。直接從客戶端系統中的受信任頒發機構 CA 列表中去驗證
    AFSSLPinningModePublicKey,//根據本地固定證書公鑰驗證服務器證書,不驗證證書的有效期等信息
    AFSSLPinningModeCertificate,//根據本地固定證書驗證服務器證書
};

AFSSLPinningModeNone不做本地證書驗證,直接從客戶端系統中的受信任頒發機構 CA 列表中去驗證服務端返回的證書,若證書是信任機構簽發的就通過,若是自己服務器生成的證書,這里是不會通過的。
AFSSLPinningModePublicKey用ssl pinning方式驗證服務端返回的證書,只驗證公鑰。客戶端要有服務端證書拷貝
AFSSLPinningModeCertificate用ssl pinning方式驗證服務端返回的證書。客戶端要有服務端證書拷貝

  1. AFSecurityPolicy中重要的屬性
//ssl pinning的模式,默認是AFSSLPinningModeNone
@property (readonly, nonatomic, assign) AFSSLPinningMode SSLPinningMode;
//本地證書,用于驗證服務器
@property (nonatomic, strong, nullable) NSSet <NSData *> *pinnedCertificates;
//是否信任無效或者過期的ssl證書,默認不信任(比如自簽名證書)
@property (nonatomic, assign) BOOL allowInvalidCertificates;
//是否驗證證書域名,默認是YES
@property (nonatomic, assign) BOOL validatesDomainName;
//本地證書公鑰
@property (readwrite, nonatomic, strong) NSSet *pinnedPublicKeys;

5.2初始化及設置

1.初始化

//創建一個默認的AFSecurityPolicy,SSLPinningMode是不驗證
+ (instancetype)defaultPolicy {
    AFSecurityPolicy *securityPolicy = [[self alloc] init];
    securityPolicy.SSLPinningMode = AFSSLPinningModeNone;
    return securityPolicy;
}

+ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode {
    return [self policyWithPinningMode:pinningMode withPinnedCertificates:[self defaultPinnedCertificates]];
}

//根據指定的驗證模式、證書創建AFSecurityPolicy
+ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode withPinnedCertificates:(NSSet *)pinnedCertificates {
    AFSecurityPolicy *securityPolicy = [[self alloc] init];
    securityPolicy.SSLPinningMode = pinningMode;
    [securityPolicy setPinnedCertificates:pinnedCertificates];
    return securityPolicy;
}

- (instancetype)init {
    self = [super init];
    if (!self) {
        return nil;
    }
    self.validatesDomainName = YES;//驗證證書域名
    return self;
}

這里沒有什么地方值得解釋的,根據需要選擇創建一個默認的AFSecurityPolicy,或者根據指定的AFSSLPinningMode、PinnedCertificates創建AFSecurityPolicy。
在AF中是這樣創建一個securityPolicy的:self.securityPolicy = [AFSecurityPolicy defaultPolicy];

2.設置本地證書PinnedCertificates
在調用- setPinnedCertificates:方法設置本地證書時,會把全部證書的公鑰取出來存放到pinnedPublicKeys屬性中,方便之后用于AFSSLPinningModePublicKey方式的驗證

//設置用于評估服務器是否可信的證書(本地證書)
//把證書中每個公鑰放在了self.pinnedPublicKeys中,用于AFSSLPinningModePublicKey方式的驗證
- (void)setPinnedCertificates:(NSSet *)pinnedCertificates {
    _pinnedCertificates = pinnedCertificates;

    if (self.pinnedCertificates) {
        //遍歷取出所有證書中的公鑰,然后保存在self.pinnedPublicKeys屬性中
        NSMutableSet *mutablePinnedPublicKeys = [NSMutableSet setWithCapacity:[self.pinnedCertificates count]];
        for (NSData *certificate in self.pinnedCertificates) {
            id publicKey = AFPublicKeyForCertificate(certificate);//從證書中獲取公鑰
            if (!publicKey) {
                continue;
            }
            [mutablePinnedPublicKeys addObject:publicKey];
        }
        self.pinnedPublicKeys = [NSSet setWithSet:mutablePinnedPublicKeys];
    } else {
        self.pinnedPublicKeys = nil;
    }
}

3.其他

//以NSData的形式獲取某個目錄下的所有證書
+ (NSSet *)certificatesInBundle:(NSBundle *)bundle;
//以NSData的形式獲取當前class目錄下的所有證書
+ (NSSet *)defaultPinnedCertificates;

5.3核心方法

- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain
這個方法可以說是這個類的核心了。用于驗證服務器是否可信。
這個方法有兩個參數:SecTrustRef類型serverTrustNSString類型的domain

  • SecTrustRef是啥
    在這個方法中,這個serverTrust是服務器傳過來的,包含了服務器的證書信息。
    大概是用來執行X.509證書信任評估的。

其實就是一個容器,裝了服務器端需要驗證的證書的基本信息、公鑰等等,不僅如此,它還可以裝一些評估策略,還有客戶端的錨點證書,這個客戶端的證書,可以用來和服務端的證書去匹配驗證的。
每一個SecTrustRef對象包含多個SecCertificateRef 和 SecPolicyRef。其中 SecCertificateRef 可以使用 DER 進行表示。

  • domain服務器域名,用于域名驗證

代碼解析如下:

//驗證服務端是否可信,這個serverTrust是服務器傳過來的,里面包含了服務器的證書信息,是用于我們本地客戶端去驗證該證書是否合法用的
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(NSString *)domain
{
    //判斷矛盾的條件
    //如果有服務器域名、設置了允許信任無效或者過期證書(自簽名證書)、需要驗證域名、沒有提供證書或者不驗證證書,返回no。后兩者和allowInvalidCertificates為真的設置矛盾,說明這次驗證是不安全的。
    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;
    }

    //生成驗證策略。如果要驗證域名,就以域名為參數創建一個策略,否則創建默認的basicX509策略
    NSMutableArray *policies = [NSMutableArray array];
    if (self.validatesDomainName) {
        //SecPolicyCreateSSL函數,創建用于評估SSL證書鏈的策略對象。第一個參數:true將為SSL服務器證書創建一個策略。第二個參數:如果這個參數存在,證書鏈上的葉子節點表示的那個domain要和傳入的domain相匹配
        [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
    } else {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];//該策略不檢驗域名
    }
    
    //設置評估中要使用的策略
    SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);//為serverTrust設置驗證的策略

    //如果是AFSSLPinningModeNone(不做本地證書驗證,從客戶端系統中的受信任頒發機構 CA 列表中去驗證)
    if (self.SSLPinningMode == AFSSLPinningModeNone) {
        //不使用ssl pinning 但允許自建證書,直接返回YES;否則進行第二個條件判斷,去客戶端系統根證書里找是否有匹配的證書,驗證serverTrust是否可信,直接返回YES;否則進行第二個條件判斷,去客戶端系統根證書里找是否有匹配的證書,驗證serverTrust是否可信
        return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
    }
    //如果serverTrust不可信且不允許自簽名,返回NO
    else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
        return NO;
    }

    //根據不同的SSLPinningMode分情況驗證
    switch (self.SSLPinningMode) {
        //不驗證
        case AFSSLPinningModeNone://上一部分已經判斷過了,如果執行到這里的話就返回NO
        default:
            return NO;
        //驗證證書
        case AFSSLPinningModeCertificate: {
            NSMutableArray *pinnedCertificates = [NSMutableArray array];
            //把nsdata證書(der編碼的x.509證書)轉成SecCertificateRef類型的數據
            for (NSData *certificateData in self.pinnedCertificates) {
                [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
            }
            // 將本地證書設置成需要參與驗證的錨點證書,設為服務器信任的證書(錨點證書通常指:嵌入到操作系統中的根證書,通過SecTrustSetAnchorCertificates設置了參與校驗錨點證書之后,假如驗證的數字證書是這個錨點證書的子節點,即驗證的數字證書是由錨點證書對應CA或子CA簽發的,或是該證書本身,則信任該證書)
            //第二個參數,表示在驗證證書時被SecTrustEvaluate函數視為有效(可信任)錨點的錨定證書集。 傳遞NULL以恢復默認的錨證書集。
            //自簽證書在這步之前驗證通過不了,把本地證書添加進去后就能驗證成功。
            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]) {//reverseObjectEnumerator逆序
                //如果本地證書中,有一個和它證書鏈中的證書匹配的,就返回YES
                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;//判斷如果相同 trustedPublicKeyCount+1
                    }
                }
            }
            return trustedPublicKeyCount > 0;
        }
    }
    
    return NO;
}

總結一下這個方法做了什么
1.判斷設置上的矛盾條件。
允許使用自建證書self.allowInvalidCertificates=YES,還想驗證域名是否有效self.validatesDomainName=YES,那么必須使用SSL Pinning方式驗證,但AFSSLPinningModeNone表示不使用SSL Pinning。再者,如果沒有pinnedCertificates(在客戶端保存的服務器頒發的證書拷貝,在下文稱為“本地證書”),表示無法驗證自建證書。

2.創建證書評估策略
如果要驗證域名,就創建評估SSL證書鏈的策略;如果不驗證域名,就使用默認的X509驗證策略。

3.設置評估策略

4.(在還沒設置本地錨點證書下,做第一次服務器驗證)
4.1如果是AFSSLPinningModeNone,不使用ssl pinning 但允許自建證書,直接返回YES;或者使用SecTrustEvaluate去客戶端系統根證書里找是否有匹配的證書,驗證serverTrust是否可信。
4.2serverTrust不可信且不允許自建證書,返回NO。

5.根據不同的SSL Pinning Mode驗證
5.1 AFSSLPinningModeNone
直接返回NO,因為前面的處理應該可以解決這種情況了。
5.2 AFSSLPinningModeCertificate
將本地這書設置成錨點證書,然后調用SecTrustEvaluate驗證服務端證書是否可信。拿到服務端證書鏈,如果本地證書中有一個和它證書鏈中的證書匹配的(相當于是認為服務端證書在本地信任的證書列表中?)就返回YES。
假設是自簽證書,因為APP bundle中的證書不是CA機構頒發的,不被信任。要調用SecTrustSetAnchorCertificates將本地證書設置成serverTrust證書鏈上的錨點證書(好比于將這些證書設置成系統信任的根證書),然后調用SecTrustEvaluate校驗,如果遇到錨點證書就終止校驗了。
5.3 AFSSLPinningModePublicKey
取出服務端證書公鑰,和本地證書公鑰進行匹配。

這個核心方法中用到一些私有函數,這里簡單講一下。
1.AFPublicKeyForCertificate、AFServerTrustIsValid、AFCertificateTrustChainForServerTrust
這三個函數的實現比較相似,這里放一起講。

//在證書中獲取公鑰
static id AFPublicKeyForCertificate(NSData *certificate) {
    //1.初始化臨時變量
    id allowedPublicKey = nil;
    SecCertificateRef allowedCertificate;//SecCertificateRef包含有關證書的信息
    SecPolicyRef policy = nil;
    SecTrustRef allowedTrust = nil;
    SecTrustResultType result;

    //2.創建SecCertificateRef對象,判斷返回值是否為null
    //通過DER格式的證書(NSData)生成SecCertificateRef類型的證書引用。 如果傳入的數據不是有效的DER編碼的X.509證書,則返回NULL。
    //傳入的第二個參數是CFDataRef類型,要用__bridge把oc對象轉Core Foundation對象
    allowedCertificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificate);
    //__Require_Quiet這個宏,判斷allowedCertificate != NULL表達式是否為假,如果allowedCertificate=NULL,就跳到_out標簽處執行
    __Require_Quiet(allowedCertificate != NULL, _out);

    //3.1.新建默認策略為X.509的SecPolicyRef策略對象
    policy = SecPolicyCreateBasicX509();
    /*3.2.
     OSStatus SecTrustCreateWithCertificates(CFTypeRef certificates,
     CFTypeRef __nullable policies, SecTrustRef * __nonnull CF_RETURNS_RETAINED trust)
     基于給定的證書和策略創建一個SecTrustRef信任引用對象,然后賦值給trust。
     這個函數返回一個結果碼,判斷是否出錯
     */
    //__Require_noErr_Quiet,第一個參數是錯誤碼表達式,如果不等于0(出錯了)就跳到_out標簽處執行
    __Require_noErr_Quiet(SecTrustCreateWithCertificates(allowedCertificate, policy, &allowedTrust), _out);//創建SecTrustRef,如果出錯就跳到_out
    //3.3對SecTrustRef進行信任評估,確認它是值得信任的
    __Require_noErr_Quiet(SecTrustEvaluate(allowedTrust, &result), _out);

    //4.獲取證書公鑰
    //__bridge_transfer會將結果橋接成OC對象,然后將 SecTrustCopyPublicKey 返回的指針釋放
    allowedPublicKey = (__bridge_transfer id)SecTrustCopyPublicKey(allowedTrust);

_out:
    //5.釋放c指針
    if (allowedTrust) {
        CFRelease(allowedTrust);
    }

    if (policy) {
        CFRelease(policy);
    }

    if (allowedCertificate) {
        CFRelease(allowedCertificate);
    }

    return allowedPublicKey;
}

這里用到的系統宏__Require_Quiet,是用來判斷allowedCertificate != NULL表達式是否為假,如果allowedCertificate=NULL,就跳到_out標簽處執行代碼。

AFPublicKeyTrustChainForServerTrust函數的實現和它差不多,這里就不具體展開了,用于取出服務器返回的證書鏈的每個證書公鑰。

Q:一點疑問,如果是自簽證書,在獲取本地證書公鑰和服務器證書公鑰的函數中,是怎么在沒有設置錨點證書的情況下,通過SecTrustEvaluate檢驗證書可信的?

//取出服務器返回的證書鏈上所有證書
static NSArray * AFCertificateTrustChainForServerTrust(SecTrustRef serverTrust) {
    CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);//獲取評估證書鏈中的證書數目。
    NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount];
    //遍歷獲取證書鏈中的每個證書,并添加到trustChain中//獲取的順序,從證書鏈的葉節點到根節點
    for (CFIndex i = 0; i < certificateCount; i++) {
        SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i);//取得證書鏈中對應下標的證書
        //返回der格式的x.509證書
        [trustChain addObject:(__bridge_transfer NSData *)SecCertificateCopyData(certificate)];
    }

    return [NSArray arrayWithArray:trustChain];
}

2.AFServerTrustIsValid

//驗證serverTrust是否有效
static BOOL AFServerTrustIsValid(SecTrustRef serverTrust) {
    BOOL isValid = NO;
    SecTrustResultType result;
    __Require_noErr_Quiet(SecTrustEvaluate(serverTrust, &result), _out);//評估證書是否可信,確認它是值得信任的.
    /*SecTrustResultType結果有兩個維度。 1.serverTrust評估是否成功,2.是否由用戶決定評估成功。
     如果是用戶決定的,成功是 kSecTrustResultProceed 失敗是kSecTrustResultDeny。
     非用戶定義的, 成功是kSecTrustResultUnspecified 失敗是kSecTrustResultRecoverableTrustFailure
     用戶決策通過使用SecTrustCopyExceptions()和SecTrustSetExceptions()*/
    isValid = (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);

 _out:
    return isValid;
}
/*
#ifndef __Require_noErr_Quiet
    #define __Require_noErr_Quiet(errorCode, exceptionLabel)                      \
      do                                                                          \
      {                                                                           \
          if ( __builtin_expect(0 != (errorCode), 0) )                            \
          {                                                                       \
              goto exceptionLabel;                                                \
          }                                                                       \
      } while ( 0 )
#endif
*/

這個函數核心是用SecTrustEvaluate函數來驗證serverTrust是否有效,返回一個SecTrustResultType類型的result。

SecTrustResultType的結果有兩個維度。 1.serverTrust評估是否成功,2.是否由用戶決定評估成功。
如果是用戶決定的(比如系統彈窗出來讓用戶決定是否信任證書),成功是kSecTrustResultProceed失敗是kSecTrustResultDeny
非用戶定義的, 成功是kSecTrustResultUnspecified失敗是kSecTrustResultRecoverableTrustFailure

關于__Require_noErr_Quiet這個宏,是用來判斷errorCode是否為0的,如果不為0就跳到exceptionLabel標簽處執行代碼。所以這里的意思就是,如果SecTrustEvaluate評估出錯,就跳到_out標簽那執行代碼令isValid=0。

以下用到的原生c函數:

//1.創建用于評估SSL證書鏈的策略對象。第一個參數:true將為SSL服務器證書創建一個策略。第二個參數:如果這個參數存在,證書鏈上的葉子節點表示的那個domain要和傳入的domain相匹配
SecPolicyCreateSSL(<#Boolean server#>, <#CFStringRef  _Nullable hostname#>)
//2.默認的BasicX509驗證策略,不驗證域名。
SecPolicyCreateBasicX509();
//3.為serverTrust設置驗證策略
SecTrustSetPolicies(<#SecTrustRef  _Nonnull trust#>, <#CFTypeRef  _Nonnull policies#>)
//4.驗證serverTrust,并且把驗證結果返回給第二參數 result
//函數內部遞歸地從葉節點證書到根證書驗證。使用系統默認的驗證方式驗證Trust Object,根據上述證書鏈的驗證可知,系統會根據Trust Object的驗證策略,一級一級往上,驗證證書鏈上每一級證書有效性。
SecTrustEvaluate(<#SecTrustRef  _Nonnull trust#>, <#SecTrustResultType * _Nullable result#>)
//5.根據證書data,去創建SecCertificateRef類型的數據。
SecCertificateCreateWithData(<#CFAllocatorRef  _Nullable allocator#>, <#CFDataRef  _Nonnull data#>)
//6.給serverTrust設置錨點證書,即如果以后再次去驗證serverTrust,會從錨點證書去找是否匹配。
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
//7.拿到證書鏈中的證書個數
SecTrustGetCertificateCount(serverTrust);
//8.去取得證書鏈中對應下標的證書。
SecTrustGetCertificateAtIndex(serverTrust, i)
//8.根據證書獲取公鑰。
SecTrustCopyPublicKey(trust)

5.4在AF中的調用

- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
    /*挑戰處理類型
            NSURLSessionAuthChallengeUseCredential              使用指定證書
            NSURLSessionAuthChallengePerformDefaultHandling     默認方式處理
            NSURLSessionAuthChallengeCancelAuthenticationChallenge  取消挑戰The entire request will be canceled; the credential parameter is ignored
            NSURLSessionAuthChallengeRejectProtectionSpace拒接認證請求。
     */
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    __block NSURLCredential *credential = nil;

    //sessionDidReceiveAuthenticationChallenge是自定義方法,用來處理如何應對服務器端的認證挑戰
    if (self.sessionDidReceiveAuthenticationChallenge) {
        disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential);
    } else {
        // 也就是說服務器端需要客戶端返回一個根據認證挑戰的保護空間提供的信任(即challenge.protectionSpace.serverTrust)產生的挑戰證書。
        //要求對保護空間執行服務器證書認證
        if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
            // 基于客戶端的安全策略來決定是否信任該服務器,不信任的話,也就沒必要響應挑戰
            if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
                // 創建挑戰證書
                //創建并返回一個NSURLCredential對象,以使用給定的可接受的信任進行服務器信任身份驗證。
                credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
                // 確定挑戰的方式
                if (credential) {
                    //證書挑戰  設計policy,none,則跑到這里
                    disposition = NSURLSessionAuthChallengeUseCredential;
                } else {
                    disposition = NSURLSessionAuthChallengePerformDefaultHandling;
                }
            } else {
                //取消挑戰
                disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
            }
        } else {
            //默認挑戰方式
            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        }
    }
    //完成挑戰
    if (completionHandler) {
        completionHandler(disposition, credential);
    }
}

這個方法大概做了什么:
1)首先指定了處理認證挑戰的默認方式。
2)判斷有沒有自定義Block:sessionDidReceiveAuthenticationChallenge,有的話,使用我們自定義Block,自定義處理應對服務器端的認證挑戰。
3)如果沒有自定義Block,我們判斷如果服務端的認證方法要求是NSURLAuthenticationMethodServerTrust,則只需要驗證服務端證書是否安全(即https的單向認證,這是AF默認處理的認證方式,其他的認證方式,只能由我們自定義Block的實現)
3.1)接著我們就執行了AFSecurityPolicy相關的一個方法,做了一個AF內部的一個對服務器的認證:
[self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host])
AF默認的處理是,如果這行返回NO、說明AF內部認證失敗,則取消https認證,即取消請求。返回YES則進入if塊,用服務器返回的一個serverTrust去生成了一個認證證書。然后如果有證書,則用證書認證方式,否則還是用默認的驗證方式。
最后調用completionHandler傳遞認證方式和要認證的證書,去做系統根證書驗證。

總結:這里securityPolicy存在的作用就是,使得在系統底層自己去驗證之前,AF可以先去驗證服務端的證書。如果通不過,則直接越過系統的驗證,取消https的網絡請求。否則,繼續去走系統根證書的驗證。
系統驗證的流程:
系統的驗證,首先是去系統的根證書找,看是否有能匹配服務端的證書,如果匹配,則驗證成功,返回https的安全數據。
如果不匹配則去判斷ATS是否關閉,如果關閉,則返回https不安全連接的數據。如果開啟ATS,則拒絕這個請求,請求失敗。
AF的驗證方式不是必須的,但是對有特殊驗證需求的用戶確是必要的。

系統api上的一些用法記錄:

  • NSURLAuthenticationChallenge
@property (readonly, copy) NSURLProtectionSpace *protectionSpace;
NSURLProtectionSpace對象,受保護空間,代表了服務器上的一塊需要授權信息的區域。包括了服務器地址、端口等信息。

 @property (nullable, readonly, copy) NSURLCredential *proposedCredential;
這個認證挑戰 建議使用的證書

 @property (readonly) NSInteger previousFailureCount;
認證失敗的次數

 @property (nullable, readonly, copy) NSURLResponse *failureResponse;
最后一次認證失敗的響應信息

 @property (nullable, readonly, copy) NSError *error;
認真失敗的錯誤信息

 @property (nullable, readonly, retain) id<NSURLAuthenticationChallengeSender> sender;
代理對象,challenge的發送者, NSURLSession、connection對象之類的

NSURLAuthenticationChallenge類型的參數簡單理解來說,就是服務端發起的認證挑戰,客戶端要根據認證挑戰的類型提供響應的挑戰憑證(NSURLCredential)

由于- URLSession:didReceiveChallenge:completionHandler:回調時不止HTTPS服務器身份鑒別,因此首先判斷一下身份鑒別的類型。通過challenge.protectionSpace.authenticationMethod可以獲取。NSURLAuthenticationMethodServerTrust指對protectionSpace執行服務器證書驗證。

響應挑戰
通過sender代理實例,讓客戶端來選擇怎樣的挑戰響應方式。

// 用憑證響應挑戰。如果是雙向驗證,不僅客戶端要驗證服務器身份,服務器也需要客戶端提供證書,因此需要提供憑證
 - (void)useCredential:(NSURLCredential *)credential forAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
//不提供憑證繼續請求
 - (void)continueWithoutCredentialForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
//取消憑證驗證
 - (void)cancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
//使用默認方式處理認證挑戰
 - (void)performDefaultHandlingForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
//拒絕當前提供的受保護空間并且嘗試不提供憑證繼續請求
 - (void)rejectProtectionSpaceAndContinueWithChallenge:(NSURLAuthenticationChallenge *)challenge;
  • NSURLCredential
    表示身份驗證證書(憑證)。URL Lodaing支持3種類型證書:password-based user credentials, certificate-based user credentials, 和certificate-based server credentials(需要驗證服務器身份時使用)。NSURLCredential可以表示由用戶名/密碼組合、客戶端證書及服務器信任創建的認證信息,適合大部分的認證請求。
    對于NSURLCredential也存在三種持久化機制:

    • NSURLCredentialPersistenceNone :要求 URL 載入系統 “在用完相應的認證信息后立刻丟棄”。
  • NSURLCredentialPersistenceForSession :要求 URL 載入系統 “在應用終止時,丟棄相應的 credential ”。

  • NSURLCredentialPersistencePermanent :要求 URL 載入系統 “將相應的認證信息存入鑰匙串(keychain),以便其他應用也能使用。

  • 如何處理挑戰。 NSURLSessionAuthChallengeDisposition類型的數據,是一個常數:

NSURLSessionAuthChallengeUseCredential              使用指定證書
NSURLSessionAuthChallengePerformDefaultHandling     默認方式處理
NSURLSessionAuthChallengeCancelAuthenticationChallenge  取消挑戰The entire request will be canceled; the credential parameter is ignored
NSURLSessionAuthChallengeRejectProtectionSpace拒接認證請求。

詳細源碼注釋請戳:https://github.com/huixinHu/AFNetworking-

參考文章:
圖解SSL/TLS協議
iOS安全系列之二:HTTPS進階
iOS 中 HTTPS 證書驗證淺析
iOS 中對 HTTPS 證書鏈的驗證
AFNetworking之于https認證

深入理解HTTPS及在iOS系統中適配HTTPS類型網絡請求(上)
深入理解HTTPS及在iOS系統中適配HTTPS類型網絡請求(下)
iOS HPPTS證書驗證

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容