天天看點

Netty系列之Netty安全性

2014年上半年對網絡安全影響最大的問題就是openssl heart bleed漏洞,來自codenomicon和谷歌安全部門的研究人員發現openssl的源代碼中存在一個漏洞,可以讓攻擊者獲得伺服器上64k記憶體中的資料内容。該漏洞在國内被譯為” openssl心髒出血漏洞”,因其破壞性之大和影響的範圍之廣,堪稱網絡安全裡程碑事件。

openssl是為網絡通信提供安全及資料完整性的一種安全協定,囊括了主要的密碼算法、常用的密鑰和證書封裝管理功能以及ssl協定.多數ssl加密網站是用名為openssl的開源軟體包,由于這也是網際網路應用最廣泛的安全傳輸方法,被網銀、線上支付、電商網站、門戶網站、電子郵件等重要網站廣泛使用,是以漏洞影響範圍廣大。

全球第一個被攻擊通告的案例是加拿大稅務局确認openssl heart bleed漏洞導緻了900個納稅人的社會保障号被盜,這900個納稅人的社保号被攻擊者在系統中完全删除了。

任何網絡攻擊都能夠給企業造成破壞,但是如何将這些破壞具體量化成金融資料呢?2013年,b2b international聯合卡巴斯基實驗室基于對全球企業的調查結果,計算出網絡攻擊平均造成的損失。

根據調查報告得出的結論,當企業遭遇網絡攻擊後平均損失為649,000美元。損失主要包括兩方面:

安全事件本身造成的損失,即由重要資料洩漏、業務連續性以及安全修複專家費用相關成本;

為列入計劃的”響應”成本,用于阻止未來發生類似的攻擊事件,包括雇傭、教育訓練員工成本以及硬體、軟體和其它基礎設施安全更新成本。

作為一個高性能的nio通信架構,基于netty的行業應用非常廣泛,不同的行業、不同的應用場景,面臨的安全挑戰也不同,下面我們根據netty的典型應用場景,分析下netty面臨的安全挑戰。

随着業務的發展,網站規模的擴大,傳統基于mvc的垂直架構已經無法應對業務的快速發展。需要對資料和業務進行水準拆分,基于rpc的分布式服務架構成為最佳選擇。

業務水準拆分之後,内部的各個子產品需要進行高性能的通信,傳統基于rmi和hession的同步阻塞式通信已經無法滿足性能和可靠性要求。是以,高性能的nio架構成為建構分布式服務架構的基石。

網站的架構演進過程如下:

Netty系列之Netty安全性

圖1-1 網站的架構演進

高性能的rpc架構,各子產品之間往往采用長連接配接通信,通過心跳檢測保證鍊路的可靠性。由于rpc架構通常是在内部各子產品之間使用,運作在授信的内部安全域中,不直接對外開放接口。是以,不需要做握手、黑白名單、ssl/tls等,正所謂是“防君子不防小人”。

在這種應用場景下,netty的安全性是依托企業的防火牆、安全加強作業系統等系統級安全來保障的,它自身并不需要再做額外的安全性保護工作。

如果使用netty做rpc架構或者私有協定棧,rpc架構面向非授信的第三方開放,例如将内部的一些能力通過服務對外開放出去,此時就需要進行安全認證,如果開放的是公網ip,對于安全性要求非常高的一些服務,例如線上支付、訂購等,需要通過ssl/tls進行通信。

它的原理圖如下:

Netty系列之Netty安全性

圖1-2 對第三方開放的通信架構

對第三方開放的通信架構的接口調用存在三種場景:

在企業内網,開放給内部其它子產品調用的服務,通常不需要進行安全認證和ssl/tls傳輸;

在企業内網,被外部其它子產品調用的服務,往往需要利用ip黑白名單、握手登陸等方式進行安全認證,認證通過之後雙方使用普通的socket進行通信,如果認證失敗,則拒絕用戶端連接配接;

開放給企業外部第三方應用通路的服務,往往需要監聽公網ip(通常是防火牆的ip位址),由于對第三方服務調用者的監管存在諸多困難,或者無法有效監管,這些第三方應用實際是非授信的。為了有效應對安全風險,對于敏感的服務往往需要通過ssl/tls進行安全傳輸。

作為高性能、異步事件驅動的nio架構,netty非常适合建構上層的應用層協定,相關原理,如下圖所示:

Netty系列之Netty安全性

圖1-3 基于netty建構應用層協定

由于絕大多數應用層協定都是公有的,這意味着底層的netty需要向上層提供通信層的安全傳輸,也就是需要支援ssl/tls。

