iOS 中 HTTPS 證書驗證淺析

  • 2019 年 10 月 7 日
  • 筆記

iOS 中 HTTPS 證書驗證淺析

一、HTTPS請求過程

下面看一個普通的HTTPS請求過程:

https.png

第一階段:ClientHello

客戶端發起請求,以明文傳輸請求資訊,包含版本資訊,加密套件候選列表,壓縮演算法候選列表,隨機數random_C,擴展欄位等資訊。

第二階段:ServerHello-ServerHelloDone

iOS Request.jpeg

如上圖可以看出這個階段包含4個過程( 有的伺服器是單條發送,有的是合併一起發送)。服務端返回協商的資訊結果,包括選擇使用的協議版本,選擇的加密套件,選擇的壓縮演算法、隨機數random_S等,其中隨機數用於後續的密鑰協商。伺服器也會配置並返回對應的證書鏈Certificate,用於身份驗證與密鑰交換。然後會發送ServerHelloDone資訊用於通知伺服器資訊發送結束。

第三階段:證書校驗

在上圖中的5-6之間,客戶端這邊還需要對伺服器返回的證書進行校驗。只有證書驗證通過後,才能進行後續的通訊。(具體分析可參看後續的證書驗證過程)

第四階段:ClientKeyExchange-Finished

伺服器返回的證書驗證合法後, 客戶端計算產生隨機數字Pre-master,並用server證書中公鑰加密,發送給伺服器。同時客戶端會根據已有的三個隨機數根據相應的生成協商密鑰。客戶端會通知伺服器後續的通訊都採用協商的通訊密鑰和加密演算法進行加密通訊。然後客戶端發送Finished消息用於通知客戶端資訊發送結束。

第五階段:伺服器端生成協商密鑰

伺服器也會根據已有的三個隨機數使用相應的演算法生成協商密鑰,會通知客戶端後續的通訊都採用協商的通訊密鑰和加密演算法進行加密通訊。然後發送Finished消息用於通知伺服器資訊發送結束

第六階段:握手結束

在握手階段結束後,客戶端和伺服器數據傳輸開始使用協商密鑰進行加密通訊。

總結

簡單來說,HTTPS請求整個過程主要分為兩部分。一是握手過程:用於客戶端和伺服器驗證雙方身份,協商後續數據傳輸時使用到的密鑰等。二是數據傳輸過程:身份驗證通過並協商好密鑰後,通訊雙方使用協商好的密鑰加密數據並進行通訊。在握手過程協商密鑰時,使用的是非對稱密鑰交換演算法, 密鑰交換演算法本身非常複雜,密鑰交換過程涉及到隨機數生成,模指數運算,空白補齊,加密,簽名等操作。在數據傳輸過程中,客戶端和伺服器端使用協商好的密鑰進行對稱加密解密。

二、證書

PKI (Public Key Infrastructure),公開密鑰基礎設施。它是一個標準,在這個標準之下發展出的為了實現安全基礎服務目的的技術統稱為PKI。 權威的第三方機構CA(認證中心)是PKI的核心, CA負責核實公鑰的擁有者的資訊,並頒發認證」證書」,同時能夠為使用者提供證書驗證服務。 x.509是PKI中最重要的標準,它定義了公鑰證書的基本結構。

證書申請過程

證書申請者向頒發證書的可信第三方CA提交申請證書相關資訊,包括:申請者域名、申請者生成的公鑰(私鑰自己保存)及證書請求文件.cer等 CA通過線上、線下等多種手段驗證證書申請者提供的資訊合法和真實性。 當證書申請者提供的資訊審核通過後,CA向證書申請者頒發證書,證書內容包括明文資訊和簽名資訊。其中明文資訊包括證書頒發機構、證書有效期、域名、申請者相關資訊及申請者公鑰等,簽名資訊是使用CA私鑰進行加密的明文資訊。當證書申請者獲取到證書後,可以通過安裝的CA證書中的公鑰對簽名資訊進行解密並與明文資訊進行對比來驗證簽名的完整性。

證書驗證過程
  • 驗證證書本身的合法性(驗證簽名完整性,驗證證書有效期等)
  • 驗證證書頒發者的合法性(查找頒發者的證書並檢查其合法性,這個過程是遞歸的)
  • 證書驗證的遞歸過程最終會成功終止,而成功終止的條件是:證書驗證過程中遇到了錨點證書,錨點證書通常指:嵌入到作業系統中的根證書(權威證書頒發機構頒發的自簽名證書)。
