1. 程式人生 > >iOS 中對 HTTPS 證書鏈的驗證

iOS 中對 HTTPS 證書鏈的驗證

稍微整理了下,共扯了三部分內容:

  1. HTTPS 簡要原理;
  2. 數字證書的內容、生成及驗證;
  3. iOS 上對證書鏈的驗證。

HTTPS 概要

HTTPS 是執行在 TLS/SSL 之上的 HTTP,與普通的 HTTP 相比,在資料傳輸的安全性上有很大的提升。
要了解它安全性的巧妙之處,需要先簡單地瞭解對稱加密非對稱加密的區別:

  • 對稱加密只有一個金鑰,加密和解密都用這個金鑰;
  • 非對稱加密有公鑰和私鑰,私鑰加密後的內容只有公鑰才能解密,公鑰加密的內容只有私鑰才能解密。

為了提高安全性,我們常用的做法是使用對稱加密的手段加密資料。可是隻使用對稱加密的話,雙方通訊的開始總會以明文的方式傳輸金鑰。那麼從一開始這個金鑰就洩露了,談不上什麼安全。所以 TLS/SSL 在握手的階段,結合非對稱加密的手段,保證只有通訊雙方才知道對稱加密的金鑰。大概的流程如下:


TSL:SSL_handshake.png

所以,HTTPS 實現傳輸安全的關鍵是:在 TLS/SSL 握手階段保證僅有通訊雙方得到 Session Key!

數字證書的內容

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) 通過簽名演算法計算證書內容後得到的資料,用於驗證證書是否被篡改

除了上述所列的欄位,還有很多拓展欄位,在此不一一詳述。

下圖為 Wikipedia 的公鑰證書:


wikipedia_cer.png

數字證書的生成及驗證

數字證書的生成是分層級的,下一級的證書需要其上一級證書的私鑰簽名。
所以後者是前者的證書頒發者,也就是說上一級證書的 Subject Name 是其下一級證書的 Issuer Name。

在得到證書申請者的一些必要資訊(物件名稱,公鑰私鑰)之後,證書頒發者通過 SHA-256 雜湊得到證書內容的摘要,再用自己的私鑰給這份摘要加密,得到數字簽名。綜合已有的資訊,生成分別包含公鑰和私鑰的兩個證書。

扯到這裡,就有幾個問題:

問:如果說釋出一個數字證書必須要有上一級證書的私鑰加密,那麼最頂端的證書——根證書怎麼來的?

根證書是自簽名的,即用自己的私鑰簽名,不需要其他證書的私鑰來生成簽名。

問:怎麼驗證證書是有沒被篡改?

當客戶端走 HTTPS 訪問站點時,伺服器會返回整個證書鏈。以下圖的證書鏈為例:


chain_hierarchy.png

要驗證 *.wikipedia.org 這個證書有沒被篡改,就要用到 GlobalSign Organization Validation CA - SHA256 - G2 提供的公鑰解密前者的簽名得到摘要 Digest1,我們的客戶端也計算前者證書的內容得到摘要 Digest2。對比這兩個摘要就能知道前者是否被篡改。後者同理,使用 GlobalSign Root CA 提供的公鑰驗證。當驗證到到受信任的根證書時,就能確定 *.wikipedia.org 這個證書是可信的。

問:為什麼上面那個根證書 GlobalSign Root CA 是受信任的

數字證書認證機構(Certificate Authority, CA)簽署和管理的 CA 根證書,會被納入到你的瀏覽器和作業系統的可信證書列表中,並由這個列表判斷根證書是否可信。所以不要隨便匯入奇奇怪怪的根證書到你的作業系統中。

問:生成的數字證書(如 *.wikipedia.org)都可用來簽署新的證書嗎?

不一定。如下圖,拓展欄位裡面有個叫 Basic Constraints 的資料結構,裡面有個欄位叫路徑長度約束(Path Length Constraint),表明了該證書能繼續簽署 CA 子證書的深度,這裡為0,說明這個 GlobalSign Organization Validation CA - SHA256 - G2 只能簽署客戶端證書,而客戶端證書不能用於簽署新的證書,CA 子證書才能這麼做。


path_length_constraint.png

iOS 上對證書鏈的驗證

When a TLS certificate is verified, the operating system verifies its chain of trust. If that chain of trust contains only valid certificates and ends at a known (trusted) anchor certificate, then the certificate is considered valid.

所以在 iOS 中,證書是否有效的標準是:

信任鏈中如果只含有有效證書並且以可信錨點(trusted anchor)結尾,那麼這個證書就被認為是有效的。

