1. 程式人生 > >TLS 握手優化詳解

TLS 握手優化詳解

提醒:本文最後更新於 913 天前,文中所描述的資訊可能已發生改變,請謹慎使用。

隨著 HTTP/2 的逐漸普及,以及國內網路環境越來越糟糕(運營商劫持和篡改),HTTPS 已經開始成為主流。HTTPS 在 TCP 和 HTTP 之間增加了 TLS(Transport Layer Security,傳輸層安全),提供了內容加密、身份認證和資料完整性三大功能,同時也給 Web 效能優化帶來新的挑戰。上次寫的「使用 BoringSSL 優化 HTTPS 加密演算法選擇」一文中,我介紹瞭如何針對不同平臺啟用最合適的傳輸加密演算法。本篇文章我打算繼續寫 HTTPS 優化 —— TLS 握手優化。

TLS 的前身是 SSL(Secure Sockets Layer,安全套接字層),由網景公司開發,後來被 IETF 標準化並改名。通常沒有特別說明時,SSL 和 TLS 指的是同一個協議,不做嚴格區分。

TLS 握手

在傳輸應用資料之前,客戶端必須與服務端協商金鑰、加密演算法等資訊,服務端還要把自己的證書發給客戶端表明其身份,這些環節構成 TLS 握手過程,如下圖所示:

tls-handshake

可以看到,假設服務端和客戶端之間單次傳輸耗時 28ms,那麼客戶端需要等到 168ms 之後才能開始傳送 HTTP 請求報文,這還沒把客戶端和服務端處理時間算進去。光是 TLS 握手就需要消耗兩個 RTT(Round-Trip Time,往返時間),這就是造成 HTTPS 更慢的主要原因。當然,HTTPS 要求資料加密傳輸,加解密相比 HTTP 也會帶來額外的開銷,不過對稱加密本來就很快,加上硬體效能越來越好,所以這部分開銷還好。

詳細的 TLS 握手過程這裡就不介紹了,大家可以通過這兩篇文章去了解:

通過 Wireshark 抓包可以清楚地看到完整 TLS 握手過程所需的兩個 RTT,如下圖:

tls-full-handshake

False Start

False Start 有搶跑的意思,意味著不按規則行事。TLS False Start 是指客戶端在傳送 Change Cipher Spec Finished 同時傳送應用資料(如 HTTP 請求),服務端在 TLS 握手完成時直接返回應用資料(如 HTTP 響應)。這樣,應用資料的傳送實際上並未等到握手全部完成,故謂之搶跑。這個過程如下圖所示:

tls-handshake-with-false-start

可以看到,啟用 False Start 之後,TLS 階段只需要一次 RTT 就可以開始傳輸應用資料。False Start 相當於客戶端提前傳送加密後的應用資料,不需要修改 TLS 協議,目前大部分瀏覽器預設都會啟用,但也有一些前提條件:

  • 服務端必須在 Server Hello 握手中通過 NPN(Next Protocol Negotiation,下一代協議協商,Google 在 SPDY 協議中開發的 TLS 擴充套件,用於握手階段協商應用協議)或 ALPN(Application Layer Protocol Negotiation,應用層協議協商,NPN 的官方修訂版)表明自己支援的 HTTP 協議,例如:http/1.1、http/2;
  • 使用支援前向安全性(Forward Secrecy)的加密演算法。False Start 在尚未完成握手時就傳送了應用資料,Forward Secrecy 可以提高安全性;

通過 Wireshark 抓包可以清楚地看到 False Start 帶來的好處(服務端的 ChangeCipherSpec 出現在 158 號包中,但在之前的 155 號包中,客戶端已經發出了請求,相當於 TLS 握手只消耗了一個 RTT):

tls-false-start

Certificate

TLS 的身份認證是通過證書信任鏈完成的,瀏覽器從站點證書開始遞迴校驗父證書,直至出現信任的根證書(根證書列表一般內置於作業系統,Firefox 自己維護)。站點證書是在 TLS 握手階段,由服務端傳送的。

Certificate-Chain

