一次iOS客戶端PKI及HTTPS雙向認證的踩坑記錄

背景

需求背景:

  • 最近公司的產品涉及App 與硬件端的交互,App與雲端的交互,硬件端與雲端的數據交換, 公司需要保障每一端的身份得到信任,於是引入PKI體系,從外部尋找了對應的PKI供應商負責協助搭建與提供PKI相關服務;
  • 當硬件端變成server角色時,需要對請求的客戶端client進行身份認證。 如果僅僅只認證server的身份,我們只用單項認證就可以了。 但是如果要對client也進行身份認證,那麼就需要用到雙向認證的相關理論和技術。
  • 要保障各個端的通信安全,需要保障在各個端通信時數據是被加密的,並且採用可信的證書籤名數據進而交換;

基礎理論

做的過程中,涉及到一些技術的名詞,先做一些解釋和理解, 包括PKI,密碼學與加解密 (公私鑰,證書鏈,CA), 證書格式, HTTPS, 雙向認證以及單項認證, 抓包分析;

PKI

A public key infrastructure (PKI) is a set of roles, policies, hardware, software and procedures needed to create, manage, distribute, use, store and revoke digital certificates and manage public-key encryption.

PKI的解釋可以參見 百度百科PKI ,或者 維基百科PKI 。 它稱作公鑰基礎設施。本質上它是一種方式方法,用來管理與實現數據的加密交換,證書管理等,是一套加解密安全保障機制;

證書

首先談一下證書,CA這些。 因爲這些內容和密碼學密切相關,可以先初步對現代密碼學的一些基礎理論和技術進行了解與熟悉。
加解密與簽名:

  • 在現在的數據安全中,涉及數據的加解密,一般有對稱加解密,非對稱加解密;
  • 當我們對數據進行散列得到摘要,在用私鑰進行加密,就得到數據的簽名;
    至於具體的加解密過程和簽名算法之類,可以自行網上搜索瞭解;

那麼,證書是什麼? 它從何而來?它又有什麼作用呢?

證書生成:

其實這裏提到的是CA證書(Certificate Authority Certificate),CA是Certificate Authority的縮寫,也叫“證書授權中心”。CA證書其實本質是一段明文數據和加密數據的組合。 CA證書可採用openssl生成;

openssl生成證書的過程可參考:

通過openssl生成私鑰: openssl genrsa -out server.key 1024
根據私鑰生成證書申請文件csr :     openssl req -new -key server.key -out server.csr 
使用私鑰對證書申請進行簽名從而生成證書:   openssl x509 -req -in server.csr -out server.crt -signkey server.key -days 3650 
這樣就生成了有效期爲:10年的證書文件 
認識證書

x509 證書一般會用到三類文件,key,csr,crt。Key是私用密鑰,openssl格式,通常是rsa算法。 csr是證書請求文件,用於申請證書。在製作csr文件的時候,必須使用自己的私鑰來簽署申請,還可以設定一個密鑰。crt是CA認證後的證書文件(windows下面的csr,其實是crt),簽署人用自己的key給你簽署的憑證。

數字證書的內容(CA證書內容)

X.509是常見通用的證書格式。所有的證書都符合爲Public Key Infrastructure (PKI) 制定的 ITU-T X509 國際標準。

X.509 是比較流行的 SSL 數字證書標準,包含(但不限於)以下的字段:

字段 值說明
對象名稱(Subject Name) 用於識別該數字證書的信息
共有名稱(Common Name) 對於客戶證書,通常是相應的域名
證書頒發者(Issuer Name) 發佈並簽署該證書的實體的信息
簽名算法(Signature Algorithm) 簽名所使用的算法
序列號(Serial Number) 數字證書機構(Certificate Authority, CA)給證書的唯一整數,一個數字證書一個序列號
生效期(Not Valid Before )
失效期(Not Valid After)
公鑰(Public Key) 可公開的密鑰
簽名(Signature) 通過簽名算法計算證書內容後得到的數據,用於驗證證書是否被篡改
指紋(fingerPrint) 證書的ID

那麼 ,CA證書一般是什麼樣的格式存在呢?

