1. 程式人生 > 實用技巧 >iOS開發筆記 - 單向認證

iOS開發筆記 - 單向認證

關於HTTPS

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

場景分析

訪問HTTPS://xxx的網站,對於HTTPS而言,在整個傳送請求返回資料過程中,除了傳送請求-伺服器響應請求-結果返回並顯示外,還涉及到通訊雙方證書驗證、資料加密、資料完整性校驗等。
在瀏覽器與伺服器進行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:方法。
程式碼如下:

#pragma mark - NSURLConnectionDelegate

- (void)connection:(NSURLConnection *)connection
willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
    SecTrustRef trust = challenge.protectionSpace.serverTrust;
    SecTrustResultType result;
    
    OSStatus status = SecTrustEvaluate(trust, &result);
    if (status == errSecSuccess &&
        (result == kSecTrustResultProceed ||
         result == kSecTrustResultUnspecified))
    {
        [challenge.sender useCredential:[NSURLCredential credentialForTrust:trust]
             forAuthenticationChallenge:challenge];
    }
    else
    {
        [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.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。
[[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];

對於非自建的證書,即使伺服器返回的證書是信任的CA頒發的,而為了確定返回的證書正是客戶端需要的證書,這需要本地匯入證書,並將證書設定成需要參與驗證的錨點證書,再呼叫SecTrustEvaluate通過本地匯入的證書來驗證伺服器證書是否是可信的。如果伺服器證書是這個錨點證書對應CA或者子CA頒發的,或伺服器證書本身就是這個錨點證書,則證書信任通過。如下程式碼

    // - 認證服務端證書
    if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust) {
        // - 錨點證書路徑
        NSString *filePath = [[NSBundle mainBundle] pathForResource:@"ca.cer" ofType:nil];
        NSData *cerData    = [NSData dataWithContentsOfFile:filePath];``
        // - 讀取匯入的證書的資料生成一個證書物件
        SecCertificateRef cert = SecCertificateCreateWithData(kCFAllocatorDefault,(CFDataRef)cerData);
        // - 將證書物件新增到陣列中
        self.trustedCerArr = @[(__bridge_transfer id)cert];

        OSStatus err;
        SecTrustResultType  trustResult = kSecTrustResultInvalid;

        // - 獲取伺服器的trust object
        SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;

        // - 將讀取的證書設定為serverTrust的根證書  假設你的伺服器返回:[自簽名的根證書] -- [二級證書] -- [客戶端證書],系統是不信任這個三個證書的。所以你在驗證的時候需要將這三個的其中一個或多個設定為錨點證書
        err = SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)self.trustedCerArr);

        if(err == noErr){
            // - 通過本地匯入的證書來驗證伺服器的證書是否可信,如果將SecTrustSetAnchorCertificatesOnly設定為NO,則只要通過本地或者系統證書鏈任何一方認證就行
            err = SecTrustEvaluate(serverTrust, &trustResult);
        }
        if (err == errSecSuccess && (trustResult == kSecTrustResultProceed || trustResult == kSecTrustResultUnspecified)){
            // - 認證成功
            [challenge.sender useCredential:[NSURLCredential credentialForTrust:serverTrust]
                 forAuthenticationChallenge:challenge];
        }
        else if (err == errSecSuccess && trustResult == kSecTrustResultRecoverableTrustFailure) {
            // - 域名和證書中的不匹配,或者證書過期等
            [challenge.sender cancelAuthenticationChallenge:challenge];
//            [challenge.sender useCredential:[NSURLCredential credentialForTrust:serverTrust]
//                 forAuthenticationChallenge:challenge];

        }
        else{
            // - 認證失敗
            [challenge.sender cancelAuthenticationChallenge:challenge];

        }

    }

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

- (void)connection:(NSURLConnection *)connection
willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
    [challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]
         forAuthenticationChallenge:challenge];
    [challenge.sender continueWithoutCredentialForAuthenticationChallenge: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的數字簽名未在作業系統的信任列表中。

WKWebview認證

在didReceiveAuthenticationChallenge方法中寫入上面的方法

// -- 信任HTTPS
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler {
     // - 錨點證書路徑
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"ca.cer" ofType:nil];
    NSData *cerData    = [NSData dataWithContentsOfFile:filePath];
    // - 讀取匯入的證書的資料生成一個證書物件
    SecCertificateRef cert = SecCertificateCreateWithData(kCFAllocatorDefault,(CFDataRef)cerData);
    // - 將證書物件新增到陣列中
    NSArray * trustedCerArr = @[(__bridge_transfer id)cert];

    OSStatus err;
    SecTrustResultType  trustResult = kSecTrustResultInvalid;

    // - 獲取伺服器的trust object
    SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;

    // - 將讀取的證書設定為serverTrust的根證書  假設你的伺服器返回:[自簽名的根證書] -- [二級證書] -- [客戶端證書],系統是不信任這個三個證書的。所以你在驗證的時候需要將這三個的其中一個或多個設定為錨點證書
    err = SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)trustedCerArr);
    if(err == noErr){
        // - 通過本地匯入的證書來驗證伺服器的證書是否可信,如果將SecTrustSetAnchorCertificatesOnly設定為NO,則只要通過本地或者系統證書鏈任何一方認證就行
        err = SecTrustEvaluate(serverTrust, &trustResult);
    }
    if (err == errSecSuccess && (trustResult == kSecTrustResultProceed || trustResult == kSecTrustResultUnspecified)){
        // - 認證成功
            NSURLCredential *credential = [[NSURLCredential alloc] initWithTrust:serverTrust];
            [challenge.sender useCredential:credential forAuthenticationChallenge:challenge];
            completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
    }
    else if (err == errSecSuccess && trustResult == kSecTrustResultRecoverableTrustFailure) {
        // - 域名和證書中的不匹配,或者證書過期等
        completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
    }
    else{
        // - 認證失敗
        completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);

    }
    
}

如果是忽略認證,忽略不受信任的https則如下程式碼

- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler {
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        NSURLCredential *credential = [[NSURLCredential alloc]initWithTrust:challenge.protectionSpace.serverTrust];
        completionHandler(NSURLSessionAuthChallengeUseCredential,credential);
    }
}

參考:https://www.jianshu.com/p/b909a9223c3b