AFNetworking之于https認證

寫在開頭:
  • 本來這篇內容準備寫在AFNetworking到底做了什么?(三)中的,但是因為我想在三中完結這個系列,礙于篇幅所限、并且這一塊內容獨立性比較強,所以單獨拎出來,寫成一篇。

  • 本文從源碼的角度,去分析AFNetworking對https的認證過程。旨在讓讀者明白我們去做https請求:

  • 如果使用AF,需要做什么。

  • 不使用的話,直接用原生NSUrlSession,又需要做什么。

  • 當我們使用自簽證書的https,又需要注意哪些問題。

  • 單獨看并不影響閱讀。如果有需要了解更多AF相關內容,可以關注樓主的系列文章:
    AFNetworking到底做了什么?
    AFNetworking到底做了什么?(二)

那么正文開始了:

簡單的理解下https:https在http請求的基礎上多加了一個證書認證的流程。認證通過之后,數據傳輸都是加密進行的。
關于https的更多概念,我就不贅述了,網上有大量的文章,小伙伴們可以自行查閱。在這里大概的講講https的認證過程吧,如下圖所示:

https單向認證過程.jpg

1. 客戶端發起HTTPS請求
  這個沒什么好說的,就是用戶在瀏覽器里輸入一個https網址,然后連接到server的443端口。
2. 服務端的配置
  采用HTTPS協議的服務器必須要有一套數字證書,可以自己制作,也可以向組織申請。區別就是自己頒發的證書需要客戶端驗證通過,才可以繼續訪問,而使用受信任的公司申請的證書則不會彈出提示頁面。這套證書其實就是一對公鑰和私鑰。如果對公鑰和私鑰不太理解,可以想象成一把鑰匙和一個鎖頭,只是全世界只有你一個人有這把鑰匙,你可以把鎖頭給別人,別人可以用這個鎖把重要的東西鎖起來,然后發給你,因為只有你一個人有這把鑰匙,所以只有你才能看到被這把鎖鎖起來的東西。
3. 傳送證書
  這個證書其實就是公鑰,只是包含了很多信息,如證書的頒發機構,過期時間等等。
4. 客戶端解析證書
  這部分工作是有客戶端的TLS/SSL來完成的,首先會驗證公鑰是否有效,比如頒發機構,過期時間等等,如果發現異常,則會彈出一個警告框,提示證書存在問題。如果證書沒有問題,那么就生成一個隨機值。然后用證書對該隨機值進行加密。就好像上面說的,把隨機值用鎖頭鎖起來,這樣除非有鑰匙,不然看不到被鎖住的內容。
5. 傳送加密信息
  這部分傳送的是用證書加密后的隨機值,目的就是讓服務端得到這個隨機值,以后客戶端和服務端的通信就可以通過這個隨機值來進行加密解密了。
6. 服務段解密信息
  服務端用私鑰解密后,得到了客戶端傳過來的隨機值(私鑰),然后把內容通過該值進行對稱加密。所謂對稱加密就是,將信息和私鑰通過某種算法混合在一起,這樣除非知道私鑰,不然無法獲取內容,而正好客戶端和服務端都知道這個私鑰,所以只要加密算法夠彪悍,私鑰夠復雜,數據就夠安全。
7. 傳輸加密后的信息
  這部分信息是服務段用私鑰加密后的信息,可以在客戶端被還原。
8. 客戶端解密信息
  客戶端用之前生成的私鑰解密服務段傳過來的信息,于是獲取了解密后的內容。整個過程第三方即使監聽到了數據,也束手無策。

這就是整個https驗證的流程了。簡單總結一下:

  • 就是用戶發起請求,服務器響應后返回一個證書,證書中包含一些基本信息和公鑰。
  • 用戶拿到證書后,去驗證這個證書是否合法,不合法,則請求終止。
  • 合法則生成一個隨機數,作為對稱加密的密鑰,用服務器返回的公鑰對這個隨機數加密。然后返回給服務器。
  • 服務器拿到加密后的隨機數,利用私鑰解密,然后再用解密后的隨機數(對稱密鑰),把需要返回的數據加密,加密完成后數據傳輸給用戶。
  • 最后用戶拿到加密的數據,用一開始的那個隨機數(對稱密鑰),進行數據解密。整個過程完成。