PKCS#7 常用的後綴是: .P7B .P7C .SPC ;
PKCS#12 常用的後綴有: .P12 .PFX (iOS都會知道開發者證書導入的時候用到了 .P12文件,)
X.509 DER 編碼(ASCII)的後綴是: .DER .CER .CRT ,der,cer文件一般是二進制格式的,只放證書,不含私鑰 (在iOS中一般是這種.der, .cer格式);
.cer/.crt是用於存放證書,它是2進制形式存放的,不含私鑰(crt文件可能是二進制的,也可能是文本格式的,應該以文本格式居多,功能同der/cer);
X.509 PAM 編碼(Base64)的後綴是: .PEM .CER .CRT (.pem 跟crt/cer的區別是它以 Ascii來 表示,pem文件一般是文本格式的,可以放證書或者私鑰,或者兩者都有,pem如果只含私鑰的話,一般用.key擴展名,而且可以有密碼保護)
pfx/p12用於存放個人證書/私鑰,他通常包含保護密碼,2進制方式(pfx,p12文件是二進制格式,同時含私鑰和證書,通常有保護密碼)
p10是證書請求、p7r是CA對證書請求的回覆,只用於導入、p7b以樹狀展示證書鏈(certificate chain),同時也支持單個證書,不含私鑰。

怎麼判斷是文本格式還是二進制?

用記事本打開,如果是規則的數字字母,如
—–BEGIN CERTIFICATE—–
MIIE9jCCA96gAwIBAgIQVXD9d9wgivhJM//a3VIcDjANBgkqhkiG9w0BAQUFADBy
—–END CERTIFICATE—–
就是文本的,上面的BEGIN CERTIFICATE,說明這是一個證書,如果是—–BEGIN RSA PRIVATE KEY—–,說明這是一個私鑰

自簽證書的生成:
使用openssl來生成一些列的自簽名證書
(1)創建根證書私鑰:
openssl genrsa -out root.key 1024
(2)創建根證書請求文件:
openssl req -new -out root.csr -key root.key
(3)創建根證書
openssl x509 -req -in root.csr -out root.crt -signkey root.key -CAcreateserial -days 3650

HTTPS

爲什麼要用HTTPS? 解決了什麼問題?

我們知道http的缺點:
1. 數據明文
2. 不驗證通信方的身份
3. 數據報文容易被篡改
4. 容易遭受MITM攻擊

HTTPS 是運行在 TLS/SSL 之上的 HTTP,與普通的 HTTP 相比,在數據傳輸的安全性上有很大的提升。
爲了提高安全性,我們常用的做法是使用對稱加密的手段加密數據。可是隻使用對稱加密的話,雙方通信的開始總會以明文的方式傳輸密鑰。那麼從一開始這個密鑰就泄露了,談不上什麼安全。所以 TLS/SSL 在握手的階段,結合非對稱加密的手段,保證只有通信雙方纔知道對稱加密的密鑰。

So, 爲什麼要用雙向認證? 雙向認證解決了什麼問題?

雙向認證,顧名思義,客戶端和服務器端都需要驗證對方的身份,在建立https連接的過程中,握手的流程比單向認證多了幾步。單向認證的過程,客戶端從服務器端下載服務器端公鑰證書進行驗證,然後建立安全通信通道。
雙向通信流程,客戶端除了需要從服務器端下載服務器的公鑰證書進行驗證外,還需要把客戶端的公鑰證書上傳到服務器端給服務器端進行驗證,等雙方都認證通過了,纔開始建立安全通信通道進行數據傳輸。
能保證通信的雙方是指定的端,在很多P2P(端對端)的通信中,也有很多實際的應用。

雙向認證

什麼是單向認證?

  1. 客戶端發起建立HTTPS連接請求,將SSL協議版本的信息發送給服務器端;
  2. 服務器端將本機的公鑰證書(server.crt)發送給客戶端;
  3. 客戶端讀取公鑰證書(server.crt),取出了服務端公鑰;
  4. 客戶端生成一個隨機數(密鑰R),用剛纔得到的服務器公鑰去加密這個隨機數形成密文,發送給服務端;
  5. 服務端用自己的私鑰(server.key)去解密這個密文,得到了密鑰R
  6. 服務端和客戶端在後續通訊過程中就使用這個密鑰R進行通信了。