配置服務端證書鏈時,有兩點需要注意:1)證書是在握手期間傳送的,由於 TCP 初始擁塞視窗的存在,如果證書太長可能會產生額外的往返開銷;2)如果證書沒包含中間證書,大部分瀏覽器可以正常工作,但會暫停驗證並根據子證書指定的父證書 URL 自己獲取中間證書。這個過程會產生額外的 DNS 解析、建立 TCP 連線等開銷,非常影響效能。

配置證書鏈的最佳實踐是隻包含站點證書和中間證書,不要包含根證書,也不要漏掉中間證書。大部分證書鏈都是「站點證書 - 中間證書 - 根證書」這樣三級,這時服務端只需要傳送前兩個證書即可。但也有的證書鏈有四級,那就需要傳送站點證書外加兩個中間證書了。

通過 Wireshark 可以檢視服務端傳送的證書鏈情況,如下圖。可以看到本站傳送了兩個證書,共 2270 位元組,被分成 2 個 TCP 段來傳輸。這已經算小的了,理想的證書鏈應該控制在 3kb 以內。

tls-certificate

ECC Certificate

如果需要進一步減小證書大小,可以選擇 ECC(Elliptic Curve Cryptography,橢圓曲線密碼學)證書。256 位的 ECC Key 等同於 3072 位的 RSA Key,在確保安全性的同時,體積大幅減小。下面是一個對比:

對稱加密 Key 長度 RSA Key 長度 ECC Key 長度
80 1024 160
112 2048 224
128 3072 256
192 7680 384
256 15360 521

如果證書提供商支援 ECC 證書,使用以下命令生成 CSR(Certificate Signing Request,證書籤名請求)檔案並提交給提供商,就可以獲得 ECC 證書:

openssl ecparam -genkey -name secp256r1 | openssl ec -out ecc.key
openssl req -new -key ecc.key -out ecc.csr

以上命令中可以選擇的演算法有 secp256r1 和 secp384r1,secp521r1 已被 Chrome 和 Firefox 拋棄。

ECC 證書這麼好,為什麼沒有普及呢?最主要的原因是它的支援情況並不好。例如 Windows XP 不支援,導致使用 ECC 證書的網站在 Windows XP 上只有 Firefox 能訪問(Firefox 證書那一套完全自己實現,不依賴作業系統)。另外,Android 平臺也只有 Android 4+ 才支援 ECC 證書。所以,確定使用 ECC 證書前需要明確使用者系統分佈情況。

Session Resumption

另外一個提高 TLS 握手效率的機制是會話複用。會話複用的原理很簡單,將第一次握手辛辛苦苦算出來的對稱金鑰存起來,後續請求中直接使用。這樣可以節省證書傳送等開銷,也可以將 TLS 握手所需 RTT 減少到一個,如下圖所示:

tls-handshake-with-session-resumption

可以看到會話複用機制生效時,雙方几乎不怎麼交換資料就協商好金鑰了,這是怎麼做到的呢?

Session Identifier

Session Identifier(會話識別符號),是 TLS 握手中生成的 Session ID。服務端可以將 Session ID 協商後的資訊存起來,瀏覽器也可以儲存 Session ID,並在後續的 ClientHello 握手中帶上它,如果服務端能找到與之匹配的資訊,就可以完成一次快速握手。

Session Ticket

Session Identifier 機制有一些弊端,例如:1)負載均衡中,多機之間往往沒有同步 Session 資訊,如果客戶端兩次請求沒有落在同一臺機器上就無法找到匹配的資訊;2)服務端儲存 Session ID 對應的資訊不好控制失效時間,太短起不到作用,太長又佔用服務端大量資源。

而 Session Ticket(會話記錄單)可以解決這些問題,Session Ticket 是用只有服務端知道的安全金鑰加密過的會話資訊,最終儲存在瀏覽器端。瀏覽器如果在 ClientHello 時帶上了 Session Ticket,只要伺服器能成功解密就可以完成快速握手。

配置 Session Ticket 策略後,通過 Wireshark 可以看到服務端傳送 Ticket 的過程:

tls-new-session-ticket