當然這僅僅是一個單向認證,https還會有雙向認證,相對于單向認證也很簡單。僅僅多了服務端驗證客戶端這一步。感興趣的可以看看這篇:Https單向認證和雙向認證。

了解了https認證流程后,接下來我們來講講AFSecurityPolicy這個類,AF就是用這個類來滿足我們各種https認證需求。

在這之前我們來看看AF用來做https認證的代理:

- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
    //挑戰處理類型為 默認
    /*
     NSURLSessionAuthChallengePerformDefaultHandling:默認方式處理
     NSURLSessionAuthChallengeUseCredential:使用指定的證書
     NSURLSessionAuthChallengeCancelAuthenticationChallenge:取消挑戰
     */
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    __block NSURLCredential *credential = nil;

    // sessionDidReceiveAuthenticationChallenge是自定義方法,用來如何應對服務器端的認證挑戰

    if (self.sessionDidReceiveAuthenticationChallenge) {
        disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential);
    } else {
         // 此處服務器要求客戶端的接收認證挑戰方法是NSURLAuthenticationMethodServerTrust
        // 也就是說服務器端需要客戶端返回一個根據認證挑戰的保護空間提供的信任(即challenge.protectionSpace.serverTrust)產生的挑戰證書。
       
        // 而這個證書就需要使用credentialForTrust:來創建一個NSURLCredential對象
        if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
            
            // 基于客戶端的安全策略來決定是否信任該服務器,不信任的話,也就沒必要響應挑戰
            if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
                // 創建挑戰證書(注:挑戰方式為UseCredential和PerformDefaultHandling都需要新建挑戰證書)
                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)首先指定了https為默認的認證方式。
2)判斷有沒有自定義Block:sessionDidReceiveAuthenticationChallenge,有的話,使用我們自定義Block,生成一個認證方式,并且可以給credential賦值,即我們需要接受認證的證書。然后直接調用completionHandler,去根據這兩個參數,執行系統的認證。至于這個系統的認證到底做了什么,可以看文章最后,這里暫且略過。
3)如果沒有自定義Block,我們判斷如果服務端的認證方法要求是NSURLAuthenticationMethodServerTrust,則只需要驗證服務端證書是否安全(即https的單向認證,這是AF默認處理的認證方式,其他的認證方式,只能由我們自定義Block的實現)
4)接著我們就執行了AFSecurityPolicy相關的一個方法,做了一個AF內部的一個https認證:

[self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host])

AF默認的處理是,如果這行返回NO、說明AF內部認證失敗,則取消https認證,即取消請求。返回YES則進入if塊,用服務器返回的一個serverTrust去生成了一個認證證書。(注:這個serverTrust是服務器傳過來的,里面包含了服務器的證書信息,是用來我們本地客戶端去驗證該證書是否合法用的,后面會更詳細的去講這個參數)然后如果有證書,則用證書認證方式,否則還是用默認的驗證方式。最后調用completionHandler傳遞認證方式和要認證的證書,去做系統根證書驗證。

  • 總結一下這里securityPolicy存在的作用就是,使得在系統底層自己去驗證之前,AF可以先去驗證服務端的證書。如果通不過,則直接越過系統的驗證,取消https的網絡請求。否則,繼續去走系統根證書的驗證。
接下來我們看看AFSecurityPolicy內部是如果做https認證的:

如下方式,我們可以創建一個securityPolicy

AFSecurityPolicy *policy = [AFSecurityPolicy defaultPolicy];

內部創建:

+ (instancetype)defaultPolicy {
    AFSecurityPolicy *securityPolicy = [[self alloc] init];
    securityPolicy.SSLPinningMode = AFSSLPinningModeNone;
    return securityPolicy;
}

