如何在 Apache、Ngnix 和 Lighttpd 上啟用 HTTP 公鑰固定擴充套件

linux.cn發表於2015-04-26

編者按:前段時間,Google 報告說 CNNIC 簽發的一箇中級 CA 簽發了一個偽造的 Google 證照,從而導致 Google 和 Mozilla 在其產品中取消了對 CNNIC 後繼簽發的證照信任。

本文就來講述一下,這種偽造證照是如何被 Google 發現的,其技術機制是什麼?如何在網站伺服器上實現偽造證照防禦和報告機制。

公鑰固定(Public Key Pinning)是指一個證照鏈中必須包含一個白名單中的公鑰,也就是說只有被列入白名單的證照籤發機構(CA)才能為某個域名*.example.com簽發證照,而不是你的瀏覽器中所儲存的任何 CA 都可以為之簽發。本文講述了這種機制的背景知識,並提供了 Apache、 Lighttpd 和 NGINX 上的配置範例。

在 Apache、NGINX 和 Lighttpd 上啟用 HTTP 公鑰固定擴充套件(HPKP)

HTTP 公鑰固定擴充套件

用你使用的銀行做個例子,它一直使用 CA 公司 A 為其簽發證照。但是在當前的證照體系下,CA 公司 B、CA 公司 C 和 NSA 的 CA 都能給你的銀行建立證照,而你的瀏覽器會毫無疑慮的接受它們,因為這些公司都是你所信任的根 CA。

如果你的銀行實現了 HPKP 並固定了它們的第一個中級證照(來自 CA 公司 A),那麼瀏覽器將不會接受來自CA 公司 B 和 CA 公司 C 的證照,即便它們也有一個有效的信任鏈。HPKP 也允許你的瀏覽器將這種違例行為報告給該銀行,以便銀行知道被偽造證照攻擊了。

HTTP 公鑰固定擴充套件是一個從2011年開始開發的針對 HTTP 使用者代理(即瀏覽器)的公鑰固定標準。它由 Google 發起,甚至在 Chrome 中實現的固定機制可以使用一個人工維護的網站公鑰固定列表,這個列表包含了固定的幾個網站的公鑰簽名。(LCTT 譯註:Chrome 和 FireFox 32 及以後版本都支援公鑰固定機制,並使用內建的人工維護的公鑰固定列表資料,這些資料隨著瀏覽器軟體的更新而更新,主要包括幾個大型站點。目前還只有 Chrome 38+ 支援通過 HTTP 響應頭傳遞公鑰固定資訊。)

以下是 HPKP 的幾個功能簡述:

  • HPKP 是在 HTTP 層面設定的,使用 Public-Key-Pins (PKP)響應頭。
  • 該規則的保留週期通過 max-age 引數設定,單位是秒。
  • PKP 響應頭只能用於正確的安全加密通訊裡面。
  • 如果出現了多個這樣的響應頭,則只處理第一個。
  • 固定機制可以使用includeSubDomains引數擴充套件到子域。
  • 當接收到一個新的 PKP 響應頭時,它會覆蓋之前儲存的公鑰固定和後設資料。
  • 公鑰固定是用雜湊演算法生成的,其實是一個“主題公鑰資訊(SKPI)”指紋。

本文首先會介紹一些 HPKP 工作的原理,接下來我們會展示給你如何得到需要的指紋並配置到 web 伺服器中。

SPKI 指紋 – 理論

以下摘自 Adam Langley 的帖子,我們雜湊的是一個公鑰,而不是證照:

通常來說,對證照進行雜湊是一個顯而易見的解決方案,但是其實這是錯的。不能這樣做的原因是 CA 證照可以不斷重新簽發:同一個公鑰、主題名可以對應多個證照,而這些證照有不同的延展或失效時間。瀏覽器從下至上地在證照池中構建證照鏈時,另外一個版本的證照可能就替代匹配了你原本所期望的證照。

舉個例子,StartSSL 有兩個根證照:一個是以 SHA1 簽名的,另外是一個是 SHA256。如果你希望固定住 StartSSL 作為你的 CA,那麼你該使用哪個證照呢?你也許可以使用這兩個,但是如果我不告訴你,你怎麼會知道還有一個根證照呢?

