介绍
在本博客中我们基于一个简单的HTTPS会话示例,展示如何通过各种加密算法等,在TLSv1.2协议规则下计算并验证各个域,为学习HTTPS协议以及实现HTTPS协议的初学者提供参考示例。
本博客示例TLS协议的基本信息如下:
- TLS版本:
TLSv1.2
; - Cipher:
TLS_RSA_WITH_AES_128_CBC_SHA
, 选择该套件的原因是其不是十分复杂,适合用于展示; - 扩展:
Extened Master Secret
:Master Secret
的计算不同于最初的TLS v1.2
RFC文档使用的方式,具体参见对应的RFC。 - 扩展:
Encrypt-then-MAC
:最初的TLSv1.2
协议使用MAC-then-Encrypt
进行消息加密和验证,但是该扩展先加密再计算摘要验证。
下面的讲解会假定你已经初步了解TLS协议的某些细节,如果遇到不清楚的概念,你随时可以参考博客HTTPS协议的正文和附录部分,包括Extened Master Secret
,Encrypt-then-MAC
扩展和PRF
等内容。
此外,下面的所有内容都可以在github仓库net-protocol-tools:demo/demo-TLS_RSA_WITH_AES_128_CBC_SHA中找到。
Setup
准备工具
此次分析实践用到的主要工具是基于Wireshark, openssl和nginx:
- Wireshark:抓取流量包,导入RSA私钥后可以解析各个加密的域;可以用来验证我们手动分析的结果;
- openssl:用于生成私钥、证书、计算摘要和加解密;
- nginx: 配置HTTPS服务器。
此次分析在Windows
上进行,其中openssl
工具使用cygwin
安装。对于Linux
或Macos
,可以使用对应的包管理器安装openssl
,版本信息:1.1.1c
。
上面的工具安装完成后,我们创建一个目录demo
并进入其中,后面的所有数据都会保存在demo
中。
openssl命令参考博客:openssl及其使用。
配置Nginx服务器和相关证书
参考另一篇博客nginx配置HTTPS实践的案例1部分,主要信息如下:
- 网址:
https://my-ssl.test:443
- 私钥:
my-ssl.test.key
- 证书:
my-ssl.test.crt
将上面的my-ssl.test.key
和my-ssl.test.crt
复制到demo
目录方便后续引用。
打开Wireshark并准备过滤数据
- 1.准备监听数据
我们将使用本地端口12443
连接到上面我们配置的nginx服务器,因此我们使用tcp.port==12443 && tls
来过滤包,如下图所示:
- 2.导入私钥进行解密
在Edit > Preferences > Protocols
下找到TLS
协议选项,按照下面的指引导入私钥:
- 3.点击Wireshark工具栏中左侧的按钮开始进行捕获
使用openssl构造请求
打开一个命令行并切换到demo
目录,执行下面的命令:
echo -ne 'GET / HTTP/1.1\r\nHost: my-ssl.test\r\n\r\n'|openssl s_client -CAfile my-ssl.test.crt -cipher AES128-SHA -tls1_2 -servername my-ssl.test -bind localhost:12443 -connect my-ssl.test:443 -keylogfile key.log
各选项含义如下:
-CAfile
指定证书,保证证书校验成功;-cipher AES128-SHA
指定加密套件TLS_RSA_WITH_AES_128_CBC_SHA
;-tls1_2
指定TLSv1.2
版本-keylogfile
记录master secret
信息,用于后面计算验证
其他选项参见上面提及的博客。
我们可以在Wireshark中看到相应的请求记录:
验证
协议
查看ClientHello中包含两个扩展
证书签名验证
参见博客证书的证书验证一节。
pre_master_secret
我们使用RSA交换,因此在ClientKeyExchange
中能够看到使用公钥加密的pre_master_secret
右键 > Export Packet Bytes...
将文件保存到demo
目录并命名为pre_master_secret_encrypted.bin
使用下面的命令解密pre_master_secret_encrypted.bin
:
openssl rsautl -decrypt -inkey my-ssl.test.key -in pre_master_secret_encrypted.bin -out pre_master_secret
解密后的文件为pre_master_secret
将解密的结果与key.log
中的RSA xxxx yyyy
中的yyyy
部分对比,相同即解密成功:
extended_master_secret
根据Extened Master Secret
扩展的规定,master_secret
生成规则:
master_secret = PRF(pre_master_secret, “extended master secret”, session_hash) [0…47];
首先需要session数据,将ClientHello
,ServerHello
,ServerCertificate
,ServerHelloDone
,ClientKeyExchange
部分的消息体(注意:不包括TSL Record的头部)导出,分别命名为:client_hello_handshake.bin
,server_hello_handshake.bin
,server_certificate_handshake.bin
,server_hello_done_handshake.bin
,client_key_exchange_handshake.bin
。
导出参考:
应用博客HTTPS协议的附录 > PRF部分的shell脚本,计算extended_master_secret
:
./prf.sh pre_master_secret 'extended master secret' <(openssl dgst -sha256 -binary <(cat client_hello_handshake.bin server_hello_handshake.bin server_certificate_handshake.bin server_hello_done_handshake.bin client_key_exchange_handshake.bin)) 48 extended_master_secret
key_block
TLS_RSA_WITH_AES_128_CBC_SHA
的加密算法是AES128
,即加密key长度16字节(128位), MAC算法是SHA1
,因此MAC的key长度是20字节,所以总长度是 20*2 + 16*2 = 72
根据key_block
的计算规则:
key_block = PRF(SecurityParameters.master_secret,
“key expansion”,
SecurityParameters.server_random +
SecurityParameters.client_random);
我们需要ServerHello.random
和ClientHello.random
作为种子,从Wireshark导出命名为server_random.bin
和client_random.bin
。
然后生成key_block
:
./prf.sh extended_master_secret 'key expansion' <(cat server_random.bin client_random.bin) $((20*2+16*2)) key_block
然后将其拆分为4个key:
head -c 20 key_block > client_MAC_key
tail -c +21 key_block|head -c 20 > server_MAC_key
tail -c 32 key_block|head -c 16 > client_ENC_key
tail -c 32 key_block|tail -c 16 > server_ENC_key
计算Finished消息体的验证码
根据RFC文档:
签名数据=
PRF(master_secret, “client finished”, Hash(handshake_messages))
[0…verify_data_length-1];
因此计算如下:
./prf.sh extended_master_secret 'client finished' <(openssl dgst -sha256 -binary <(cat client_hello_handshake.bin server_hello_handshake.bin server_certificate_handshake.bin server_hello_done_handshake.bin client_key_exchange_handshake.bin)) 12 client_finished_verify_data
解密Client Finished
根据Encrypt-then-MAC
扩展,TLS Record的结构为:
{
uint8 contentType; // Handshake 为0x16
uint16 version; // TLSv1.2 = 0x0303
uint16 length; // IV(16)字节 + 加密消息体长度 + MAC长度(20)
uint8[] IV; // CBC模式独有,对于AES-128,为16字节
uint8[] encBody; // 加密消息体
uint8[] MAC; // SHA1的MAC长度为20字节
}
在Wireshark中,Change Cipher Spec
之后我们可以看到客户端发送的Finished
消息体是加密的,将消息体(包括TLS Record头部)导出为client_finished_record_encrypted.bin
因此,解密encBody
如下:
openssl enc -aes-128-cbc -d -in <(tail -c +22 client_finished_record_encrypted.bin|head -c -20) -iv "$(hexdump -ve '1/1 "%02x"' <(tail -c +6 client_finished_record_encrypted.bin|head -c 16))" -K "$(hexdump -ve '1/1 "%02x"' client_ENC_key)" -nopad
注意:tail -c +22
意味着跳过前面的21个字节,即跳过contentType
,version
,length
和IV
部分。
与client_finished_verify_data比较:
计算Finished加密体中的MAC
根据Encrypt-Then-MAC
扩展,MAC计算如下:
MAC(MAC_write_key, seq_num +
TLSCipherText.type +
TLSCipherText.version +
TLSCipherText.length +
IV +
ENC(content + padding + padding_length));
特别注意,其中TLSCipherText.length
指的是IV
和encBody
的长度之和。
计算如下:
openssl dgst -sha1 -mac hmac -macopt "hexkey:$(hexdump -ve '1/1 "%02x"' client_MAC_key)" <(cat <(echo -ne '\x00\x00\x00\x00\x00\x00\x00\x00') <(echo -ne '\x16\x03\x03\x00\x30') <(tail -c +6 client_finished_record_encrypted.bin|head -c -20))
在上面的命令中,第2个echo -ne
部分,\x16
是协议类型,\x03\x03
是协议版本,\x00\x30(=48)
是IV(16)
和encBody(68-16-20=32)
的长度和。
从Wireshark导出的client_finished_record_encrypted.bin
的后20个字节时MAC的数据,对比:
Application Data数据解密
解密过程同Client Finished
,首先从Wireshark导出ApplicationData(包括TLS Record头部),保存为client_application_data_record_encrypted.bin
,解密:
openssl enc -aes-128-cbc -d -in <(tail -c +22 client_application_data_record_encrypted.bin|head -c -20) -iv "$(hexdump -ve '1/1 "%02x"' <(tail -c +6 client_application_data_record_encrypted.bin|head -c 16))" -K "$(hexdump -ve '1/1 "%02x"' client_ENC_key)" -nopad|hexdump -C
注意padding是最后一个字节的重复,而不是\x00
.