默認指定了一個SSLPinningMode模式為AFSSLPinningModeNone
對于AFSecurityPolicy,一共有4個重要的屬性:

//https驗證模式
@property (readonly, nonatomic, assign) AFSSLPinningMode SSLPinningMode;
//可以去匹配服務端證書驗證的證書
@property (nonatomic, strong, nullable) NSSet <NSData *> *pinnedCertificates;
//是否支持非法的證書(例如自簽名證書)
@property (nonatomic, assign) BOOL allowInvalidCertificates;
//是否去驗證證書域名是否匹配
@property (nonatomic, assign) BOOL validatesDomainName;

它們的作用我添加在注釋里了,第一條就是AFSSLPinningMode, 共提供了3種驗證方式:

typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
    //不驗證
    AFSSLPinningModeNone,
    //只驗證公鑰
    AFSSLPinningModePublicKey,
    //驗證證書
    AFSSLPinningModeCertificate,
};

我們接著回到代理https認證的這行代碼上:

[self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]
  • 我們傳了兩個參數進去,一個是SecTrustRef類型的serverTrust,這是什么呢?我們看到蘋果的文檔介紹如下:

CFType used for performing X.509 certificate trust evaluations.

大概意思是用于執行X。509證書信任評估,
再講簡單點,其實就是一個容器,裝了服務器端需要驗證的證書的基本信息、公鑰等等,不僅如此,它還可以裝一些評估策略,還有客戶端的錨點證書,這個客戶端的證書,可以用來和服務端的證書去匹配驗證的。

  • 除此之外還把服務器域名傳了過去。

我們來到這個方法,代碼如下:

//驗證服務端是否值得信任
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(NSString *)domain
{
    //判斷矛盾的條件
    //判斷有域名,且允許自建證書,需要驗證域名,
    //因為要驗證域名,所以必須不能是后者兩種:AFSSLPinningModeNone或者添加到項目里的證書為0個。
    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];
    //要驗證域名
    if (self.validatesDomainName) {
        
        // 如果需要驗證domain,那么就使用SecPolicyCreateSSL函數創建驗證策略,其中第一個參數為true表示驗證整個SSL證書鏈,第二個參數傳入domain,用于判斷整個證書鏈上葉子節點表示的那個domain是否和此處傳入domain一致
        //添加驗證策略
        [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
    } else {
        // 如果不需要驗證domain,就使用默認的BasicX509驗證策略
        [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
    }
    
    //serverTrust:X。509服務器的證書信任。
    // 為serverTrust設置驗證策略,即告訴客戶端如何驗證serverTrust
    SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);

    
    //有驗證策略了,可以去驗證了。如果是AFSSLPinningModeNone,是自簽名,直接返回可信任,否則不是自簽名的就去系統根證書里去找是否有匹配的證書。
    if (self.SSLPinningMode == AFSSLPinningModeNone) {
        //如果支持自簽名,直接返回YES,不允許才去判斷第二個條件,判斷serverTrust是否有效
        return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
    }
    //如果驗證無效AFServerTrustIsValid,而且allowInvalidCertificates不允許自簽,返回NO
    else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
        return NO;
    }

    //判斷SSLPinningMode
    switch (self.SSLPinningMode) {
        // 理論上,上面那個部分已經解決了self.SSLPinningMode)為AFSSLPinningModeNone)等情況,所以此處再遇到,就直接返回NO
        case AFSSLPinningModeNone:
        default:
            return NO;
        
        //驗證證書類型
        case AFSSLPinningModeCertificate: {
            
            NSMutableArray *pinnedCertificates = [NSMutableArray array];
            
            //把證書data,用系統api轉成 SecCertificateRef 類型的數據,SecCertificateCreateWithData函數對原先的pinnedCertificates做一些處理,保證返回的證書都是DER編碼的X.509證書

            for (NSData *certificateData in self.pinnedCertificates) {
                [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
            }
            // 將pinnedCertificates設置成需要參與驗證的Anchor Certificate(錨點證書,通過SecTrustSetAnchorCertificates設置了參與校驗錨點證書之后,假如驗證的數字證書是這個錨點證書的子節點,即驗證的數字證書是由錨點證書對應CA或子CA簽發的,或是該證書本身,則信任該證書),具體就是調用SecTrustEvaluate來驗證。
            //serverTrust是服務器來的驗證,有需要被驗證的證書。
            SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);

            //自簽在之前是驗證通過不了的,在這一步,把我們自己設置的證書加進去之后,就能驗證成功了。
            
            //再去調用之前的serverTrust去驗證該證書是否有效,有可能:經過這個方法過濾后,serverTrust里面的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);
            
            //reverseObjectEnumerator逆序
            for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
                
                //如果我們的證書中,有一個和它證書鏈中的證書匹配的,就返回YES
                if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
                    return YES;
                }
            }
            //沒有匹配的
            return NO;
        }
            //公鑰驗證 AFSSLPinningModePublicKey模式同樣是用證書綁定(SSL Pinning)方式驗證,客戶端要有服務端的證書拷貝,只是驗證時只驗證證書里的公鑰,不驗證證書的有效期等信息。只要公鑰是正確的,就能保證通信不會被竊聽,因為中間人沒有私鑰,無法解開通過公鑰加密的數據。
        case AFSSLPinningModePublicKey: {
            
            NSUInteger trustedPublicKeyCount = 0;
            
            // 從serverTrust中取出服務器端傳過來的所有可用的證書,并依次得到相應的公鑰
            NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);

            //遍歷服務端公鑰
            for (id trustChainPublicKey in publicKeys) {
                //遍歷本地公鑰
                for (id pinnedPublicKey in self.pinnedPublicKeys) {
                    //判斷如果相同 trustedPublicKeyCount+1
                    if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
                        trustedPublicKeyCount += 1;
                    }
                }
            }
            return trustedPublicKeyCount > 0;
        }
    }
    
    return NO;
}