相反地,對公鑰進行雜湊則不會有這個問題:

瀏覽器假定子證照是固定不動的:它總是證照鏈的起點。子證照所攜帶的簽名一定是一個有效的簽名,它來自其父證照給這個證照專門簽發的。這就是說,父證照的公鑰相對於子證照來說是固定的。所以可推論公鑰鏈是固定的。

唯一的問題是你不能固定到一個交叉認證的根證照上。舉個例子,GoDaddy 的根證照是 Valicert 簽名的,這是為了讓那些不能識別 GoDaddy 根證照的老客戶可以信任其證照。然而,你不能固定到 Valicert 上,因為新的客戶在證照鏈上發現了 GoDaddy 證照就會停止上溯(LCTT 譯註:所以就找不到固定資訊了)。

此外,我們是對 SubjectPublicKeyInfo(SPKI)進行雜湊而不是對公鑰位串。SPKI 包括了公鑰型別、公鑰自身及其相關引數。這很重要,因為如果對公鑰進行雜湊就有可能導致發生曲解攻擊。對於一個 Diffie-Hellman 公鑰而言:如果僅對公鑰進行雜湊,而不是對完整的 SPKI,那麼攻擊者可以使用同樣的公鑰而讓客戶端將其解釋為其它組。同樣地,這樣也有可能強制將一個 RSA 金鑰當成 DSA 金鑰解釋等等。

固定在哪裡

你應該固定在什麼地方?固定你自己的公鑰並不是一個最好的辦法。你的金鑰也許會改變或撤銷。你也許會使用多個證照,經常輪換證照的話金鑰就改變了。也許由於伺服器被入侵而撤銷證照。

最容易但是不是太安全的方法是固定第一個中級 CA 證照。該證照是簽名在你的網站證照之上的,所以簽發該證照的 CA 的公鑰肯定是在證照鏈上的。

採用這種方法你可以從同一個 CA 更新你的證照而不用擔心固定資訊不對。如果該 CA 發行了一個不同的根證照,也許你會遇到一些問題,對此並沒有太好的解決方案。不過你可以通過如下做法來減輕這種問題的影響:

  • 從一個不同的 CA 申請一個備用的證照,並固定該備份。

RFC 裡面說你至少需要做兩個固定。一個是當前連線所使用的證照鏈上的,另外一個是備份的。

另外的固定是對備份公鑰的,它可以是來自另外一個給你簽發證照的不同 CA 的 SKPI 指紋。

在這個問題上還有一種更安全的方法,就是事先建立好至少三個獨立的公鑰(使用 OpenSSL,參見此頁 瞭解 Javascript OpenSSL 命令生成器),並將其中兩個備份到一個安全的地方,離線儲存、不要放到網上。

為這三個證照建立 SPKI 指紋並固定它們,然後僅使用第一個作為當前的證照。當需要時,你可以使用備份金鑰之一。不過你需要讓 CA 給你做簽名來生成證照對,這可能需要幾天,依你的 CA 的工作情況而定。

對於 HPKP 來說這沒有問題,因為我們使用的是公鑰的 SPKI 雜湊,而不是證照。失效或不同的 CA 簽名鏈並不影響。

如果你按照上述方法生成並安全儲存了至少三個獨立的金鑰,並固定它們,也可以防止你的 CA 撤銷你的網站證照並簽發一個假證照時出現問題。

SPKI 指紋

可以使用如下的 OpenSSL 命令來生成 SPKI 指紋,它出現在 RFC 草案 中:

openssl x509 -noout -in certificate.pem -pubkey | /
openssl asn1parse -noout -inform pem -out public.key;
openssl dgst -sha256 -binary public.key | openssl enc -base64

結果:

klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=

