iOS 中 HTTPS 證書驗證淺析

本文來自於騰訊Bugly公眾號(weixinBugly),未經作者同意,請勿轉載,原文地址:iOS 中 HTTPS 證書驗證淺析

導語

本文的目的,一是簡要分析下對伺服器身份驗證的完整握手過程,二是證書鏈的驗證,三是探索下iOS中原生庫NSURLConnection或NSURLSession如何支持實現https。

一、HTTPS

HTTPS是承載在TLS/SSL之上的HTTP,相較於HTTP明文數據傳輸方面所暴露出的缺點,HTTPS具有防止信息被竊聽、篡改、劫持,提供信息加密,完整性校驗及身份驗證等優勢。TLS/SSL是安全傳輸層協議,介於TCP和HTTP之間。TLS1.0是建立在SSL3.0規範之上的,可以理解為SSL3.0的升級版本。目前推薦使用的版本是TLS1.2。

TLS/SSL協議通常分為兩層:TLS記錄協議(TLS Record Protocol)和TLS握手協議(TLS Handshake Protocol)。 TLS記錄協議建立在可靠的傳輸協議(如TCP)之上,為高層協議提供數據封裝、壓縮、加密等基本功能的支持。TLS握手協議建立在記錄協議之上,用於在實際的數據傳輸開始前,通訊雙方進行身份認證、協商加密演算法、交換加密密鑰等。除了這倆協議以外,還存在其它三種輔助協議: Changecipher spec 協議用來通知對端從handshake切換到record協議(有點冗餘,在TLS1.3裡面已經被刪掉了)。alert協議,用來通知各種返回碼。application data協議,就是把http,smtp等的數據流傳入record層做處理並傳輸。

想像一種場景:通常我們會訪問 http://xxx 的網站,當你在瀏覽器地址欄輸入支持HTTPS協議的URL地址後,伺服器返回的數據會顯示在頁面上。對於不了解HTTPS協議工作原理的小夥伴可能覺得這個過程很簡單:發送請求-伺服器響應請求-結果返回並顯示。但對於HTTPS而言,在整個發送請求返回數據過程中還涉及到通訊雙方證書驗證、數據加密、數據完整性校驗等。

下面以登錄qq郵箱為例,通過Wireshark抓包可以看到如下圖:

在瀏覽器與伺服器進行Application Data傳輸之前,還經歷了Client Hello-Server Hello-Client Key Exchange-Change Cipher Spec等過程。而這些過程正是TLS/SSL提供的服務所決定的:

  • 認證伺服器身份,確保數據發送到正確的伺服器;
  • 加密數據以防止數據中途被竊取;
  • 維護數據的完整性,確保數據在傳輸過程中不被改變。

上述單向驗證的完整握手過程,總結如下:

第一階段:ClientHello

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

第二階段:ServerHello-ServerHelloDone

如上圖可以看出這個階段包含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:方法。代碼如下(參考文檔):

當客戶端發送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.connection

SecTrustRef

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

SecTrustResultType

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

SecTrustEvaluate

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

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。 如下代碼:

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

自簽名證書驗證實現

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

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

綜上對非自建和自建證書驗證過程的分析,可以總結如下:

  • 獲取需要驗證的信任對象(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的數字簽名未在操作系統的信任列表中。

參考文檔:

Overriding TLS Chain Validation Correctly

Making HTTP and HTTPS Request

HTTPS Server Trust Evaluation

NSURLConnection -HTTPS Server Trust Evaluation

Glossary-HTTPS Server Trust Evaluation

更多精彩內容歡迎關注騰訊 Bugly的微信公眾賬號:

騰訊 Bugly是一款專為移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的情況以及解決方案。智能合併功能幫助開發同學把每天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響用戶數最多的崩潰,精準定位功能幫助開發同學定位到出問題的代碼行,實時上報可以在發布後快速的了解應用的質量情況,適配最新的 iOS, Android 官方操作系統,鵝廠的工程師都在使用,快來加入我們吧!


推薦閱讀:

從零部署一個https網站
如何評價 Chrome Android 不再選用 ChaCha20 作為首選演算法?
TLS完全指南(三):用Go語言寫HTTPS程序
部署全站HTTPS有哪些值得分享的經驗和教訓?
搭建基於 Nginx 的 Https 站點

TAG:iOS开发 | HTTPS | SSL |