一張證書引發的噱案

felix021發表於2022-03-13

- 引 -

我也沒想到在神策資料這大半年能遇到好幾次和證照相關的問題。

- 起 -

2021年9月3號,一個新客戶接入到我們的 SaaS 系統。在某個環節,我們會給客戶發個 HTTPS 請求,沒想到竟然遇到了個 SSLHandshakeException:

Caused by: javax.net.ssl.SSLHandshakeException: ... unable to find valid certification path to requested target

在伺服器上用 curl 試一把,也報錯:

$ curl -v https://some.domain/
CAfile: /etc/pki/tls/certs/ca-bundle.crt
...
curl: (60) Peer's Certificate issuer is not recognized.

但用瀏覽器開啟這個 URL,卻是沒問題的,這說明問題應該出在我們的伺服器端。

- 析 -

我們知道,HTTPS 是靠證照保證通訊安全的;但客戶端如何保證服務端給的證照是可信的呢?

由於證照總是由某個證照頒發機構(Certificate issuer,或 Certificate Authority,簡寫成 CA)簽發的,如果我們事先將一批可信的證照頒發機構儲存在本地,就可以在發起請求的時候判斷證照是否可信了。

有時情況會更復雜一些:某些機構不在我們的列表裡,但他的證照是由我們信任的某個機構頒發的,我們也認為他是可信的,因此他頒發的證照也是可信的。

於是這就構成了一個信任鏈,鏈的末端是「根證照頒發機構」(Root CA),這些機構通常是國際上公認可靠的大型機構,或者國家權威機關背書的機構。

理解了這點,就可以推測,應當是我們伺服器上的機構列表沒有及時更新;只要把該客戶證照的頒發機構加入本地的列表就應該能解決該問題。

- 解 -

再細看上面 curl 命令的輸出,有一行 CAfile: /etc/pki/tls/certs/ca-bundle.crt,這就是 curl 使用到的證照頒發機構列表。

www.baidu.com 為例,我們可以通過如下命令獲取客戶證照的信任鏈:

$ openssl s_client -showcerts -servername server -connect www.baidu.com:443 > cacert.pem

在得到的 cacert.pem 中,我們可以看到如下內容(略作簡化):

Certificate chain
 0 s:/CN=baidu.com
   i:/CN=GlobalSign Organization Validation CA - SHA256 - G2

-----BEGIN CERTIFICATE-----
MIIKQDCCCSigAwIBAgIMEZhyT2Z0o9Yhv76iMA0GCSqGSIb3DQEBCwUAMGYxCzAJ
...(略)...
n3XcFtwQLBY9Iuqh8Mn7vtiv5k2azdGsYhZcFBCBAeUoRhDC
-----END CERTIFICATE-----

 1 s:/CN=GlobalSign Organization Validation CA - SHA256 - G2
   i:/OU=Root CA/CN=GlobalSign Root CA

-----BEGIN CERTIFICATE-----
MIIEaTCCA1GgAwIBAgILBAAAAAABRE7wQkcwDQYJKoZIhvcNAQELBQAwVzELMAkG
...(略)...
K1pp74P1S8SqtCr4fKGxhZSM9AyHDPSsQPhZSZg=
-----END CERTIFICATE-----

...(略)...

可以看到裡面有兩段用 --BEGIN CERTIFICATE----END CERTIFICATE-- 包起來的 base64 編碼字串,這就是被編碼為 PEM 格式(Privacy Enhanced Mail)的證照了(有時也會用 .crt 作為副檔名)。

在 BEGIN 前面有一些摘要,可以幫助我們瞭解證照的內容,比如 s:/CN=baidu.com 表示這個證照的主體(s 即 subject)是 baidu.com(CN 即 common name),i:/CN=GlobalSign 表示它的頒發機構(i 即 issuer)是 GlobalSign。

因此可以看到,這個 cacert.pem 實際上包含了兩個證照,一個是百度使用的證照,另一個是頒發該證照的 GlobalSign 這個機構(CA)自己的證照。

通過 curl --cacert cacert.pem https://www.baidu.com 我們可以確認,這個信任鏈能用來驗證 www.baidu.com 的證照(實際上我們只需要裡面第二個證照,將第一個證照刪除,不影響 curl 的執行)。

回到該客戶的情況,我們用相同的方法取得客戶證照頒發機構的證照,將它放到 /etc/pki/ca-trust/source/anchors/ 目錄,執行 update-ca-trust 將其加入到證照列表中,就可以正常使用 curl 命令來請求了。

FBI WARNING:除非完全明白你在做什麼,否則不要直接更新生產系統的 CA 列表(本文的操作步驟是用於驗證);實際操作請交給專業的安全人員執行。

- 然 -

沒有「但是」的文章不是好文章。

