當今社會,電子商務大行其道,做爲網絡安全 infrastructure 之一的 -- SSL/TLS 協議的重要性已不用多說。
OpenSSL 則是基於該協議的目前應用最普遍的開源實現,其影響之大,以致於四月初爆出的 OpenSSL Heartbleed 安全漏洞(CVE-2014-0160) 到如今還餘音未消。算法
本節就以出問題的 OpenSSL 1.0.1f 做爲實例進行分析;整個分析過程仍採用【參考 RFC、結合報文抓包、外加工具驗證】的老方法。
同時咱們利用 OpenSSL 自帶的調試功能,來觀察運行的內部細節,起到事半功倍的做用。
一般狀況須要同時運行客戶端和服務器,本文使用 OpenSSL 提供的子命令 s_server/s_client 進行 TLS 通訊。安全
1、下載 && 修改 && 編譯 OpenSSL 1.0.1f
官網(www.openssl.org)下載 openssl-1.0.1f.tar.gz 並解壓
按以下修改文件 ssl\ssl_locl.h,打開內部調試開關
[554] /*#define DES_OFB_DEBUG */
[555] /*#define SSL_DEBUG */
[556] /*#define RSA_DEBUG */
改成
[554] /*#define DES_OFB_DEBUG */
[555] #define SSL_DEBUG
[556] #define KSSL_DEBUG
[557] /*#define RSA_DEBUG */
說明:s_server/s_client 命令提供了一些調試參數(好比 -debug/-msg/-state),用於輸出協議運行時的內部狀態信息,
但更詳細的細節,例如:「加密/認證的密鑰是如何產生」則看不到。
編譯 ssl\t1_enc.c 時報錯,查看源文件,原來是打開宏 KSSL_DEBUG 後暴露的一個未聲明變量錯誤,去掉或註釋掉此行,以下
[1133] #ifdef KSSL_DEBUG
[1134] // printf ("tls1_export_keying_material(%p,%p,%d,%s,%d,%p,%d)\n", s, out, olen, label, llen, p, plen);
[1135] #endif /* KSSL_DEBUG */
繼續編譯,最終成功。
2、運行 OpenSSL,抓取交互報文
前後運行服務器、客戶端,命令以下
D:\>openssl s_server -cert server.pem -key server_plainkey.pem -tls1 -no_dhe -no_ecdhe -no_ticket
D:\>openssl s_client
本文咱們僅分析 TLS 協議(指定 -tls1 選項)。另外,爲了聚焦協議核心,使用參數 -no_ticket 的關閉「Session Ticket」特性。
參數 -no_dhe/-no_ecdhe 關閉「Diffie-Hellman 和橢圓曲線 Diffie-Hellman 密鑰交換功能」。
(啓用該功能後,安全性獲得進一步提升,但同時 Wireshark 沒法查看解密後的明文,參見後面)
鏈接成功後,在客戶端輸入 Hello, OpenSSL 並回車,服務器端正確顯示解密後的明文。
一樣,在服務器端輸入內容並回車,客戶端也正確顯示解密後的明文。
將運行中服務器和客戶端的輸出信息分別保存成文件(server.txt/client.txt),能夠用於驗證後面的計算過程。
另外 s_server/s_client 之間的交互報文,是發生在本地迴環接口上,目前 Wireshark 還不能抓取這種報文。
能夠運行支持本地迴環接口抓包的工具 RawCap(http://www.netresec.com)進行抓包。
3、協議分析
Wireshark 查看抓包文件,下面是其交互過程及簡短說明
Client TLSv1 Server
ClientHello(列出支持的算法套件) -------->
ServerHello(這是我選定的密碼算法套件)
Certificate(這是個人證書,你能夠驗證下)
<-------- ServerHelloDone(我說完,輪到你了)
雙方就使用密碼算法套件達成一致
ClientKeyExchange(加密的PreMasterSecret)
ChangeCipherSpec(後續消息已經作好密碼保護準備)
Finished(覈對下前面達成的結論) -------->
ChangeCipherSpec(後續消息已經作好密碼保護準備)
<-------- Finished(覈對下前面達成的結論)
雙方相互覈對對方發來的 Finished 消息
發送 "Hello, OpenSSL\r\n" 加密報文
Application Data -------->
(1)通訊協議都有一個主動發起方,在這裏就是客戶端發起的 ClientHello 報文,從名字上看這只是打個招呼,告訴服務器「我來了」。
固然報文的內容並不只限於此,它包含了一些重要的字段,好比客戶端支持的協議版本號,密碼算法套件,及一些擴展特性(好比橢圓曲線參數),
其中還有一個字段 Random(記爲 Client.random),它是客戶端生成的一次性隨機數,直接決定了後面的密鑰生成。
TLSv1 Record Layer: Handshake Protocol: Client Hello
Content Type: Handshake (22)
Version: TLS 1.0 (0x0301)
Length: 314
Handshake Protocol: Client Hello
Handshake Type: Client Hello (1)
Length: 310
Version: TLS 1.2 (0x0303) -- 與上一字段版本號不一樣,奇怪
Random
Session ID Length: 0
Cipher Suites Length: 160
Cipher Suites (80 suites)
Compression Methods Length: 1
Compression Methods (1 method)
Extensions Length: 109
Extension: ec_point_formats
Extension: elliptic_curves
......
Extension: Heartbeat
(2)客戶端先說話了,做爲服務器就應該回應對方,這就是 ServerHello 報文。咱們看下回應報文中有什麼
服務器說它只支持 TLS 1.0 版本(命令行參數 -tls1),而且選取了 TLS_RSA_WITH_AES_256_CBC_SHA 做爲密碼算法套件
爲何稱爲套件呢,原來它是影響後續報文交互的一整套密碼算法,包括初始密鑰生成算法、加密使用的算法、消息認證使用的 HASH 函數
此例中:RSA 表示初始密鑰生成採用基於 RSA 的密鑰交換方法(見後文說明),AES_256_CBC 表示加密算法,消息認證將用到 SHA1。
固然不能忘記還有一個重要字段 Random(記爲 Server.random)。
另外還有一個 TLS 擴展特性 Heartbeat 服務器也支持,正是在這個特性上 OpenSSL 1.0.1f 版本的實現出現了重大漏洞。
TLSv1 Record Layer: Handshake Protocol: Server Hello
Content Type: Handshake (22)
Version: TLS 1.0 (0x0301)
Length: 86
Handshake Protocol: Server Hello
Handshake Type: Server Hello (2)
Length: 82
Version: TLS 1.0 (0x0301)
Random
Session ID Length: 32
Session ID: ......
Cipher Suite: TLS_RSA_WITH_AES_256_CBC_SHA (0x0035)
Compression Method: null (0)
Extensions Length: 10
Extension: renegotiation_info
Extension: Heartbeat
(3)不管是何種安全協議,都要解決【初始密鑰是如何獲得(或生成)的】這一問題。對於簡單和安全性要求不高的場景,通訊雙方直接使用預共享密鑰就夠了。但對於 SSL/TLS 協議,這是遠遠不夠的。爲此,人們採用了兩種思路來解決密鑰的生成(後面爲簡化,Client/Server 分別記爲 A/B):
【第一種思路】A/B雙方經過協商達成一致,來肯定密鑰究竟是什麼。
如何協商呢?這就要說到著名的 Diffie-Hellman(DH) 協議。
該協議的內容,一言以蔽之:A 給出一個值 X,B 給出一個值 Y,而後互相發送給對方,雙方算出同一個值 Z。
這看上去不算什麼,神奇的是第三方根據 X 和 Y 值卻沒法得出 Z 的值,也就是說只有 A/B 雙方纔知道 Z。
爲何會這樣?這是由於,A 除了知道 X 外,還知道關於 X 的一個祕密 x,A 知道這個祕密,再加上收到的 Y,A 就能夠算出 Z 來。
對於 B,也是相同道理。但第三方殊不知道祕密 x/y,於是不能算出 Z 來。
總的過程看起來,就是 Client/Server 雙方協商出了一個密鑰 Z,所以該方法稱爲基於 DH 協議的密鑰交換(協商)。
X Y
Client -------> 初始密鑰Z <------- Server
【第二種思路】A 直接告訴 B:密鑰是什麼。
固然這種狀況下,不能直接使用明文傳輸密鑰。這須要一個加密通道,那麼加密的密鑰又是什麼?
一個天然的想法是,A 使用 B 的公鑰將其選好的密鑰加密,再將密文發送給 B。這一過程就稱爲基於公鑰算法的密鑰交換(其實稱爲密鑰傳輸更爲貼切)。
既然要用到 B 的公鑰,就涉及到B的證書。A 又是如何獲得 B 的證書呢?B 直接將證書發送給 A 就好了。
加密的初始密鑰Z
Client -------------------> Server
無論用哪一種方法,最終雙方都獲得(不爲第三方所知的)一個密鑰,在 SSL/TLS 協議中,該密鑰稱爲 PreMasterSecret
上面的 TLS 運行過程,初始密鑰的生成就是採用第二種辦法:基於公鑰算法(RSA)的密鑰交換(基於 DH 協議的密鑰協商將在後面討論)。
Server 發送 ServerHello 給 Client 後,接着再發送 Server 證書,最後再發送 ServerHelloDone 消息,表示:我作完了,下面輪到你了
TLSv1 Record Layer: Handshake Protocol: Certificate
Content Type: Handshake (22)
Version: TLS 1.0 (0x0301)
Length: 637
Handshake Protocol: Certificate
Handshake Type: Certificate (11)
Length: 633
Certificates Length: 630
Certificates (630 bytes)
TLSv1 Record Layer: Handshake Protocol: Server Hello Done
Content Type: Handshake (22)
Version: TLS 1.0 (0x0301)
Length: 4
Handshake Protocol: Server Hello Done
Handshake Type: Server Hello Done (14)
Length: 0
Client 收到 Server 發過來的證書,第一步就是驗證該證書的合法性(是否爲可信 CA 簽發、是否過時、是否吊銷),只有證書合法的狀況下,Client 纔會繼續下去;不然應該中斷協議運行。上面的例子中,客戶端並不關注這一點(unable to verify the first certificate),因此協議繼續進行。
(4)Client 收到 Server 的證書後,本身選擇一個初始密鑰 PreMasterSecret,使用證書中的公鑰加密,將密文發送給 Server。
這個 PreMasterSecret 是什麼值?利用 Wireshark 的 Export Selected Packet Bytes 功能將密文導出存爲文件 ciphertext。運行命令
D:\>openssl rsautl -decrypt -raw -in ciphertext -inkey server_plainkey.pem -out plaintext
D:\>od -An -tx1 plaintext
00 02 8d 68 2a ce 41 15 fe 99 53 a7 c2 a0 05 e6 -- 灰色背景爲填充部分
59 2b c3 74 2f b8 4e a2 60 a0 26 3f 3a bb 11 91
09 4b d0 8f 09 96 0c cf a0 ab fe 6e bb 23 b7 73
f0 a8 7e 94 38 1c fd 69 61 ee 9a 26 3e b1 80 4f
ac 02 c6 04 e4 30 05 3d e1 dc 4a 96 f2 d3 95 00
03 03 07 5e 2a 52 e9 88 c4 29 54 c6 9e a1 3c 4c
e4 33 1c c9 6b 6d 24 3e 79 56 f9 df 45 8f a9 55
e9 23 37 ec a3 e9 51 cf dd 90 c3 09 80 95 19 6d
原來是 PKCS#1 格式,其中紫色部分是就是 PreMasterSecret 明文
PreMasterSecret 到底起什麼做用?注意到它只有 48 字節,單從長度上看就可能沒法知足加密/消息認證的密鑰需求。
(AES_256 密鑰長 32 字節,IV 是 16 字節,還沒算上 MAC 密鑰)
事實上,它確實不是能夠直接使用的密鑰,而是生成(或稱爲導出)加密/消息認證等密鑰的一個種子。
嚴格說來,它是生成種子的種子,下圖能夠解釋這句話
+-------------+ +---------------+ +-------------+
|Client.random| |PreMasterSecret| |Server.random|
+-------------+ +---------------+ +-------------+
\ \ | / /
\ \ | / / 1 -- client_write_MAC_secret 客戶端MAC密鑰,生成消息的認證碼,對方用其驗證消息
\ V V V / 2 -- server_write_MAC_secret 服務器MAC密鑰,生成消息的認證碼,對方用其驗證消息
\ +---------------+ / 3 -- client_write_key 客戶端加密密鑰,加密客戶端發送的消息,對方用其解密
\ | MasterSecret | / 4 -- server_write_key 服務器加密密鑰,服務器加密發送的消息,對方用其解密
\ +---------------+ / 5 -- client_write_IV 客戶端IV,與客戶端加密密鑰配合使用(分組密碼算法)
\ | / 6 -- server_write_IV 服務器IV,與服務器加密密鑰配合使用(分組密碼算法)
\ | /
V V V
+---+---+---+---+---+---+
| 1 | 2 | 3 | 4 | 5 | 6 | KeyBlock
+---+---+---+---+---+---+
上面能夠概括爲三個步驟公式
MasterSecret = PRF(PreMasterSecret, "master secret", Client.random || Server.random)[0..47] -- 固定取前 48 字節
KeyBlock = PRF(MasterSecret, "key expansion", Server.random || Client.random) -- 長度爲由雙方肯定的密碼算法套件決定
其中 PRF 是一個僞隨機生成函數,它接收三個入參,生成的隨機數能夠爲任意長(實際應用中只要取所需的長度)
通過兩次 PRF 調用,最終生成的 KeyBlock(密鑰塊),纔是後面真正用到的密鑰,並且它被依次分割爲六部分子密鑰,如上所示
初始密鑰(或稱爲密鑰種子)肯定後,進一步演化出實際使用的加解密/認證密鑰,這種密鑰擴展模式,在安全協議中已經成爲一個範式。
至於初始密鑰從哪裏來,能夠概括爲預共享(事先共享)、(基於公鑰的)密鑰交換、(基於 DH 協議的)密鑰協商三種模式。
(習慣上,密鑰交換和密鑰協商這兩種說法常常混用)
好比,WiFi 的 PSK 模式,密鑰種子(PMK)是由無線路由器的密碼和無線網絡名稱共同決定的,爲預共享模式。
IKE 協議中,密鑰種子(SKEYID)的生成有幾種狀況:
對於簽名認證模式,SKEYID 取決於雙方隨機數(明文)和 DH 協商結果,屬於密鑰協商模式。
對於 PSK 認證模式,SKEYID 取決於預共享密鑰和雙方的隨機數(明文),預共享和密鑰協商模式的特色都具有。
SSL/TLS 協議中,密鑰種子(PreMasterSecret)則採用的是密鑰交換、密鑰協商兩種模式。
在這些協議中,無論密鑰種子如何生成,密鑰擴展過程都或多或少地受通訊雙方的影響(好比以隨機數、DH 參數),具體能夠參考相關 RFC。
但有一點能夠確認:加解密採用對稱算法。而公鑰算法只在協議開始時用於密鑰交換或數字簽名(身份認證),後面就沒有太大的做用了。
關於公式 PRF 是怎麼算的,參見下面的腳本 TLS_PRF.pl服務器
1 # Computer PRF -- from <<RFC 2246: The TLS Protocol Version 1.0>> 2 # 3 # PRF(secret, label, seed) = P_MD5(S1, label + seed) XOR 4 # P_SHA-1(S2, label + seed) 5 # 6 # Let L_S = strlen(secret) and half_L_S = ceil(L_S / 2) 7 # S1 = the first half_L_S bytes of secret 8 # S2 = the last half_L_S bytes of secret 9 # 10 # P_hash(secret, seed) = HMAC_hash(secret, A(1) + seed) + 11 # HMAC_hash(secret, A(2) + seed) + 12 # HMAC_hash(secret, A(3) + seed) + ... 13 # 14 # P_MD5(S1, label + seed) = HMAC_MD5(S1, A(1) + label + seed) + 15 # HMAC_MD5(S1, A(2) + label + seed) + ... 16 # 17 # P_SHA-1(S2, label + seed) = HMAC_SHA1(S2, A(1) + label + seed) + 18 # HMAC_SHA1(S2, A(2) + label + seed) + ... 19 # 20 # A() is defined as: 21 # A(0) = seed 22 # A(i) = HMAC_hash(secret, A(i-1)) 23 # 24 # +------+ +---------+ 25 # |secret| |A(0)=seed| 26 # +--+---+ +----+----+ 27 # | | 28 # | V 29 # | +---------+ 30 # +---->|HMAC_hash|--+---------------> A(1) 31 # | +---------+ | 32 # | V 33 # | +---------+ 34 # +------------>|HMAC_hash|--+-------> A(2) 35 # | +---------+ | 36 # | V 37 # | +---------+ 38 # +-------------------->|HMAC_hash|--> A(3) 39 # | +---------+ 40 # | 41 # +----------------------------------> ... 42 43 use Digest::HMAC_MD5 qw(hmac_md5 hmac_md5_hex); 44 use Digest::HMAC_SHA1 qw(hmac_sha1 hmac_sha1_hex); 45 46 if( $#ARGV != 3) 47 { 48 print "Usage: perl $0 secret label seed outlen\n" . 49 "Note: secret AND seed should be hexadecimal characters\n" . 50 " label should be literal string\n" . 51 " outlen length of PRF output\n" . 52 "Example: perl $0 01234567 \"ssl and tls\" 89ABCDEF 32\n"; 53 exit 0; 54 } 55 56 $debug = 0; 57 $max_loop = 100; 58 59 $secret = pack 'H*', $ARGV[0]; 60 $label = $ARGV[1]; 61 $seed = pack 'H*', $ARGV[2]; 62 $outlen = $ARGV[3]; 63 $half_L_S = (length($secret) + 1)/2; 64 $S1 = substr($secret, 0, $half_L_S); 65 $S2 = substr($secret, -$half_L_S); # 第二個參數是負數,表示最右邊 $half_L_S 個字符串 66 if ($debug) 67 { 68 print "S1 = ", unpack('H*', $S1), "\n"; 69 print "S2 = ", unpack('H*', $S2), "\n"; 70 } 71 72 # PRF(secret, label, seed) = P_MD5(S1, label + seed) XOR 73 # P_SHA-1(S2, label + seed); 74 $hmac_md5_value = P_hash(\&hmac_md5, $S1, $label.$seed); 75 $hmac_sha1_value = P_hash(\&hmac_sha1, $S2, $label.$seed); 76 if ($debug) 77 { 78 print "P_MD5 = ", unpack('H*', substr($hmac_md5_value, 0, $outlen)), "\n"; 79 print "P_SHA-1 = ", unpack('H*', substr($hmac_sha1_value, 0, $outlen)), "\n"; 80 } 81 print unpack('H*', substr($hmac_md5_value, 0, $outlen) ^ 82 substr($hmac_sha1_value, 0, $outlen) 83 ); 84 85 # P_hash(secret, seed) = HMAC_hash(secret, A(1) + seed) + 86 # HMAC_hash(secret, A(2) + seed) + 87 # HMAC_hash(secret, A(3) + seed) + ... 88 # A(0) = seed 89 # A(i) = HMAC_hash(secret, A(i-1)) 90 sub P_hash{ # 入參 -- hmac_func, secret, seed 91 my @A; 92 my $hash; 93 my $hmac_func = shift; 94 my $secret = shift; # 避免覆蓋全局變量 95 my $seed = shift; 96 $A[0] = $seed; 97 for ( $i = 1; $i <= $max_loop; $i++ ) 98 { 99 $A[$i] = $hmac_func->($A[$i-1], $secret); 100 $hash .= $hmac_func->($A[$i].$seed, $secret); 101 } 102 $hash; 103 }
剩下咱們來肯定最終六份密鑰材料的長度
client/server_write_MAC_secret 20 -- 查 RFC 5246,HMAC-SHA1 密鑰長 20 字節
client/server_write_key 32 -- AES 密鑰長 32 字節(256位)
client/server_write_IV 16 -- AES 分組大小 16 字節
能夠用前面獲得的 server.txt/client.txt 中的信息驗證 MasterSecret/KeyBlock 的生成過程
(5)到此爲止,全部的密鑰也肯定了,是否是雙方後面發送的報文就所有變成加密形式?
TLS 協議並無這樣作,它又引進了一種消息類型:ChangeCipherSpec。該消息用於告訴對方:我已經作好準備,開始使用前面協商好的算法和密鑰材料。
也就是說,從下個報文開始,發送的內容將使用這些密碼算法加以保護。
這就比如在實際用餐以前,甲對乙先說聲:請,而後雙方再開始動筷子:)
然而,客戶端在發送完 ChangeCipherSpec 消息後,這種正式的意味並未就此結束,客戶端還會再發送 Finished 類型的消息給服務器。
Finished 消息又是什麼?爲何搞得這麼複雜。
回想下,到如今爲止,雙方發送的報文都是明文傳輸,通訊內容的機密性、完整性保證都沒有作。
服務器惟一作了的就是,向客戶端出示了一張證書,並且客戶端承認這張證書(咱們的例子中,客戶端忽略了證書檢查,實際環境中不能這樣)
至於服務器是否是真正擁有這張證書(的私鑰),到目前爲止是不知道的,由於證書信息通常是公開的,任何人均可以拿別人的證書去冒充一下
也就是說,協商出來的各項參數,好比生成的隨機數、雙方肯定的密碼算法套件等,沒法保證沒有被第三方篡改,甚至連基本的身份認證都沒有作到。
怎麼辦?先考慮首要的身份認證問題,讓咱們開始推理吧
服務器要證實本身確實是證書持有人,只要證實:它知道 PreMasterSecret 的值(用私鑰解密獲得)。
要證實它知道 PreMasterSecret 的值,只要證實:它掌握最終的密鑰材料(經過密鑰擴展過程獲得)。
要證實它掌握密鑰材料,只要發送一條符合約定格式、並且使用這些密鑰材料加密的消息給客戶端。
客戶端收到後,用它算出的密鑰解密,若是解密後的內容符合約定格式,就能夠斷定【此消息確實是服務器發出的,即服務器的真實身份獲得確認】
並且被加密消息,因爲其內容格式特殊,不只能夠認證服務器,還能證實以前的全部通信信息,都沒有被篡改過(見後文)。
對客戶端的認證,有兩種方法:
在 SSL/TLS 協議中強制認證(這種狀況不多使用),或在上層協議中認證客戶端(好比說,登陸網上銀行都須要輸入用戶名、密碼,這是應用層的事)
通訊雙方在身份認證的過程當中,協商出一系列相關密鑰,來實現後續通訊中,數據的機密性和完整性保護,這是安全協議中使用的另外一個典型範例。
繼續看報文,客戶端發送的 Finished 消息報文格式以下
TLSv1 Record Layer: Handshake Protocol: Finished
Content Type: Handshake (22)
Version: TLS 1.0 (0x0301)
Length: 48
Handshake Protocol: Finished
Handshake Type: Finished (20)
Length: 12
Verify Data
0000 16 03 01 00 30 99 86 fe 27 2e f8 ed 0c 50 48 a1 -- Handshake Protocol: Finished 消息(加密形式)
0010 85 22 7c ec e7 44 e2 1e de d7 ab 15 3d 62 31 e7
0020 d7 61 f0 8e 6d 03 ef bc 2c 29 cd 98 f8 05 29 89
0030 32 9f a1 13 a3
SSL/TLS 的總體報文結構(以下),底層是 Record Layer 協議,再往上是 Handshake 協議層,Finished 消息就位於其中,
HTTP/SMTP 等應用
|
Hello/HelloDone/Certificate |
ClientKeyExchange/Finished |
| |
+------------+ +-----+ +---------+ +-----------+
|ChangeCipher| |Alert| |Handshake| |Application|
+------------+ +-----+ +---------+ +-----------+
+----------------------------------------------+
| Record Layer |
+----------------------------------------------+
+----------------------------------------------+
| TCP |
+----------------------------------------------+
參考 RFC,Finished 消息用相似 C 語言的數據結構表示以下
struct {
HandshakeType msg_type; /* handshake type */
uint24 length; /* bytes in message */
struct {
opaque verify_data[12];
} Finished;
} Handshake;
verify_data = PRF(MasterSecret, "client finished", MD5(handshake_messages) + SHA-1(handshake_messages))[0..11]
而 handshake_messages 是雙方到目前爲止全部發送的 Handshake 消息(不包含當前這條 Finished 消息)。更精確地說,handshake_messages 是按雙方發送的 Handshake 消息的前後順序(ClientHello|ServerHello|Certificate|...)鏈接起來而獲得的。
最後,客戶端再用 client_write_key/IV 加密上述 Handshake 結構,將加密結果發送給服務器;服務器收到後,按相同邏輯再計算一遍。
咱們來分析,若是 Handshake 消息在中途被攻擊者篡改過(或者傳輸過程當中意外發生變化),服務器爲什麼會識別出來。
舉個簡單的例子,假設 ClientKeyExchange 部分有變化(其它部分未變),則服務器解密獲得的 MasterSecret 值也發生了變化(假設解密成功)。
結果服務器計算的 verify_data 值與報文中客戶端發送的 verify_data 值確定不相同(事實上服務器解密 verify_data 時就會發現不對)。
這時服務器有理由相信報文是有問題的,於是停止協議。
須要說明,Finished 消息的發送是雙向的,客戶端和服務器都要向對方發送,以證實本身掌握整個過程的相關信息(MasterSecret、握手信息等)。
上面提到的 Handshake/Finished 消息還停留在明文層次,客戶端用導出的對稱密鑰 client_write_key 將其加密,再發送到服務器。
這是否就萬事大吉?
答案是否認的:僅有機密性保證,仍是不夠;在明文多是任意內容的狀況下,密文也被更改的話,接收者如何察覺解密後的內容已經發生了變化?
這時就要請消息認證碼(MAC)登場了。本質上,它是一個有密鑰參與運算的 HASH 函數,輸出結果稱爲 MAC。
具體使用時,就把 MAC 附加在明文消息以後,再一塊加密並傳輸。經常使用 HMAC_hash 表示 MAC 運算函數。
Enckey(明文消息|MAC)
Sender ------------------------------------------> Receiver
MAC = HMAC_hash(消息認證密鑰,明文消息)
如今再看,若是密文在中途發生了變化,而後被接收,會發生什麼狀況?
假設解密過程沒發現異常,但這時還原獲得的「明文消息」和「MAC」都會發生變化,接收者接着會校驗
「MAC」== HMAC_hash(消息認證密鑰,「明文消息」)?
正如咱們期待的,兩邊相等的可能性微乎其微(這是由 MAC 函數的性質決定,要詳細瞭解能夠參考密碼學教材)
接收者發現兩邊的結果不一樣後,天然認爲原始消息發生了改變,從而將其丟棄。
事實上,數據的機密性和完整性保護就像是一對好幫手,它們常常在一塊兒出現,如影隨形。
對於 TLS_RSA_WITH_AES_256_CBC_SHA 套件,MAC 的計算公式爲 HMAC_SHA1(MAC_write_secret, seq_num + MAC覆蓋的範圍),其中
seq_num:8 字節序號,初始值爲 0x0000000000000000 開始,每一次加密操做,該序號依次遞增一。引入該序號,是爲了防止消息的重放攻擊。
MAC 覆蓋的範圍:見後面詳細說明
因爲算法套件使用分組加密,還面臨一個 Padding 的問題。
前面整個 Handshake/Finished 消息部分爲 16 字節,加上長 20 字節的 MAC,共 36 字節。AES 分組長度爲 16,還要填充 12 字節,才能湊夠 48(3*16)。這 12 字節又分爲兩部分,填充內容(長11字節)和填充長度(長1字節),並且協議規定:填充內容的每一個字節值必須等於填充長度字節的值。
因此最終獲得的填充結果爲 0x0B 0x0B ... 0x0B,連續 12 個相同的 0x0B 字節。
結合 RFC,加密後消息格式以下(黃色背景爲密文部分)
+------------+-------+------+
|Content Type|Version|Length| -- Record Layer 頭
+------------+-------+------+
| Handshake Protocol 消息 | -- 本例中爲 Handshake.msg_type|Handshake.length|Finished.verify_data[12]
+---------------------------+
| MAC | -- HMAC_SHA1(MAC_write_key, seq_num|Content Type|Version|Length|Handshake Protocol)
+---------------------------+ MAC 覆蓋範圍包括 Record Layer 頭,運算順序是先 MAC 後加密,計算 MAC 時 Length 取值爲 0x0010
| Padding | 可是不包括 MAC 和填充字段部分。在加密完成後,Length 的值將改成密文長度(0x0030)
| +------------------+
| | Padding_Len | -- Padding = 0x0B0B0B0B0B0B0B0B0B0B0B Padding_Len = 0x0B
+--------+------------------+
如今開始實際驗證,先看明文(即 Handshake Protocol 消息),其內容爲 14 | 00 00 0c | verify_data[12],而 verify_data 又依賴前面的握手消息。用 Wireshark 導出當前爲止全部的 Handshake 消息(不包括底層的 Record Layer 消息頭,每一個消息保存爲一個文件),再運行下面命令,將導出內容合成一個文件。
D:\>perl -pe "BEGIN{binmode STDOUT;}" client_hello server_hello certificate server_hello_done client_key_exchange > client_handshake
說明:客戶端發出的 ChangeCipherSpec 消息不是 Handshake 類型,因此沒被包括進去
計算握手消息的 MD5 和 SHA-1 值
D:\>openssl dgst client_handshake
MD5(client_handshake)= 8f915bad748e346aca6832c3cd811b57
D:\>openssl dgst -sha1 client_handshake
SHA1(client_handshake)= f192350bea91e1074809304b8e9d2238147e8f5c
計算 verify_data
D:\>perl TLS_PRF.pl MasterSecret(16進制) "client finished" MD5(client_handshake)|SHA1(client_handshake) 12
3314ceb077b52b54c8bbdd60
故 Handshake Protocol 消息內容爲 14 00 00 0c 33 14 ce b0 77 b5 2b 54 c8 bb dd 60
HMAC 計算能夠利用下面的 hmac.pl 腳本網絡
1 use Digest::HMAC_MD5 qw(hmac_md5 hmac_md5_hex); 2 use Digest::HMAC_SHA1 qw(hmac_sha1 hmac_sha1_hex); 3 use Digest::SHA1 qw(sha1 sha1_hex sha1_base64); 4 use Digest::MD5 qw(md5 md5_hex md5_base64); 5 6 if ( $#ARGV != 1 ) 7 { 8 print "usage: perl hmac.pl key(hex) data(hex)"; 9 exit 0; 10 } 11 $key=pack 'H*',$ARGV[0]; 12 $data=pack 'H*',$ARGV[1]; 13 14 print "\nHMAC_SHA1 = ",hmac_sha1_hex($data, $key),"\n"; 15 print "\nHMAC_MD5 = ",hmac_md5_hex($data, $key),"\n"; 16 17 print "\nSHA1 = ", sha1_hex($data),"\n"; 18 print "\nMD5 = ", md5_hex($data),"\n";
全部明文材料準備完畢,合成到文件 in.txt,再調用以下命令進行加密
D:\>openssl enc -aes-256-cbc -nopad -in in.txt -K client_write_key(16進制) -iv client_write_IV(16進制) -out out.txt
如你所願,輸出文件的內容,就是客戶端發出的 Record Layer/Handshake/Finished 密文
(6)咱們走到哪裏了?
客戶端連續發送 ClientKeyExchange、ChangeCipherSpec、Finished 三條消息給服務器,進展以下
一、掌握了後續通訊的全部密鑰信息
二、向對方覈對這些密鑰信息(證實第一點)
三、告訴對方,已經爲發送/接收應用層消息(Application Data Protocol)作好了準備
服務器收到 ClientKeyExchange 後,一樣要證實本身作到了上述三點,這隻要發送 ChangeCipherSpec、Finished 兩條消息給客戶端就能夠了
計算驗證過程與前面徹底相同(須要注意:服務器在計算 verify_data 時包括客戶端發送的 Finished 消息,該消息是明文形式)
雙方的 Finished 都發送完畢,而後各自檢查對方的 Finished 消息是否正確,若是都沒問題,則能夠安全地進行通訊
咱們以客戶端發出 "Hello, OpenSSL\r\n" 的消息處理過程爲例,再複習一遍 TLS 的加密流程。報文結構以下
TLSv1 Record Layer: Application Data Protocol: tcp
Content Type: Application Data (23)
Version: TLS 1.0 (0x0301)
Length: 32
Encrypted Application Data: ......
TLSv1 Record Layer: Application Data Protocol: tcp
Content Type: Application Data (23)
Version: TLS 1.0 (0x0301)
Length: 48
Encrypted Application Data: ......
客戶端發了兩條 Record,上面承載的是加密形式的 Application Data(應用數據)。咱們知道,被加密的數據包含明文消息、MAC 和可能的填充內容。
僅僅根據密文長度沒法判斷,明文消息中究竟是什麼內容。那就先解密看看,將第一段密文導出,另存爲文件 cipher,運行下列命令
D:\>openssl enc -d -aes-256-cbc -nopad -in cipher -K client_write_key -iv 上次加密結果最後一個分組 -out plain
SSL/TLS 規定,每一次加/解密 IV 的取值都不一樣:IV 老是上次加密報文的最後一個分組。
本例中,最後一個分組就是客戶端發送的 Finished 消息最後一個分組。
D:\>od -An -tx1 plain
9b 9b 1f 9f 62 9a 3f 13 8e 2b 85 bf 08 dc bf 43
85 8b 3d 60 0b 0b 0b 0b 0b 0b 0b 0b 0b 0b 0b 0b
咱們發現,去掉 MAC(20 字節) 和 填充內容(12 字節) 後,明文長度居然爲零。
出乎意料之餘,咱們繼續驗算 MAC 是否正確。
D:\>perl hmac.pl 14450225389d224d5a78975703174bd455a3c2f3 00000000000000011703010000
其中 0000000000000001 是 MAC 序號(已經遞增爲一),1703010000 爲上層 Record Layer 頭(注意其 Length 字段,值爲零)。結果也正確。
按相同步驟解密第二段密文,你會發現,"Hello, OpenSSL\r\n" 就包含在其中(用 Wireshark 能夠驗證)
(7)關於 DH 密鑰交換協議
從前面的計算過程可知,若是保護 PreMasterSecret 的私鑰泄露給第三方,那麼後續全部的通訊明文都會被還原出來。
怎麼辦?咱們還有辦法:可讓通訊雙方使用 DH 協議臨時協商出 PreMasterSecret。
因爲 PreMasterSecret 不被第三方得知,從而保證了後續通訊的安全。DH 協議的這種特性稱爲 Perfect Forward Secrecy(PFS)。
在前面的命令行參數中去掉 -no_dhe 就會開啓 DH 密鑰協商模式。 抓包能夠看到,服務器多發了一條 ServerKeyExchange 消息,同時客戶端
發送的 ClientKeyExchange 消息格式也有變化。其本質就是雙方分別發送本身的 X/Y,報文具體格式請參考 RFC。
要知道對應的祕密 x/y,能夠在 crypto\dh\dh_key.c 中增長一行打印語句(以下)
[185] prk = priv_key;
[186] BN_print_fp(stdout, prk);
[187] if (!dh->meth->bn_mod_exp(dh, pub_key, dh->g, prk, dh->p, ctx, mont)) goto err;
根據打印值和報文中公開的X/Y,就能夠算出 PreMasterSecret=(gx)y=(gy)x(mod p) ,其中X=gx,Y=gy。數據結構
4、尾聲
從上面的分析可知,SSL/TLS 協議在理論上已經很是安全。
但理論歸理論,實際實現又是另外一回事,開發人員的不當心,每每會致使漏洞,在這一點上,大名鼎鼎的 OpenSSL 也不例外:)
5、參考
一、RFC 5246 The Transport Layer Security (TLS) Protocol Version 1.2
二、<<SSL & TLS Essentials: Securing the Web>>
三、<<SSL 與 TLS>>dom