jdk的安全類庫提供了javax.net.ssl.sslsocket和javax.net.ssl.sslserversocket類庫用于支援ssl/tls安全傳輸,對于nio非阻塞socket通信,jdk并沒有提供現成可用的類庫簡化使用者開發。

netty通過jdk的sslengine,以sslhandler的方式提供對ssl/tls安全傳輸的支援,極大的簡化了使用者的開發工作量,降低開發難度。

對于netty預設提供的http協定,netty利用sslhandler,同樣支援https協定。

單向認證,即用戶端隻驗證服務端的合法性,服務端不驗證用戶端。下面我們通過netty的ssl單向認證代碼開發來掌握基于netty的ssl單向認證。

首先,利用jdk的keytool工具,netty服務端依次生成服務端的密鑰對和證書倉庫、服務端自簽名證書。

生成netty服務端私鑰和證書倉庫指令:

生成netty服務端自簽名證書:

生成用戶端的密鑰對和證書倉庫,用于将服務端的證書儲存到用戶端的授信證書倉庫中,指令如下:

随後,将netty服務端的證書導入到用戶端的證書倉庫中,指令如下:

keytool -import -trustcacerts -alias securechat -file schat.cer -storepass cnetty -keystore cchat.jks

上述工作完成之後,我們就開始編寫ssl服務端和用戶端的代碼,下面我們對核心代碼進行講解。

首先看服務端的代碼,在tcp鍊路初始化的時候,建立sslcontext并對其進行正确的初始化,下面我們對sslcontext的建立進行講解:

因為是用戶端認證服務端,是以服務端需要正确的設定和加載私鑰倉庫keystore,相關代碼如下:

Netty系列之Netty安全性

初始化keymanagerfactory之後,建立sslcontext并初始化,代碼如下:

Netty系列之Netty安全性

由于是單向認證,服務端不需要驗證用戶端的合法性,是以,trustmanager為空,安全随機數不需要設定,使用jdk預設建立的即可。

服務端的sslcontext建立完成之後,利用sslcontext建立ssl引擎sslengine,設定sslengine為服務端模式,由于不需要對用戶端進行認證,是以needclientauth不需要額外設定,使用預設值false。相關代碼如下:

Netty系列之Netty安全性

ssl服務端建立完成之後,下面繼續看用戶端的建立,它的原理同服務端類似,也是在初始化tcp鍊路的時候建立并設定sslengine,代碼如下:

Netty系列之Netty安全性

由于是用戶端認證服務端,是以,用戶端隻需要加載存放服務端ca的證書倉庫即可。

加載證書倉庫完成之後,初始化sslcontext,代碼如下:對于用戶端隻需要設定信任證書trustmanager。

Netty系列之Netty安全性

用戶端sslcontext初始化完成之後,建立sslengine并将其設定為用戶端工作模式,代碼如下:

Netty系列之Netty安全性

将sslhandler添加到pipeline中,利用sslhandler實作socket安全傳輸,代碼如下:

Netty系列之Netty安全性

用戶端和服務端建立完成之後,測試下ssl單向認證功能是否ok,為了檢視ssl握手過程,我們打開ssl握手的調測日志,eclipse設定如下:

Netty系列之Netty安全性

圖2-1 打開ssl調測日志

分别運作服務端和用戶端,運作結果如下:

Netty系列之Netty安全性

圖2-2 用戶端ssl握手日志

Netty系列之Netty安全性

圖2-3 服務端ssl握手日志

在用戶端輸入資訊,服務端原樣傳回,測試結果如下:

Netty系列之Netty安全性

到此,netty ssl單向認證已經開發完成,下個小節我們将結合ssl握手日志,詳細解讀下ssl單向認證的原理。

ssl單向認證的過程總結如下:

ssl用戶端向服務端傳送用戶端ssl協定的版本号、支援的加密算法種類、産生的随機數,以及其它可選資訊;

服務端傳回握手應答,向用戶端傳送确認ssl協定的版本号、加密算法的種類、随機數以及其它相關資訊;

服務端向用戶端發送自己的公鑰;

用戶端對服務端的證書進行認證,服務端的合法性校驗包括:證書是否過期、發行伺服器證書的ca是否可靠、發行者證書的公鑰能否正确解開伺服器證書的“發行者的數字簽名”、伺服器證書上的域名是否和伺服器的實際域名相比對等;

用戶端随機産生一個用于後面通訊的“對稱密碼”,然後用服務端的公鑰對其加密,将加密後的“預主密碼”傳給服務端;