以下是 Session Resumption 機制生效時的握手情況,可以看到沒有傳送證書等環節:

tls-session-ticket

測試

Github 上有一個名為 rfc5077 的專案,非常適合用來測試服務端對 Session ID 和 Session Ticket 這兩種 TLS 會話複用機制的支援情況。下面簡單介紹如何使用這個工具。

首先,安裝工具所需依賴(以下所有步驟僅在 Ubuntu 14.04.4 LTS 測試通過):

sudo apt-get install -y pkg-config libssl-dev libev-dev libpcap-dev libgnutls-dev libnss3-dev

然後就可以獲取原始碼,開始編譯:

git clone https://github.com/vincentbernat/rfc5077.git
cd rfc5077/
git submodule init
git submodule update
make

編譯完成後,當前目錄會出現多個可執行檔案。這裡我們只會用到 rfc5077-client,它的用法是這樣的:

./rfc5077-client [-p {port}] [-s {sni name}] [-4] host [host ...]
  • -p 用於指定連線的埠,預設是 443;
  • -s 用於指定 SNI,如果同 IP 同埠部署了多個 HTTPS 網站,需要通過這個引數指定 SNI;
  • -4 表示只使用 IPV4 地址;
  • 命令最後需要可以跟一個或多個 HOST(域名或 IP);

這個工具會先禁用 Session Ticket 將所有 HOST 都測試一遍,然後開啟 Session Ticket 再測試一遍。下面是對本站兩個 IP 進行測試的命令:

./rfc5077-client -s imququ.com 114.215.116.12 139.162.98.188

測試結果如下(去掉了部分不重要的資訊):

[√] Check arguments.
[√] Solve 114.215.116.12:
    │ Got 1 result:
    │ 114.215.116.12
[√] Solve 139.162.98.188:
    │ Got 1 result:
    │ 139.162.98.188
[√] Using SNI name imququ.com.
[√] Prepare tests.
[√] Run tests without use of tickets.
[√] Display result set:
    │   IP address   │ Try │ Reuse │ SSL Session ID │ Master key │ Ticket
    │ ───────────────┼─────┼───────┼────────────────┼────────────┼────────
    │ 114.215.116.12 │  0  │   ×   │ BAF0834EA3896… │ 132A7A2DC… │   ×   
    │ 114.215.116.12 │  1  │   √   │ BAF0834EA3896… │ 132A7A2DC… │   ×   
    │ 114.215.116.12 │  2  │   √   │ BAF0834EA3896… │ 132A7A2DC… │   ×   
    │ 114.215.116.12 │  3  │   √   │ BAF0834EA3896… │ 132A7A2DC… │   ×   
    │ 114.215.116.12 │  4  │   √   │ BAF0834EA3896… │ 132A7A2DC… │   ×   
    │ 139.162.98.188 │  0  │   ×   │ 2F8143213E3B9… │ 1BFE00946… │   ×   
    │ 139.162.98.188 │  1  │   √   │ 2F8143213E3B9… │ 1BFE00946… │   ×   
    │ 139.162.98.188 │  2  │   √   │ 2F8143213E3B9… │ 1BFE00946… │   ×   
    │ 139.162.98.188 │  3  │   √   │ 2F8143213E3B9… │ 1BFE00946… │   ×   
    │ 139.162.98.188 │  4  │   √   │ 2F8143213E3B9… │ 1BFE00946… │   ×   
