寫給 iOS 開發者看的 HTTPS 指南

發表於2016-12-28

春天來了,到了動物交配強制 ATS(App Transport Security)的季節,廣袤的 iOS 大草原上到處都瀰漫著一種叫 HTTPS 的荷爾蒙氣息。

  • 白巖鬆趙忠祥

蘋果在 WWDC 2016 上宣佈:2016 年底將要求所有 APP 適配蘋果的 App Transport Security,簡單地說就是除了特殊情況(瀏覽器、第三方服務、媒體)外,APP 跟服務端的通訊必須使用 HTTPS 協議,否則 iOS 9 和 macOS 10.11 起,作業系統將有能力阻止所有的明文 HTTP 請求。在上面的 session 中,蘋果還對具體的細節做出了要求。不過,就在今天,蘋果宣佈將這個 deadline 無限期推遲

本文將著重以大部分 iOS 開發者能理解的方式介紹 APP 啟用 HTTPS 支援的過程中跟 APP 相關的部分,剩餘的協議細節將一筆帶過。

HTTPS 基礎

定義

HTTPS 看似跟 HTTP 一樣,其實它只是看起來跟 HTTP 一樣,實際上是一種新的網路架構。在當前情況下,HTTPS 的英文全稱應該是 HTTP over TLS。

HTTPS 請求 和 HTTP 請求的異同

普通 HTTP 請求直接基於 TCP,在網際網路上明文傳播,而且沒有任何校驗,鏈路上的每一個節點都可以對資料包進行篡改,使用手機網路訪問 HTTP 網站被插入流量球甚至廣告等運營商劫持行為就是最常見的例子。而 HTTPS 請求執行在 TLS 層之上,TLS 執行在 TCP 上,TLS 有獨特的握手、建立連線、資料驗證機制,讓執行商劫持無處下手:只要任何一個資料包被篡改,資料校驗就會失敗,這個請求會客戶端直接拋棄,網頁不會顯示。當我們用 HTTP 協議來解釋 TLS 層攜帶的內容時,這個東西就被稱為 HTTPS 啦。

HTTP 協議簡析

HTTP 協議是一個非常簡單而強壯的協議,它規定了以文字方式解析資料後哪一部分該代表什麼:頭部攜帶特定資訊,正文部分被渲染為網頁。所以,任何資料都可以被 HTTP 協議解析,無論他是基於 TCP 還是 TLS 傳輸,或者只是硬碟上的一個檔案。

請注意,此處的 HTTP 協議和上一小節中的 HTTP 請求是兩個概念。

HTTPS 證書

證書是什麼

下面兩張圖分別是我的個人部落格的一張舊證書的 cer 和 crt 兩種格式,在 Finder 中點選空格預覽的結果:

112016-12-22-14824001042971

122016-12-22-14824001210991

下面是 crt 格式的證書內容:

—–BEGIN CERTIFICATE—–
MIIErzCCA5egAwIBAgIQYrp2Mj1s3GeeVubYEGEguTANBgkqhkiG9w0BAQsFADBV
MQswCQYDVQQGEwJDTjEaMBgGA1UEChMRV29TaWduIENBIExpbWl0ZWQxKjAoBgNV
BAMTIVdvU2lnbiBDQSBGcmVlIFNTTCBDZXJ0aWZpY2F0ZSBHMjAeFw0xNTA5Mjkx
MjMyMDFaFw0xNjA5MjkxMjMyMDFaMBcxFTATBgNVBAMMDGx2d2VuaGFuLmNvbTCC
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOJMpMrIcfdxScuslpFWWLAP
sLq45P/b7V8WXfTZnMY9nZGc012olTI/Su8DlrBxWfks/iBS1WkGgxkhxirz096c
IPvlddRXyz1LI1Npl0BCbjD1j+jx40e3PIVbcpff5Z1OLlvu/5ehub4SyDl3wIRM
6zwTSzldbAkQ4yXFl2OUyoSecQEqRXBdpbvAcL5/Q1M/wlxNi4KBGmemckABMqNg
30N9OipTCxbGwYbH/RBMYrxncwbIoCAC/P189nQ+RJ3szx3tgjiTpAtHref4uHdv
XI6wy6tP8FXh2HIXbx1kNfY+8YbSWDnvHiN8dL+fkKsr5KU5nNG54KGkdOdlljkC
AwEAAaOCAbcwggGzMAsGA1UdDwQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAgYI
KwYBBQUHAwEwCQYDVR0TBAIwADAdBgNVHQ4EFgQUkjyOi+6xj5KkgATKLXS12K9x
SvMwHwYDVR0jBBgwFoAU0qcWIHyv2ZWe60MKGfLguXQOqMcwfQYIKwYBBQUHAQEE
cTBvMDQGCCsGAQUFBzABhihodHRwOi8vb2NzcDEud29zaWduLmNvbS9jYTYvc2Vy
dmVyMS9mcmVlMDcGCCsGAQUFBzAChitodHRwOi8vYWlhMS53b3NpZ24uY29tL2Nh
Ni5zZXJ2ZXIxLmZyZWUuY2VyMD0GA1UdHwQ2MDQwMqAwoC6GLGh0dHA6Ly9jcmxz
MS53b3NpZ24uY29tL2NhNi1zZXJ2ZXIxLWZyZWUuY3JsMCkGA1UdEQQiMCCCDGx2
d2VuaGFuLmNvbYIQd3d3Lmx2d2VuaGFuLmNvbTBRBgNVHSAESjBIMAgGBmeBDAEC
ATA8Bg0rBgEEAYKbUQYBAgIBMCswKQYIKwYBBQUHAgEWHWh0dHA6Ly93d3cud29z
aWduLmNvbS9wb2xpY3kvMA0GCSqGSIb3DQEBCwUAA4IBAQApspu1V2KlscwQints
4zqm6xnfoXkjZJjaanXmbOER8iRd1hmzN6vRYcOJ2t0QmkjBisRgi4ffJVZ9QF30
LzmaZYg+PQUX15CxDlG9XTRB6ufqdlsGdc44NqGdHuo8uIsko1MjubS8mQw7ClLc
4VEvP2KUBZFgaenfG6Y8v5DWWV+NfiCkmCMFXu6nbqEvEf+ev3arnaQSiDhH98Y+
ivysKu8aYlsb2VsRaF8e9bMvE2PgfZCg93lSou/vQS1VUED7ih5lLb2CQqW8ksMp
XKK1E/BNFR7GR8i1NfL15KdIGUmsklT60vooRd7zM9ai8vtmkg9xykwpgUPbTjcd
mRAb
—–END CERTIFICATE—–

證書就是使用特殊格式加密的一段字串,可以被讀取並拿出關鍵資訊。iOS 中 NSURLSession 驗證的就是 cer 格式的這個證書。

證書周邊知識

下面是一個 HTTPS 證書典型的購買、部署流程:

  1. 在 *UNIX 環境下使用 openssl 工具生成一對一匹配的 私鑰 和 CERTIFICATE REQUEST 檔案(以 —–BEGIN CERTIFICATE REQUEST—– 開頭)。私鑰為絕密,絕對不能洩露,最好在生產伺服器直接生成,這樣就不需要網路傳輸,更加安全。
  2. 將 CERTIFICATE REQUEST 檔案提交到證書服務機構,服務機構根據證書級別進行 域名認證、公司認證、安全認證 等不同級別的安全驗證。
  3. 驗證通過後,服務機構將基於我們提交的 CERTIFICATE REQUEST 內容,使用他已有的證書派生出子證書,提供給我們下載。
  4. 我們拿到服務機構頒發的兩個 crt 格式的證書(root 證書 及我們的域名證書),再配合本地的私鑰,到 Apache、Nginx 等 web server 上部署,部署時會驗證“私鑰”是否和“域名證書”匹配。
  5. 使用者在以 HTTPS 協議訪問網站時,瀏覽器會進行如下幾步安全驗證:
    • 域名證書中的域名和實際域名是否一致
    • 域名證書和 root 證書是否匹配
    • root 證書是否可信

需要注意的點

  1. 在某些低安全級別證書申請中(如僅驗證域名所有權的證書),私鑰可以讓簽發伺服器代為生成,但這樣做有一定的安全風險。
  2. root 證書也可以在我們本機生成,如 12306 的自簽名證書,但這樣不會被普通瀏覽器信任。
  3. 私鑰為絕密,因為證書全部都是公開的,任何人都可以提取,如果私鑰被別人獲取,被部署到別人的伺服器中,那所有人就會認為那臺伺服器是完全合法的。這已經不是中間人攻擊了,這時候他就是你。

NSURLSession 對證書的驗證

證書驗證方法

在 URLSessionDelegate 中有一個方法 func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) 專門用來處理 HTTPS 證書的處理。具體操作可以參考 Pitaya 中的程式碼

我簡單複述一下處理流程:

  1. 發現是 HTTPS 請求,取出證書
  2. 進入證書處理流程:
    1. 本地證書分兩種情況:① 本地儲存著證書服務機構頒發的 crt 檔案轉換而來的 cer 檔案,使用 NSData 進行內容對比 ② 本地儲存著自簽名的 cer 格式的證書,使用 NSData 進行內容對比
    2. 匹配成功,手動讓請求繼續,這一步可以讓自簽名證書繞過 iOS 系統的證書合法性驗證
    3. 匹配失敗,進入錯誤處理流程
    4. 如果自簽名證書不做手動處理,那麼在這個方法結束後連結就會被系統關閉,因為 root 證書不合法。

所以,在 APP 提交的時候,蘋果會檢查是否將 ATS 配置為了“全部無腦通過”,這種操作是被禁止的。當然,蘋果也可以在系統層面一刀切,但是那樣得掛多少個 APP 啊,蘋果不會那麼做。所以會被影響的應該只是新提交的 ipa。

SSL 鋼釘原理

如果有人通過一些手段通過了域名所有權認證(非常容易,域名郵箱、DNS指向甚至在根目錄放一個檔案都可以驗證通過),拿到了一個合法的對應你的域名的 HTTPS 證書,這時候他在廣場開放了一個沒有密碼的 wifi,命名為 CMCC,這樣,幾乎所有的開著 wifi 的手機都會自動連線,這時候他只需要做一個簡單的 DNS 劫持,就可以把所有應該向你網站傳送的需求劫持到他那裡。

但是:如果你做了 SSL 鋼釘驗證,那麼他的證書就不會被驗證通過。他也沒法用你的證書啟動服務,因為私鑰和證書的驗證是 SSL 協議強制做的,他沒有你的私鑰,他的 web server 就沒法啟動。所以,私鑰千萬不能洩露。

關於自簽名證書

自簽名證書不會被瀏覽器信任,因為每次有新的 HTTPS 證書到達某個作業系統時,系統會去訪問 root 證書的伺服器以確定域名證書的身份,這些合法的 root 證書服務商就是固定的那幾家,顯然自簽名證書不會被信任,所以我們在 12306 搶票的時候需要先下載他的自簽名證書的 root 證書並手動信任,不然就打不開頁面。

我們可以看到,自簽名證書的驗證在程式碼層面,在稽核的時候是完全不可感知的,所以就沒有什麼“蘋果不接受自簽名證書”之類的問題了。而且,自簽名證書被廣泛的用於各種系統內部的連線加密,不是蘋果可以一刀切的:如果粗暴的在作業系統層面阻止了自簽名證書,導致企業客戶的系統突然掛掉,後果不可想象。

關於證書更換

證書都有有效期,在過期之前需要申請新證書,這時候 SSL 鋼釘該怎麼處理呢?動態下發當然是不行的,為什麼要驗證證書?就是因為網路不可信任。Pitaya 前兩天加入了這個邏輯:在新證書和舊證書交接的一段時間內,上線新版本,同時包含新舊證書,這樣可以保證更新過的使用者可以對證書更換無感。

另外,設定 SSL 鋼釘不適合面向普通使用者的 APP,因為總是有人萬年不更新,這更適合企業內部 APP,可以通過行政手段及自驅力(業績啊,提成啊)推動更新。

關於所謂的雙向驗證

感覺這裡是大家誤解最大的地方:大部分人所謂的“雙向驗證”就是自簽名證書的驗證並手動繼續而已。

HTTPS 是支援雙向認證的,不過那指的是客戶端(瀏覽器或 APP)也像服務端一樣,在傳送請求給服務端的時候帶上證書,再由服務端使用對應的私鑰進行驗證。一般 APP 不需要這麼做。

蘋果的要求

後端

132016-12-22-14824099116718

前兩條拿給後端看就行,第三條用 Nginx 也很容易實現。最後一條 Exception 就是蘋果馬上就不支援的。

iOS 端

142016-12-22-14824101057809

使用這些 API 可以單獨繞過。

152016-12-22-14824101870249

流媒體檔案可以新增例外,WKWebView 可以直接設定為繞過。

總結

購買的證書:什麼都不用管,改一下伺服器地址就行,程式碼完全不用改。

自簽名證書:找後端哥們兒要一個 crt,自己提取也行,轉換成 cer ,放到相應的方法裡,手動讓處理流程繼續即可。

相關文章