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 上對證書鏈的驗證

Overriding TLS Chain Validation Correctly 中提到:

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:

-(void)URLSession:(NSURLSession *)session 
        didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
        completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, 
                    NSURLCredential * _Nullable))completionHandler {
    
    // 如果使用默認的處置方式,那么 credential 就會被忽略
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    NSURLCredential *credential = nil;
    
    if ([challenge.protectionSpace.authenticationMethod
            isEqualToString: 
            NSURLAuthenticationMethodServerTrust]) {

        /* 調用自定義的驗證過程 */
        if ([self myCustomValidation:challenge]) {  
            credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
            if (credential) {
                disposition = NSURLSessionAuthChallengeUseCredential;
            }   
        } else {
            /* 無效的話,取消 */
            disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge
        }
    }        
    if (completionHandler) {
        completionHandler(disposition, credential);
    } 
}

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

SecTrustRef trust = challenge.protectionSpace.serverTrust;

拿到 trust 對象之后,可以用下面這個函數對它進行驗證。

static BOOL serverTrustIsVaild(SecTrustRef trust) {
    BOOL allowConnection = NO;

// 假設驗證結果是無效的
SecTrustResultType trustResult = kSecTrustResultInvalid;

// 函數的內部遞歸地從葉節點證書到根證書的驗證
OSStatus statue = SecTrustEvaluate(trust, &trustResult);

    if (statue == noErr) {
    // kSecTrustResultUnspecified: 系統隱式地信任這個證書
    // kSecTrustResultProceed: 用戶加入自己的信任錨點,顯式地告訴系統這個證書是值得信任的

    allowConnection = (trustResult == kSecTrustResultProceed 
                                || trustResult == kSecTrustResultUnspecified);
    }
    return allowConnection;
}

這個函數什么時候調用完全取決于你的需求,如果你不想對驗證策略做修改而直接調用的話,那你居然還看到這里?。?╯‵□′)╯︵┻━┻

域名驗證

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

CFArrayRef policiesRef;
SecTrustCopyPolicies(trust, &policiesRef);

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

NSMutableArray *policies = [NSMutableArray array];
    
// BasicX509 不驗證域名是否相同
SecPolicyRef policy = SecPolicyCreateBasicX509();
[policies addObject:(__bridge_transfer id)policy];
SecTrustSetPolicies(trust, (__bridge CFArrayRef)policies);

然后再調用 serverTrustIsVaild() 驗證。

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

試想你要傳輸報文到 https://www.real-website.com ,然而由于域名劫持,把你帶到了 https://www.real-website.cn 這個??網站,大概有以下兩種結果:

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

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

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

自簽名的證書鏈驗證

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

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

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

NSMutableArray *certificates = [NSMutableArray array];

NSDate *cerData = /* 在 App Bundle 中你用來做錨點的證書數據,證書是 CER 編碼的,常見擴展名有:cer, crt...*/

SecCertificateRef cerRef = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)cerData);

[certificates addObject:(__bridge_transfer id)cerRef];

// 設置錨點證書。
SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)certificates);

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

// true 代表僅被傳入的證書作為錨點,false 允許系統 CA 證書也作為錨點
SecTrustSetAnchorCertificatesOnly(trust, false);

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

CA 證書鏈的驗證

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

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

參考閱讀

iOS安全系列之一:HTTPS

iOS安全系列之二:HTTPS進階

Overriding TLS Chain Validation Correctly

HTTPS Server Trust Evaluation

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容