什麼是雙向認證?

  1. 客戶端發起建立HTTPS連接請求,將SSL協議版本的信息發送給服務端;
  2. 服務器端將本機的公鑰證書(server.crt)發送給客戶端;
  3. 客戶端讀取公鑰證書(server.crt),取出了服務端公鑰;
  4. 客戶端將客戶端公鑰證書(client.crt)發送給服務器端;
  5. 服務器端解密客戶端公鑰證書,拿到客戶端公鑰;
  6. 客戶端發送自己支持的加密方案給服務器端;
  7. 服務器端根據自己和客戶端的能力,選擇一個雙方都能接受的加密方案,使用客戶端的公鑰加密後發送給客戶端;
  8. 客戶端使用自己的私鑰解密加密方案,生成一個隨機數R,使用服務器公鑰加密後傳給服務器端;
  9. 服務端用自己的私鑰去解密這個密文,得到了密鑰R
  10. 服務端和客戶端在後續通訊過程中就使用這個密鑰R進行通信了。

可以理解爲,在SSL握手階段,客戶端和服務端通過非對稱加密,以及證書鏈校驗,協商並確保雙方得到一個會話祕鑰,連接成功後,採用這個會話祕鑰對數據進行加密傳輸,就可以保障數據的安全性;

證書的校驗是如何執行的?

首先,客戶端收到服務端發來的證書鏈數據,類似下圖:


服務端證書一般由中間證書籤發,而中間證書由根證書進行簽發,根證書由CA機構生成,上一級的證書是下一級證書的簽發者,由此形成的證書樹形結構,就是證書鏈。證書鏈校驗過程如下:

1.取上級證書的公鑰,對下級證書的簽名進行解密得出下級證書的摘要digest1 ;
2.對下級證書進行信息摘要digest2;
3.判斷digest1是否等於digest2,相等則說明下級證書校驗通過;

  1. 依次對各個相鄰級別證書實施1~3步驟,直到根證書(或者可信任錨點[trusted anchor]);

備註:因爲下級證書是上級證書CA進行簽發頒佈的,上級CA會用自己的私鑰,對簽發的下級證書的相關信息進行加密,得到下級證書的簽名;
所以上級證書的公鑰能夠解密下級證書的簽名,也能證明下級證書的上級CA 是正確的。同時根證書或者錨點證書是內置在系統中作爲可信證書的

另外: 查看證書的數據顯示,還有一個叫做指紋的,而指紋是證書的唯一值,通常用於在證書庫中查找特定證書。

指紋不是證書的一部分。相反,它是通過獲取整個證書(包括簽名)的加密哈希來計算的。
不同的加密實現可能使用不同的散列算法來計算指紋,從而爲同一證書提供不同的指紋。
(例如,Windows Crypto API計算證書的SHA-1哈希以計算指紋,而OpenSSL可以生成SHA-256或SHA-1哈希。)因此,您需要確保使用數據庫指紋的客戶端使用相同的API或一致的哈希算法。


iOS中的雙向認證:

目前我們iOS的項目是OC語言的項目,項目中的網絡請求用了著名的第三方開源庫AFNetwoking, 我們知道AFNetworking也是基於蘋果原生網絡類NSURLSession封裝得到。  
目前獲取數據的接口API後端環境 ,已開啓了雙向認證,服務端雙向認證的配置由IT人員完成。於是需要在AFN請求接口的時候完成HTTPS雙向認證的處理。

還有一部分也需要注意,App當中會涉及利用WKWebView加載一些H5頁面,而前端的網頁部署是同一套環境的時候,訪問也需要完成HTTPS的認證,同時網頁當中也會通過我們的API接口訪問一些數據,這些也是需要完成HTTPS雙向認證。 所以,iOS當中會有至少兩部分的HTTPS認證處理。
實際處理如下:

  1. 第一部分,項目工程ATS的配置
  2. 第二部分,針對接口網絡請求: NSURLSession,NSURLConnection 等的認證處理;
  3. 第三部分,針對WKWebView加載H5頁面的處理;

iOS的網絡請求中,有URL Loading System,是整個網絡請求上層的基礎:

