问题背景

最近 4G 充电桩的项目遇到一个问题,实测设备每天消耗的 4G 流量,远远超出理论计算的值

能够理解 TCP 头部、IP 头部、以太网头部等网络协议头导致报文长度增长,但从实际抓包情况来看,去掉这些网络协议头后的报文长度,也远超出原始明文长度:

  1. 设备端发送明文的心跳报文长度应该是 34 字节,为什么加密后实际发了 64+96 字节?云端的心跳报文回复长度应该是 28 字节,为什么加密后实际收到了 80 字节?
  2. 为什么设备与云端交互时,总会发送两个报文,第一个报文的长度始终为 64 字节?

为了搞清楚原因,决定从 TLS 协议入手进行分析

TLS 协议

SSL/TLS 是一种密码通信框架,他是世界上使用最广泛的密码通信方法。SSL/TLS 综合运用了密码学中的对称密码,消息认证码,公钥密码,数字签名,伪随机数生成器等,可以说是密码学中的集大成者

SSL(Secure Socket Layer)安全套接层,是 1994 年由 Netscape 公司设计的一套协议,并与 1995 年发布了 3.0 版本

TLS(Transport Layer Security)传输层安全是 IETF 在 SSL3.0 基础上设计的协议,实际上相当于 SSL 的后续版本

TLS 协议主要分为两层,底层是 TLS 记录协议,负责使用对称密码对消息进行加密

上层是 TLS 握手协议,负责在客户端和服务端商定密码算法和共享密钥

TLS 握手协议

从抓包来看,我们的设备和基础云进行 TLS 握手时,经历了以下阶段:

  1. Client Hello,客户端向服务器端发送一个 client hello 的消息,包含下面内容:

    可用版本号、当前时间、客户端随机数、会话 ID、可用的密码套件清单、可用的压缩方式清单

  2. Server Hello,服务器端收到 client hello 消息后,会向客户端返回一个 server hello 消息,包含如下内容:

    使用的版本号、当前时间、服务器随机数、会话 ID、使用的密码套件、使用的压缩方式
    可以看到,我们的设备和基础云协商的加密方式为 TLS_RSA_WITH_AES_128_CBC_SHA256,压缩方式无

  3. Certificate,服务端发送自己的证书清单,因为证书可能是层级结构的,所以除了服务器自己的证书之外,还需要发送为服务器签名的证书

  4. Server Hello Done,服务器端发送 server hello done 的消息告诉客户端自己的消息结束了
    ![image-20220820163300335](TLS 是如何导致报文长度膨胀的.assets/image-20220820163300335.png)

  5. Client Key Exchange,公钥或者 RSA 模式情况下,客户端将根据客户端生成的随机数和服务器端生成的随机数,生成预备主密码,通过该公钥进行加密,返送给服务器端

  6. Client Change Cipher Spec、Finish,客户端准备切换密码,表示后面的消息将会以前面协商过的密钥进行加密

  7. Server Change Cipher Spec、Finish,服务端准备切换密码,表示后面的消息将会以前面协商过的密钥进行加密

TLS 记录协议

TLS 记录协议主要负责消息的压缩,加密及数据的认证

首先,消息被分割成多个较短的片段,然后分别对每个片段进行压缩,压缩算法需要与通信对象协商决定

接下来,经过压缩的片段会被加上消息认证码,这是为了保证完整性,并进行数据的认证。通过附加消息认证码的 MAC 值,可以识别出篡改。与此同时,为了防止重放攻击,在计算消息认证码时,还加上了片段的编码。单项散列的函数的算法,以及消息认证码所使用的共享密钥都需要与通信对象协商决定

再接下来,经过压缩的片段再加上消息认证码会一起通过对称加密进行加密。加密使用 CBC 模式,CBC 模式的初始向量 IV 通过主密码生成,而对称密码的算法以及共享密码需要与通信对象协商决定

分析

从握手阶段的 ServerHello 报文,我们知道了此次通信无压缩、使用的加密方式为 TLS_RSA_WITH_AES_128_CBC_SHA256