服務端将用自己的私鑰解開加密的“預主密碼”,然後執行一系列步驟來産生主密碼;

用戶端向服務端發出資訊,指明後面的資料通訊将使用主密碼為對稱密鑰,同時通知伺服器用戶端的握手過程結束;

服務端向用戶端發出資訊,指明後面的資料通訊将使用主密碼為對稱密鑰,同時通知用戶端伺服器端的握手過程結束;

ssl的握手部分結束,ssl安全通道建立,用戶端和服務端開始使用相同的對稱密鑰對資料進行加密,然後通過socket進行傳輸;

下面,我們結合jdk的ssl工作原理對netty的ssl單向認證過程進行講解,首先,我們看下jdk ssl單向認證的流程圖:

Netty系列之Netty安全性

圖2-4 ssl單向認證流程圖

下面結合jdk ssl引擎的調測日志資訊我們對ssl單向認證的流程進行詳細講解,對于比較簡單的流程會進行步驟合并。

步驟1:用戶端使用tls協定版本發送一個clienthello消息,這個消息包含一個随機數、建議的加密算法套件和壓縮方法清單,如下所示:

步驟2:服務端使用serverhello消息來響應,這個消息包含由客戶提供的資訊基礎上的另一個随機數和一個可選的會話id,以及服務端選擇的加密套件算法,響應消息如下:

步驟3:服務端發送自簽名的證書消息,包含完整的證書鍊:

步驟4:服務端向用戶端發送自己的公鑰資訊,最後發送serverhellodone:

步驟5:用戶端對服務端自簽名的證書進行認證,如果用戶端的信任證書清單中包含了服務端發送的證書,對證書進行合法性認證,相關資訊如下:

步驟6:用戶端通知伺服器改變加密算法,通過change cipher spec消息發給服務端,随後發送finished消息,告知伺服器請檢查加密算法的變更請求:

步驟7:服務端讀取到change cipher spec變更請求消息,向用戶端傳回确認密鑰變更消息,最後通過發送finished消息表示ssl/tls握手結束。

我們在2.1章節的基礎上進行開發,與單向認證不同的是服務端也需要對用戶端進行安全認證。這就意味着用戶端的自簽名證書也需要導入到服務端的數字證書倉庫中。

首先,生成用戶端的自簽名證書:

最後,将用戶端的自簽名證書導入到服務端的信任證書倉庫中:

證書導入之後,需要對ssl用戶端和服務端的代碼同時進行修改,首先我們看下服務端如何修改。

由于服務端需要對用戶端進行驗證,是以在初始化服務端sslcontext的時候需要加載證書倉庫。首先需要對trustmanagerfactory進行初始化,代碼如下:

Netty系列之Netty安全性

初始化sslcontext的時候根據trustmanagerfactory擷取trustmanager數組,代碼如下:

Netty系列之Netty安全性

最後,建立sslengine之後,設定需要進行用戶端認證,代碼如下:

Netty系列之Netty安全性

完成服務端修改之後,再回頭看下用戶端的修改,由于服務端需要認證用戶端的證書,是以,需要初始化和加載私鑰倉庫,向服務端發送公鑰,初始化keystore的代碼如下:

Netty系列之Netty安全性

初始化sslcontext的時候需要傳入keymanager數組,代碼如下:

Netty系列之Netty安全性

用戶端開發完成之後,測試下程式是否能夠正常工作,運作結果如下所示。

用戶端運作結果:

Netty系列之Netty安全性

圖2-5 netty ssl雙向認證用戶端運作結果

服務端運作結果:

Netty系列之Netty安全性

圖2-6 netty ssl雙向認證服務端運作結果

在用戶端控制台進行輸入,看ssl傳輸是否正常:

Netty系列之Netty安全性

圖2-7 netty ssl 安全傳輸測試

ssl雙向認證相比單向認證,多了一步服務端發送認證請求消息給用戶端,用戶端發送自簽名證書給服務端進行安全認證的過程。下面,我們結合netty ssl調測日志,對雙向認證的差異點進行分析。

相比于用戶端,服務端在發送serverhello時攜帶了要求用戶端認證的請求資訊,如下所示:

用戶端接收到服務端要求用戶端認證的請求消息之後,發送自己的證書資訊給服務端,資訊如下:

服務端對用戶端的自簽名證書進行認證,資訊如下:

使用jdk keytool生成的數字證書是自簽名的。自簽名就是指證書隻能保證自己是完整且沒有經過非法修改,但是無法保證這個證書是屬于誰的。為了對自簽名證書進行認證,需要每個用戶端和服務端都交換自己自簽名的私有證書,對于一個大型網站或者應用伺服器,這種工作量是非常大的。