curl 正常了,但是我們的 Java 程式碼依然報錯,這說明 java 和 curl 使用了不同的 CA 列表。

問題倒是好解決,簡單搜尋一下,就瞭解到 jre 的證照是存放在 $JAVA_HOME/jre/lib/security/cacerts 這個檔案裡,需要使用專門的 keytool 工具來更新它:

$ keytool -import -trustcacerts -file cacert.pem -alias 證照頒發機構的名稱 -keystore $JAVA_HOME/jre/lib/security/cacerts

Enter keystore password:  changeit (這是jre自帶的預設密碼)

Certificate was added to keystore

再次驗證,Java 程式碼就可以正常執行了。

注:如果想要單獨驗證某個證照,可以這樣

  1. 先建立一個空的 keyStore(密碼為 storePassword):

    $ keytool -genkeypair -alias boguscert -storepass storePassword -keypass secretPassword -keystore keystore -dname "CN=Developer"
    $ keytool -delete -alias boguscert -storepass storePassword -keystore emptyStore.keystore
  2. 新增證照到該 keyStore:

    $ keytool -import -trustcacerts -file cacert.pem -alias 機構名稱 -keystore keystore
  3. 指定 keyStore 啟動 java 程式:

    $ java -Djavax.net.ssl.trustStore=keystore -Djavax.net.ssl.trustStorePassword=storePassword -cp $CLASS_PATH CLASS_NAME

- 劫 -

不巧的是,這周又遇到了一個證照信任的問題,這次是客戶的環境向我們的伺服器發起請求,報了相同的錯誤。

有了前車之鑑,上面這些命令執行起來可謂得心應手,但是這次卻不靈了。

排查過程比較瑣碎,也因為陷入思維定勢而走了一些彎路,但其實原因很簡單,這裡就不賣關子了。

這家客戶是一家泛金融類的企業,其生產環境的網路安全級別非常高,不僅有嚴格的外網訪問限制,而且針對所有 https 請求都會預設劫持,用一個自簽名證照返回錯誤資訊。

經過與客戶溝通,將神策資料的域名新增到白名單後,就不再劫持證照了,問題得以解決。

- 故事 -

講完了事故,再講講故事。

非對稱加密、證照、信任鏈這一系列發明,構成了現在 web 通訊安全的基石,很難想象如果沒有這些基礎設施,現在網際網路還能做些什麼。

但是這裡隱藏了一個大bug:我們憑什麼相信本地這些證照頒發機構是可信的?

至少有三種情況會打破這個假設:

  1. 本地 CA 列表被汙染

可能你的電腦/手機被病毒匯入了 CA 證照;或者你自己可能就做過這個事情,比如公司網管要求新增公司的自簽名證照,又或者你為了能使用 Charles 來抓 https 請求,匯入了它自簽名的 Root CA 證照。

  1. 機構的私鑰洩漏

我沒有在公開渠道查到相關的事故(倒是有一個代理商把客戶證照的私鑰給洩漏了);如果某個機構的私鑰洩漏,這家機構應該離倒閉也不遠了。

  1. 看起來正經的機構也可能不正經

各國政府控制的 CA 機構大概都幹過些「不乾淨」的事情(至少有這種衝動),有一些被發現了,有一些還沒有。出於本文的安全考慮,這裡就不展開細節了。此外,「不被政府控制」的那些機構,就一定乾淨麼?說到底,機構總是被所在國管轄的,當遇到政府行政命令的時候,不一定有反抗的能力。

綜上,理論上並不存在 100% 可靠的通訊安全方案。

如果你的應用對通訊安全要求非常嚴格,連本地的 CA 列表都不相信,可以考慮加入更多的手段來提高通訊的安全等級。

簡單一點的場景(例如 app 不想被抓包破解協議),可以自己校驗伺服器的證照(證照指紋,或者自己指定證照頒發機構列表);要求更高的場景(例如需要訪問內部控制系統),可以給客戶端頒發證照,瀏覽器會在請求時提供證照用於校驗,感興趣的話可以參考 這個不太完善的專案

- 收 -

結尾照例做一個小結:

  1. HTTPS 是基於證照鏈來保證通訊安全的;
  2. 信任的基石是本地的證照頒發機構(CA)列表;
  3. 可以通過向本地列表新增 CA 證照的方式來解決需要信任的證照;
  4. 本地的 CA 不一定都是可信的;
  5. 可以通過更嚴格的校驗,或者客戶端證照來加強通訊的安全等級。

最後,神策在北京、上海、成都、武漢、深圳等多地均在招聘開發、產品、QA等崗位,感興趣的小夥伴歡迎私信勾搭;也可以點選我的 內推連結 或掃碼檢視 JD 並投遞簡歷:

歡迎關注

weixin1.png

相關文章