其中可信錨點指的是系統隱式信任的證書,通常是包括在系統中的 CA 根證書。不過你也可以在驗證證書鏈時,設定自定義的證書作為可信的錨點。

NSURLSession 實現 HTTPS

具體到使用 NSURLSession 走 HTTPS 訪問網站,-URLSession:didReceiveChallenge:completionHandler: 回撥中會收到一個 challenge,也就是質詢,需要你提供認證資訊才能完成連線。這時候可以通過 challenge.protectionSpace.authenticationMethod 取得保護空間要求我們認證的方式,如果這個值是 NSURLAuthenticationMethodServerTrust 的話,我們就可以插手 TLS 握手中“驗證數字證書有效性”這一步。

預設的實現

系統的預設實現(也即代理不實現這個方法)是驗證這個信任鏈,結果是有效的話則根據 serverTrust 建立 credential 用於同服務端確立 SSL 連線。否則會得到 “The certificate for this server is invalid...” 這樣的錯誤而無法訪問。

比如在訪問 https://www.google.com 的時候咧,我們不實現這個方法也能訪問成功的。系統對 Google 伺服器返回來的證書鏈,從葉節點證書往根證書層層驗證(有效期、簽名等等),遇到根證書時,發現作為可信錨點的它存在與可信證書列表中,那麼驗證就通過,允許與服務端建立連線。


google.png

而當我們訪問 https://www.12306.cn 時,就會出現 "The certificate for this server is invalid. You might be connecting to a server that is pretending to be “www.12306.cn” which could put your confidential information at risk." 的錯誤。原因就是系統在驗證到根證書時,發現它是自簽名、不可信的。


12306.png

自定義實現

如果我們要實現這個代理方法的話,需要提供 NSURLSessionAuthChallengeDisposition(處置方式)和 NSURLCredential(資格認證)這兩個引數給 completionHandler 這個 block:

複製程式碼
 1 -(void)URLSession:(NSURLSession *)session 
 2         didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 3         completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, 
 4                     NSURLCredential * _Nullable))completionHandler {
 5 
 6     // 如果使用預設的處置方式,那麼 credential 就會被忽略
 7     NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
 8     NSURLCredential *credential = nil;
 9 
10     if ([challenge.protectionSpace.authenticationMethod
11             isEqualToString: 
12             NSURLAuthenticationMethodServerTrust]) {
13 
14         /* 呼叫自定義的驗證過程 */
15         if ([self myCustomValidation:challenge]) {    
16             credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
17             if (credential) {
18                 disposition = NSURLSessionAuthChallengeUseCredential;
19             }    
20         } else {
21             /* 無效的話,取消 */
22             disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge
23         }
24     }        
25     if (completionHandler) {
26         completionHandler(disposition, credential);
27     } 
28 }
複製程式碼

在 [self myCustomValidation:challenge] 呼叫自定義驗證過程,結果是有效的話才建立 credential 確立連線。
自定義的驗證過程,需要先拿出一個 SecTrustRef 物件,它是一種執行信任鏈驗證的抽象實體,包含著驗證策略(SecPolicyRef)以及一系列受信任的錨點證書,而我們能做的也是修改這兩樣東西而已。

 1 SecTrustRef trust = challenge.protectionSpace.serverTrust; 

拿到 trust 物件之後,可以用下面這個函式對它進行驗證。

複製程式碼
 1 static BOOL serverTrustIsVaild(SecTrustRef trust) {
 2     BOOL allowConnection = NO;
 3 
 4 // 假設驗證結果是無效的
 5 SecTrustResultType trustResult = kSecTrustResultInvalid;
 6 
 7 // 函式的內部遞迴地從葉節點證書到根證書的驗證
 8 OSStatus statue = SecTrustEvaluate(trust, &trustResult);
 9 
10     if (statue == noErr) {
11     // kSecTrustResultUnspecified: 系統隱式地信任這個證書
12     // kSecTrustResultProceed: 使用者加入自己的信任錨點,顯式地告訴系統這個證書是值得信任的
13 
14     allowConnection = (trustResult == kSecTrustResultProceed 
15                                 || trustResult == kSecTrustResultUnspecified);
16     }
17     return allowConnection;
18 }
複製程式碼

這個函式什麼時候呼叫完全取決於你的需求,如果你不想對驗證策略做修改而直接呼叫的話,那你居然還看到這裡!?(╯‵□′)╯︵┻━┻

域名驗證

可以通過以下的程式碼獲得當前的驗證策略:

1 CFArrayRef policiesRef;

2 SecTrustCopyPolicies(trust, &policiesRef); 

列印 policiesRef 後,你會發現預設的驗證策略就包含了域名驗證,即“伺服器證書上的域名和請求域名是否匹配”。如果你的一個證書需要用來連線不同域名的主機,或者你直接用 IP 地址去連線,那麼你可以重設驗證策略以忽略域名驗證:

複製程式碼
 1 NSMutableArray *policies = [NSMutableArray array];
 2 
 3 // BasicX509 不驗證域名是否相同
 4 SecPolicyRef policy = SecPolicyCreateBasicX509();
 5 [policies addObject:(__bridge_transfer id)policy];
 6 SecTrustSetPolicies(trust, (__bridge CFArrayRef)policies);
 7 
 8 
複製程式碼

後再呼叫 serverTrustIsVaild() 驗證。

但是如果不驗證域名的話,安全性就會大打折扣。拿瀏覽器舉�:

  1. 這個偽造網站的證書是非 CA 頒佈的偽造證書的話,那麼瀏覽器會提醒你這個證書不可信;
  2. 這個偽造網站也使用了 CA 頒佈的證書,由於我們不做域名驗證,你的瀏覽器不會有任何的警告。

你可能會問:公鑰證書是每個人都能得到的,釣魚網站能不能返回真正的公鑰證書給我們呢?

我覺得是可以的,然而這並沒有什麼卵用。沒有私鑰的釣魚伺服器無法獲得第三個隨機數,無法生成 Session Key,也就不能對我們傳給它的資料進行解密了。

自簽名的證書鏈驗證

在 App 中想要防止上面提到的中間人公雞攻擊,比較好的做法是將公鑰證書打包進 App 中,然後在收到服務端證書鏈的時候,能夠有效地驗證服務端是否可信,這也是驗證自簽名的證書鏈所必須做的。

假設你的伺服器返回:[你的自簽名的根證書] -- [你的二級證書] -- [你的客戶端證書],系統是不信任這個三個證書的。
所以你在驗證的時候需要將這三個的其中一個設定為錨點證書,當然,多個也行。

比如將 [你的二級證書] 作為錨點後,SecTrustEvaluate() 函式只要驗證到 [你的客戶端證書] 確實是由 [你的二級證書] 簽署的,那麼驗證結果為 kSecTrustResultUnspecified,表明了 [你的客戶端證書] 是可信的。下面是設定錨點證書的做法:

複製程式碼
 1 NSMutableArray *certificates = [NSMutableArray array];
 2 
 3 NSDate *cerData = /* 在 App Bundle 中你用來做錨點的證書資料,證書是 CER 編碼的,常見副檔名有:cer, crt...*/
 4 
 5 SecCertificateRef cerRef = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)cerData);
 6 
 7 [certificates addObject:(__bridge_transfer id)cerRef];
 8 
 9 // 設定錨點證書。
10 SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)certificates);
複製程式碼

只調用 SecTrustSetAnchorCertificates () 這個函式的話,那麼就只有作為引數被傳入的證書作為錨點證書,連繫統本身信任的 CA 證書不能作為錨點驗證證書鏈。要想恢復系統中 CA 證書作為錨點的功能,還要再呼叫下面這個函式:

1 // true 代表僅被傳入的證書作為錨點,false 允許系統 CA 證書也作為錨點

2 SecTrustSetAnchorCertificatesOnly(trust, false); 

這樣,再呼叫 serverTrustIsVaild() 驗證證書有效性就能成功了。

CA 證書鏈的驗證

上面說的是沒經過 CA 認證的自簽證書的驗證,而 CA 的證書鏈的驗證方式也是一樣,不同點在不可信錨點的證書型別不一樣而已:前者的錨點是自籤的需要被打包進 App 用於驗證,後者的錨點可能本來就存在系統之中了。不過我腦補了這麼的一個坑:

假如我們使用的是 CA 根證書籤署的數字證書,而且只用這個 CA 根證書作為錨點,在不驗證域名的情況下,是不是就會在握手階段信任被同一個 CA 根證書籤名的偽造證書呢?

參考閱讀

上文有什麼我理解得不正確、或表達不準確的地方,煩請指教。�

文/StanOz(簡書作者)
原文連結:http://www.jianshu.com/p/31bcddf44b8d
著作權歸作者所有,轉載請聯絡作者獲得授權,並標註“簡書作者”。 其他:

