安全協議系列(四)----SSL與TLS


當今社會,電子商務大行其道,作為網絡安全 infrastructure 之一的 -- SSL/TLS 協議的重要性已不用多說。
OpenSSL 則是基於該協議的目前應用最廣泛的開源實現,其影響之大,以至於四月初爆出的 OpenSSL Heartbleed 安全漏洞(CVE-2014-0160) 到現在還余音未消。

本節就以出問題的 OpenSSL 1.0.1f 作為實例進行分析;整個分析過程仍采用【參考 RFC、結合報文抓包、外加工具驗證】的老方法。
同時我們利用 OpenSSL 自帶的調試功能,來觀察運行的內部細節,起到事半功倍的作用。
通常情況需要同時運行客戶端和服務器,本文使用 OpenSSL 提供的子命令 s_server/s_client 進行 TLS 通信。

一、下載 && 修改 && 編譯 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 */

  繼續編譯,最終成功。

二、運行 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)進行抓包。

三、協議分析
  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 }
View Code

剩下我們來確定最終六份密鑰材料的長度
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 后加密,計算 MACLength 取值為 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";
View Code

所有明文材料准備完畢,合成到文件 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 三條消息給服務器,進展如下
1、掌握了后續通信的所有密鑰信息
2、向對方核對這些密鑰信息(證明第一點)
3、告訴對方,已經為發送/接收應用層消息(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

四、尾聲
從上面的分析可知,SSL/TLS 協議在理論上已經非常安全。
但理論歸理論,實際實現又是另一回事,開發人員的不小心,往往會導致漏洞,在這一點上,大名鼎鼎的 OpenSSL 也不例外:)

五、參考
1、RFC 5246 The Transport Layer Security (TLS) Protocol Version 1.2
2、<<SSL & TLS Essentials: Securing the Web>>
3、<<SSL 與 TLS>>


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM