微信通訊錄加粉拉群黑產行業背後的深度技術剖析,協議密鑰協商

微信 通信 算法 技術 星戰前夜 通訊錄加粉拉群 2019-04-19
微信通訊錄加粉拉群黑產行業背後的深度技術剖析,協議密鑰協商

要做微信通訊錄加粉系統,在應用層上來說並不複雜,複雜的是底層的協議部分,這也就是為什麼全國沒幾個公司能開發這類系統,大部分有系統的團隊或者公司都是OEM一兩個源頭公司的系統分發給眾多代理。

如果覺得協議簡單的,那麼可以看一下下面的祕鑰協商過程,相信大家看過後就知道要做通訊錄加粉拉群系統背後的深層次技術難度,而這裡僅僅是一小個知識點,背後還有眾多的加密字段加密算法還沒有說。

當然如果大家對這類黑產系統感興趣,也可以注意我在評論區的留言,歡迎交流。

下文比較長,如果看不懂的請直接跳過。

微信通訊錄加粉拉群黑產行業背後的深度技術剖析,協議密鑰協商

Handshake協議 --- 安全地協商出對稱加密密鑰

Handshake協議其實做的最主要的事情就是完成加密密鑰的協商,即讓通信雙方安全地獲得一致的對稱密鑰,以進行加密數據傳輸。在此基礎上,還完成了一些優化工作,如複用session以減少握手時間。

在這裡說明一下,為什麼mmtls以及TLS協議需要一個Handshake子協議和Record子協議?其實“認證密鑰協商+對稱加密傳輸”這種混合加密結構,是絕大多數加密通信協議的通用結構,在mmtls/TLS中Handshake子協議負責密鑰協商, Record子協議負責數據對稱加密傳輸。造成這種混合加密結構的本質原因還是因為單獨使用公鑰加密組件或對稱加密組件都有不可避免的缺點。公鑰加密組件計算效率往往遠低於對稱加密組件,直接使用公鑰加密組件加密業務數據,這樣的性能損耗任何Server都是無法承受的。

而如果單獨使用對稱加密組件進行網絡加密通信,在Internet這種不安全的信道下,這個對稱加密密鑰如何獲取往往是一個難以解決的問題,因此結合兩類密碼組件的優勢,產生了“認證密鑰協商+對稱加密傳輸”這種混合加密結構。另外,mmtls/TLS這種安全性和擴展性都很強的安全通信協議,在解決實際安全通信問題的時候,會有非常多的細節問題,因此分離出兩個子協議來隔離複雜性。

1 帶認證的密鑰協商

根據TLS1.3的描述,實際上有2種1-RTT的密鑰協商方式(1-RTT ECDHE、 1-RTT PSK)和4種0-RTT的密鑰協商方式(0-RTT PSK, 0-RTT ECDH, 0-RTT PSK-ECDHE, 0-RTT ECDH-ECDHE),mmtls結合微信的特點,在保證安全性和性能的前提下,只保留了三種密鑰協商方式(1-RTT ECDHE, 1-RTT PSK, 0-RTT PSK),並做了一些優化,後面會詳細分析如何產生這種決策的。

1.1 1-RTT密鑰協商

1.ECDH密鑰協商

首先看一個,會遭受到攻擊的密鑰協商過程。通信雙方Alice和Bob使用ECDH密鑰交換協議進行密鑰協商,ECDH密鑰交換協議擁有兩個算法:

密鑰生成算法ECDH_Generate_Key,輸出一個公鑰和私鑰對(ECDH_pub_key, ECDH_pri_key),ECDH_pri_key需要祕密地保存,ECDH_pub_key可以公開發送給對方。

密鑰協商算法ECDH_compute_key,以對方的公鑰和自己的私鑰作為輸入,計算出一個密鑰Key,ECDH_compute_key算法使得通信雙方計算出的密鑰Key是一致的。