这个加密方式实际上分为了好几个部分:

  • RSA 表示使用 RSA 非对称加密来传输 AES 密钥
  • AES_128_CBC 表示应用数据使用 AES_128_CBC 模式来加密
  • SHA256 表示生成 MAC 的摘要算法使用 SHA256

RSA

个没啥好分析的,握手阶段就已经通过 RSA 进行了 AES 密钥传输

AES_128_CBC

AES_128_CBC 是一种分组对称加密算法,即用同一组 key 进行明文和密文的转换,key 的长度为 128bit:

  • 以 128bit 为一组,128bit=16Byte,意思就是明文的 16 字节为一组,对应加密后的 16 字节的密文

  • 若最后剩余的明文不够 16 字节,需要进行填充,通常采用 PKCS7 进行填充。比如最后缺 3 个字节,则填充 3 个字节的 0x03;若最后缺 10 个字节,则填充 10 个字节的 0x0a

  • 若明文正好是 16 个字节的整数倍,最后要再加入一个 16 字节 0x10 的组再进行加密

  • CBC 模式为:用初始向量和密钥加密第一组数据,然后把第一组数据加密后的密文重新赋值给 IV,然后进行第二组加密,循环进行直到结束

那么通过 AES_128_CBC 加密原始明文,得到的最终长度一定是 16 字节的整数倍,会导致 1-16 字节的膨胀(密文长度比明文长度大 1-16 字节)

SHA256

对于任意长度的消息,SHA256 都会产生一个 256 位的哈希值,也就是 32 个字节。

在 TLS 记录协议中,对压缩后的消息片段进行 MAC 值的计算用的散列函数就是 SHA256,详细的 MAC 值计算方法不展开了,反正最终 MAC 长度是 32 字节。

加密数据长度

回到 TLS 记录协议上,TLS 1.2 记录协议中,报文经过 ASE_CBC 块加密后的完整组成结构如下:

struct {
    opaque IV[SecurityParameters.record_iv_length];
    block-ciphered struct {
        opaque content[TLSCompressed.length];
        opaque MAC[SecurityParameters.mac_length];
        uint8 padding[GenericBlockCipher.padding_length];
        uint8 padding_length;
    };
} GenericBlockCipher

总长度 = 向量 IV 长度 + 明文压缩后的长度 + MAC 长度 + 填充长度

  • IV 是指 AES_CBC 加密是使用的初始向量,其长度为块加密的 block_size,在 AES 加密中为 16 字节
  • 无压缩,压缩后长度为原始明文长度
  • MAC 长度为 32 字节
  • 填充长度为 1-16 字节

再来看看设备发的心跳报文,原始明文为 34 字节,拆成了 64 字节和 96 字节两个包发送,猜测是这样的:

  • 第一个包:length = 0(plainText) + 16(IV) + 32(MAC) + 16(padding) = 64
  • 第二个包:length = 34(plainText) + 16(IV) + 32(MAC) + 14(padding) = 96

至于为什么设备端总会发送数据之前发送一个内容为空的包,就涉及到具体设备端实现了,参考资料中有提到 发送 fragment 长度为 0 的应用数据在进行流量分析时是有用的

云端回复的报文,原始明文为 28 字节,实际加密后为 80 字节:

length = 28(plainText) + 16(IV) + 32(MAC) + 4(padding) = 80

至此,TLS 加密导致报文长度膨胀的原因基本弄清楚了

总结

设备和云端使用 TLS 加密协议进行通信,协商的通信方法是 TLS_RSA_WITH_AES_128_CBC_SHA256,因为块加密填充、计算消息认证码等原因导致加密后的消息长度原大于原始明文长度

设备在发送消息时,总会把先发送一个原始明文长度为 0 的消息,与设备端实现有关

具体到 4G 充电桩这个项目上,产品让步不再进行流量限制,后续如果有对使用流量极为敏感的设备,可从加密角度着手,优化通信方式和加密协议,减少 TLS 加密带来的报文长度膨胀

参考

一篇文章让你彻底弄懂 SSL/TLS 协议 - 知乎 (zhihu.com)

对加密算法 AES-128-CBC 的一些理解 - 简书 (jianshu.com)

https://halfrost.com/https_record_layer/