TLSv1.3 概述
背景

SSL 是1994年網景公司提出,主要解決安全傳輸從0到1的過程,真正被大規模應用是1996年釋出的 SSLv3,經過了幾年的發展,在1999年被IETF納入标準化,改名叫 TLS,其實本質是一樣的,TLSv1.0 跟 SSLv3 沒有太多差異,TLSv1.1 做了一些 bug 修複和支援更多的參數,TLSv1.2 基于 TLSv1.1 做了更多的擴充和算法上改進,從2008年到今年近10年的時間,TLSv1.3 在今年3月份被 IETF 讨論組評正式納入标準化,8月初出了 RFC8446,但是 TLSv1.3 在2014年已經被提出來了,經曆了4年時間的讨論和優化,但是因為 TLSv1.2 已經被大量應用,一些網絡裝置并不相容之前提出的TLSv1.3草案,是以後來做了很多次優化,在第28個草案時才确定沒有問題,正式納入标準化。
趨勢
TLS 最大的應用就是 HTTPS,我們來看看 chrome 統計的 HTTPS 網頁趨勢,2015年的時候,大多數國家的HTTPS網頁加載次數隻占了不到 50%,2016年美國這個占比到了将近 60%,去年就已經超過70%, 目前美國的統計資料是 85%,可見,離 100% 已經很近了,從圖上可以看出,日本目前接近 70%,這裡面沒有統計到中國的資料,我估計也就 40% 左右,落後其他國家三年左右時間,空間還有很大,而且未來HTTPS趨勢是非常明顯的。至于國内大多數使用者不願意使用HTTPS的原因,無非幾個:安全意識、技術難度、性能和使用者體驗、成本等等原因,這方面阿裡雲 CDN 也在努力幫助使用者解決這些技術問題,在這裡不就展開了。
握手原理
TLSv1.2 握手原理
完成握手
先來看看 TLSv1.2 的完整握手流程:
SSL 握手之前是 TCP 握手,這裡面沒畫出來,SSL 握手總是以 ClientHello 消息開始,就跟 TCP 握手總是以 SYN 包開始一樣。
整個握手過程分為兩個過程:
-
參數協商
主要通過 Client Hello 和 Server Hello消息來協商,Client Hello 提供用戶端支援的參數(比如:密鑰套件、簽名算法、應用層協定清單等等),另外還包含一些必要的參數(比如:用戶端随機數、SNI、session id 等等),伺服器從中選取自己支援的參數,然後在 Server Hello 消息中傳回,告知用戶端本次會話要使用哪些參數。
-
密鑰交換
主要通過 Server Key Exchange 和 Client Key Exchange 來交換,其中 Server Key Exchange 是用來發送伺服器橢圓曲線算法的參數、公鑰資訊以及該資訊的簽名,Client Key Exchange 用來發送用戶端橢圓曲線算法的公鑰資訊,雙方得到對方橢圓曲線算法公鑰之後,與自己本地生成的臨時私鑰,通過橢圓曲線算法生成預主密鑰,再導出主密鑰和會話密鑰,握手完成之後通過會話密鑰來加密通信。
可以看出,完整的握手需要2個 RTT,而且每次握手都用到了非對稱加密算法簽名或者解密的操作,比較耗時和耗 CPU。假如1個 RTT 需要 100ms 的話,TLSv1.2 的完整握手時間就需要 200ms,再加上 HTTP 請求時間,那首位元組時間就是 300ms 左右了。
SSL 的握手消息雖然比較多,但很多消息都是放在一個 TCP 包中發送的,從抓包可以看出完整的 SSL 握手需要2個 RTT。
會話恢複
剛才通過完整的 SSL 握手可以看出幾個缺點:
- 2個 RTT,首包時間較長
- 每次都要做非對稱加密運算,比較耗 CPU,影響性能
-
每次都要傳證書,證書一般都比較大,浪費帶寬
SSL 握手的目的是協商參數和會話密鑰,如果能把這些會話參數緩存起來那就可以沒有必要每次都傳證書和做非對稱加密運算,這樣就可以提高握手性能,而且可以将 RTT 減到1個,提高使用者體驗。
TLSv1.2 有兩種會話恢複的方式:Session ID 和 Session Ticket
-
Session ID
每次完整握手之後都将協商好的會話參數緩存在用戶端和伺服器中,用戶端下次握手時會将上次握手的 Session ID 帶上,伺服器通過 Session ID 查詢是否有會話緩存,有的話就直接複用,沒有的話就重新走完整握手流程。
但是 Session ID 這種方式也有缺點,比較難支援分布式緩存以及耗費伺服器的記憶體。
-
Session Ticket
Session Ticket 的原理可以了解為跟 HTTP cookie 一樣,伺服器将協商好的會話參數和密鑰加密後發送給用戶端,用戶端下次握手時會将這個 Session Ticket 帶上,伺服器解密成功就複用上次的會話參數和密鑰,否則就重新走完整握手流程。可以看出 Session Ticket 這種方式不需要伺服器緩存什麼,天生支援分布式環境,有很大的優勢。
這種方式有一個缺點是并不是所有用戶端都支援,支援率比較低,但随着用戶端版本的更新疊代,以後各種用戶端會都支援。因為 Session ID 用戶端支援率比較高,是以目前這兩種方式都在使用。
這是我們一個CDN節點上了分布式 Session ID 複用的效果,Session ID 複用的比例在沒有開啟分布式緩存時隻占了 3% 左右,複用率很低,上了分布式 Session 緩存之後,這個比例提升到 20% 多。握手時間也從 75ms左右降低到 65ms 左右,性能提升效果還是很明顯的。
TLSv1.3 握手原理
完整握手
剛才講到 TLSv1.2 握手主要是兩步:參數協商和密鑰交換,TLSv1.3 做了優化,把這兩步合并成一步來完成了:
Client Hello 包含了用戶端支援的參數資訊,以及橢圓曲線算法的公鑰資訊,伺服器通過 Server Hello 來響應選取的參數資訊和伺服器橢圓曲線公鑰資訊,删除了 TLSv1.2 中的 Server Key Exchange 和 Client Key Exchange,這樣通過1個 RTT 就可以完成了參數協商和密鑰交換,雙方都知道了對方的橢圓曲線公鑰,然後用自己的臨時私鑰就可以計算出預主密鑰(PMS),接着就可以導出資料通信的加密密鑰,但不是像 TLSv1.2 那樣的主密鑰和會話密鑰,TLSv1.3 有5個主要的密鑰,一會兒我們再細講。
這樣,假如1個 RTT 需要 100ms 的話,TLSv1.3 完整握手時間隻需要 100ms,HTTP 的首位元組時間隻需要200ms,比 TLSv1.2 就少了1個 RTT。這個是一個非常重要的改進。
TLSv1.3 的會話恢複跟 TLSv1.2 不一樣,取消了 Sesion ID 的方式,采用 PSK 機制,類似 TLSv1.2 的 Session Ticket,并在 Session Ticket 中添加了過期時間。
TLSv1.2 是通過 Client Hello 的 SessionTicket 擴充選項來傳輸這個加密的會話緩存參數,但在 TLSv1.3 裡面是通過 PSK(pre_shared_key)擴充選項來傳輸。
伺服器收到 PSK 之後解密成功就可以複用會話了,不需要再重新傳輸證書和協商密鑰了,跟 TLSv1.2 的會話恢複一樣,隻需要1個 RTT 就可以完成握手,提高了握手的性能。
抓包截圖可以看到:
- 上面是 TLSv1.3 完整握手,下面是 TLSv1.3 會話恢複握手,都是1個 RTT 就完成。
- Server Hello 之後的資料包都是加密的。
- 連響應的證書是什麼證書都看不到,調試比較不友善。
0-RTT
我們再來看看 TLSv1.3 另一個重要特性 0-RTT:
所謂 0-RTT 是指在會話複用的基礎上做到的,在完整握手時沒法做到 0-RTT,這是因為請求是用 PSK 導出的一個叫做 early_data 的密鑰加密的,伺服器收到之後可以用 PSK 導出的 ealy_data 密鑰解密,進而得出請求明文資料,這樣就可以做到 0-RTT。
但是 TLSv1.3 的 0-RTT 是犧牲了一定的安全性的,沒法做到完全前向安全(PFS),因為知道了 SessionTicket 的加密 key 就可以導出 early_data 密鑰,進而可以解密出 0-RTT 資料。比如帶有鑒權資訊的請求被中間網絡裝置解密出來了可能就導緻盜鍊這些問題;另外 0-RTT 也有重播攻擊的問題,是以 TLSv1.3 的 0-RTT 并不是必須的,隻是協定層面支援了 0-RTT 模式,對于請求資訊不敏感的業務可以使用 0-RTT 來提高性能。目前呢,Chrome 浏覽器并不支援 0-RTT 模式,Firefox 浏覽器支援,但是我配置了并沒有抓到 0-RTT 的包。伺服器方面:tengine 已經支援了 0-RTT,nginx 和 openresty 還沒有支援 0-RTT。
RTT 對比
目前,TLS 主要應用在 HTTPS 和 HTTP/2,這個表是一個完整 HTTP/2 請求從 TCP 開始所需的 RTT 對比,可以看出,不管是 TLSv1.2 還是 TLSv1.3、首次連接配接還是會話複用,TCP 握手和 HTTP 請求各 1 個 RTT 是不可避免的,能優化的就隻有 TLS 握手了,從剛才講的 TLSv1.2 和 TLSv1.3 的握手原理可以知道,在首次連接配接和會話複用情況下,TLSv1.3 都比 TLSv1.2 少一個 RTT。
核心改進
主要差異
總結一下相比 TLSv1.2,TLSv1.3 主要的差異有哪些:
握手時間:同等情況下,TLSv1.3 比 TLSv1.2 少一個 RTT
應用資料:在會話複用場景下,支援 0-RTT 發送應用資料
握手消息:從 ServerHello 之後都是密文。
TLSv1.3 協定版本的協商是從擴充選項裡面選的,不是從 ClientHello 消息的 version 字段協商的,這是因為中間有一些網絡裝置對 version 字段做了識别和限制,如果對 version 進行更新,那 TLSv1.3的資料包可能在一些網絡裝置中被當成異常包,不利于 TLSv1.3 的部署,是以 TLSv1.3 使用了 ClientHello 的一個新加的擴充字段 supported_versions 來協商協定版本。TLSv1.3 的記錄頭以及握手消息中 version 字段跟 TLSv1.2 一樣的。另外一個握手層面的差異就是 TLSv1.3 禁止重協商。
會話複用機制:棄用了 Session ID 方式的會話複用,采用 PSK 機制的會話複用。
密鑰算法:TLSv1.3 隻支援 PFS (即完全前向安全)的密鑰交換算法,禁用 RSA 這種密鑰交換算法,這是因為使用 RSA 密鑰交換的話,如果拿到 SSL 私鑰就可以解密抓包資料,不具備完全前向安全性。對稱密鑰算法隻采用 AEAD 類型的加密算法,像 CBC 模式的 AES、RC4 這些算法在 TLSv1.3 是禁用的。
密鑰導出算法:TLSv1.3 使用新設計的叫做 HKDF 的算法,而 TLSv1.2 是使用PRF算法,稍後我們再來看看這兩種算法的差别。
密鑰套件
TLSv1.3 目前定義了這5個密鑰套件,從表面上看已經隐藏了密鑰交換算法了,因為都是使用橢圓曲線密鑰交換算法。
密鑰導出函數 PRF
我們來看看 TLSv1.2 的密鑰導出算法 PRF,PRF 主要目的是用預主密鑰、用戶端随機數、伺服器随機數導出主密鑰,再從主密鑰、雙方随機數導出會話密鑰,最終用會話密鑰來加密通信。
對于 RSA 來說,預主密鑰是用戶端生成,用伺服器證書公鑰加密之後發給伺服器,伺服器用證書對應的私鑰來解密得到。
對于橢圓曲線算法來說,預主密鑰是雙方通過橢圓曲線算法來生成的,雙方各自生成臨時公私鑰對,保留私鑰,将公鑰發給對方,然後就可以用自己的私鑰以及對方的公鑰通過橢圓曲線算法來生成預主密鑰。
可以看出,隻要我們知道預主密鑰或者主密鑰便可以解密抓包資料,是以 TLSv1.2 抓包解密調試隻需要一個主密鑰即可,SSLKEYLOG 就是将主密鑰導出來,在 Wireshark 裡面導入就可以解密相應的抓包資料。
另外,Session ID 緩存和 Session Ticket 裡面儲存的也是主密鑰,而不是會話密鑰,這樣每次會話複用的時候再用雙方的随機數和主密鑰導出會話密鑰,進而實作每次加密通信的會話密鑰不一樣,即使一個會話被破解了也不會影響到另一個會話。
HKDF
但是在 TLSv1.3裡面,不再使用 PRF 這種算法了,而是采用更标準的 HKDF 算法來進行密鑰的推導。
而且在 TLSv1.3 中對密鑰進行了更細粒度的優化,每個階段或者方向的加密都不是使用同一個密鑰,我們知道TLSv1.3 在 ServerHello 消息之後的資料都是加密的,那握手期間伺服器給用戶端發送的消息用 server_handshake_traffic_secret 通過 HKDF 算法導出的密鑰加密的,用戶端發送給伺服器的握手消息是用 client_handshake_traffic_secret 通過 HKDF 算法導出的密鑰加密的。這兩個密鑰是通過 Handshake Secret 密鑰來導出的,而 Handshake Secret 密鑰又是由 PMS (預主密鑰)和 Early Secret 密鑰導出,然後通過 Handshake Secret 密鑰導出主密鑰 Master Secret。
再由主密鑰 Master Secret 導出這幾個密鑰:
client_application_traffic_secret:用來導出用戶端發送給伺服器應用資料的對稱加密密鑰
server_application_traffic_secret:用來導出伺服器發送給用戶端應用資料的對稱加密密鑰
resumption_master_secret:sessoin ticket 裡的主密鑰
在會話複用的時候略有差别,主要是要導出 0-RTT 時 early_data 要加密的密鑰 client_ealy_traffic_secret,這個是由 PSK 來導出的。
可以看出要解密 TLSv1.3 需要5個密鑰才行。
應用實踐
支援 TLSv1.3 的伺服器
伺服器要使用 TLSv1.3 并不難,tengine-2.2.2 開始支援 TLSv1.3,nginx-1.13.8 開始支援 TLSv1.3,但是TLSv1.3 的核心實作是在 openssl 庫,但目前 openssl 穩定版本并不支援 TLSv1.3,隻有 openssl-1.1.1 的預發版本或者開發版本上才支援 TLSv1.3,我們下載下傳支援 TLSv1.3 的 openssl 代碼之後,在編譯參數中需要加上 enable-tls1_3,編譯出 openssl 動态庫或者靜态庫,在 tengine 中依賴支援這個 openssl 庫就可以使用 TLSv1.3 了。
需要注意的是 openssl 的一些接口做了改動,比如之前是一個函數,現在變成了宏,這個就導緻 lua 中不能調用這個 ffi 接口了,如果自己的業務中使用了老版本 openssl 的 ffi 接口需要特别注意。
Tengine 配置 TLSv1.3
編譯出支援 TLSv1.3 的 tengine 後,隻需要做一下配置就可以了,在 ssl_protocols 指令中加了 TLSv1.3,同時在 ssl_ciphers 指令中加上 TLSv1.3 相關的密鑰套件。
但是這裡有一個坑:
ssl_protocols 這個指令隻在預設 server 中生效,在同一個 IP 的其他 server 塊并不生效,比如有這樣的需求:兩個域名一個開啟 TLSv1.3,另一個不開啟 TLSv1.3,目前開源 tengine 和 nginx 是做不到的。
我們來看看為什麼做不到。
這是在 tengine 裡面處理一個 HTTPS 請求的大緻流程,先經過握手,握手成功之後再處理 HTTP 請求。
在握手流程中,openssl 提高了幾個回調函數接口讓我們介入,最主要的接口有這麼幾個:
client_hello_cb、get_session_cb、servername_cb、cert_cb。
client_hello_cb 是收到 ClientHello 消息後執行的第一個回調,然後選擇協定版本和選擇密鑰套件,接着調用 get_session_cb 來擷取緩存的 Session,在這個階段我們可以做分布式緩存的擷取,解決分布式環境中Session ID複用率低的問題。
openssl 解析完SNI後調用 servername_cb,這個回調主要用來切換 server 塊配置和 SSL_CTX,實作多個 server 塊不同 SSL 配置的問題,但可以看出協定版本和密鑰套件早已經選擇好了,在這個階段做 SSL_CTX 切換對這兩個配置并沒有作用。
最後調用 cert_cb 來切換證書,在這個階段可以用 lua 介入,實作證書的熱加載。
要想實作協定版本和密鑰套件域名定制的話,必須要在 client_hello_cb 這個階段進行切換,但是 client_hello_cb 這個回調是新版 openssl 才支援。
是以,我們内部已經實作了這個接口,在這個階段切換 server 塊配置和 SSL_CTX,進而實作所有 SSL 配置的熱加載。這部分的實作後面我會提到開源 tengine 中,有需要的話到時關注一下。
支援 TLSv1.3 的浏覽器
看一下目前支援 TLSv1.3 的浏覽器,主要是 Chrome 和 Firefox,Chrome 是 63 之後開始支援,Firefox 是 61 版本之後支援,但每個版本支援的 TLSv1.3 的 draft 版本可能不一樣,目前 Chrome 主要支援 draft23 和 draft28。Firefox 支援 draft28。IE 和 Safri 目前還沒有支援 TLSv1.3,但未來也會支援的。
Chrome 開啟 TLSv1.3
在 chrome://flags 開關配置中選擇開啟的 TLSv1.3 哪個草案版本,然後重新開機 chrome 後生效。
通路一個開啟 TLSv1.3 域名之後,打開 chrome 的開發者工具調試面闆,便可以看出目前 SSL 連接配接是否已經采用了 TLSv1.3
Firefox 開啟 TLSv1.3
在 Firefox 的配置中心将 security.tls.version.max 設定成 4,就可以開啟 TLS 1.3 了,目前 Firefox 隻支援了最新的 TLS 1.3 Draft28
同樣的,在 Firefox 浏覽器中通路一個開啟 TLSv1.3 的域名之後,打開 Firefox 的調試工具,也可以看出是否已經使用了 TLSv1.3
TLSv1.3 調試
最後來看看平時怎麼用 openssl 來調試 TLSv1.3
用 openssl 這兩個指令來啟動 server 端和用戶端,通過參數 tls1_3 參數來啟動 TLSv1.3,用 early_data 參數來實作 0-RTT 的調試。用 keylogfile 參數來記錄 TLSv1.3 裡面用到的密鑰。
這是TLSv1.3 的 keylog,可以看出主要有這5個密鑰,其中:
CLIENT_EARLY_TRAFFIC_SECRET 是 0-RTT 資料的加密密鑰;
SERVER_HANDSHAKE_TRAFFIC_SECRET 是 Server 端加密握手消息用的密鑰;
SERVER_TRAFFIC_SECRET_0 是 Server 端加密應用資料的密鑰;
CLIENT_HANDSHAKE_TRAFFIC_SECRET 是 Client 端加密握手消息用的密鑰;
CLIENT_TRAFFIC_SECRET_0 是 Client 端加密應用資料的密鑰。
然後我們用 wireshark 導入這個 keylog 檔案,就可以實作 TLSv1.3 資料包的解密了。
這個是 TLSv1.3 0-RTT 解密後的截圖,可以看出在 TCP 握手之後,0-RTT 的資料在 Client Hello 之後就發送了。
有興趣的話,可以自己動手調試一下。
最後插播個招聘廣告:如果你對 tengine/nginx/openssl 這些技術感興趣,歡迎自薦或者推薦。
(全文完)