[√] Dump results to file.
[√] Run tests with use of tickets.
[√] Display result set:
    │   IP address   │ Try │ Reuse │ SSL Session ID │ Master key │ Ticket
    │ ───────────────┼─────┼───────┼────────────────┼────────────┼────────
    │ 114.215.116.12 │  0  │   ×   │ 96A21A1849BD4… │ C15030CF8… │   √   
    │ 114.215.116.12 │  1  │   √   │ 96A21A1849BD4… │ C15030CF8… │   √   
    │ 114.215.116.12 │  2  │   √   │ 96A21A1849BD4… │ C15030CF8… │   √   
    │ 114.215.116.12 │  3  │   √   │ 96A21A1849BD4… │ C15030CF8… │   √   
    │ 114.215.116.12 │  4  │   √   │ 96A21A1849BD4… │ C15030CF8… │   √   
    │ 139.162.98.188 │  0  │   √   │ 96A21A1849BD4… │ C15030CF8… │   √   
    │ 139.162.98.188 │  1  │   √   │ 96A21A1849BD4… │ C15030CF8… │   √   
    │ 139.162.98.188 │  2  │   √   │ 96A21A1849BD4… │ C15030CF8… │   √   
    │ 139.162.98.188 │  3  │   √   │ 96A21A1849BD4… │ C15030CF8… │   √   
    │ 139.162.98.188 │  4  │   √   │ 96A21A1849BD4… │ C15030CF8… │   √   
[√] Dump results to file.

從以上結果可以看出:禁用 Session Ticket 時,每次連線到不同 IP 都會導致 Session 無法複用;而啟用 Session Ticket 後,不同 IP 之間也可以複用 Session。符合前面的結論。

值得注意的是,為了讓一臺伺服器生成的 Session Ticket 能被另外伺服器承認,往往需要對 Web Server 進行額外配置。例如在 Nginx 中,就需要通過 ssl_session_ticket_key 引數讓多臺機器使用相同的 key 檔案,否則 Nginx 會使用隨機生成的 key 檔案,無法複用 Session Ticket。出於安全考慮,key 檔案應該定期更換,並且確保換下來的 key 檔案被徹底銷燬。

OCSP Stapling

出於某些原因,證書頒發者有時候需要作廢某些證書。那麼證書使用者(例如瀏覽器)如何知道一個證書是否已被作廢呢?通常有兩種方式:CRL(Certificate Revocation List,證書撤銷名單)和 OCSP(Online Certificate Status Protocol,線上證書狀態協議)。

CRL 是由證書頒發機構定期更新的一個列表,包含了所有已被作廢的證書,瀏覽器可以定期下載這個列表用於驗證證書合法性。不難想象,CRL 會隨著時間推移變得越來越大,而且實時性很難得到保證。OCSP 是一個線上查詢介面,瀏覽器可以實時查詢單個證書的合法性。在每個證書的詳細資訊中,都可以找到對應頒發機構的 CRL 和 OCSP 地址。

OCSP 的問題在於,某些客戶端會在 TLS 握手階段進一步協商時,實時查詢 OCSP 介面,並在獲得結果前阻塞後續流程,這對效能影響很大。而 OCSP Stapling(OCSP 封套),是指服務端在證書鏈中包含頒發機構對證書的 OCSP 查詢結果,從而讓瀏覽器跳過自己去驗證的過程。服務端有更快的網路,獲取 OCSP 響應更容易,也可以將 OCSP 響應快取起來。

OCSP 響應本身經過了數字簽名,無法偽造,所以 OCSP Stapling 技術既提高了握手效率,也不會影響安全性。啟用這項技術後,也可以通過 Wireshark 來驗證:

tls-ocsp-response

可以看到,服務端在傳送完證書後,緊接著又發來了它的 OCSP 響應,從而避免了瀏覽器自己去驗證證書造成阻塞。需要注意的是,OCSP Stapling 只能包含一個 OCSP 響應,瀏覽器還是可能自己去驗證中間證書。另外,OCSP 響應本身會佔用幾 kb 的大小。

OCSP Stapling 功能需要 Web Server 的支援,主流的 Nginx、Apache 和 H2O 都支援 —— 但同時還取決於使用的 SSL 庫 —— 例如 BoringSSL 不支援 OCSP Stapling,使用 BoringSSL + Nginx 就無法開啟 OCSP Stapling。

如何使用 Nginx 配置本文這些策略,可以參考我之前的文章:本部落格 Nginx 配置之效能篇

最後,強烈推薦 Qualys SSL Labs 的 SSL Server Test 工具,可以幫你查出 HTTPS 很多配置上的問題。本部落格的測試結果見這裡

--EOF--

提醒:本文最後更新於 913 天前,文中所描述的資訊可能已發生改變,請謹慎使用。