這樣一來Alice和Bob僅僅通過交換自己的公鑰ECDH_pub_key,就可以在Internet這種公開信道上共享一個相同密鑰Key,然後用這個Key作為對稱加密算法的密鑰,進行加密通信。

但是這種密鑰協商算法仍然存在一個問題。當Bob將他的Bob_ECDH_pub_key發送給Alice時,攻擊者可以截獲Bob_ECDH_pub_key,自己運行ECDH_Generate_Key算法產生一個公鑰/私鑰對,然後把他產生的公鑰發送給Alice。同理,攻擊者可以截獲Alice發送給Bob的Alice_ECDH_pub_key,再運行ECDH_Generate_Key算法產生一個公鑰/私鑰對,並把這個公鑰發送給Bob。Alice和Bob仍然可以執行協議,產生一個密鑰Key。但實際上,Alice產生的密鑰Key實際上是和攻擊者Eve協商的;Bob產生的密鑰Key也是和攻擊者協商Eve的。這種攻擊方法被稱為中間人攻擊(Man In The Middle Attack)。

那麼,有什麼解決辦法中間人攻擊呢?產生中間人攻擊的本質原因是協商過程中的數據沒有經過端點認證,通信兩端不知道收到的協商數據是來自對端還是來自中間人,因此單純的“密鑰協商”是不夠的,還需要“帶認證的密鑰協商”。對數據進行認證其實有對稱和非對稱的兩種方式:基於消息認證碼(Message Authentication Code)的對稱認證和基於簽名算法的非對稱認證。消息認證碼的認證方式需要一個私密的Key,由於此時沒有一個私密的Key,因此ECDH認證密鑰協商就是ECDH密鑰協商加上數字簽名算法。在mmtls中我們採用的數字簽名算法為ECDSA。

雙方密鑰協商時,再分別運行簽名算法對自己發出的公鑰ECDH_pub_key進行簽名。收到信息後,首先驗證簽名,如果簽名正確,則繼續進行密鑰協商。注意到,由於簽名算法中的公鑰ECDSA_verify_key是一直公開的,攻擊者沒有辦法阻止別人獲取公鑰,除非完全掐斷髮送方的通信。這樣一來,中間人攻擊就不存在了,因為Eve無法偽造簽名。具體過程如圖5所示:

事實上,在實際通信過程中,只需要通信中某一方簽名它的協商數據就可以保證不被中間人攻擊,mmtls就是隻對Server做認證,不對Client做認證,因為微信客戶端發佈出去後,任何人都可以獲得,只要能夠保證客戶端程序本身的完整性,就相當於保證了客戶端程序是由官方發佈的,為認證合法的客戶端,而客戶端程序本身的完整性不是mmtls協議保護的範疇。

在這一點上,TLS要複雜一些,TLS作為一個通用的安全通信協議,可能會存在一些需要對Client進行認證的場合,因此TLS提供了可選的雙方相互認證的能力,通過握手協商過程中選擇的CipherSuite是什麼類型來決定是否要對Server進行認證,通過Server是否發送CertificateRequest握手消息來決定是否要對Client進行認證。由於mmtls不需要對Client做認證,在這塊內容上比TLS簡潔許多,更加輕量級。

PSK密鑰協商

PSK是在一次ECDH握手中由server下發的內容,它的大致數據結構為PSK{key,ticket{key}},即PSK包含一個用來做對稱加密密鑰的key明文,以及用ticket_key對key進行加密的密文ticket,當然PSK是在安全信道中下發的,也就是說在網絡中進行傳輸的時候PSK是加密的,中間人是拿不到key的。其中ticket_key只有server才知道,由server負責私密保存。

PSK協商比較簡單,Client將PSK的ticket{key}部分發送給Server,由於只有Server才知道ticket_key,因此key是不會被竊聽的。Server拿到ticket後,使用ticket_key解密得到key,然後Server用基於協商得到的密鑰key,對協商數據計算消息認證碼來認證,這樣就完成了PSK認證密鑰協商。PSK認證密鑰協商使用的都是對稱算法,性能上比ECDH認證密鑰協商要好很多。

0-RTT密鑰協商

上述的兩種認證密鑰協商方式(1-RTT ECDHE, 1-RTT PSK)都需要一個額外RTT去獲取對稱加密key,在這個協商的RTT中是不帶有業務數據的,全部都是協商數據。那麼是否存在一種密鑰協商方式是在握手協商的過程中就安全地將業務數據傳遞給對端呢?答案是有的,TLS1.3草案中提到了0-RTT密鑰協商的方法。

0-RTT ECDH密鑰協商

0-RTT 握手想要達到的目標是在握手的過程中,捎帶業務數據到對端,這裡難點是如何在客戶端發起協商請求的時候就生成一個可信的對稱密鑰加密業務數據。在1-RTT ECDHE中,Client生成一對公私鑰(cli_pub_key, cli_pri_key),然後將公鑰cli_pub_key傳遞給Server,然後Server生成一對公私鑰(svr_pub_key, svr_pri_key)並將公鑰svr_pub_key傳遞給Client,Client收到svr_pub_key後才能計算出對稱密鑰。

上述過程(svr_pub_key, svr_pri_key)由於是臨時生成的,需要一個RTT將svr_pub_key傳遞給客戶端,如果我們能夠預先生成一對公私鑰(static_svr_pub_key, static_svr_pri_key)並將static_svr_pub_key預置在Client中,那麼Client可以在發起握手前就通過static_svr_pub_ke和cli_pub_key生成一個對稱密鑰SS(Static Secret),然後用SS加密第一個業務數據包(實際上是通過SS衍生的密鑰對業務數據進行加密,後面詳述),這樣將SS加密的業務數據包和cli_pub_key一起傳給Server,Server通過cli_pub_key和static_server_private_key算出SS,解密業務數據包,這樣就達到了0-RTT密鑰協商的效果。

這裡說明一下:ECDH協商中,如果公私鑰對都是臨時生成的,一般稱為ECDHE,因此1-RTT的ECDH協商方式被稱為1-RTT ECDHE握手,0-RTT 中有一個靜態內置的公鑰,因此稱為0-RTT ECDH握手。

0-RTT PSK密鑰協商

0-RTT PSK握手比較簡單,回顧1-RTT PSK握手,其實在進行1-RTT PSK握手之前,Client已經有一個對稱加密密鑰key了,就直接拿這個對稱加密密鑰key加密業務數據,然後將其和握手協商數據ticket{key}一起傳遞給Server就可以了。

提高0-RTT密鑰協商的安全性

PFS(perfect forward secrecy),中文可叫做完全前向保密。它要求一個密鑰被破解,並不影響其他密鑰的安全性,反映的密鑰協商過程中,大致的意思是用來產生會話密鑰的長期密鑰洩露出去,不會造成之前通信時使用的會話密鑰的洩露;或者密鑰協商方案中不存在長期密鑰,所有協商材料都是臨時生成的。

上面所述的0-RTT ECDH密鑰協商加密的數據的安全性依賴於長期保存的密鑰static_svr_pri_key,如果static_svr_pri_key洩露,那麼所有基於0-RTT ECDH協商的密鑰SS都將被輕鬆計算出來,它所加密的數據也沒有任何保密性可言,為了提高前向安全性,我們在進行0-RTT ECDH協商的過程中也進行ECDHE協商,這種協商方式稱為0-RTT ECDH-ECDHE密鑰協商。如下圖所示: 這樣,我們基於static_svr_pri_key保護的數據就只有第一個業務數據包AppData,後續的包都是基於ES(Ephemeral Secret)對業務數據進行保護的。這樣即使static_svr_pri_key洩露,也只有連接的第一個業務數據包能夠被解密,提高前向安全性。

同樣的,0-RTT PSK密鑰協商加密的數據的安全性依賴於長期保存密鑰ticket_key,如果ticket_key洩露,那麼所有基於ticket_key進行保護的數據都將失去保密性,因此同樣可以在0-RTT PSK密鑰協商的過程中,同時完成ECDHE密鑰協商,提高前向安全性。

2.2 密鑰協商需要關注的細節

根據前面的描述可以知道,要使得密鑰協商過程不被中間人攻擊,就必須要對協商數據進行認證。下面拿1-RTT ECDHE握手方式來說明在進行認證過程中需要注意的細節。在1-RTT ECDHE中的認證方式是使用ECDSA簽名算法的非對稱認證方式,整個過程大致如下:Server在收到客戶端的cli_pub_key後,隨機生成一對ECDH公私鑰(svr_pub_key, svr_pri_key),然後用簽名密鑰sign_key對svr_pub_key進行簽名,得到簽名值Signature,並把簽名值Signature和svr_pub_key一起發送給客戶端。客戶端收到之後,用verify_key進行驗籤(verify_key和sign_key是一對ECDSA密鑰),驗籤成功後才會繼續走協商對稱密鑰的流程。

上面的認證過程,有三個值得關注的點:

Verify_Key如何下發給客戶端?

這實際上是公鑰派發的問題,TLS是使用證書鏈的方式來派發公鑰(證書),對於微信來說,如果使用證書鏈的方式來派發Server的公鑰(證書),無論自建Root CA還是從CA處申請證書,都會增加成本且在驗簽過程中會存在額外的資源消耗。由於客戶端是由我們自己發佈的,我們可以將verify_key直接內置在客戶端,這樣就避免證書鏈驗證帶來的時間消耗以及證書鏈傳輸帶來的帶寬消耗。

如何避免簽名密鑰sign_key洩露帶來的影響?

如果sign_key洩露,那麼任何人都可以偽造成Server欺騙Client,因為它拿到了sign_key,它就可以簽發任何內容,Client用verify_key去驗證簽名必然驗籤成功。因此sign_key如果洩露必須要能夠對verify_key進行撤銷,重新派發新的公鑰。這其實和前一問題是緊密聯繫的,前一問題是公鑰派發問題,本問題是公鑰撤銷問題。TLS是通過CRL和OCSP兩種方式來撤銷公鑰的,但是這兩種方式存在撤銷不及時或給驗證帶來額外延遲的副作用。由於mmtls是通過內置·verify_key·在客戶端,必要時通過強制升級客戶端的方式就能完成公鑰撤銷及更新。另外,sign_key是需要Server高度保密的,一般不會被洩露,對於微信後臺來說,類似於sign_key這樣,需要長期私密保存的密鑰在之前也有存在,早已形成了一套方法和流程來應對長期私密保存密鑰的問題。

用sign_key進行簽名的內容僅僅只包含svr_pub_key是否有隱患?

回顧一下,上面描述的帶認證的ECDH協商過程,似乎已經足夠安全,無懈可擊了,但是,面對成億的客戶端發起ECDH握手到成千上萬臺接入層機器,每臺機器對一個TCP連接隨機生成不同的ECDH公私鑰對,這裡試想一種情況,假設某一臺機器某一次生成的ECDH私鑰svr_pri_key1洩露,這實際上是可能的,因為臨時生成的ECDH公私鑰對本身沒有做任何保密保存的措施,是明文、短暫地存放在內存中,一般情況沒有問題,但在分佈式環境,大量機器大量隨機生成公私鑰對的情況下,難保某一次不被洩露。