基于自簽名的ssl雙向認證,隻要用戶端或者服務端修改了密鑰和證書,就需要重新進行簽名和證書交換,這種調試和維護工作量是非常大的。是以,在實際的商用系統中往往會使用第三方ca證書頒發機構進行簽名和驗證。我們的浏覽器就儲存了幾個常用的ca_root。每次連接配接到網站時隻要這個網站的證書是經過這些ca_root簽名過的。就可以通過驗證了。

ca數字證書認證服務往往是收費的,國内有很多數字認證中心都提供相關的服務,如下所示:

Netty系列之Netty安全性

圖2-8 商業的數字認證中心

作為示例,我們自己生成一個ca_root的密鑰對,部署應用時,把這個ca_root的私鑰部署在所有需要ssl傳輸的節點就可以完成安全認證。作為示例,如果要生成ca_root,我們使用開源的openssl。

在windows上安裝和使用openssl網上有很多教程,也不是本文的重點,是以,openssl的安裝和使用本文不詳細介紹。

下面我們對基于第三方ca認證的步驟進行詳細介紹。

步驟1:利用openssl生成ca證書:

步驟2:生成服務端密鑰對:

步驟3:生成證書簽名請求:

步驟4:用ca私鑰進行簽名:

步驟5:導入信任的ca根證書到keystore:

步驟6:将ca簽名後的server端證書導入keystore:

步驟1:生成用戶端密鑰對:

步驟2:生成證書簽名請求:

步驟3:用ca私鑰進行簽名:

步驟4:導入信任的ca根證書到keystore:

步驟5:将ca簽名後的client端證書導入keystore:

基于ca認證的開發和測試與ssl雙向和單向認證代碼相同,此處不再贅述。

當用戶端和服務端的tcp鍊路建立成功之後,sslhandler的channelactive被觸發,ssl用戶端通過ssl引擎發起握手請求消息,代碼如下:

Netty系列之Netty安全性

發起握手請求之後,需要将sslengine建立的握手請求消息進行ssl編碼,發送給服務端,是以,握手之後立即調用wrapnonappdata方法,下面具體對該方法進行分析:

Netty系列之Netty安全性

因為隻需要發送握手請求消息,是以source bytebuf為空,下面看下wrap方法的具體實作:

Netty系列之Netty安全性

将ssl引擎中建立的握手請求消息編碼到目标bytebuffer中,然後對寫索引進行更新。判斷寫入操作是否越界,如果越界說明out容量不足,需要調用ensurewritable對bytebuf進行動态擴充,擴充之後繼續嘗試編碼操作。如果編碼成功,傳回ssl引擎操作結果。

對編碼結果進行判斷,如果編碼位元組數大于0,則将編碼後的結果發送給服務端,然後釋放臨時變量out。

判斷ssl引擎的操作結果,ssl引擎的操作結果定義如下:

finished:sslengine 已經完成握手;

need_task:sslengine 在繼續進行握手前需要一個(或多個)代理任務的結果;

need_unwrap:在繼續進行握手前,sslengine 需要從遠端接收資料,是以應帶調用sslengine.unwrap();

need_wrap:在繼續進行握手前,sslengine 必須向遠端發送資料,是以應該調用 sslengine.wrap();

not_handshaking:sslengine 目前沒有進行握手。

下面我們分别對5種操作的代碼進行分析:

Netty系列之Netty安全性

如果握手成功,則設定handshakepromise的操作結果為成功,同時發送sslhandshakecompletionevent.succes給ssl監聽器,代碼如下:

Netty系列之Netty安全性

如果是need_task,說明異步執行ssl task,完成後續可能耗時的操作或者任務,netty封裝了一個任務立即執行線程池專門處理ssl的代理任務,代碼如下:

Netty系列之Netty安全性

如果是need_unwrap,則判斷是否由unwrap發起,如果不是則執行unwrap操作。

如果是not_handshaking,則調用unwrap,繼續接收服務端的消息。

服務端應答消息的接收跟服務端接收用戶端的代碼類似,唯一不同之處在于ssl引擎的用戶端模式設定不同,一個是服務端,一個是用戶端。上層的代碼處理是相同的,下面我們在ssl服務端章節分析握手消息的接收。

ssl服務端接收用戶端握手請求消息的入口方法是decode方法,下面對它進行詳細分析。

首先擷取接收緩沖區的讀寫索引,并對讀取的偏移量指針進行備份:

Netty系列之Netty安全性