證書驗證失敗的原因
  • 無法找到證書的頒發者
  • 證書過期
  • 驗證過程中遇到了自簽名證書,但該證書不是錨點證書。
  • 無法找到錨點證書(即在證書鏈的頂端沒有找到合法的根證書)
  • 訪問的server的dns地址和證書中的地址不同

三、iOS實現支援HTTPS

在OC中當使用NSURLConnection或NSURLSession建立URL並向伺服器發送https請求獲取資源時,伺服器會使用HTTP狀態碼401進行響應(即訪問拒絕)。此時NSURLConnection或NSURLSession會接收到伺服器需要授權的響應,當客戶端授權通過後,才能繼續從伺服器獲取數據。如下圖所示:

針對非自簽名證書驗證實現

在接收到伺服器返回的狀態碼為401的響應後,對於NSURLSession而言,需要代理對象實現URLSession:task:didReceiveChallenge:completionHandler:方法。對於NSURLConnection而言,需要代理對象實現connection:willSendRequestForAuthenticationChallenge: 方法(OS X v10.7和iOS5及以上),對於早期的版本代理對象需要實現代理對象要實現connection:canAuthenticateAgainstProtectionSpace:和connection:didReceiveAuthenticationChallenge:方法。程式碼如下:

-(void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {      //1)獲取trust object      SecTrustRef trust = challenge.protectionSpace.serverTrust;      SecTrustResultType result;      //2)使用系統SecTrustEvaluate對trust object驗證      OSStatus status = SecTrustEvaluate(trust, &result);      if (status == errSecSuccess && (status == kSecTrustResultProceed || status == kSecTrustResultUnspecified)) {          //3)驗證成功繼續請求操作          [challenge.sender useCredential:[NSURLCredential credentialForTrust:trust] forAuthenticationChallenge:challenge];      } else {          //4)不成功則馬上取消請求          [challenge.sender cancelAuthenticationChallenge:challenge];      }  }

當客戶端發送https請求後,伺服器會返回需要授權的相關資訊,然後connection:willSendRequestForAuthenticationChallenge:方法被調用。客戶端根據返回的challenge資訊,首先獲取需要驗證的信任對象trust,然後調用SecTrustEvaluate方法是用系統默認的驗證方式對信任對象進行驗證,當驗證通過時,使用該信任對象trust生成證書憑證,然後self.connection使用該憑證繼續連接。如下詳解:

NSURLAuthenticationChallenge包含如下資訊:
  • error :最後一次授權失敗的錯誤資訊
  • failureResponse :最後一次授權失敗的錯誤資訊
  • previousFailureCount :授權失敗的次數
  • proposedCredential :建議使用的證書
  • protectionSpace :NSURLProtectionSpace對象,代表了伺服器上的一塊需要授權資訊的區域。包括了伺服器地址、埠等資訊。在此指的是* challenge.protectionSpace。其中Auth-scheme指protectionSpace所支援的驗證方法,NSURLAuthenticationMethodServerTrust指對protectionSpace執行證書驗證。
  • sender:發送者,在此指的是self.session
SecTrustRef

表示需要驗證的信任對象(Trust Object),在此指的是challenge.protectionSpace.serverTrust。包含待驗證的證書和支援的驗證方法等。

SecTrustEvaluate

函數內部遞歸地從葉節點證書到根證書驗證。使用系統默認的驗證方式驗證Trust Object,根據上述證書鏈的驗證可知,系統會根據Trust Object的驗證策略,一級一級往上,驗證證書鏈上每一級證書有效性。

SecTrustResultType

表示驗證結果。其中 kSecTrustResultProceed表示serverTrust驗證成功,且該驗證得到了用戶認可(例如在彈出的是否信任的alert框中選擇always trust)。 kSecTrustResultUnspecified表示 serverTrust驗證成功,此證書也被暗中信任了,但是用戶並沒有顯示地決定信任該證書。 兩者取其一就可以認為對serverTrust驗證成功。

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),以便其他應用也能使用。

對於已經驗證通過的信任對象,客戶端也可以不提供證書憑證。

  • 對於NSURLSession,傳遞如下之一的值給completion handler回調:

NSURLSessionAuthChallengePerformDefaultHandling處理請求,就好像代理沒有提供一個代理方法來處理認證請求 NSURLSessionAuthChallengeRejectProtectionSpace拒接認證請求。基於伺服器響應的認證類型,URL載入類可能會多次調用代理方法。

  • 對於 NSURLConnection 和 NSURLDownload,在[challenge sender] 上調用continueWithoutCredentialsForAuthenticationChallenge:方法。不提供證書的話,可能會導致連接失敗,調用connectionDidFailWithError:方法 ,或者會返回一個不需要驗證身份的替代的URL。 如下程式碼:
//3)驗證成功繼續請求操作  //[challenge.sender useCredential:[NSURLCredential credentialForTrust:trust] forAuthenticationChallenge:challenge];  [challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
對於非自簽名的證書

即使伺服器返回的證書是信任的CA頒發的,而為了確定返回的證書正是客戶端需要的證書,這需要本地導入證書,並將證書設置成需要參與驗證的錨點證書,再調用SecTrustEvaluate通過本地導入的證書來驗證伺服器證書是否是可信的。如果伺服器證書是這個錨點證書對應CA或者子CA頒發的,或伺服器證書本身就是這個錨點證書,則證書信任通過。如下程式碼(參考文檔):

NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"xxx" ofType:@"cer"];  NSData *cerData = [NSData dataWithContentsOfFile:cerPath];  SecCertificateRef certificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)cerData);  self.trustedCertificates = @[CFBridgingRelease(certificate)];    -(void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {      //1)獲取trust object      SecTrustRef trust = challenge.protectionSpace.serverTrust;      SecTrustResultType result;      //將上面導入Xcode項目中的證書設置成下面驗證trust object的錨點證書      SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)self.trustedCertificates);        //2)使用系統SecTrustEvaluate對trust object驗證      OSStatus status = SecTrustEvaluate(trust, &result);      if (status == errSecSuccess && (status == kSecTrustResultProceed || status == kSecTrustResultUnspecified)) {          //3)驗證成功繼續請求操作          [challenge.sender useCredential:[NSURLCredential credentialForTrust:trust] forAuthenticationChallenge:challenge];  //        [challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];      } else {          //4)不成功則馬上取消請求          [challenge.sender cancelAuthenticationChallenge:challenge];      }  }
自簽名證書驗證實現

對於自簽名證書,這樣Trust Object中的伺服器證書是不可信任的CA頒發的,直接使用SecTrustEvaluate驗證是不會成功的。可以採取下述簡單程式碼繞過HTTPS的驗證:

-(void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {      //判斷如果是要驗證證書的話 直接使用它期望的證書      if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {          [challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge];      } else {          [challenge.sender cancelAuthenticationChallenge:challenge];      }  }

上述程式碼一般用於當伺服器使用自簽名證書時,為了方便測試,客戶端可以通過該方法信任所有自簽名證書。

綜上對非自建和自建證書驗證過程的分析,可以總結如下:
  • 獲取需要驗證的信任對象(Trust Object)。對於NSURLConnection來說, 是從delegate方法-connection: willSendRequestForAuthenticationChallenge:回調回來的參數challenge中獲取(challenge.protectionSpace.serverTrust) 。
  • 使用系統默認驗證方式驗證Trust Object。SecTrustEvaluate會根據Trust Object的驗證策略,一級一級往上,驗證證書鏈上每一級數字簽名的有效性,從而評估證書的有效性。
  • 如第二步驗證通過了,一般的安全要求下,就可以直接驗證通過,進入到下一步:使用Trust Object生成一份憑證([NSURLCredential credentialForTrust:serverTrust]),傳入challenge的sender中([challenge.sender useCredential:cred forAuthenticationChallenge:challenge])處理,建立連接。
  • 假如有更強的安全要求,可以繼續對Trust Object進行更嚴格的驗證。常用的方式是在本地導入證書,驗證Trust Object與導入的證書是否匹配。
  • 假如驗證失敗,取消此次Challenge-Response Authentication驗證流程,拒絕連接請求。
  • 假如是自建證書的,則不使用第二步系統默認的驗證方式,因為自建證書的根CA的數字簽名未在作業系統的信任列表中。

轉載

原文地址