寫在開頭:
本來這篇內容准備寫在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可以先去驗證服務端的證書。如果通不過,則直接越過系統的驗證,取消http的網絡請求。否則,繼續去走系統根證書的驗證。
接下來我們看看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 *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 證書)環節和已簽名證書環節。自我簽名的證書僅有一個環節的長度—信任錨環節就是已簽名證書本身。
簡單來說,證書鏈就是就是根證書,和根據根證書簽名派發得到的證書。
3.如果是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(, ) SecPolicyCreateBasicX509(); //2.默認的BasicX509驗證策略,不驗證域名。 SecPolicyCreateBasicX509(); //3.為serverTrust設置驗證策略,即告訴客戶端如何驗證serverTrust SecTrustSetPolicies(, ) //4.驗證serverTrust,並且把驗證結果返回給第二參數 result SecTrustEvaluate(, ) //5.判斷前者errorCode是否為0,為0則跳到exceptionLabel處執行代碼 __Require_noErr(, ) //6.根據證書data,去創建SecCertificateRef類型的數據。 SecCertificateCreateWithData(, ) //7.給serverTrust設置錨點證書,即如果以後再次去驗證serverTrust,會從錨點證書去找是否匹配。 SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates); //8.拿到證書鏈中的證書個數 CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust); //9.去取得證書鏈中對應下標的證書。 SecTrustGetCertificateAtIndex(serverTrust, i) //10.根據證書獲取公鑰。 SecTrustCopyPublicKey(trust)
其功能如注釋,大家可以對比著源碼,去加以理解~
分割圖.png
可能看到這,又有些小伙伴迷糊了,講了這麼多,那如果做https請求,真正需要我們自己做的到底是什麼呢?這裡來解答一下,分為以下兩種情況:
如果你用的是付費的公信機構頒發的證書,標准的https,那麼無論你用的是AF還是NSUrlSession,什麼都不用做,代理方法也不用實現。你的網絡請求就能正常完成。
如果你用的是自簽名的證書:
首先你需要在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審核)。
最後的最後,希望大家能點個贊,關注一下~(樓主看到贊和關注會很開心...) 有什麼不同意見或者建議可以評論或者簡信我~萬一有人轉載,麻煩注明出處,謝謝~~
後續文章:
AFNetworking之UIKit擴展與緩存實現
AFNetworking到底做了什麼?(終)