代碼的注釋很多,這一塊確實比枯澀,大家可以參照著源碼一起看,加深理解。

  • 這個方法是AFSecurityPolicy最核心的方法,其他的都是為了配合這個方法。這個方法完成了服務端的證書的信任評估。我們總結一下這個方法做了什么(細節可以看注釋):
  1. 根據模式,如果是AFSSLPinningModeNone,則肯定是返回YES,不論是自簽還是公信機構的證書。
  2. 如果是AFSSLPinningModeCertificate,則從serverTrust中去獲取證書鏈,然后和我們一開始初始化設置的證書集合self.pinnedCertificates去匹配,如果有一對能匹配成功的,就返回YES,否則NO。
    看到這可能有小伙伴要問了,什么是證書鏈?下面這段是我從百科上摘來的:

證書鏈由兩個環節組成—信任錨(CA 證書)環節和已簽名證書環節。自我簽名的證書僅有一個環節的長度—信任錨環節就是已簽名證書本身。

簡單來說,證書鏈就是就是根證書,和根據根證書簽名派發得到的證書。

  1. 如果是AFSSLPinningModePublicKey公鑰驗證,則和第二步一樣還是從serverTrust,獲取證書鏈每一個證書的公鑰,放到數組中。和我們的self.pinnedPublicKeys,去配對,如果有一個相同的,就返回YES,否則NO。至于這個self.pinnedPublicKeys,初始化的地方如下:
  //設置證書數組
   - (void)setPinnedCertificates:(NSSet *)pinnedCertificates {
    
    _pinnedCertificates = pinnedCertificates;

    //獲取對應公鑰集合
    if (self.pinnedCertificates) {
        //創建公鑰集合
        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;
    }
}

AF復寫了設置證書的set方法,并同時把證書中每個公鑰放在了self.pinnedPublicKeys中。