在認證過程中,我們最主要接觸到和處理的是: NSURLSession 、NSURLCredential 、NSURLProtocol

接口網絡請求的雙向認證處理:
  • 當在iOS中發起一個網絡請求,如果是HTTPS的域名,NSURLSession會觸發回調方法,-URLSession:didReceiveChallenge:completionHandler: 回調中會收到一個 challenge,也就是質詢,需要你提供認證信息才能完成連接。通過challenge.protectionSpace.authenticationMethod 取得保護空間protectionSpace要求我們認證的方式;
  • 如果這個值是 NSURLAuthenticationMethodServerTrust 的話,代表需要對服務端證書的認證挑戰進行處置,如果這個值是 NSURLAuthenticationMethodClientCertificate 的話,代表服務端要求客戶端提供證書接受認證挑戰;

查看AFN的源碼知道,AFHTTPSessionManager實例有個方法

  • setSessionDidReceiveAuthenticationChallengeBlock:
    就是用來實現 -URLSession:didReceiveChallenge:completionHandler: 的代理方法的, 所以,對於接口的HTTPS雙向認證,我們都可以放在這個代理方法中去實現, 具體實現後面再說。
WKWebview加載網頁H5的雙向認證處理:

UIWebView UIWebView does not provide any way for an app to customize its HTTPS server trust evaluations (r. 10131336) . You can work around this limitation using an NSURLProtocol subclass, as illustrated by Sample Code 'CustomHTTPProtocol'.

從官方 HTTPS Server Trust Evaluation 的介紹來看,對於webView加載自簽名的HTTPS網站,不能直接採用NSURLSession的方式處理。根據 iOS使用NSURLProtocol來Hook攔截WKWebview請求 的介紹, 因爲webView的內核通信是IPC(進程間通信),和APP是不同的進程,不能對web的請求直接進行攔截處理。

WKWebView 在獨立於 App Process 進程之外的進程中執行網絡請求,請求數據不經過主進程,因此,在WKWebView 上直接使用 NSURLProtocol 無法攔截請求。

第一種處理方式: 我們可以使用私有類 WKBrowsingContextController 通過 registerSchemeForCustomProtocol 方法向 WebProcessPool 註冊全局自定義 scheme 來達到我們的目的

同時,在讓WKWebview支持NSURLProtocol 的時候,也需要注意一些官方對於WKWebView的審覈規則,避免出現私有API的調用,具體實現方式見後面。

因爲以上第一種處理方式,經驗證,避免不了Web當中存在的POST請求body數據被清空,需要額外處理,同時,處理的方式也比較複雜。 但發現WKWebView已經提供了代理方法,用來處理自定義證書信任策略等,於是採用 第二種處理方式 ,我們在

- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *_Nullable credential))completionHandler

當中去實現H5的雙向認證邏輯

實踐

編碼

根據整個業務流程,我們把編碼實現過程大致分爲以下幾步:

  1. App啓動時候,通過PKI供應商的HTTPS通道,進行證書的申請和校驗,並將證書保存在App中,並且完成ATS的設置;
  2. 從證書當中導出雙向認證需要的自簽名的根證書,以及該移動設備關聯的客戶端證書數據;
  3. 在App當中接口請求的時候,通過根證書,以及客戶端證書數據完成雙向認證;
  4. 在App當中完成加載H5頁面時,攔截並重構請求,完成網頁及網頁請求接口的雙向認證,通過代理實現雙向認證;
  5. 優化App應用證書的啓動過程,以及證書管理的安全等;

證書的申請,因爲採用第三方的SDK實現的,編碼工作較爲簡單,簡單調用SDK的API實現即可,本質上是一個文件下載的過程,下載下來的證書數據格式爲 .pfx / .p12,保存在App的沙河目錄下,有口令可以對P12文件數據進行導出導入,而ATS設置中,需要對於公司HTTPS域名進行例外處理,而ATS不開啓會觸發額外的審覈,上架時候需要說明ATS設置的緣由

