引子
本文將會利用mbedTLS協(xié)議棧,通過dump協(xié)議棧調(diào)試信息,抓包,代碼分析等方式來對DTLS的握手協(xié)議進行介紹。
DTLS簡介
簡單說,DTLS(Datagram Transport Layer Security)實現(xiàn)了在UDP協(xié)議之上的TLS安全層。由于基于TCP的SSL/TLS沒有辦法處理UDP報文的丟包及重排序(這些問題一般交給UDP的上層應(yīng)用解決),DTLS在原本TLS的基礎(chǔ)上做了一些小改動(復(fù)用大部分TLS的代碼)來解決如下UDP上實現(xiàn)TLS的問題:
TLS記錄層內(nèi)記錄的強關(guān)聯(lián)性及無序號
握手協(xié)議的可靠性
包丟失重傳機制(UDP無重傳機制)
無法按序接收(握手需要對包按順序處理,而UDP包的到達(dá)并非按序,包頭沒有TCP那樣的Seq/Ack number)
握手協(xié)議包長(證書之類傳輸可能達(dá)到KB級別)導(dǎo)致的UDP分包組包(類似于UDP在IP層的分包)
重復(fù)包(Replay)檢測
由于UDP/DTLS相較于TCP/TLS的輕量化及較小的開銷,目前被更多的運用的嵌入式環(huán)境中。例如CoAP使用DTLS來實現(xiàn)安全通路,CoAP及其上層的LWM2M則運用在物聯(lián)網(wǎng)和云端的通訊上。
如果你已經(jīng)很熟悉TLS,那么看到這里后就請忽略此文:)
如何借助mbedTLS來分析握手協(xié)議
mbedTLS(前身PolarSSL)是面向嵌入式系統(tǒng),實現(xiàn)的一套易用的加解密算法和SSL/TLS庫。mbedTLS系統(tǒng)開銷極小,對于系統(tǒng)資源要求不高。mbedTLS是開源項目,并且使用Apache 2.0許可證,使得用戶既可以講mbedTLS使用在開源項目中,也可以應(yīng)用于商業(yè)項目。目前使用mbedTLS的項目很多,例如Monkey HTTP Daemon,LinkSYS路由器。
我們在這里簡單的利用mbedTLS自帶的dtls_client/dtls_server的測試程序來分析握手協(xié)議。這里要說明的是mbedTLS這個自帶的DTLS 測試程序,服務(wù)器只在localhost做bind,客戶端也只連接到localhost。實際上是個loopback測試。并且使用了TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384的Cipher Suite,也就是用橢圓曲線(EC)的DH算法來實現(xiàn)密鑰(Session Key)的協(xié)商,用RSA來實現(xiàn)密鑰協(xié)商時交換的EC類型,DH公鑰等數(shù)據(jù)的簽名加密。所以握手的流程和其他一些加密方式會有所差別。比如和單純的RSA密鑰交換方式比起來,會多一個“Server Key Exchange”報文。
下載mbedTLS
$git clone https://github.com/ARMmbed/mbedtls
打開調(diào)試
在program/dtls_client.c, dtls_server.c里,增大DEBUG_LEVEL,然后將my_debug里加上時間戳信息。使能協(xié)議棧的內(nèi)部調(diào)試信息,方便對比客戶端和服務(wù)端的流程:
#define DEBUG_LEVEL 100
static void my_debug( void *ctx, int level,
const char *file, int line,
const char *str )
{
struct timeval tv;
((void) level);
gettimeofday(&tv, NULL);
//strftime(outstr, sizeof(outstr), "%H:%M:%S", tmp);
mbedtls_fprintf( (FILE *) ctx, "[%06ld.%ld]%s:%04d: %s", tv.tv_sec, tv.tv_usec, file, line, str );
fflush( (FILE *) ctx );
}
編譯
在Ubuntu上可以直接編譯。
$ make
編譯結(jié)果為
服務(wù)器:program/ssl/dtls_server
客戶端: program/ssl/dtls_client
抓包
打開wireshark之類的抓包工具,對本地網(wǎng)卡進行抓包。先跑server,后跑client。在抓包結(jié)束后,加dtls的display filter既可以:
Wireshark抓包截圖: (導(dǎo)出pcapng包下載)
有了TLS協(xié)議棧的調(diào)試信息,Wireshark的實際的抓包數(shù)據(jù)再加上源代碼,我們就很容易來分析DTLS的握手協(xié)議。當(dāng)然gdb拿過來直接調(diào)試也行,不過需要把DEBUG宏在編譯時打開:
$ make DEBUG=1
DTLS握手協(xié)議分析
DTLS握手協(xié)議和TLS類似。DTLS協(xié)議在UDP之上實現(xiàn)了客戶機與服務(wù)器雙方的握手連接,在握手過程中驗證對方的身份,并且使用RSA或者DH(Diffie-Hellman)實現(xiàn)會話密鑰的建立,以便在后面的數(shù)據(jù)傳輸中對數(shù)據(jù)加密。它利用cookie驗證機制和證書實現(xiàn)了通信雙方的身份認(rèn)證;并且用在報文段頭部加上序號,緩存亂序到達(dá)的報文段;還利用重傳機制實現(xiàn)了可靠傳送。在握手完成后,通信雙方就可以利用握手階段協(xié)商好的會話密鑰來對應(yīng)用數(shù)據(jù)進行加解密。
簡易握手流程圖:
從流程圖上看,有(1)(3)兩個“Client Hello”請求,他兩之間的區(qū)別是第二個包含有(2)”Hello Verify Request”里服務(wù)端發(fā)來的Cookie。要使得DTLS握手正真開始,服務(wù)端必須要判斷發(fā)送請求的客戶端是否是有效客戶端。通過這樣的Cookie交互,可以很大程度上保護服務(wù)端不受DoS的攻擊。如果利用Cookie,服務(wù)端會在收到每個客戶請求后返回一個體積大很多的證書給被攻擊者,超大量證書有可能造成被攻擊者的癱瘓。當(dāng)首次建立連接時,(1)請求包中的cookie為空,服務(wù)端根據(jù)客戶端的源IP地址通過哈希方法隨機生成一個cookie,并填入(2)”Hello Verify Request”包中發(fā)送給客戶端??蛻舳耸盏紺ookie后,再次發(fā)送帶有該Cookie的“Client Hello”包(3),服務(wù)端收到該包后便檢查報文段里面的cookie值和之前發(fā)給該客戶端的Cookie值是否完全相同,若是,則通過Cookie驗證,繼續(xù)進行握手連接;若不是,則拒絕建立連接。所以說(1)(2)步驟只在第一次連接時發(fā)生,之后在Cookie有效的情況下,DTLS握手從步驟(3)開始。
客戶端的實現(xiàn)都在ssl_cli.c里,狀態(tài)機由mbedtls_ssl_handshake_client_step()處理
服務(wù)端的實現(xiàn)則在ssl_srv.c里,狀態(tài)機由mbedtls_ssl_handshake_server_step()處理
(3)”Client Hello”由函數(shù)ssl_write_client_hello()實現(xiàn)報文填充和發(fā)送,內(nèi)容主要包含:
Random 32字節(jié)隨機數(shù),前4字節(jié)為當(dāng)前時間+28字節(jié)隨機數(shù)
Cookie,從報文(2)中獲得
Cipher Suite,客戶端可以支持的密鑰交換,數(shù)據(jù)加密方式
Compression methods,是否壓縮,及壓縮方式
Extension,例如服務(wù)器主機名;支持的簽名加密方式,EC曲線類型等
服務(wù)端收到報文(3)后,會調(diào)用函數(shù)ssl_parse_client_hello()做一系列協(xié)商工作:
將Random保存;驗證Cookie是否和客戶端的IP匹配;根據(jù)客戶端提供的Cipher Suite找最佳匹配的,能提供的Cipher算法集合。在mbedTLS的例子里使用了ECDHE_RSA_WITH_AES_256_GCM_SHA384(ECDHE密鑰協(xié)商算法、RSA簽名、GCM-AES對稱密鑰加密傳輸數(shù)據(jù)、SHA384哈希簽名)。如果協(xié)商沒有問題,服務(wù)端就調(diào)用ssl_write_server_hello()會發(fā)送報文(4)”Server Hello”,告訴客戶端使用什么Cipher Suite做握手,什么壓縮方式,然后利用隨機數(shù)生成一個Session ID。并且以客戶端同樣的方式生成的Random隨機數(shù),將隨機數(shù)和Session ID放入報文中。緊接著服務(wù)端會接連發(fā)送報文(5)(6)(7)
(5)”Certification”服務(wù)端會將他自己的證書發(fā)送給客戶端。證書中的肯定有一個證書的Subject是和Server Name相匹配的(commonName=”localhost”,organizationName=”PolarSSL”)。測試程序中其實發(fā)送了3個證書,subject分別是localhost, PolarSSL Test CA及PolarSSL Test EC CA。PolarSSL Test CA實際上是localhost的父證書,這里就涉及到一個CA Chain的概念?,F(xiàn)實情況中(例如瀏覽器訪問HTTPS服務(wù)器),服務(wù)器發(fā)給客戶端的證書一般是終端用戶證書(end-user CA),客戶端需要通過證書認(rèn)證來檢查服務(wù)器是否可信。首先客戶端在自己安裝(瀏覽器安裝時都會默認(rèn)安裝可靠證書)的一系列中間證書(intermediate CA)和根證書(ROOT CA)中查找該終端用戶證書的父證書,以及該父證書的父證書,直到追溯到根證書,建立起這些證書的關(guān)系:CA Chain。然后使用該終端用戶證書的父證書的公鑰來驗證終端用戶證書的完整性,然后找到父證書的父證書,以同樣的方式驗證父證書完整性,直到遇到根證書。一般來講,終端客戶證書的Issuer都會和某個中間或者根證書的Subject相匹配,也就意味著客戶證書是由某個中間或根證書發(fā)行機構(gòu)簽發(fā),并且終端客戶證書中的簽名是由父證書擁有者的私鑰加密的。由于根證書的Subject和Issuer都是自己,所以客戶端的根證書一定要保證是Trust CA頒發(fā)的,否則沒有辦法自己驗證自己。
回到我們的例子,客戶端在初始化的時候加載了PolarSSL Test CA的根證書(當(dāng)然只是測試的根證書),這個和現(xiàn)實的情況類似??蛻舳耸盏?5)”Certification”報文后,很快就可以查找到”localhost”這個終端用戶證書的根證書是”PolarSSL Test CA”,一次便驗證通過了。驗證的代碼在:mbedtls_x509_crt_verify_with_profile()。驗證通過后,客戶端實際上就獲得了證書中的公鑰。證書驗證完畢,說明服務(wù)端的身份沒有問題,可以進行下一步密鑰協(xié)商。
(6)”Server Key Exchange”是Session Key協(xié)商的重要一步ssl_write_server_key_exchange(),測試程序中使用了ECDHE_RSA的協(xié)商方式。服務(wù)端首先要將在(3)”Client Hello”階段和客戶端協(xié)商使用的EC曲線類型找出來,這里協(xié)商的曲線類型是secp512r1(0x0019)。利用該曲線類型,加載對應(yīng)于該曲線的參數(shù)p,b,G(x,y),n (服務(wù)端和客戶端參數(shù)相同,參考:library/ecp_curves.c)。調(diào)用mbedtls_ecdh_gen_public(),首先生成一個隨機數(shù)私鑰a(范圍[1, n-1]),然后計算Qs=aG,產(chǎn)生ECDHE的Public Key Qs,再用服務(wù)端的私鑰(和localhost證書中的公鑰配對的)對Qs做簽名(SHA512做哈希,RSA加密)。最后將曲線類型,公鑰Qs,簽名算法及簽名寫入(6)的報文中,發(fā)送給客戶端。由于ECDHE(Elliptic curve Diffie–Hellman)算法較為復(fù)雜,我也不是非常理解,在這里就不深入討論了。具體可以參考:
WikiPage
總之,ECDHE算法很快,而且不需要暴露預(yù)主密碼(premaster secret,馬上談到)。后面客戶端也會做同樣的操作,生成自己的私鑰b,計算Qc。雙方交換Q后,可以計算得到相同的預(yù)主密碼:bQs=baG=abG=aQc。之后雙方就可以用這個abG預(yù)主密碼和之前(3)(4)報文中的客戶端、服務(wù)端的Random來生成對稱密鑰Session Key(master secret)。預(yù)主密碼如何生成密鑰,可以參考mbedtls_ssl_derive_keys():
master = PRF( premaster, "master secret", randbytes )[0..47]
因為premaster secret不需要做交換,而是在本地計算產(chǎn)生,所以說ECDHE的密鑰協(xié)商方式比RSA更安全。
(7)”Server Hello Done”會緊接著(6)發(fā)送,內(nèi)容很簡單,標(biāo)示了handshake type 14,,告訴客戶端Hello階段結(jié)束。
(8)”Client Key Exchange”客戶端在收到(6)報文后,獲取服務(wù)端發(fā)送過來的EC曲線類型和Qs,使用和服務(wù)端一樣的流程生成私鑰b,計算公鑰Qc=bG,將Qc放入該報文,并發(fā)送給服務(wù)端。此時不需要再做簽名,后面”Finish”報文會再次讓雙方驗證密鑰的一致性。
(9)”Change Cipher Spec”客戶端接著發(fā)送該報文,告訴服務(wù)端Session Key我已經(jīng)生成,雙方可以用密鑰Session Key(master secret)開始加密通訊了。
(10)”Finished”該報文由客戶端發(fā)送mbedtls_ssl_write_finished(),這是第一個用Session Key加密的密文,內(nèi)容是一段驗證數(shù)據(jù):
valid_data = PRF(master_secret, "client finished", MD5(handshake_messages) + SHA(handshake_messages))[0..11]
handshake_messages實際上是客戶端在握手階段發(fā)出的所有報文(不包含該Finished報文),服務(wù)端在收到該報文后,會以同樣的方式計算出valid_data,并且做比較。以確認(rèn)雙方協(xié)商的密鑰一致。確認(rèn)完畢后,服務(wù)端以同樣的方式發(fā)送(11)”Change Cipher Spec”和(12)”Finished”給客戶端。最后完成握手。
完成握手后,雙發(fā)就可以發(fā)送加密的Application Data數(shù)據(jù)包來進行安全通訊,并且報文中都包含一個sequence number來標(biāo)識順序。
到這里握手協(xié)議部分就介紹完了,如果大家想要深入研究算法,去看mbedTLS的代碼不失為一個好途徑。
結(jié)尾
記得很早以前,我是看過SSL/TLS的協(xié)議,握手也是了解過。沒想到,最近看CoAP,很多細(xì)節(jié)現(xiàn)在都回想不起來。好記心不如爛筆頭,寫下來也是分享,今后碰到問題也可以做個參考。