非死book為快速安全的移動連接打造零往返協議
每天都有數十億人在Android和iOS設備上通過非死book與朋友建立聯系。對我們移動應用和服務器之間傳輸的數據提供保護,可以幫助人們更安全地使用非死book。
我們的移動應用使用了一種名為Mobile Proxygen的自定義網絡棧,這是一種使用C++14開發的跨平臺HTTP客戶端,使用我們自己的開源 Proxygen庫 構建而來。借此我們可以在服務器和客戶端之間共享同一套代碼,更快速地提供新的安全和性能改進。
我們在移動應用中使用了傳輸層安全(TLS)1.2協議,并使用帶OpenSSL的 Folly 作為TLS的具體實現。由于增加了至少一輪往返,TLS 1.2會延長建立連接所需的時間,為了降低TLS的延遲,過去多年來人們提出了多種新的協議和改進。非死book傳輸安全團隊曾通過多種方式設法盡可能改善TLS的速度,包括通過技術手段在距離用戶最近的邊緣位置終止TLS連接,重復使用HTTP2連接,使用會話重用(Session resumption)和TLS搶跑(False start),推斷式連接啟動,以及使用現代化的Cipher套件。從我們的移動應用到非死book建立的大部分TLS連接僅額外增加了一輪往返(1-RTT)。
回顧一年前的數據,我們發現在建立連接的過程中,1-RTT優化后的安全握手過程依然需要較長的時間。例如在印度等新興市場,用戶往往要花費600ms(75百分位數)的時間才能建立TLS連接。我們認為有必要采取一些措施,降低這些請求的延遲,進而減少建立安全連接所需的時間。
我們打算使用零往返(0-RTT)安全協議進行一些實驗。與TLS 1.2等1-RTT安全協議不同,這些協議意在確保安全性,并且不產生額外的往返延遲的前提下建立安全的連接。TCP已經深度融入到我們的基礎架構中,為了避免一次性對整個基礎架構進行較大的調整,我們希望逐步進行這樣的實驗。同樣基于TCP協議的TLS 1.3目前提供了0-RTT功能,然而在我們研究各類選項的時候,TLS 1.3還處于萌芽狀態,暫未提供0-RTT功能。此外還可以選擇基于UDP的QUIC,這也是一種0-RTT協議,在分析過該協議的安全模式后,很多學術機構對該協議的加密模式產生了一定的關注。我們希望讓基于UDP的QUIC所具備的低延遲特性能夠適用于TCP,借此更快速地建立安全連接,因此我們使用QUIC加密協議構建了一個基于TCP的實驗性零往返協議。
過去一年來,我們已經為移動應用和負載均衡器構建并部署了零往返協議,并獲得了顯著的性能改進,例如連接延遲降低了41%,處理請求的總時間降低了2%。在有關0-RTT協議的實踐過程中,我們還收獲了很多寶貴的工程經驗,例如API設計、安全屬性,以及部署,并將我們的一些成果貢獻給了業已成熟的TLS 1.3。希望我們通過本文分享的經驗也能適用于未來打算部署TLS 1.3的應用。
對QUIC協議進行的改動
為了使其更加安全和高效,我們在零往返協議中對QUIC加密模式進行了大量改動。此外我們還設法讓該協議可以通過TCP運行。因此可以認為,我們的零往返協議是在原本 QUIC加密規范 基礎上進行的一系列改進。本節將介紹有關加該協議密碼學的相關細節,以及幫助大家理解這一加密模式所需掌握的相關知識。
概括來看,QUIC的加密協議是這樣工作的:如果某個客戶端以前從未與服務器通信過,會發送一則Inchoate Client Hello消息并通過1-RTT下載一個名為Server Config(SCFG)的暫存消息。該消息中包含一個Diffie-Hellman共享,下一次客戶端將使用該共享派生初始密鑰(或0-RTT密鑰),并立刻使用該密鑰加密數據。1-RTT完成后,服務器將發出一個新的暫存Diffie-Hellman共享,借此派生出一組名為前向(Forward)安全密鑰的新密鑰。
QUIC密鑰派生過程的變化
原始的 QUIC規范 包含兩種類型的密鑰:
- 初始密鑰(或0-RTT密鑰),用于發送初始數據,可從長存的服務器配置中派生而來。
- 前向安全密鑰(或1-RTT密鑰),用于在服務器向客戶端發送Server Hello消息后傳輸數據所用。
客戶端發送Client Hello(CHLO)后,服務器使用加密的Server Hello(SHLO)消息作為回應。其中包一組新的公鑰(PUBS),這是一種可用于派生出Forward安全密鑰的Diffie-Hellman共享。該消息會使用初始0-RTT密鑰進行加密,服務器通過正確解密SHLO可成功完成身份驗證。
然而我們發現這種密鑰派生方法存在密鑰被重復使用的弱點。初始密鑰以及對SHLO進行的現時(Nonce)加密完全是通過Client Hello消息派生而來的,因此如果攻擊者“重播”相同的CHLO,服務器會使用相同密鑰對不同的SHLO消息進行現時加密。AEAD密碼算法的安全特性被破壞了,進而威脅到QUIC的安全性,除非我們能通過額外的有狀態方法檢測相同的CHLO消息。
在零往返協議中,我們引入了另一種通過明文方式傳輸的現時機制,并會通過一個新的密鑰加密SHLO。此外我們也已經將該弱點報告給谷歌,他們為QUIC提供的“多樣化現時(Diversification nonce)”解決了這個問題。
帶內服務器配置輪換
我們對服務器配置(Server Config)的有效時間進行了限制,原因在于,如果該配置在有效時段內被盜,將能被一直用于冒充服務器。在QUIC協議中,讓包含已緩存老舊SCFG的客戶端使用新SCFG的唯一方法是繼續使用原來的SCFG,被拒絕,然后獲得新的SCFG。這種方法的不足之處在于,如果客戶端發送了0-RTT數據,在輪換配置時必須丟棄這些數據并進行重播。
我們對協議進行了改進,使得我們可以在帶內(In-band)直接發送新的SCFG。服務器隨時維護著包含三個配置的清單:上一個配置、當前配置,以及下一個配置。如果檢測到客戶端在使用老的SCFG,我們會讓客戶端完成連接,隨后通過加密的SHLO為客戶端提供新的SCFG,進而客戶端可以將自己的SCFG更新為新版本。這種方式可以避免客戶端因為使用老舊配置而被拒絕的退化情況。
TLS 1.2還提供了一種通過刷新會話票證(Session Ticket)實現相同目的的做法,然而這種方法無法保障前向安全,因為新老會話票證會共享同一個主密鑰。在QUIC協議中,新密鑰需要另一個Diffie-Hellman操作,刷新后可以保證前向安全。
被拒后重試行為的安全性
就算通過帶內的方式刷新服務器配置,依然會遇到客戶端繼續使用老配置的情況。此時無法避免要拒絕客戶端并回退至1-RTT,但連接依然無法防范重播。客戶端可以發送0-RTT數據,但不能立刻發送常規數據。
我們對零往返協議進行了性能優化,可以在客戶端的服務器配置被拒絕的時間段內向客戶端發送額外的服務器現時,這樣即可使用該現時,立即開始發送常規的1-RTT非重播安全數據。
0-RTT數據的時效
相比通過1-RTT或TLS 1.2等協議發送的常規數據,0-RTT數據有著不同的安全特性。與常規的1-RTT數據不同,攻擊者可以無窮盡地重播0-RTT數據,如果應用無法妥善地保護自己,這會造成一種非常有意思的攻擊。例如,攻擊者可以將一個HTTP POST請求重播兩次,并在缺乏遏制機制的情況下讓該請求被執行兩次。攻擊者還可以將發往銀行的同一個GET請求重播任意次數,通過查看響應的長度判斷銀行賬戶余額的變化情況。0-RTT數據必須以截然不同的方式妥善應對。1-RTT完成后,客戶端將可以發送任何數據,因為連接又可以防范重播了。
我們使用的一種緩解措施是減小0-RTT的有效時長。客戶端可以向我們發送啟動連接的時間,我們會將該時間與服務器時間進行對比,以確定該0-RTT數據是在多久之前創建的。如果0-RTT數據在有效期過期之后重播,服務器將拒絕這樣的數據,借此禁止攻擊者無窮盡地重播這些數據。然而隨著降低有效期,我們發現很多客戶端的時鐘存在較大偏差,進而產生了很多誤報。
為了解決這個問題,當客戶端成功連接后,我們會下發一個時鐘偏差校正值。客戶端下一次連接時,需要這樣計算自己的客戶端時間:
client_time = client_real_time + clock_skew_correction
我們還發現客戶端的時鐘偏差存在一定的方差,但由于該方差并不是那么大,因此可以強制實施嚴格的0-RTT數據有效期。
對TCP的修改
為了兼容TCP,我們在零往返協議中取消了QUIC數據包的顯式序列編號,并增加了顯式長度字段。QUIC是基于UDP的,因此不需要長度字段,而由于UDP數據包可以重新排序,因此必須具備顯式序列編號。
零往返協議的部署
端口的選擇
我們決定將零往返協議運行在與TLS相同的443端口上。在服務器端,我們在已接受套接字(Accepted socket)上使用MSG_PEEK預覽連接的前幾個字節內容,并確定是要使用TLS或是零往返協議。我們還需要通過更細化的方法讓客戶端決定是否使用零往返協議,因此決定不使用Alt-svc。
Zero RTT API
我們還面臨一個重大的問題:如何將0-RTT 集成于Mobile Proxygen。網絡棧是一種復雜的猛獸,而我們非常有必要確保0-RTT以后必須能輕松地測試和維護。
我們考慮過兩種可行的API:
- 更改原有的套接字API,讓 connect() 也能接受數據,例如 connectWithData (ip, port, data) 。
- 讓客戶端繼續使用相同的 connect() 和 write() 套接字API。為了啟用0-RTT,客戶端可調用新的 enableZeroRTT() API。隨后我們可以立即將調用返回至 connect() ,這樣客戶端就可以使用 write() 寫入0-RTT數據。
在考慮如何將0-RTT集成到客戶端時,我們發現以我們這種復雜度的網絡棧來說,很難用可行的方式集成第一種API。該API實際上破壞了建立和使用連接的不同組件之間的分隔。諸如HTTP等組件本身是通過連接發送數據的,但也需要負責處理部分連接邏輯,這會使這些組件變得更復雜。這種方式還會妨礙數據的流動,例如,如果整個網絡的RTT有較大差異,就很難判斷需要等待緩沖多少數據再調用 connectWithData 。
因此我們選擇構建第二種API。網絡棧的其他部分可以像以前一樣使用相同的API。這種方法的一個不足在于,整個方法的復雜度被轉移到零往返協議本身的實現中,因為需要處理0-RTT的狀態。但該方法的優勢在于,可以讓我們在獲得Server Hello 消息之前持續流傳輸0-RTT數據。RTT的方差非常大,因此我們可以根據經驗估算在等待服務器發送Server Hello的同時,我們可以發送多少數據,這就產生了1-RTT。此外我們發現該數據本身的方差也很大。如果不使用流式API,Mobile Proxygen就必須精確判斷在綁定一個0-RTT connectWithData() 之前,必須等待應用生成多少數據,這一點實現起來也很復雜。由于數據方差大,我們不需要流式API事先判斷要等待的數據量,因此部署起來更簡單,也更高效。
選擇對0-RTT來說不會造成危險的請求
在決定構建流式API后,我們需要構建一種機制,以確保只通過0-RTT發送安全的請求。通過0-RTT發送非冪等請求是一種不安全的做法,因為攻擊者可以重播這種請求,但就算冪等的請求,這樣做也可能不夠安全。舉例來說,如果有個GET事務可以返回銀行賬戶余額,攻擊者可以將這樣的0-RTT請求重播多次,通過查看回應的長度判斷余額的變化情況。
只有應用本身的代碼可以真正確定通過0-RTT發送數據是否是安全的做法。
因此我們為Mobile Proxygen增加了一個API:
setRequestIdempotency(RETRY_SAFE).
該API可以告訴網絡棧數據不僅可以通過0-RTT安全地發送,而且可以執行其他操作,例如重試請求。我們與HTTP工作組就這個API進行了討論,一致認同“重試安全”是一個必要的特性。我們只通過0-RTT發送符合重試安全要求的請求,并且只在應用的代碼明確指定這是一種安全做法的情況下執行這種操作。而各種瀏覽器的計劃是通過0-RTT發送所有數據。
決定發送“重試安全”請求的時機
我們的產品可以一次發送多種不同類型的請求。一旦確定了哪些請求是重試安全的,我們還需要知道什么時候可以安全地發送非重試安全的請求。在發送重試安全的請求,而非發送非重試安全請求的過程中,零往返協議的套接字必須處于一種特性狀態下,因為此時還沒有得到來自服務器的回應。
我們希望確保整個抽象盡可能簡單,并且避免在內存中緩沖太多的數據。
在Mobile Proxygen中我們構建了一個可根據多種條件對請求進行調度的請求調度器。例如高優先級請求會比低優先級請求更快速進行調度。我們還為重試安全請求提供了一個自定義的請求調度器,如果某個請求是非重試安全的,并且傳輸工作尚未開始執行1-RTT,重試安全調度器會阻止這種請求調度自己的頭部或正文,將其保留在隊列中。
當傳輸符合重試安全要求時,重試安全調度器會得到一個回調,此時可以安全地調度非重試安全請求,并釋放請求隊列。
數據交付的可靠性
在對TLS和零往返協議進行性能分析對比時,我們發現TLS連接的錯誤率比零往返協議略低一些。通過我們自己的網絡棧數據,我們發現大部分請求錯誤發生在建立連接的過程中。由于使用了流式API,當我們知道可以發送0-RTT加密數據后,我們會立刻將零往返協議連接返回給Mobile Proxygen。在網絡棧獲得能夠發送數據的連接時,零往返連接只進行了一次TCP往返,而TLS此時已經進行了兩次往返(包括TCP)。
提到這個事情是因為,在建立連接時,網絡棧會試圖打開多個連接。TLS連接成功概率高于零往返連接是因為TLS連接會等待更多的往返,實際上這等同于進行了額外的連接重試。
為了讓零往返協議與TLS實現相似的結果,我們在Mobile Proxygen中增加了重試行為,借此在我們知道請求在獲得服務器回應前就已失敗的情況下加快重試速度。該方法提高了零往返連接的可靠性,同時也能讓TLS 1.3客戶端從中獲益。
為了適應不同的中間設備(Middlebox),我們還構建了從零往返協議到TLS 1.2的回退,但實際上這些設備的使用并不廣泛,主要出現在少數幾個ASN中。
重播緩存
縮短0-RTT數據有效期的時間窗口可以大幅降低攻擊者無止境重播0-RTT請求可能造成的風險。然而在這個時間窗口內,依然有可能多次重播請求,因此攻擊者依然有可能用統計學的方式分析回應的時間,進而對請求獲得進一步了解。為了防范這種問題,我們實驗了重播緩存,該技術可對每個時間窗口內發送的0-RTT Client Hello進行緩存,進而拒絕重復的消息。重播緩存并不能徹底禁止重播,畢竟我們的目標是讓客戶端自動將被拒絕的0-RTT請求以1-RTT數據的方式重新發送,但該技術可以將重播的次數限制為客戶端的重試次數。通過使用Bloom篩選器,我們的重播緩存可以用最少量資源處理大量握手,而代價僅僅是很少量的誤報率。我們尚未在零往返協議中全面啟用重播緩存(對于零往返協議,我們可以細化地控制哪些請求可作為0-RTT數據發送,因為我們可以控制客戶端選擇重試安全請求的代碼),不過我們認為可以在部署TLS 1.3 時開始部署重播緩存。
收益
性能
相比TLS 1.2,零往返協議有了顯著的性能改進。我們發現建立連接所需的時間降低了41%(75百分位數),請求處理總時間整體減少了2%。各種請求有著自己的差異,而零往返協議對應用啟動時因為無法重復使用連接而發出的請求能帶來最大價值。這樣改進也讓我們應用的冷啟動速度有了飛速提升。
針對TLS 1.3的貢獻
零往返協議目前還是實驗性的,但如我們預期,在性能改進放面取得了非常大的成功。從我們的Android和iOS應用中產生的大部分流量已經在使用零往返協議。同時我們還將這一過程中獲得的經驗貢獻給了TLS 1.3和QUIC。例如,TLS 1.3中的票證壽命功能就得到了零往返協議的啟發。我們還在TRON 2上介紹了自己的API設計,并就流傳輸功能進行了討論,借此服務器無需等待上一個數據傳輸操作完成,即可開始發出響應。為了明確0-RTT對瀏覽器的影響,我們還針對重試安全進行了多次討論。希望真個社區在未來可以通過TLS 1.3獲得類似的性能收益。
未來計劃
我們的傳輸安全團隊正在構建自己的TLS 1.3實現,并會在可行時納入零往返協議。我們認為在大量社區成員的貢獻下,TLS 1.3的協議設計非常出色。TLS 1.3不僅改善了性能,同時提供了一種更簡單并且更安全的設計。我們非常期待著在不遠的未來能夠實現并部署該協議。
相比TLS 1.3,我們更愿意讓零往返協議成為一種實驗和探索。我們為零往返協議進行的大部分工程抽象和設計都會立刻應用到TLS 1.3中。
任何在意安全性和性能的應用都應該考慮使用TLS 1.3,并考慮本文中提到的有關0-RTT數據的問題。零往返協議幫助我們更好地理解了0-RTT數據的影響,借此我們也對TLS 1.3的開發做出了自己的貢獻。
來自:http://www.infoq.com/cn/articles/非死book-create-protocol-for-fast-secure-mobile-connections