上面輸入的 certificate.pem 檔案是本站(https://raymii.org)的證照鏈中第一個證照。(在寫本文時, COMODO RSA Domain Validation Secure Server CA, 序列號 2B:2E:6E:EA:D9:75:36:6C:14:8A:6E:DB:A3:7C:8C:07 )。

你也需要同樣對你的另外兩個備份公鑰生成指紋。

故障

在寫本文時(2015/1),唯一支援 HPKP 的瀏覽器(chrome)有一個嚴重的問題:Chrome 並不能夠區分 HSTS 和 HPKP 響應頭中的 max-age 和 includeSubdomains 引數。也就是說,如果你的 HSTS 和 HPKP 設定了不同的 max-age 和 includeSubdomains 引數,它們會互相搞亂。關於這個故障的更多資訊參見:https://code.google.com/p/chromium/issues/detail?id=444511。感謝 Scott Helme(https://scotthelme.co.uk)發現並告訴我這個 Chromium 專案的問題。

Web 伺服器配置

下面你可以看到三個主流 Web 伺服器的配置方法。這只是一個 HTTP 響應頭,絕大多數 Web 伺服器都可以設定它。它只需要設定到 HTTPS 網站上。

下面的例子固定到 COMODO RSA Domain Validation Secure Server CA 及備份的 Comodo PositiveSSL CA 上,30天失效期,包括所有的子域。

Apache

編輯你的 Apache 配置檔案(如 /etc/apache2/sites-enabled/website.conf 或 /etc/apache2/httpd.conf),並新增下列行到你的 VirtualHost 中:

# 如需要,載入 headers 模組。
LoadModule headers_module modules/mod_headers.so

Header set Public-Key-Pins "pin-sha256=/"klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=/"; pin-sha256=/"633lt352PKRXbOwf4xSEa1M517scpD3l5f79xMD9r9Q=/"; max-age=2592000; includeSubDomains"

Lighttpd

Lighttpd 更簡單一些,將下列行新增到你的 Lighttpd 配置檔案(如 /etc/lighttpd/lighttpd.conf):

server.modules += ( "mod_setenv" )
$HTTP["scheme"] == "https" {
    setenv.add-response-header  = ( "Public-Key-Pins" => "pin-sha256=/"klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=/"; pin-sha256=/"633lt352PKRXbOwf4xSEa1M517scpD3l5f79xMD9r9Q=/"; max-age=2592000; includeSubDomains")
}

NGINX

NGINX 的配置更簡短。新增以下行到你的 HTTPS 配置的 server 塊中:

add_header Public-Key-Pins 'pin-sha256="klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY="; pin-sha256="633lt352PKRXbOwf4xSEa1M517scpD3l5f79xMD9r9Q="; max-age=2592000; includeSubDomains';

報告功能

HPKP 報告功能允許瀏覽器報告任何違例給你。

如果你在響應頭中新增了附加的 report-uri=”http://example.org/hpkp-report” 引數,並用該 URI 處理接收到的資料的話,客戶端會在發現違例時傳送報告給你。這個報告是以 POST 方式傳送到你指定的 report-uri 上,並以類似下面的 JSON 格式:

{
    "date-time": "2014-12-26T11:52:10Z",
    "hostname": "www.example.org",
    "port": 443,
    "effective-expiration-date": "2014-12-31T12:59:59",
    "include-subdomains": true,
    "served-certificate-chain": [
        "-----BEGINCERTIFICATE-----/nMIIAuyg[...]tqU0CkVDNx/n-----ENDCERTIFICATE-----"
    ],
    "validated-certificate-chain": [
        "-----BEGINCERTIFICATE-----/nEBDCCygAwIBA[...]PX4WecNx/n-----ENDCERTIFICATE-----"
    ],
    "known-pins": [
        "pin-sha256=/"dUezRu9zOECb901Md727xWltNsj0e6qzGk/"",
        "pin-sha256=/"E9CqVKB9+xZ9INDbd+2eRQozqbQ2yXLYc/""
    ]
}

非強制,只報告

HPKP 也可以設定為非強制的,可以使用 Public-Key-Pins-Report-Only 來只傳送違例報告給你。

這樣可以讓你在網站不可訪問或 HPKP 配置不正確時不固定,之後你可以將這個響應頭改為 Public-Key-Pins 來強制固定。

相關文章