這個方法中關聯了一系列的函數,我在這邊按照調用順序一一列出來(有些是系統函數,不在這里列出,會在下文集體描述作用):

函數一:AFServerTrustIsValid
//判斷serverTrust是否有效
static BOOL AFServerTrustIsValid(SecTrustRef serverTrust) {
    
    //默認無效
    BOOL isValid = NO;
    //用來裝驗證結果,枚舉
    SecTrustResultType result;  
      
    //__Require_noErr_Quiet 用來判斷前者是0還是非0,如果0則表示沒錯,就跳到后面的表達式所在位置去執行,否則表示有錯就繼續往下執行。
  
    //SecTrustEvaluate系統評估證書的是否可信的函數,去系統根目錄找,然后把結果賦值給result。評估結果匹配,返回0,否則出錯返回非0
    //do while 0 ,只執行一次,為啥要這樣寫....
    __Require_noErr_Quiet(SecTrustEvaluate(serverTrust, &result), _out);

    //評估沒出錯走掉這,只有兩種結果能設置為有效,isValid= 1
    //當result為kSecTrustResultUnspecified(此標志表示serverTrust評估成功,此證書也被暗中信任了,但是用戶并沒有顯示地決定信任該證書)。
    //或者當result為kSecTrustResultProceed(此標志表示評估成功,和上面不同的是該評估得到了用戶認可),這兩者取其一就可以認為對serverTrust評估成功
    isValid = (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);

    //out函數塊,如果為SecTrustEvaluate,返回非0,則評估出錯,則isValid為NO
_out:
    return isValid;
}
  • 這個方法用來驗證serverTrust是否有效,其中主要是交由系統APISecTrustEvaluate來驗證的,它驗證完之后會返回一個SecTrustResultType枚舉類型的result,然后我們根據這個result去判斷是否證書是否有效。
  • 其中比較有意思的是,它調用了一個系統定義的宏函數__Require_noErr_Quiet,函數定義如下:
#ifndef __Require_noErr_Quiet
    #define __Require_noErr_Quiet(errorCode, exceptionLabel)                      \
      do                                                                          \
      {                                                                           \
          if ( __builtin_expect(0 != (errorCode), 0) )                            \
          {                                                                       \
              goto exceptionLabel;                                                \
          }                                                                       \
      } while ( 0 )
#endif

這個函數主要作用就是,判斷errorCode是否為0,不為0則,程序用goto跳到exceptionLabel位置去執行。這個exceptionLabel就是一個代碼位置標識,類似上面的_out
說它有意思的地方是在于,它用了一個do...while(0)循環,循環條件為0,也就是只執行一次循環就結束。對這么做的原因,樓主百思不得其解...看來系統原生API更是高深莫測...經冰霜大神的提醒,這么做是為了適配早期的API??!

函數二、三(兩個函數類似,所以放在一起):獲取serverTrust證書鏈證書,獲取serverTrust證書鏈公鑰
//獲取證書鏈
static NSArray * AFCertificateTrustChainForServerTrust(SecTrustRef serverTrust) {
    //使用SecTrustGetCertificateCount函數獲取到serverTrust中需要評估的證書鏈中的證書數目,并保存到certificateCount中
    CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);
    //創建數組
    NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount];

    //// 使用SecTrustGetCertificateAtIndex函數獲取到證書鏈中的每個證書,并添加到trustChain中,最后返回trustChain
    for (CFIndex i = 0; i < certificateCount; i++) {
        SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i);
        [trustChain addObject:(__bridge_transfer NSData *)SecCertificateCopyData(certificate)];
    }

    return [NSArray arrayWithArray:trustChain];
}