<key>NSAppTransportSecurity</key>
    <dict>
        <key>NSAllowsArbitraryLoadsInWebContent</key>
        <true/>
        <key>NSAllowsArbitraryLoads</key>
        <false/>
        <key>NSExceptionDomains</key>
        <dict>
            <key>xxxx.com</key>
            <dict>
                <key>NSExceptionAllowsInsecureHTTPLoads</key>
                <true/>
                <key>NSIncludesSubdomains</key>
                <true/>
            </dict>
        </dict>
    </dict>

從證書當中導出所需數據

    NSData *PKCS12Data = [NSData dataWithContentsOfFile:localFilePath];
    self.pkcs12FileData = PKCS12Data;
    if (PKCS12Data) {
        SecTrustRef trust = NULL;
        if ([self extractServerTrustData:&trust fromPKCS12Data:PKCS12Data]) {
            CFIndex certCount;
            certCount = SecTrustGetCertificateCount(trust);
            if (certCount >= 1) {
                SecCertificateRef certificate = SecTrustGetCertificateAtIndex(trust, certCount - 1);
                NSData *data = (__bridge_transfer NSData *)SecCertificateCopyData(certificate);
                self.serverRootCerData = data;
            }
        }
    }

// extractServerTrustData當中實現數據的獲取
NSDictionary *optionsDictionary = [NSDictionary dictionaryWithObject:authCode
                                                                  forKey:(__bridge id)kSecImportExportPassphrase];
    CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
   securityError = SecPKCS12Import((__bridge CFDataRef)inPKCS12Data, (__bridge CFDictionaryRef)optionsDictionary, &items);
    if (securityError == 0) {
        CFDictionaryRef trustDataDict = CFArrayGetValueAtIndex(items, 0);
        const void *tempTrust = NULL;
        tempTrust = CFDictionaryGetValue(trustDataDict, kSecImportItemTrust);
        *outTrust = (SecTrustRef)tempTrust;
        *outIdentity = (SecIdentityRef)CFDictionaryGetValue(trustDataDict, kSecImportItemIdentity);//客戶端證書憑證數據
        *certArr = (CFArrayRef)CFDictionaryGetValue(certDataDict, kSecImportItemCertChain);
    }

接口進行雙向認證

// 如果是NSURLSession請求,代理中進行處理, AFN請求中,調用 [manager setSessionDidReceiveAuthenticationChallengeBlock:]

- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *_Nullable))completionHandler {

NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    NSURLCredential *customCredential = nil;
     // 對服務端證書進行認證
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        OSStatus err;
        SecTrustRef trust = [[challenge protectionSpace] serverTrust];
        if (trust == NULL) {
            return;
        }
        // 設置錨點證書
        NSMutableArray *policies = [NSMutableArray array];
        [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
        SecTrustSetPolicies(trust, (__bridge CFArrayRef)policies);

        NSMutableArray *pinnedCertificates = [NSMutableArray array];
        // 證書的數據是之前獲取的根證書數據
        [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)serverRootCerData)];
        err = SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)pinnedCertificates);
        if (err == noErr) {
            err = SecTrustSetAnchorCertificatesOnly(trust, false);
        }
        CFErrorRef error = NULL;
        if (@available(iOS 12.0, *)) {
            __unused bool r = SecTrustEvaluateWithError(trust, &error);
            if (error == noErr) {
                customCredential = [NSURLCredential credentialForTrust:trust];
            } 
        } else {
            SecTrustResultType trustResult = kSecTrustResultInvalid;
            err = SecTrustEvaluate(trust, &trustResult); //kSecTrustResultRecoverableTrustFailure
            if (err == noErr) {
                if ((trustResult == kSecTrustResultProceed) || (trustResult == kSecTrustResultUnspecified)) {
                    customCredential = [NSURLCredential credentialForTrust:trust];
                }
            }
        }
    } else {
        // 服務端接收客戶端證書認證
        SecIdentityRef identity = NULL;
        CFArrayRef certArray = NULL;
        if ([self extractIdentity:&identity fromPKCS12Data:pkcs12FileData certArray:&certArray]) {
            // identity 系統使用的證書數據身份,客戶端證書對應的數據
            // certificates 建議傳nil,除非服務端需要傳遞 intermediate certifate,一般服務端內置了中間證書
            // NSURLCredentialPersistenceForSession  對於網絡雙向認證,只用填寫這個值就可以
            if (identity) {
                customCredential = [NSURLCredential credentialWithIdentity:identity certificates:(__bridge NSArray *)certArray persistence:NSURLCredentialPersistenceForSession];
            }
            disposition = NSURLSessionAuthChallengeUseCredential;
        }
    }

    if (completionHandler) {
        completionHandler(disposition, customCredential);
    }
 }

