理解SSL/TLS系列 (四) 记录协议
TLS 在实现上分为记录协议和 握手协议两层,位于下层的记录协议负责进行加密传输,而位于上层的握手协议则负责其他操作,握手协议又包含四个子协议: 握手协议 (handshake protocol)、更改加密规范协议 (change cipher spec protocol)、应用数据协议 (application data protocol) 和警告协议 (alert protocol)。
一、连接状态
TLS连接状态是TLS记录协议的运行环境。它规定了压缩算法、加密算法和MAC算法。另外,这些算法的参数也已经得知了(由握手协议协商得出):读写两个方向上连接的MAC密钥和块加密算法的密钥。理论上来讲,显然有四个连接状态:当前读写状态和下一个的读写状态。所有的记录都在当前的读写状态下处理。下一个状态的安全参数由TLS握手协议决,而且改变密码标准协议可以选择以下两个行为之一:
- 选择适当的当前状态转让并由下一个状态替换
- 将下一个状态重新初始化为空状态
将未经安全参数初始化的状态置为当前状态是非法的。
TLS连接读写状态的安全元素由下列值决定:
- 连接结束:由当前连接的客户端和服务端决定
- PRF算法:从主秘密中生成密钥
- 块加密算法:包含密钥长度,无论是块、流或者是AEAD(Authenticated Encryption with Associated Data)加密,它们的块大小,还有初始向量的长度。
- MAC算法:消息认证的算法。包含返回值长度。
- 压缩算法:包含压缩时算法所需的所有信息
- 主秘密:连接双方共享的48字节的秘密
- 客户端随机数:客户端提供的32字节的值
- 服务端随机数:服务端提供的32字节的值
上述信息使用规范语言来描述为:
enum { server, client } ConnectionEnd;
enum { tls_prf_sha256 } PRFAlgorithm;
enum { null, rc4, 3des, aes }BulkCipherAlgorithm;
enum { stream, block, aead } CipherType;
enum { null, hmac_md5, hmac_sha1, hmac_sha256,hmac_sha384, hmac_sha512} MACAlgorithm;
enum { null(0), (255) } CompressionMethod;
/* The algorithms specified in CompressionMethod, PRFAlgorithm,
BulkCipherAlgorithm, and MACAlgorithm may be added to. */
struct {
ConnectionEnd entity;
PRFAlgorithm prf_algorithm;
BulkCipherAlgorithm bulk_cipher_algorithm;
CipherType cipher_type;
uint8 enc_key_length;
uint8 block_length;
uint8 fixed_iv_length;
uint8 record_iv_length;
MACAlgorithm mac_algorithm;
uint8 mac_length;
uint8 mac_key_length;
CompressionMethod compression_algorithm;
opaque master_secret[48];
opaque client_random[32];
opaque server_random[32];
} SecurityParameters;
记录层将使用上面的安全元素生成下列六项(有些内容并不是所有密码算法都要求,因此为空):
- 客户端写MAC密钥(client write MAC key)
- 服务端写MAC密钥(server write MAC key)
- 客户端写加密密钥(client write encryption key)
- 服务端写加密密钥(server write encryption key)
- 客户端写初始向量(client write IV)
- 服务端写初始向量(server write IV)
只要安全参数设置完成,密钥已经生成,连接状态就可以被实体化转化为当前状态。每个连接状态包括以下元素:
- 压缩状态:当前状态的压缩算法
- 加密状态:当前状态的加密算法。由当前连接已定的密钥组成。对于流加密算法,也包括所有继续加解密流信息所需要的状态信息。
- MAC密钥:这次连接的MAC密钥。
- 序列号(seq):每个连接状态包含一个序列号,读写状态分别维护。序列号是uint64型的,一般不超过2^64-1。
二、记录协议
记录协议负责在传输连接上交换的所有底层消息,并且可以配置加密。每一条 TLS 记录以一个短标头开始。标头包含记录内容的类型 (或子协议)、协议版本和长度。原始消息经过分段 (或者合并)、压缩、添加认证码、加密转为 TLS 记录的数据部分。
分片 (Fragmentation)
记录协议将信息块分割成携带 2^14 字节 (16KB) 或更小块的数据的 TLSPlaintext 记录。
记录协议传输由其他协议层提交给它的不透明数据缓冲区。如果缓冲区超过记录的长度限制(2^14),记录协议会将其切分成更小的片段。反过来也是可能的,属于同一个子协议的小缓冲区也可以组合成一个单独的记录。可以更为正式地将TLS记录的字段定义为如下所示
struct {
uint8 major, minor;
} ProtocolVersion;
enum {
change_cipher_spec(20),
alert(21),
handshake(22),
application_data(23), (255)
} ContentType;
struct {
ContentType type; // 用于处理封闭片段的较高级协议
ProtocolVersion version; // 使用的安全协议版本
uint16 length; // TLSPlaintext.fragment 的长度(以字节为单位),不超过 2^14
opaque fragment[TLSPlaintext.length]; // 透明的应用数据,被视为独立的块,由类型字段指定的较高级协议处理
} TLSPlaintext;
复制代码
除了这些可见的字段,还会给每一个TLS记录指定唯一的64位序列号,但不会在线路上传输。 任一端都有自身的序列号并跟踪来自另一端记录的数量。这些值是对抗重放攻击的一部分。
记录压缩和解压缩 (Record compression and decompression)
压缩算法将 TLSPlaintext 结构转换为 TLSCompressed 结构。如果定义 CompressionMethod 为 null 表示不压缩
struct {
ContentType type; // same as TLSPlaintext.type
ProtocolVersion version; // same as TLSPlaintext.version
uint16 length; // TLSCompressed.fragment 的长度,不超过 2^14 + 1024
opaque fragment[TLSCompressed.length];
} TLSCompressed;
复制代码
理论上,在加密之前透明地对数据进行压缩非常好,可是实践中几乎没有人这样做。这 主要是因为每个应用在HTTP层就已经对它们的出口流量进行过压缩。这个特性在2012年遭受过一次严重打击,当时CRIME攻击使压缩成为不安全的特性,所以现在也不会再被使用。
加密和完整性保护
记录层协议有 3 种加密方式:
struct {
ContentType type;
ProtocolVersion version;
uint16 length;
select (SecurityParameters.cipher_type) {
case stream: GenericStreamCipher;
case block: GenericBlockCipher;
case aead: GenericAEADCipher;
} fragment;
} TLSCiphertext;
-
type:
这里的 type 值与 TLSCompressed.type 相同。 -
version:
这里的 version 值与 TLSCompressed.version 相同。 -
length:
length 代表接下来的 TLSCiphertext.fragment 的长度(以字节为单位)。这个长度不能超过2^14 + 2048。 -
fragment:
TLSCompressed.fragment 的加密形态。
注意 这里提到的都是先 MAC 再加密,是基于 RFC 2246 的方案 (TLS 1.0) 写的。但新的方案选择先加密再 MAC,这种替代方案中,首先对明文和填充进行加密,再将结果交给 MAC 算法。这可以保证主动网络攻击者不能操纵任何加密数据。记录的 MAC 包含了一个序列号,这个序列号用于感知丢失、增加和重复的消息
空或标准流加密 (Null or standard stream cipher)
流加密(包括 BulkCipherAlgorithm.null)将 TLSCompressed.fragment 结构转换为流的 TLSCiphertext.fragment 结构。
stream-ciphered struct {
opaque content[TLSCompressed.length];
opaque MAC[CipherSpec.hash_size];
} GenericStreamCipher;
复制代码
MAC 产生方法如下:
HMAC_hash(MAC_write_secret, seq_num + TLSCompressed.type + TLSCompressed.version + TLSCompressed.length + TLSCompressed.fragment));
复制代码
上面的“+”表示连接。seq_num(记录的序列号)、HMAC_hash(SecurityParameters.mac_algorithm 指定的哈希算法)
CBC 块加密 (分组加密)
对于块加密算法(如 3DES 或 AES),加密和 MAC 函数将 TLSCompressed.fragment 转换为 TLSCiphertext.fragment 结构块。
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;
MAC 的生成方法流加密中的 MAC 生成方式相同。
MAC(MAC_write_key, seq_num + TLSCompressed.type + TLSCompressed.version + TLSCompressed.length + TLSCompressed.fragment);
-
IV:
初始化向量(IV)应该随机产生,并且必须是不能预测的。需要注意的是在 TLS 1.1 以前的版本是没有 IV 域的。以前的记录中最后一个密文块(CBC 分组最后一组的剩余)被用作 IV。对于块加密,IV 的长度是 SecurityParameters.record_iv_length 的值,这个值等于 SecurityParameters.block_size。 -
padding:
padding 用于强制使明文的长度是块加密块长度的整数倍,它可能是任意长度,最长是 255 字节,只要它能让 TLSCiphertext.length 是块长度的整数倍。可能需要长于所需的长度来阻止对基于对交换消息的长度的分析的协议的攻击。在 padding 数据向量中的每个 uint8 必须用 padding 的长度值填充。接收者必须检查这个 padding 且必须使用 bad_record_mac alert 警告消息来暗示 padding 错误。 -
padding_length:
padding 的长度必须使 GenericBlockCipher 的总长度是密码块长度的整数倍。合法的取值范围是从 0 到 255,包含 0 和 255。这个长度指定了 padding 字段的长度但不包含 padding_length 字段的长度。
加密块的数据长度(TLSCiphertext.length)是 TLSCompressed.length,CipherSpec.hash_size 和 padding_length 的总和加一
示例: 如果块长度为 8 字节,压缩内容长度(TLSCompressed.length)为 61 字节,MAC 长度为 20 字节,则填充前的长度为 82 字节(padding_length 占 1 字节)。 因此,为了使总长度为块长度 (8 字节) 的偶数倍,模 8 的填充长度必须等于 6,所以填充长度可以为 6,14,22 等。如果填充长度是需要的最小值,比如 6,填充将为 6 字节,每个块都包含值 6。因此,块加密之前的 GenericBlockCipher 的最后 8 个八位字节将为 xx 06 06 06 06 06 06 06,其中 xx 是 MAC 的最后一个八位字节。
AEAD 模式
AEAD 加密相比前两种加密方式,使用更加简单,安全性更高。因为它不需要使用者考虑 HMAC 算法,并且也不需要初始化向量与填充 padding。
AEAD 密码套件主要有 3 种:
AEAD 模式 | 加密 | 密码套件 |
---|---|---|
GCM | AES-128-GCM | TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 |
CCM | AES-128-CCM | TLS_RSA_WITH_AES_128_CCM |
ChaCha20-Poly1305 | ChaCha20-Poly1305 | ECDHE-ECDSA-CHACHA20-POLY1305 |
在 TLS 协议中,CCM 用的比较少,GCM 用的比较多,尤其用在具有 AES 加速的 CPU 上。ChaCha20-Poly1305 是 Google 发明的由 ChaCha20 流加密,Poly1305 消息认证码组合的一种加密算法,用在移动端上比较多。
对于 AEAD 加密(如:[CCM] 或 [GCM]), AEAD 函数将 TLSCompressed.fragment 结构转换为 AEAD TLSCiphertext.fragment结构。
struct {
opaque nonce_explicit[SecurityParameters.record_iv_length];
aead-ciphered struct {
opaque content[TLSCompressed.length];
};
} GenericAEADCipher;
AEAD加密的输入有:单个密钥,一个 nonce(只使用一次的数值),一块明文(就是 TLSCompressed.fragment),和被包含在验证检查中的“额外数据”。密钥是 client_write_key 或者 server_write_key。不使用 MAC 密钥。
每个 AEAD 密码族必须指定提供给 AEAD 操作的 nonce 是如何构建的,GenericAEADCipher.nonce_explicit 部分的长度是什么。明文是 TLSCompressed.fragment。
额外的验证数据(我们表示为 additional_data)定义如下:
additional_data = seq_num + TLSCompressed.type +
TLSCompressed.version + TLSCompressed.length;
这里“+”表示连接。
AEAD 的输出由 AEAD 加密操作所产生的密文输出构成。长度通常大于 TLSCompressed.length,在量上会随着 AEAD 加密的不同而不同。因为加密可能包含填充,开销的大小可能会因 TLSCompressed.length 值而不同。每种 AEAD 加密不能产生大于 1024 字节的长度。
AEADEncrypted = AEAD-Encrypt(write_key, nonce, plaintext,
additional_data)
为了解密和验证,加密算法将密钥、nonce、“额外数据”和 AEADEncrypted 的值作为输入。输出要么是明文要么是解密失败导致的错误。这里没有分离完整性检查。即:
TLSCompressed.fragment = AEAD-Decrypt(write_key, nonce,
AEADEncrypted,
additional_data)
如果解密失败,会产生一个 bad_record_mac alert 消息。
参考
《HTTPS权威指南》
《图解密码技术》
rfc5246: https://www.ietf.org/rfc/rfc5246.txt
更多推荐
所有评论(0)