這樣用sign_key(sign_key是長期保存,且分佈式環境共享的)對svr_pub_key1進行簽名得到簽名值Signature1,此時攻擊者已經拿到svr_pri_key1,svr_pub_key1和Signature1,這樣他就可以實施中間人攻擊,讓客戶端每次拿到的服務器ECDH公鑰都是svr_pub_key1:客戶端隨機生成ECDH公私鑰對(cli_pub_key, cli_pri_key)並將cli_pub_key發給Server,中間人將消息攔截下來,將client_pub_key替換成自己生成的client_pub_key’,並將svr_pub_key1和Signature1回給Client,這樣Client就通過計算ECDH_Compute_Key(svr_pub_key1, cli_pri_key)=Key1, Server通過計算ECDH_Compute_Key(client_pub_key’, svr_pub_key)=Key’,中間人既可以計算出Key1和Key’,這樣它就可以用Key1和Client通信,用Key’和Server進行通信。發生上述被攻擊的原因在於一次握手中公鑰的簽名值被用於另外一次握手中,如果有一種方法能夠使得這個簽名值和一次握手一一對應,那麼就能解決這個問題。

解決辦法也很簡單,就是在握手請求的ClientHello消息中帶一個Client_Random隨機值,然後在簽名的時候將Client_Random和svr_pub_key一起做簽名,這樣得到的簽名值就與Client_Random對應了。mmtls在實際處理過程中,為了避免Client的隨機數生成器有問題,造成生成不夠隨機的Client_Random,實際上Server也會生成一個隨機數Server_Random,然後在對公鑰簽名的時候將Client_Random、Server_Random、svr_pub_key一起做簽名,這樣由Client_Random、Server_Random保證得到的簽名值唯一對應一次握手。

2.3 mmtls對認證密鑰協商的選擇

上面一共介紹了2種1-RTT 密鑰協商方式和4種0-RTT 密鑰協商方式。

PSK握手全程無非對稱運算,Server性能消耗小,但前向安全性弱,ECDHE握手有非對稱運算,Server性能消耗大,但前向安全性相對更強,那麼如何結合兩者優勢進行密鑰協商方式的選擇呢?

首先PSK是如何獲得的呢?PSK是在一次成功的ECDH(E)握手中下發的(在上面的圖7、圖8沒有畫出下發PSK的部分),如果客戶端沒有PSK,那麼顯然是隻能進行ECDH(E)握手了。由於PSK握手和ECDH(E)握手的巨大性能差異,那麼在Client有PSK的情況下,應該進行PSK握手。那麼在沒有PSK的情況下,上面的1-RTT ECDHE、0-RTT ECDH、0-RTT ECDH-ECDHE具體應該選擇哪一種呢?在有PSK的情況下,應該選擇1-RTT PSK、0-RTT PSK還是0-RTT PSK-ECDHE呢?

對於握手方式的選擇,我們也是幾經過修改,最後結合微信網絡連接的特點,我們選擇了1-RTT ECDHE握手、1-RTT PSK握手、0-RTT PSK握手。微信目前有兩個數據傳輸通道:

1.基於HTTP協議的短連接 2.基於私有協議的長連接。

微信長連接有一個特點,就是在建立好TCP連接之後,會在此TCP連接上先發一個長連nooping包,目的是驗證長連接的連通性(由於長連接是私有協議,部分中間路由會過濾掉這種私有協議的數據包),這就是說長連接在建立時的第一個數據包是不會發送業務數據的,因此使用1-RTT的握手方式,由第一個握手包取代之前的nooping包去探測長連的連通性,這樣並不會增加長連的網絡延時,因此我們選取在長連接情況下,使用1-RTT ECDHE和1-RTT PSK這兩種密鑰協商方式。

微信短連接為了兼容老版本的HTTP協議,整個通信過程就只有一個RTT,也就是說Client建立TCP連接後,通過HTTP POST一個業務請求包到Server,Server回一個HTTP響應,Client處理後立馬斷掉TCP連接。對於短連接,我們應該儘量使用0-RTT的握手方式,因為一個短連接原來就只存在一個RTT,如果我們大量使用1-RTT的握手方式,那麼直接導致短連接至少需要2個RTT才能完成業務數據的傳輸,導致時延加倍,用戶體驗較差。

這裡存在兩種情況:

(1)客戶端沒有PSK,為了安全性,這時和長連接的握手方式一樣,使用1-RTT ECDHE;

(2)客戶端有PSK,這時為了減少網絡時延,應該使用0-RTT PSK或0-RTT PSK-ECDHE。

在這兩種握手方式下,由於業務請求包始終是基於PSK進行保護的,同一個PSK多次協商出來的對稱加密key是同一個,這個對稱加密key的安全性依賴於ticket_key的安全性,因此0-RTT情況下,業務請求包始終是無法做到前向安全性。0-RTT PSK-ECDHE這種方式,只能保證本短連接業務響應回包的前向安全性,這帶來安全性上的優勢是比較小的,但是與0-RTT PSK握手方式相比,0-RTT PSK-ECDHE在每次握手對server會多2次ECDH運算和1次ECDSA運算。微信的短連接是非常頻繁的,這對性能影響極大,因此綜合考慮,在客戶端有PSK的情況下,我們選擇使用0-RTT PSK握手。由於0-RTT PSK握手安全性依賴ticket_key,為了加強安全性,在實現上,PSK必須要限制過期時間,避免長期用同一個PSK來進行握手協商;ticket_key必須定期輪換,且具有高度機密的運維級別。

另外,為了提高系統可用性,實際上mmtls在一次成功的ECDH握手中會下發兩個PSK,一個生命週期短保證安全性,一個生命週期長保證可用性。在一次ECDH握手中,請求會帶上生命週期長的PSK(如果存在的話),後臺可根據負載情況進行權衡,選擇使用ECDH握手或者PSK握手。

3 Record協議 --- 使用對稱加密密鑰進行安全的通信

經過上面的Handshake過程,此時Client和Server已經協商出了一致的對稱加密密鑰pre_master_key,那麼接下來就可以直接用這個pre_master_key作為密鑰,選擇一種對稱加密算法(如常用的AES-CBC)加密業務數據,將密文發送給Server。是否真的就這麼簡單呢?實際上如果真的按這個過程進行加密通信是有很多安全漏洞的。

3.1 認證加密(Authenticated Encryption)

“加密並不是認證”在密碼學中是一個簡單的共識,但對於我們很多程序員來說,並不知道這句話的意義。加密是隱藏信息,使得在沒有正確密鑰的情況下,信息變得難以讀懂,加密算法提供保密性,上面所述的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要求必須實現的算法。

3.2 密鑰擴展

TLS1.3明確要求通信雙方使用的對稱加密Key不能完全一樣,否則在一些對稱加密算法下會被完全攻破,即使是使用AES-GCM算法,如果通信雙方使用完全相同的加密密鑰進行通信,在使用的時候也要小心翼翼的保證一些額外條件,否則會洩露部分明文信息。另外,AES算法的初始化向量(IV)如何構造也是很有講究的,一旦用錯就會有安全漏洞。也就是說,對於handshake協議協商得到的pre_master_secret不能直接作為雙方進行對稱加密密鑰,需要經過某種擴展變換,得到六個對稱加密參數:

Client Write MAC Key (用於Client算消息認證碼,以及Server驗證消息認證碼)

Server Write MAC Key (用於Server算消息認證碼,以及Client驗證消息認證碼)

Client Write Encryption Key(用做Client做加密,以及Server解密)

Server Write Encryption Key(用做Server做加密,以及Client解密)

Client Write IV (Client加密時使用的初始化向量)

Server Write IV (Server加密時使用的初始化向量)

當然,使用AES-GCM作為對稱加密組件,MAC Key和Encryption Key只需要一個就可以了。

握手生成的pre_master_secret只有48個字節,上述幾個加密參數的長度加起來肯定就超過48字節了,所以需要一個函數來把48字節延長到需要的長度,在密碼學中專門有一類算法承擔密鑰擴展的功能,稱為密鑰衍生函數(Key Derivation Function)。TLS1.3使用的HKDF做密鑰擴展,mmtls也是選用的HKDF做密鑰擴展。

