上一篇:《基于 TLS 1.3的微信安全通信協定 mmtls 介紹(上)》
經過上面的 Handshake 過程,此時 Client 和 Server 已經協商出了一緻的對稱加密密鑰 <code>pre_master_key</code>,那麼接下來就可以直接用這個 <code>pre_master_key</code> 作為密鑰,選擇一種對稱加密算法(如常用的 AES-CBC)加密業務資料,将密文發送給 Server。是否真的就這麼簡單呢?實際上如果真的按這個過程進行加密通信是有很多安全漏洞的。
"加密并不是認證"在密碼學中是一個簡單的共識,但對于我們很多程式員來說,并不知道這句話的意義。加密是隐藏資訊,使得在沒有正确密鑰的情況下,資訊變得難以讀懂,加密算法提供保密性,上面所述的 AES-CBC 這種算法隻是提供保密性,即防止資訊被竊聽。在資訊安全領域,消息認證(message authentication)或資料源認證(data origin authentication)表示資料在傳輸過程中沒有被修改(完整性),并且接收消息的實體能夠驗證消息的源(端點認證)。AES-CBC 這種加密算法隻提供保密性,但是并不提供完整性。這似乎有點違反直覺,好像對端發給我一段密文,如果我能夠解密成功,通過過程就是安全的,實則不然,就拿 AES-CBC 加密一段資料,如果中間人篡改部分密文,隻要不篡改 padding 部分,大部分時候仍舊能夠正常解密,隻是得到的明文和原始明文不一樣。現實中也有對消息追加 CRC 校驗來解決密文被篡改問題的,實際上經過精心構造,即使有 CRC 校驗仍然能夠被繞過。本質的原因是在于進行加密安全通信過程,隻使用了提供保密性的對稱加密元件,沒有使用提供消息完整性的密碼學元件。
是以隻要在用對稱加密算法加密明文後,再用消息認證碼算法對密文計算一次消息認證碼,将密文和消息認證碼發送給 Server,Server 進行驗證,這樣就能保證安全性了。實際上加密過程和計算消息認證碼的過程,到底應該如何組合,誰先誰後,在密碼學發展的曆史上先後出現了三種組合方式:(1) Encrypt-and-MAC (2) MAC-then-Encrypt (3) Encrypt-then-MAC,根據最新密碼學動态,目前學術界已經一緻同意 Encrypt-then-MAC 是最安全的,也就是先加密後算消息認證碼的方式。鑒于這個陷阱如此險惡,是以就有人提出将 Encrypt 和 MAC 直接內建在一個算法内部,讓有經驗的密碼專家在算法内部解決安全問題,不讓算法使用者選擇,這就是這就是 AEAD(Authenticated-Encryption With Addtional data)類的算法。TLS1.3 徹底禁止 AEAD 以外的其他算法。mmtls 經過綜合考慮,選擇了使用 AES-GCM 這種 AEAD 類算法,作為協定的認證加密元件,而且 AES-GCM 也是 TLS1.3 要求必須實作的算法。
TLS1.3 明确要求通信雙方使用的對稱加密 Key 不能完全一樣,否則在一些對稱加密算法下會被完全攻破,即使是使用 AES-GCM 算法,如果通信雙方使用完全相同的加密密鑰進行通信,在使用的時候也要小心翼翼的保證一些額外條件,否則會洩露部分明文資訊。另外,AES 算法的初始化向量(IV)如何構造也是很有講究的,一旦用錯就會有安全漏洞。也就是說,對于 handshake 協定協商得到的 <code>pre_master_secret</code>不能直接作為雙方進行對稱加密密鑰,需要經過某種擴充變換,得到六個對稱加密參數:
當然,使用 AES-GCM 作為對稱加密元件,MAC Key 和 Encryption Key 隻需要一個就可以了。
握手生成的 <code>pre_master_secret</code> 隻有 48 個位元組,上述幾個加密參數的長度加起來肯定就超過 48 位元組了,是以需要一個函數來把 48 位元組延長到需要的長度,在密碼學中專門有一類算法承擔密鑰擴充的功能,稱為密鑰衍生函數(Key Derivation Function)。TLS1.3 使用的 HKDF 做密鑰擴充,mmtls 也是選用的 HKDF 做密鑰擴充。
在前文中,我用 <code>pre_master_secret</code> 代表握手協商得到的對稱密鑰,在 TLS1.2 之前确實叫這個名字,但是在 TLS1.3 中由于需要支援 0-RTT 握手,協商出來的對稱密鑰可能會有兩個,分别稱為 Static Secret(SS) 和 Ephemeral Secret(ES)。從 TLS1.3 文檔中截取一張圖進行說明一下:

上圖中 Key Exchange 就是代表握手的方式,在 1-RTT ECDHE 握手方式下,
在 0-RTT ECDH 下,
在 0-RTT/1-RTT PSK 握手下,
在 0-RTT PSK-ECDHE 握手下,
前面說過 mmtls 使用的密鑰擴充元件為 HKDF,該元件定義了兩個函數來保證擴充出來的密鑰具有僞随機性、唯一性、不能逆推原密鑰、可擴充任意長度密鑰。兩個函數分别是:
該函數的作用是對 initial-keying-material 進行處理,保證它的熵均勻分别,足夠的僞随機。
參數 pseudorandom key 是已經足夠僞随機的密鑰擴充材料,HKDF-Extract 的傳回值可以作為 <code>pseudorandom key</code>,<code>info</code> 用來區分擴充出來的 Key 是做什麼用,<code>out_key_length</code> 表示希望擴充輸出的 key 有多長。mmtls 最終使用的密鑰是有 HKDF-Expand 擴充出來的。mmtls 把 info 參數分為:<code>length,label,handshake_hash</code>。其中 length 等于 <code>out_key_length</code>,<code>label</code> 是标記密鑰用途的固定字元串,<code>handshake_hash</code> 表示握手消息的 hash 值,這樣擴充出來的密鑰保證連接配接内唯一。
TLS1.3 草案中定義的密鑰擴充方式比較繁瑣,如上圖所示。為了得到最終認證加密的對稱密鑰,需要做 3 次 HDKF-Extract 和 4 次 HKDF-Expand 操作,實際測試發現,這種密鑰擴充方式對性能影響是很大的,尤其在 PSK 握手情況(PSK 握手沒有非對稱運算)這種密鑰擴充方式成為性能瓶頸。TLS1.3 之是以把密鑰擴充搞這麼複雜,本質上還是因為 TLS1.3 是一個通用的協定架構,具體的協商算法是可以選擇的,在有些協商算法下,協商出來的 <code>pre_master_key</code>(SS 和 ES)就不滿足某些特性(如随機性不夠),是以為了保證無論選擇什麼協商算法,用它來進行通信都是安全的,TLS1.3 就在密鑰擴充上做了額外的工作。而 mmtls 沒有 TLS1.3 這種包袱,可以針對微信自己的網絡通信特點進行優化(前面在握手方式選擇上就有展現)。mmtls 在不降低安全性的前提下,對 TLS1.3 的密鑰擴充做了精簡,使得性能上較 TLS1.3 的密鑰擴充方式有明顯提升。
在 mmtls 中,<code>pre_master_key</code>(SS 和 ES) 經過密鑰擴充,得到了一個長度為 <code>2*enc_key_length 2*iv_length</code> 的一段 buffer,用 <code>key_block</code> 表示,其中:
重播攻擊(Replay Attacks)是指攻擊者發送一個接收方已經正常接收過的包,由于重防的資料包是過去的一個有效資料,如果沒有防重放的處理,接收方是沒辦法辨識出來的。防重放在有些業務是必須要處理的,比如:如果收發消息業務沒有做防重放處理,就會出現消息重複發送的問題;如果轉賬業務沒有做防重放處理,就會重制重複轉賬問題。微信在一些關鍵業務層面上,已經做了防重放的工作,但如果 mmtls 能夠在下層協定上就做好防重放,那麼就能有效減輕業務層的壓力,同時為目前沒有做防重放的業務提供一個安全保障。
防重放的解決思路是為連接配接上的每一個業務包都編一個遞增的 sequence number,這樣隻要伺服器檢查到新收到的資料包的 sequence number 小于等于之前收到的資料包的 sequence number,就可以斷定新收到的資料包為重放包。當然 sequence number 是必須要經過認證的,也就是說 sequence number 要不能被篡改,否則攻擊者把 sequence number 改大,就繞過這個防重放檢測邏輯了。可以将 sequence number 作為明文的一部分,使用 AES-GCM 進行認證加密,明文變長了,不可避免的會增加一點傳輸資料的長度。實際上,mmtls 的做法是将 sequence number 作為構造 AES-GCM 算法參數 nonce 的一部分,利用 AES-GCM 的算法特性,隻要 AES-GCM 認證解密成功就可以確定 sequence number 符合預期。
上述防重放思路在 1-RTT 的握手方式下是沒有問題的,因為在 1-RTT 握手下,第一個 RTT 是沒有業務資料的,可以在這個 RTT 下由 Client 和 Server 共同決定開始計算 sequence number 的起點。但是在 0-RTT 的握手方式,第一個業務資料包和握手資料包一起發送給伺服器,對于這第一個資料包的防重放,Server 隻能完全靠 Client 發來的資料來判斷是否重放,如果用戶端發送的資料完全由自己生成,沒有包含伺服器參與的辨別,那麼這份資料是無法判斷是否為重放資料包的。在 TLS1.3 給了一個思路來解決上述這個"0-RTT 跨連接配接重放的問題":在 Server 處儲存一個跨連接配接的全局狀态,每建立一個連接配接都更新這個全局狀态,那麼 0-RTT 握手帶來的第一個業務資料也可以由這個跨連接配接的全局狀态來判斷是否重放。
但是,在一個分布式系統中每建立一個連接配接都讀寫這個全局狀态,如此頻繁的讀寫,無疑在可用性和性能消耗上都不可接受。事實上,0-RTT 跨連接配接防重放确實困難,目前沒有比較通用、高效的方案。其實在 Google 的 QUIC crypto protocol 中也存在 0-RTT 跨連接配接重放的問題,由于 QUIC 主要應用在 Chrome 浏覽器上,在浏覽器上通路網站時,建連接配接的第一個請求一般是 GET 而不是 POST,是以 0-RTT 加密的資料不涉及多少敏感性,被重放也隻是重新整理一次頁面而已,是以其選擇了不解決 0-RTT 防重放的問題。但是微信短連接配接是 POST 請求,帶給 Server 的都是上層的業務資料,是以 0-RTT 防重放是必須要解決的問題。mmtls 根據微信特有的背景架構,提出了基于用戶端和伺服器端時間序列的防重放政策,mmtls 能夠保證超過一段時間 T 的重放包被伺服器直接解決,而在短時間 T 内的重放包需要業務架構層來協調支援防重放,這樣通過 proxy 層和 logic 架構層一起來解決 0-RTT PSK 請求包防重放問題,限于篇幅,詳細方案此處不展開介紹。
mmtls 是參考 TLS1.3 草案标準設計與實作的,使用 ECDH 來做密鑰協商,ECDSA 進行簽名驗證,AES-GCM 作為對稱加密算法來對業務資料包進行認證加密,使用 HKDF 進行密鑰擴充,摘要算法為 SHA256。另外,結合具體的使用場景,mmtls 在 TLS1.3 的基礎上主要做了以下幾方面的工作:
輕量級。砍掉了用戶端認證相關的内容;直接内置簽名公鑰,避免證書交換環節,減少驗證時網絡交換次數。
安全性。選用的基礎密碼元件均是 TLS1.3 推薦、安全性最高的密碼元件;0-RTT 防重放由 proxy 層和 logic 架構層協同控制。
高性能。使用 0-RTT 握手方式沒有增加原有 Client 和 Server 的互動次數;和 TLS1.3 比,優化了握手方式和密鑰擴充方式。
高可用性。伺服器的過載保護,確定伺服器能夠在容災模式下提供安全級别稍低的有損服務。
TLS 協定分析與現代加密通信協定設計
RFC5246
The Transport Layer Security (TLS) Protocol Version 1.3