對半包辨別進行判斷,如果上一個消息是半包消息,則判斷目前可讀的位元組數是否小于整包消息的長度,如果小于整包長度,則說明本次讀取操作仍然沒有把ssl整包消息讀取完整,需要傳回io線程繼續讀取,代碼如下:

Netty系列之Netty安全性

如果消息讀取完整,則修改偏移量:同時置位半包長度辨別。

Netty系列之Netty安全性

下面在for循環中讀取ssl消息,因為tcp存在拆包和粘包,是以一個bytebuf可能包含多條完整的ssl消息。

首先判斷可讀的位元組數是否小于協定消息頭長度,如果是則退出循環繼續由io線程接收後續的封包:

Netty系列之Netty安全性

擷取ssl消息包的封包長度,具體算法不再介紹,可以參考ssl的規範文檔進行解讀,代碼如下:

Netty系列之Netty安全性

對長度進行判斷,如果ssl封包長度大于可讀的位元組數,說明是個半包消息,将半包辨別長度置位,傳回io線程繼續讀取後續的資料報,代碼如下:

Netty系列之Netty安全性

對消息進行解碼,将ssl加密的消息解碼為加密前的原始資料,unwrap方法如下:

Netty系列之Netty安全性

調用sslengine的unwrap方法對ssl原始消息進行解碼,對解碼結果進行判斷,如果越界,說明out緩沖區不夠,需要進行動态擴充。如果是首次越界,為了盡量節約記憶體,使用ssl最大緩沖區長度和ssl原始緩沖區可讀的位元組數中較小的。如果再次發生緩沖區越界,說明擴張後的緩沖區仍然不夠用,直接使用ssl緩沖區的最大長度,保證下次解碼成功。

解碼成功之後,對ssl引擎的操作結果進行判斷:如果需要繼續接收資料,則繼續執行解碼操作;如果需要發送握手消息,則調用wrapnonappdata發送握手消息;如果需要異步執行ssl代理任務,則調用立即執行線程池執行代理任務;如果是握手成功,則設定ssl操作結果,發送ssl握手成功事件;如果是

Netty系列之Netty安全性

應用層的業務資料,則繼續執行解碼操作,其它操作結果,抛出操作類型異常。

需要指出的是,ssl用戶端和服務端接收對方ssl握手消息的代碼是相同的,那為什麼ssl服務端和用戶端發送的握手消息不同呢?這些是ssl引擎負責區分和處理的,我們在建立ssl引擎的時候設定了用戶端模式,ssl引擎就是根據這個來進行區分的,代碼如下:

Netty系列之Netty安全性

無論用戶端還是服務端,隻需要圍繞ssl引擎的操作結果進行程式設計即可。

ssl的消息讀取實際就是bytetomessagedecoder将接收到的ssl加密後的封包解碼為原始封包,然後将整包消息投遞給後續的消息解碼器,對消息做二次解碼。基于ssl的消息解碼模型如下:

Netty系列之Netty安全性

ssl消息讀取的入口都是decode,因為是非握手消息,它的處理非常簡單,就是循環調用引擎的unwrap方法,将ssl封包解碼為原始的封包,代碼如下:

Netty系列之Netty安全性

握手成功之後的所有消息都是應用資料,是以它的操作結果為not_handshaking,遇到此辨別之後繼續讀取消息,直到沒有可讀的位元組,退出循環,代碼如下:

Netty系列之Netty安全性

如果讀取到了可用的位元組,則将讀取到的緩沖區加到輸出結果清單中,代碼如下:

Netty系列之Netty安全性

bytetomessagedecoder判斷解碼結果list,如果非空,則循環調用後續的handler,由後續的解碼器對解密後的封包進行二次解碼。

ssl消息發送時,由sslhandler對消息進行編碼,編碼後的消息實際就是ssl加密後的消息,它的入口是flush方法,代碼如下:

Netty系列之Netty安全性

從待加密的消息隊列中彈出消息,調用ssl引擎的wrap方法進行編碼,代碼如下:

Netty系列之Netty安全性

wrap方法很簡單,就是調用ssl引擎的編碼方法,然後對寫索引進行修改,如果緩沖區越界,則動态擴充緩沖區:

Netty系列之Netty安全性

對ssl操作結果進行判斷,因為已經握手成功,是以傳回的結果是not_handshaking,執行finishwrap方法,調用channelhandlercontext的write方法,将消息寫入發送緩沖區中,如果待發送的消息為空,則構造空的bytebuf寫入:

Netty系列之Netty安全性

編碼後,調用channelhandlercontext的flush方法消息發送給對方,代碼如下

繼續閱讀