App WKWebView加載 H5

  1. 首先需要通過NSURLProtocol 對webView請求攔截重構,實現雙向認證的處理
    // 避免私有API審覈被拒 
 Class cls = [[[WKWebView new] valueForKey:@"browsingContextController"] class];
   SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
 
    if (cls && [cls respondsToSelector:sel]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        // 只需要匹配https的scheme, 這裏不攔截http協議的請求
        [cls performSelector:sel withObject:@"https"];
#pragma clang diagnostic pop
    }
    // 註冊網絡請求協議代理類到URL Loading System
    [NSURLProtocol registerClass:LLURLProtocol.class];

在 LLURLProtocol的類中,我們把webView的請求進行了HOOK,然後通過 NSURLSession 構造了新的請求。LLURLProtocol是繼承自NSURLProtocol, NSURLProtocol需要實現幾個協議方法,可以 理解 NSURLProtocol:

+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
- (void)startLoading;
- (void)stopLoading;

於是我做了如下的實現:

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    if (![request.URL.scheme isEqualToString:@"https"]) {
        return NO;
    }
    if ([NSURLProtocol propertyForKey:HTTPHandledIdentifier inRequest:request]) {
        return NO;
    }
    // 對於指定的host,允許hook,則返回YES,否則NO
    return result;
}

// 插入防止重複請求的標誌
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    NSMutableURLRequest *mutableReqeust = [request mutableCopy];
    [NSURLProtocol setProperty:@YES
                        forKey:HTTPHandledIdentifier
                     inRequest:mutableReqeust];
    return mutableReqeust;
}

// 啓動請求
- (void)startLoading {
    self.startDate = [NSDate date];
    self.data = [NSMutableData data];
    NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
    self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
    self.dataTask = [self.session dataTaskWithRequest:self.request];
    [self.dataTask resume];
}
// 請求結束
 - (void)stopLoading {
    [self.dataTask cancel];
    self.dataTask = nil;
}

- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *_Nullable))completionHandler {
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    NSURLCredential *customCredential = nil;
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        }
     // 裏面的實現方式和第3步的是一樣的。
 }

至此,我們便完成對於WebView加載自簽名的HTTPS的H5加載了。

以上第一種實現方式有bug,採用第二種webView代理的方式直接實現:

// 處理webView雙向認證的信任邏輯
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *_Nullable credential))completionHandler {
    // 只對API的域名進行雙向認證處理,其他web頁不處理
    NSString *authHost = challenge.protectionSpace.host;
    if (![authHost containsString:@"xxx.com"]) {
        NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        NSURLCredential *customCredential = nil;
        if (completionHandler) {
            completionHandler(disposition, customCredential);
        }
        return;
    }

    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengeUseCredential;
    NSURLCredential *customCredential = nil;
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        // 服務端認證
        OSStatus err;
        SecTrustRef trust = [[challenge protectionSpace] serverTrust];
        // 設置錨點證書
        NSMutableArray *policies = [NSMutableArray array];
        [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
        SecTrustSetPolicies(trust, (__bridge CFArrayRef)policies);
        NSMutableArray *pinnedCertificates = [NSMutableArray array];
        [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)serverRootCerData)];
        [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)serverIntermediateCerData)];

        err = SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)pinnedCertificates);
        if (err == noErr) {
            err = SecTrustSetAnchorCertificatesOnly(trust, false);
        }
        if (err != noErr) {
            NSLog(@"\nset anchor certificates failed!, pinnedCertificates: %@ \n", pinnedCertificates);
        }
        CFErrorRef error = NULL;
        if (@available(iOS 12.0, *)) {
            __unused bool r = SecTrustEvaluateWithError(trust, &error);
            if (error == noErr) {
                customCredential = [NSURLCredential credentialForTrust:trust];
            } else {
                NSLog(@"\nverify server certificates failed!, pinnedCertificates: %@ \n", pinnedCertificates);
            }

        } else {
            SecTrustResultType trustResult = kSecTrustResultInvalid;
            err = SecTrustEvaluate(trust, &trustResult); //kSecTrustResultRecoverableTrustFailure
            if (err == noErr) {
                if ((trustResult == kSecTrustResultProceed) || (trustResult == kSecTrustResultUnspecified)) {
                    customCredential = [NSURLCredential credentialForTrust:trust];
                }
            }
        }
    } else {
        // 客戶端發送證書認證
        SecIdentityRef identity = NULL;
        CFArrayRef certArray = NULL;
        // 從PKCS12文件數據導出identity
        if ([self extractIdentity:&identity fromPKCS12Data:pkcs12FileData certArray:&certArray]) {
            // identity 系統使用的證書數據身份,客戶端證書對應的數據
            // certificates 建議傳nil,除非服務端需要傳遞 intermediate certifate,一般服務端內置了中間證書
            // NSURLCredentialPersistenceForSession  對於網絡雙向認證,只用填寫這個值就可以
            if (identity) {
                customCredential = [NSURLCredential credentialWithIdentity:identity certificates:(__bridge NSArray *)certArray persistence:NSURLCredentialPersistenceForSession];
            }
            disposition = NSURLSessionAuthChallengeUseCredential;
        }
    }

    if (completionHandler) {
        completionHandler(disposition, customCredential);
    }
}


調試

當需要對網絡數據包進行分析,需要和IT人員一起查找並確定網關,或者服務端代理,配置等問題時,需要有好的工具纔可以讓我們事半功倍。 而在mac上,Apple官方也給出了建議的參考工具,如 Taking Advantage of Third-Party Network Debugging Tools 所說,包括其他很多IT人員也會用,選擇 WireShark 這款應用。

Wireshark is a free and open source packet analyzer that supports macOS.

至於該如何使用,我們可以參照網絡的使用說明,簡單看一下怎麼抓包使用:
打開wireShark應用,然後我們連接的網絡是有線USB 還是無線Wi-Fi,選擇後就可以進入抓包界面。


一般情況下,我們需要過濾我們關心的端口,或者協議,支持篩選:


抓包完成停止後,就可以 在File - > Export specified Packets ,當中導出當前的抓包數據, 好了,接下來把抓包數據給IT人員一起分析,查找問題吧!


總結

  1. PKI和HTTPS雙向認證, 第一步首先得將需要的服務端證書,以及客戶端證書數據打包到App當中,至於證書的獲取形式是預先打包,還是通過可靠的下載渠道進行下載,就跟業務的規劃有關了;

  2. 要完成HTTPS雙向認證,需要分別對於接口的請求,NSURLSession的代理進行證書數據的校驗,也需要對於WKWebView加載的H5做HOOK,然後轉到代理去完成證書的認證

  3. 調試中利用好一些工具比如WireShark,從抓包數據可以直接看到SSL握手過程,以及證書數據的傳輸等,哪個環節出現了問題,更有效地排查解決掉問題。

  4. 還有個問題需要繼續測試並觀察:

    iOS - NSProtocol 攔截 WKWebView POST 請求 body 會被清空的問題解決

    【騰訊Bugly乾貨分享】WKWebView 那些坑

    可以棄用攔截WKWebView的方式,在WKWebview的代理當中完成授權認證,可以避免這種問題出現

最後:文中的很多內容都是自己摸索和不斷理解得到的,如有理解偏差的,歡迎指正交流!


參考

iOS 中對 HTTPS 證書鏈的驗證
HTTPS雙向認證研究
證書的信任鏈校驗:certificate trust chain
iOS使用NSURLProtocol來Hook攔截WKWebview請求並回放的一種姿勢
Overriding TLS Chain Validation Correctly
Creating Certificates for TLS Testing
HTTPS Server Trust Evaluation
Taking Advantage of Third-Party Network Debugging Tools
iOS 12、macOS 10.14、watchOS 5 和 tvOS 12 中可用的受信任根證書列表

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章