公司的介面一般會兩種協議的,一種HTTP,一種HTTPS的,HTTP 只要請求,伺服器就會響應,如果我們不對請求和響應做出加密處理,所有資訊都是會被檢測劫持到的,是很不安全的,客戶端加密可以使用我這套工具類進行處理:文章地址
但是不論在任何時候,都應該將服務置於HTTPS上,因為它可以避免中間人攻擊的問題,還自帶了基於非對稱金鑰的加密通道!現實是這些年湧現了大量速成的移動端開發人員,這些人往往基礎很差,完全不瞭解加解密為何物,使用HTTPS後,可以省去教育他們各種加解密技術,生活輕鬆多了。

介紹下HTTPS互動原理

簡答說,HTTPS 就是 HTTP協議加了一層SSL協議的加密處理,SSL 證書就是遵守 SSL協議,由受信任的數字證書頒發機構CA(如GlobalSign,wosign),在驗證伺服器身份後頒發,這是需要花錢滴,簽發後的證書作為公鑰一般放在伺服器的根目錄下,便於客戶端請求返回給客戶端,私鑰在伺服器的內部中心儲存,用於解密公鑰。

HTTPS 客戶端與伺服器互動過程:

1、客戶端傳送請求,伺服器返回公鑰給客戶端;
2、客戶端生成對稱加密祕鑰,用公鑰對其進行加密後,返回給伺服器;
3、伺服器收到後,利用私鑰解開得到對稱加密祕鑰,儲存;
4、之後的互動都使用對稱加密後的資料進行互動。

談下證書
簡單說,證書有兩種,一種是正經的:


CA頒發的證書


一種是不正經的:


自己生成簽發的證書

介紹下我們需要做什麼

如果遇到正經的證書,我們直接用AFNetworking 直接請求就好了,AFNetworking 內部幫我們封裝了HTTPS的請求方式,但是大部分公司介面都是不正經的證書,這時需要我們做以下幾步:
1、將伺服器的公鑰證書拖到Xcode中
2、修改驗證模式

manager.securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];

原理:
簡單來說,就是你本可以修改AFN這個設定來允許客戶端接收伺服器的任何證書,但是這麼做有個問題,就是你無法驗證證書是否是你的伺服器後端的證書,給中間人攻擊,即通過重定向路由來分析偽造你的伺服器端打開了大門。

AFSecurityPolicy *securityPolicy = [AFSecurityPolicy defaultPolicy];
securityPolicy.allowInvalidCertificates = YES;

解決方法:AFNetworking是允許內嵌證書的,通過內嵌證書,AFNetworking就通過比對伺服器端證書、內嵌的證書、站點域名是否一致來驗證連線的伺服器是否正確。由於CA證書驗證是通過站點域名進行驗證的,如果你的伺服器後端有繫結的域名,這是最方便的。將你的伺服器端證書,如果是pem格式的,用下面的命令轉成cer格式

openssl x509 -in <你的伺服器證書>.pem -outform der -out server.cer

然後將生成的server.cer檔案,如果有自建ca,再加上ca的cer格式證書,引入到app的bundle裡,AFNetworking在

AFSecurityPolicy *securityPolicy = [AFSecurityPolicy AFSSLPinningModeCertificate];

或者

AFSecurityPolicy *securityPolicy = [AFSecurityPolicy AFSSLPinningModePublicKey];

情況下,會自動掃描bundle中.cer的檔案,並引入,這樣就可以通過自簽證書來驗證伺服器唯一性了。

AFSecurityPolicy分三種驗證模式:

AFSSLPinningModeNone

這個模式表示不做SSL pinning,
只跟瀏覽器一樣在系統的信任機構列表裡驗證服務端返回的證書。若證書是信任機構簽發的就會通過,若是自己伺服器生成的證書就不會通過。

AFSSLPinningModeCertificate

這個模式表示用證書繫結方式驗證證書,需要客戶端儲存有服務端的證書拷貝,這裡驗證分兩步,第一步驗證證書的域名有效期等資訊,第二步是對比服務端返回的證書跟客戶端返回的是否一致。

AFSSLPinningModePublicKey

這個模式同樣是用證書繫結方式驗證,客戶端要有服務端的證書拷貝,
只是驗證時只驗證證書裡的公鑰,不驗證證書的有效期等資訊。只要公鑰是正確的,就能保證通訊不會被竊聽,因為中間人沒有私鑰,無法解開通過公鑰加密的資料。



文/滕先洪(簡書作者)
原文連結:http://www.jianshu.com/p/75d96b72bfb1
著作權歸作者所有,轉載請聯絡作者獲得授權,並標註“簡書作者”。