在前文中,我用pre_master_secret代表握手協商得到的對稱密鑰,在TLS1.2之前確實叫這個名字,但是在TLS1.3中由於需要支持0-RTT握手,協商出來的對稱密鑰可能會有兩個,分別稱為Static Secret(SS)和Ephemeral Secret(ES)。從TLS1.3文檔中截取一張圖進行說明一下:

上圖中Key Exchange就是代表握手的方式,在1-RTT ECDHE握手方式下

ES=SS = ECDH_Compute_Key(svr_pub_key, cli_pri_key);

在0-RTT ECDH下,

SS=ECDH_Compute_Key(static_svr_pub_key, cli_pri_key),

ES=ECDH_Compute_Key(svr_pub_Key, cli_pri_Key);

在0-RTT/1-RTT PSK握手下,

ES=SS=pre-shared key;

在0-RTT PSK-ECDHE握手下,

SS=pre-shared key,

ES=ECDH_Compute_Key(svr_pub_key, cli_pri_key);

前面說過mmtls使用的密鑰擴展組件為HKDF,該組件定義了兩個函數來保證擴展出來的密鑰具有偽隨機性、唯一性、不能逆推原密鑰、可擴展任意長度密鑰。兩個函數分別是:

HKDF-Extract( salt, initial-keying-material )

該函數的作用是對initial-keying-material進行處理,保證它的熵均勻分別,足夠的偽隨機。

HKDF-Expand( pseudorandom key, info, out_key_length )

參數pseudorandom key是已經足夠偽隨機的密鑰擴展材料,HKDF-Extract的返回值可以作為pseudorandom key,info用來區分擴展出來的Key是做什麼用,out_key_length表示希望擴展輸出的key有多長。mmtls最終使用的密鑰是有HKDF-Expand擴展出來的。mmtls把info參數分為:length,label,handshake_hash。其中length等於out_key_length,label是標記密鑰用途的固定字符串,handshake_hash表示握手消息的hash值,這樣擴展出來的密鑰保證連接內唯一。

TLS1.3草案中定義的密鑰擴展方式比較繁瑣,如上圖所示。為了得到最終認證加密的對稱密鑰,需要做3次HDKF-Extract和4次HKDF-Expand操作,實際測試發現,這種密鑰擴展方式對性能影響是很大的,尤其在PSK握手情況(PSK握手沒有非對稱運算)這種密鑰擴展方式成為性能瓶頸。TLS1.3之所以把密鑰擴展搞這麼複雜,本質上還是因為TLS1.3是一個通用的協議框架,具體的協商算法是可以選擇的,在有些協商算法下,協商出來的pre_master_key(SS和ES)就不滿足某些特性(如隨機性不夠),因此為了保證無論選擇什麼協商算法,用它來進行通信都是安全的,TLS1.3就在密鑰擴展上做了額外的工作。而mmtls沒有TLS1.3這種包袱,可以針對微信自己的網絡通信特點進行優化(前面在握手方式選擇上就有體現)。mmtls在不降低安全性的前提下,對TLS1.3的密鑰擴展做了精簡,使得性能上較TLS1.3的密鑰擴展方式有明顯提升。

在mmtls中,pre_master_key(SS和ES)經過密鑰擴展,得到了一個長度為2*enc_key_length+2*iv_length的一段buffer,用key_block表示,其中:

client_write_key = key_block[0...enc_key_length-1]

client_write_key = key_block[enc_key_length...2*enc_key_length-1]

client_write_IV = key_block[2*enc_key_length...2*enc_key_length+iv_length-1]

server_write_IV = key_block[2*enc_key_length+iv_length...2*enc_key_length+2*iv_length-1]

3.3 防重放

重放攻擊(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請求包防重放問題,限於篇幅,詳細方案此處不展開介紹。

市面上很多加粉拉群系統其實最終都是一兩家源頭公司的OEM產品。

相關推薦

推薦中...