当今社会,电子商务大行其道,作为网络安全 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>>