// 從serverTrust中取出服務器端傳過來的所有可用的證書,并依次得到相應的公鑰
static NSArray * AFPublicKeyTrustChainForServerTrust(SecTrustRef serverTrust) {
    
    // 接下來的一小段代碼和上面AFCertificateTrustChainForServerTrust函數的作用基本一致,都是為了獲取到serverTrust中證書鏈上的所有證書,并依次遍歷,取出公鑰。
    //安全策略
    SecPolicyRef policy = SecPolicyCreateBasicX509();
    CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);
    NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount];
    //遍歷serverTrust里證書的證書鏈。
    for (CFIndex i = 0; i < certificateCount; i++) {
        //從證書鏈取證書
        SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i);
        //數組
        SecCertificateRef someCertificates[] = {certificate};
        //CF數組
        CFArrayRef certificates = CFArrayCreate(NULL, (const void **)someCertificates, 1, NULL);

        SecTrustRef trust;
        
        // 根據給定的certificates和policy來生成一個trust對象
        //不成功跳到 _out。
        __Require_noErr_Quiet(SecTrustCreateWithCertificates(certificates, policy, &trust), _out);

        SecTrustResultType result;
        
        // 使用SecTrustEvaluate來評估上面構建的trust
        //評估失敗跳到 _out
        __Require_noErr_Quiet(SecTrustEvaluate(trust, &result), _out);

        // 如果該trust符合X.509證書格式,那么先使用SecTrustCopyPublicKey獲取到trust的公鑰,再將此公鑰添加到trustChain中
        [trustChain addObject:(__bridge_transfer id)SecTrustCopyPublicKey(trust)];

    _out:
        //釋放資源
        if (trust) {
            CFRelease(trust);
        }

        if (certificates) {
            CFRelease(certificates);
        }
    
        continue;
    }
    CFRelease(policy);

    // 返回對應的一組公鑰
    return [NSArray arrayWithArray:trustChain];
}

兩個方法功能類似,都是調用了一些系統的API,利用For循環,獲取證書鏈上每一個證書或者公鑰。具體內容看源碼很好理解。唯一需要注意的是,這個獲取的證書排序,是從證書鏈的葉節點,到根節點的。

函數四:判斷公鑰是否相同
//判斷兩個公鑰是否相同
static BOOL AFSecKeyIsEqualToKey(SecKeyRef key1, SecKeyRef key2) {
    
#if TARGET_OS_IOS || TARGET_OS_WATCH || TARGET_OS_TV
    //iOS 判斷二者地址
    return [(__bridge id)key1 isEqual:(__bridge id)key2];
#else
    return [AFSecKeyGetData(key1) isEqual:AFSecKeyGetData(key2)];
#endif
}

方法適配了各種運行環境,做了匹配的判斷。

接下來列出驗證過程中調用過得系統原生函數:
//1.創建一個驗證SSL的策略,兩個參數,第一個參數true則表示驗證整個證書鏈
//第二個參數傳入domain,用于判斷整個證書鏈上葉子節點表示的那個domain是否和此處傳入domain一致
SecPolicyCreateSSL(<#Boolean server#>, <#CFStringRef  _Nullable hostname#>)
SecPolicyCreateBasicX509();
//2.默認的BasicX509驗證策略,不驗證域名。
SecPolicyCreateBasicX509();
//3.為serverTrust設置驗證策略,即告訴客戶端如何驗證serverTrust
SecTrustSetPolicies(<#SecTrustRef  _Nonnull trust#>, <#CFTypeRef  _Nonnull policies#>)
//4.驗證serverTrust,并且把驗證結果返回給第二參數 result
SecTrustEvaluate(<#SecTrustRef  _Nonnull trust#>, <#SecTrustResultType * _Nullable result#>)
//5.判斷前者errorCode是否為0,為0則跳到exceptionLabel處執行代碼
__Require_noErr(<#errorCode#>, <#exceptionLabel#>)
//6.根據證書data,去創建SecCertificateRef類型的數據。
SecCertificateCreateWithData(<#CFAllocatorRef  _Nullable allocator#>, <#CFDataRef  _Nonnull data#>)
//7.給serverTrust設置錨點證書,即如果以后再次去驗證serverTrust,會從錨點證書去找是否匹配。
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
//8.拿到證書鏈中的證書個數
CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);
//9.去取得證書鏈中對應下標的證書。
SecTrustGetCertificateAtIndex(serverTrust, i)
//10.根據證書獲取公鑰。
SecTrustCopyPublicKey(trust)

其功能如注釋,大家可以對比著源碼,去加以理解~

分割圖.png

可能看到這,又有些小伙伴迷糊了,講了這么多,那如果做https請求,真正需要我們自己做的到底是什么呢?這里來解答一下,分為以下兩種情況:

  1. 如果你用的是付費的公信機構頒發的證書,標準的https,那么無論你用的是AF還是NSUrlSession,什么都不用做,代理方法也不用實現。你的網絡請求就能正常完成。
  2. 如果你用的是自簽名的證書:
  • 首先你需要在plist文件中,設置可以返回不安全的請求(關閉該域名的ATS)。
  • 其次,如果是NSUrlSesion,那么需要在代理方法實現如下:
    - (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
 {
          __block NSURLCredential *credential = nil;

        credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; 
        // 確定挑戰的方式
        if (credential) { 
             //證書挑戰 則跑到這里
           disposition = NSURLSessionAuthChallengeUseCredential; 
         }
        //完成挑戰
         if (completionHandler) {
             completionHandler(disposition, credential);
         }
   }

其實上述就是AF的相對于自簽證書的實現的簡化版。
如果是AF,你則需要設置policy:

//允許自簽名證書,必須的
policy.allowInvalidCertificates = YES;
//是否驗證域名的CN字段
//不是必須的,但是如果寫YES,則必須導入證書。
policy.validatesDomainName = NO;

當然還可以根據需求,你可以去驗證證書或者公鑰,前提是,你把自簽的服務端證書,或者自簽的CA根證書導入到項目中:


證書.png

并且如下設置證書:

NSString *certFilePath = [[NSBundle mainBundle] pathForResource:@"AFUse_server.cer" ofType:nil];
NSData *certData = [NSData dataWithContentsOfFile:certFilePath];
NSSet *certSet = [NSSet setWithObjects:certData,certData, nil]; 
policy.pinnedCertificates = certSet;

這樣你就可以使用AF的不同AFSSLPinningMode去驗證了。

最后總結一下,AF之于https到底做了什么:
  • AF可以讓你在系統驗證證書之前,就去自主驗證。然后如果自己驗證不正確,直接取消網絡請求。否則驗證通過則繼續進行系統驗證。

  • 講到這,順便提一下,系統驗證的流程:

    • 系統的驗證,首先是去系統的根證書找,看是否有能匹配服務端的證書,如果匹配,則驗證成功,返回https的安全數據。
  • 如果不匹配則去判斷ATS是否關閉,如果關閉,則返回https不安全連接的數據。如果開啟ATS,則拒絕這個請求,請求失敗。

總之一句話:AF的驗證方式不是必須的,但是對有特殊驗證需求的用戶確是必要的

寫在結尾:

  • 看完之后,有些小伙伴可能還是會比較迷惑,建議還是不清楚的小伙伴,可以自己生成一個自簽名的證書或者用百度地址等做請求,然后設置AFSecurityPolicy不同參數,打斷點,一步步的看AF是如何去調用函數作證書驗證的。相信這樣能加深你的理解。

  • 最后關于自簽名證書的問題,等2017年1月1日,也沒多久了...一個月不到。除非有特殊原因說明,否則已經無法審核通過了。詳細的可以看看這篇文章:iOS 10 適配 ATS(app支持https通過App Store審核)

  • 最后的最后,希望大家能點個贊,關注一下~(樓主看到贊和關注會很開心...) 有什么不同意見或者建議可以評論或者簡信我萬一有人轉載,麻煩注明出處,謝謝~

蘋果官網最新消息:原定于2017.1.1強制的https被延期了,具體延期到什么時候不確定,得等官方通知:
蘋果官方新聞.png
后續文章:

AFNetworking之UIKit擴展與緩存實現
AFNetworking到底做了什么?(終)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內容