完整流程
ClientHello --------> |
据结构定义
Record Layer
struct { |
Change Cipher Specs Message
struct { |
Handshake Protocol
enum { |
Hello Messages
struct { } HelloRequest; |
Server Authentication and Key Exchange Messages
opaque ASN.1Cert<2^24-1>; |
Client Authentication and Key Exchange Messages
struct { |
Handshake Finalization Message
struct { |
The Cipher Suite
以下值定义了ClientHello和ServerHello消息中使用的密码套件代码。
密码套件定义了TLS 1.2版本中支持的密码规范。
TLS_NULL_WITH_ULL_NULL已指定,是该通道上第一次握手期间TLS连接的初始状态,但不得协商,因为它提供的保护不比不安全的连接多。
CipherSuite TLS_NULL_WITH_NULL_NULL = { 0x00,0x00 }; |
以下CipherSuite定义要求服务器提供可用于密钥交换的RSA证书。服务器可以在证书请求消息中请求任何具有签名能力的证书。
CipherSuite TLS_RSA_WITH_NULL_MD5 = { 0x00,0x01 }; |
以下密码套件定义用于服务器身份验证(以及可选的客户端身份验证)Diffie-Hellman。DH表示密码套件,其中服务器的证书包含由证书颁发机构(CA)签名的Diffie-Hellman参数。DHE表示临时Diffie-Hellman,其中Diffie-Hell参数由CA签名的可签名证书签名。服务器使用的签名算法在CipherSuite名称的DHE组件后指定。服务器可以向客户端请求任何支持签名的证书进行客户端身份验证,也可以请求Diffie-Hellman证书。客户端提供的任何Diffie-Hellman证书都必须使用服务器描述的参数(组和生成器)。
CipherSuite TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA = { 0x00,0x0D }; |
以下密码套件用于完全匿名的Diffie-Hellman通信,其中任何一方都没有经过身份验证。请注意,此模式容易受到中间人攻击。因此,使用此模式的用途有限:除非应用层特别要求允许匿名密钥交换,否则TLS 1.2实现不得使用这些密码套件。(匿名密钥交换有时是可以接受的,例如,当没有设置身份验证时,或者当TLS用作具有其他确保身份验证手段的更复杂安全协议的一部分时,支持机会加密。)
CipherSuite TLS_DH_anon_WITH_RC4_128_MD5 = { 0x00,0x18 }; |
请注意,使用非匿名密钥交换而不实际验证密钥交换本质上等同于匿名密钥交换,并且适用相同的预防措施。虽然非匿名密钥交换通常比匿名密钥交换涉及更高的计算和通信成本,但当应用层允许匿名密钥交换时,不禁用非匿名密钥交易所可能有利于互操作性。
注意:为避免与SSL 3中基于Fortezza的密码套件发生冲突,保留了密码套件值{0x00,0x1C}和{ 0x00, 0x1D }
Client Hello
当客户端首次连接到服务器时,需要将ClientHello作为其第一条消息发送。客户端还可以响应HelloRequest或主动发送ClientHello,以便重新协商现有连接中的安全参数。
数据结构为:
ClientHello的定义
struct { |
Handshake的定义enum {
hello_request(0), client_hello(1), server_hello(2),
certificate(11), server_key_exchange (12),
certificate_request(13), server_hello_done(14),
certificate_verify(15), client_key_exchange(16),
finished(20), (255)
} HandshakeType;
struct {
HandshakeType msg_type; /* handshake type */
uint24 length; /* bytes in message */
select (HandshakeType) {
case hello_request: HelloRequest;
case client_hello: ClientHello;
case server_hello: ServerHello;
case certificate: Certificate;
case server_key_exchange: ServerKeyExchange;
case certificate_request: CertificateRequest;
case server_hello_done: ServerHelloDone;
case certificate_verify: CertificateVerify;
case client_key_exchange: ClientKeyExchange;
case finished: Finished;
} body;
} Handshake;
记录层
struct { |
数据示例
1603030075010000710303FC7C6231693E7A746B92D387AABBD24E3DBB45117B309E9B89A2FD4902122CD9000002C02B010000460000001900170000147273702E6573696D2E776874792E636F6D2E636E000D0012001004030503060302030401050106010201000A000400020017000B00020100FF01000100 |
进行拆分
16 -- record.type # 内容类型:Handshake = 22 (0x16) |
SNI
SNI的定义如下:
struct { |
对应的数据项
0000 0019 -- 扩展类型:SNI (0x0000) 扩展长度
0017 -- SNI 扩展数据长度
00 -- 服务器名称类型:0x00 表示主机名
0014 -- 主机名长度(大端序)
7273702E6573696D2E776874792E636F6D2E636E --主机名数据
整体结构:TLS 扩展块
一个 TLS 扩展的基本结构是:Extension {
ExtensionType extension_type; // 2 字节
opaque extension_data<0..2^16-1>; // 2 字节长度 + 数据
}
数据开头是:00 00 -- extension_type = 0x0000 (server_name)
00 19 -- extension_data 的长度 = 0x0019 = 25 字节
所以剩下的 25 字节 就是 extension_data,即 ServerNameList。
extension_data即 ServerNameList
RFC 6066 中 ServerNameList 定义为:struct {
ServerName server_name_list<1..2^16-1>;
} ServerNameList;
也就是说,server_name_list 是一个 变长向量,编码为 2 字节长度 + 元素。
从数据中,紧接着是:00 17 -- server_name_list 的长度 = 0x0017 = 23 字节
这 23 字节就是 server_name_list 的实际内容(即一个或多个 ServerName 结构)。
ServerName 结构
ServerName 定义为:struct {
NameType name_type;
select (name_type) {
case host_name: HostName;
} name;
} ServerName;
enum { host_name(0), (255) } NameType;
opaque HostName<1..2^16-1>;
内容:
name_type:1 字节,值为0x00(表示主机名)name:根据name_type选择HostName类型。HostName也是一个变长向量,编码为 2 字节长度 + 主机名字符串。
数据在 00 17 之后是:00 -- name_type = 0x00
00 14 -- HostName 的长度 = 0x0014 = 20 字节
72 73 70 2E 65 73 69 6D 2E 77 68 74 79 2E 63 6F 6D 2E 63 6E -- ASCII "rsp.esim.whty.com.cn"
signature_algorithms
结构定义为
enum { |
对应的数据
000D -- 扩展类型:signature_algorithms (0x000D)
0012 -- 扩展长度
0010 -- 算法列表长度
0403
0503
0603
0203
0401
0501
0601
0201 -- 算法列表
提供的这段数据是 TLS ClientHello 中 signature_algorithms(签名算法)扩展 的完美编码实例,它完全遵循 RFC 5246 第 7.4.1.4.1 节的定义。下面我们逐层拆解,看它如何与数据结构对应。
整体结构:TLS 扩展块
每个 TLS 扩展的通用结构是:Extension {
ExtensionType extension_type; // 2 字节
opaque extension_data<0..2^16-1>; // 2 字节长度 + 数据
}
数据开头是:
00 0D→extension_type= 0x000D,即signature_algorithms扩展。00 12→extension_data长度 = 0x0012 = 18 字节。
因此,接下来的 18 字节 就是 extension_data。
extension_data 的结构
RFC 5246 定义 extension_data 的内容为:SignatureAndHashAlgorithm
supported_signature_algorithms<2..2^16-2>;
也就是说,supported_signature_algorithms 是一个变长向量,编码为 2 字节长度 + 元素列表。
紧接着的 18 字节是:
00 10→ 列表长度 = 0x0010 = 16 字节。- 剩余的 16 字节 是算法对列表。
算法列表(16 字节 = 8 对)
每个算法对(SignatureAndHashAlgorithm)占 2 字节:struct {
HashAlgorithm hash; // 1 字节
SignatureAlgorithm signature; // 1 字节
} SignatureAndHashAlgorithm;
现在拆解剩余的 16 字节:
| 字节对 | 哈希值 (Hash) | 哈希名称 | 签名值 (Signature) | 签名名称 | 含义 |
|---|---|---|---|---|---|
04 03 |
0x04 |
SHA‑256 | 0x03 |
ECDSA | SHA256 + ECDSA |
05 03 |
0x05 |
SHA‑384 | 0x03 |
ECDSA | SHA384 + ECDSA |
06 03 |
0x06 |
SHA‑512 | 0x03 |
ECDSA | SHA512 + ECDSA |
02 03 |
0x02 |
SHA‑1 | 0x03 |
ECDSA | SHA1 + ECDSA |
04 01 |
0x04 |
SHA‑256 | 0x01 |
RSA | SHA256 + RSA |
05 01 |
0x05 |
SHA‑384 | 0x01 |
RSA | SHA384 + RSA |
06 01 |
0x06 |
SHA‑512 | 0x01 |
RSA | SHA512 + RSA |
02 01 |
0x02 |
SHA‑1 | 0x01 |
RSA | SHA1 + RSA |
注意:
0x04是 SHA256,0x05是 SHA384,0x06是 SHA512,0x02是 SHA1。0x03是 ECDSA,0x01是 RSA。
总结为什么是这种格式
- 嵌套长度:每层数据都有长度前缀(
00 12是扩展总长,00 10是列表总长),确保解析器能准确知道数据边界。 - 固定顺序:先放扩展类型,再放扩展长度,再放数据。数据内部先放列表长度,再放列表元素。
- 固定元素大小:每个算法对固定 2 字节(1 字节哈希 + 1 字节签名),方便快速遍历。
- 大端序:所有长度字段均使用大端(网络字节序)。
这种格式可以让服务器精确知道客户端支持哪些 (哈希, 签名) 组合,从而在 ServerKeyExchange 中选择一个双方都支持的算法进行签名验证。
supported_groups
结构定义
struct { |
对应的数据
000A -- 扩展类型:supported_groups (0x000A) |
提供的这段数据是 TLS ClientHello 中 supported_groups(支持的组)扩展 的一个典型编码实例。它告诉服务器客户端支持哪些椭圆曲线(或 Diffie‑Hellman 组)用于密钥交换。
现在对照 RFC 7919(或 RFC 4492) 定义的结构,逐层拆解这段数据。
整体扩展块结构
TLS 扩展的通用定义如下(RFC 5246 第 7.4.1.4 节):struct {
ExtensionType extension_type; // 2 字节
opaque extension_data<0..2^16-1>; // 2 字节长度 + 数据
} Extension;
给出的数据开头是:
00 0A→extension_type= 0x000A,即supported_groups(旧称elliptic_curves)。00 04→extension_data的长度 = 0x0004 = 4 字节。
因此,接下来的 4 字节 就是 extension_data 的内容。
extension_data 的结构
RFC 7919 第 4 节定义了 supported_groups 的扩展数据:struct {
NamedGroup named_group_list<2..2^16-1>;
} NamedGroupList;
enum {
secp256r1(0x0017), secp384r1(0x0018), secp521r1(0x0019),
x25519(0x001D), x448(0x001E),
// ...
} NamedGroup;NamedGroupList 是一个变长向量,编码为:
- 前 2 字节:列表的总长度(以字节为单位)
紧接着是
NamedGroup元素列表(每个 2 字节)。extension_data接下来的 4 字节是:00 02 -- named_group_list 的长度 = 0x0002(表示后续有 2 字节数据)
00 17 -- 第一个(也是唯一一个)NamedGroup 值 = 0x0017(即 secp256r1)
为什么长度是 00 02 而不是直接写组数量
因为 named_group_list 是一个字节向量,它的长度表示的是总字节数,而不是元素个数。一个 NamedGroup 占 2 字节,所以列表长度为 2 字节时,正好容纳一个组。如果列表包含两个组(例如 P-256 和 X25519),长度就是 00 04,后面跟着 00 17 00 1D。
为什么组值 00 17 对应 secp256r1
IANA 为每个命名组分配了固定的 16 位编号。0x0017 代表 NIST P‑256 曲线(secp256r1),这是 TLS 中最常用的曲线之一。其他常见值:
0x0018→ secp384r10x001D→ X255190x001E→ X448
ec_point_formats
它的结构严格遵循 RFC 4492(ECC 密码套件)第 5.1.2 节的定义。
对应的数据
000B -- 扩展类型:ec_point_formats (0x000B) |
整体扩展块结构
TLS 扩展的通用格式(RFC 5246 第 7.4.1.4 节):struct {
ExtensionType extension_type; // 2 字节
opaque extension_data<0..2^16-1>; // 2 字节长度 + 数据
} Extension;
数据开头是:
00 0B→extension_type= 0x000B,即ec_point_formats。00 02→extension_data的长度 = 0x0002 = 2 字节。
接下来的 2 字节 就是 extension_data 的内容。
extension_data 的结构(ECPointFormatList)
RFC 4492 定义:struct {
ECPointFormat ec_point_format_list<1..2^8-1>;
} ECPointFormatList;
enum {
uncompressed (0),
ansiX962_compressed_prime (1),
ansiX962_compressed_char2 (2),
reserved (248..255)
} ECPointFormat;ec_point_format_list 是一个 变长向量,编码为:
- 前 1 字节:列表的长度(以字节为单位,即后续格式值的个数)。
紧接着是若干个
ECPointFormat枚举值(每个 1 字节)。extension_data接下来的 2 字节是:01 -- 列表长度:表示后面有 1 个格式值
00 -- 第一个格式值:0x00(根据 RFC 表示「未压缩」格式)注意:RFC 4492 明确规定
0x00是uncompressed,0x01是压缩格式(素数域)
为什么是这 2 字节而不是更多
- 因为客户端只声明支持未压缩格式(
0x00),所以列表长度仅为1。 - 一个格式值占 1 字节,因此总数据长度为
1(长度) + 1(值) = 2字节,正好匹配扩展数据长度00 02。
如果客户端还支持压缩格式(0x01),则数据会变成 01 00 01?但注意:列表长度是字节计数,若支持两种格式(未压缩+压缩),则长度应为 2,后面跟 00 01。例如:02 00 01。
常见误区和标准纠正
0x00= 未压缩,0x01= 压缩(素数域)。- 现代 TLS 实现(包括 Go 标准库)通常只支持未压缩格式,因为压缩格式会增加实现复杂度和安全风险,且节省的带宽已不显著。
renegotiation_info
它完全遵循 RFC 5746(安全重协商扩展) 的定义。
FF01 -- 扩展类型:renegotiation_info (0xFF01) |
整体扩展块结构
TLS 扩展的通用格式(RFC 5246):struct {
ExtensionType extension_type; // 2 字节
opaque extension_data<0..2^16-1>; // 2 字节长度 + 数据
} Extension;
数据开头是:
FF 01→extension_type= 0xFF01,即renegotiation_info。00 01→extension_data的长度 = 0x0001 = 1 字节。
因此,接下来的 1 字节 就是 extension_data 的内容。
extension_data 的结构
RFC 5746 第 3.4 节定义了 renegotiation_info 的数据:struct {
opaque renegotiated_connection<0..255>;
} RenegotiationInfo;renegotiated_connection 是一个变长向量,编码为:
- 前 1 字节:后续数据的长度(最大 255 字节)
紧接着是
renegotiated_connection的数据(长度由前一字节指定)extension_data是:00 -- 表示 `renegotiated_connection` 的长度为 0(没有数据)
为什么长度是 1 字节,且数据为 0x00
- 长度是 1 字节:因为
opaque <0..255>向量使用 1 字节长度(而不是 2 字节),这是 RFC 5746 特意设计的小型向量。 - 数据为
0x00:在初始握手(即尚未进行过任何重协商)中,renegotiated_connection必须为空,所以长度字段为 0,不跟任何后续数据。
因此,extension_data 就是单个字节 0x00,扩展总长度 00 01 完全正确。
为什么是 FF 01 而不是 0x0017?
扩展类型的值由 IANA 分配。标准扩展(如 SNI、supported_groups)使用 0x0000~0xFFFF 范围内的数值,但 renegotiation_info 被特意分配为 0xFF01(位于私有扩展区域,以防与旧版本冲突)。这是协议设计的一部分。
后续重协商时的情况
如果在这个连接上进行过重协商,该扩展会携带之前连接的安全绑定信息,长度和内容都会变化。但初始握手中,这个扩展就是 00,表示“客户端支持安全重协商,但当前连接尚未有重协商上下文”。
这是 TLS 初始握手中安全重协商扩展的标准编码,简单直接,只为标识双方支持此功能,不携带额外数据。
服务端响应
服务端响应数据如下:
160303005D020000590303BB454BCFD9C01225DA7B85F7D608747EB71D412EDB098E691CB12A866097D87520B0909337BF29D5EC7C153DA5F2787C990F04343B59285615FEA3FF8A31542938C02B000011FF0100010000000000000B00040300010216030302C60B0002C20002BF0002BC308202B83082025DA00302010202107C38DC2D583C511E7B0D9E14B5E6DD59300A06082A8648CE3D040302304431183016060355040A130F47534D204173736F63696174696F6E312830260603550403131F47534D204173736F63696174696F6E202D205253503220526F6F7420434931301E170D3235313230333030303030305A170D3237303130323233353935395A306D310B300906035504061302434E310E300C06035504070C05577548616E3131302F060355040A0C28577568616E205469616E797520496E666F726D6174696F6E20496E64757374727920434F2E4C5444311B301906035504030C122A2E6573696D2E776874792E636F6D2E636E3059301306072A8648CE3D020106082A8648CE3D0301070342000492CC5576A928203F807BEE009247AE2C0CCAE0367182874E59EB574135993A02104FC6DFB32CA237E1D8052D65A955E74CAE6D4EB99A46AC2F1B116F43030C59A38201063082010230140603551D20040D300B3009060767811201020103304D0603551D1F044630443042A040A03E863C687474703A2F2F67736D612D63726C2E73796D617574682E636F6D2F6F66666C696E6563612F67736D612D727370322D726F6F742D6369312E63726C30200603551D250101FF0416301406082B0601050507030106082B06010505070302300E0603551D0F0101FF04040302078030290603551D110422302082122A2E6573696D2E776874792E636F6D2E636E880A2B06010401829E000104301D0603551D0E041604141A35C19EBD10F3D65E0A5642787548CE8C54114C301F0603551D2304183016801481370F5125D0B1D408D4C3B232E6D25E795BEBFB300A06082A8648CE3D0403020349003046022100F00BD62C6813D7D9CDAE68B89356D2EC741770968DE8FA4F7D97C00E37F8B8D4022100A6C08D79E71C8F0CF1ED406894EEAFE81185A49CCD48B7450829B3C7C3A7734716030300940C000090030017410481DE95BD3AEF79A08FB862775055647B451213759F9FF0698FE1E16042599191E162139C82BEB74F6A5FC261B090CC99692244C26447C9CB74E3E8368724E610040300473045022060638E9211495FF3E53A0D7C7C0663621BA78F560A62506D8B0860A67AC732BD022100E5E9B851A222EB381B5727C748E7498EBACF4A78DAA847BBB32849A6DC06D7D316030300040E000000 |
可以把数据拆解为以下几段
段落1 — server_hello(2)
16 |
段落2 — certificate(11)
16 |
段落3 — server_key_exchange (12)
16 |
段落4 — server_hello_done(14)
16 |
server_hello
当服务器能够找到一组可接受的算法时,它将发送此消息以响应ClientHello消息。如果它找不到这样的匹配,它将以握手失败警报进行响应。
对应的数据为
16 |
数据定义为
struct { |
该结构与Client Hello结构类似,但是注意Client Hello结构为
struct { |
在Client Hello结构中cipher_suites和compression_methods是边长的。
16 -- record.type # 内容类型:Handshake = 22 (0x16) |
certificate
当服务器能够找到一组可接受的算法时,它将发送此消息以响应ClientHello消息。如果它找不到这样的匹配,它将以握手失败警报进行响应。
结构定义为
opaque ASN.1Cert<1..2^24-1>; |
示例数据为
16 |
解析为
16 -- record.type # 内容类型:Handshake = 22 (0x16) |
server_key_exchange
此消息将在服务器证书消息(或ServerHello消息,如果这是匿名协商)之后立即发送。只有当服务器证书消息(如果已发送)包含的数据不足以允许客户端交换预主密钥时,服务器才会发送ServerKeyExchange消息。以下密钥交换方法也是如此
示例数据
16 |
数据结构为
enum { dhe_dss, dhe_rsa, dh_anon, rsa, dh_dss, dh_rsa |
先解析为
16 -- record.type # 内容类型:Handshake = 22 (0x16) |
对于ServerKeyExchange 消息体,可见 https://datatracker.ietf.org/doc/html/rfc4492
ServerKeyExchange 消息体
对于 ECDHE,消息体结构为(RFC 4492):
text
struct { |
其中:
ECParameters包含curve_type和namedcurveECPoint是变长向量(带长度前缀)Signature在 TLS 1.2 中为:text
struct {
SignatureAndHashAlgorithm algorithm; // 2字节
opaque signature<0..2^16-1>; // 2字节长度 + 数据g
}
完整定义为
struct { |
可进一步解析为
16 -- record.type # 内容类型:Handshake = 22 (0x16) |
server_hello_done
ServerHelloDone消息由服务器发送,以指示ServerHello和相关消息的结束。发送此消息后,服务器将等待客户端响应。
示例数据为
16030300040E000000 |
结构为
struct { } ServerHelloDone; |
解析结果
16 -- record.type # 内容类型:Handshake = 22 (0x16) |
客户端响应
Client Key Exchange Message
此消息始终由客户端发送。如果发送了客户端证书消息,它必须立即跟在其后。否则,它必须是客户端在收到ServerHelloDone消息后发送的第一条消息。
示例数据为
1603030046100000424104B308E8FC325436A49506BFD84D6CDCC75B16921762DEBB4298C069F83F0200000A2F059C68EF2E6ED078BCE403945C72C0248F2298E39DCDAFC12126F055D671 |
数据结构为
struct { |
RSA-Encrypted Premaster Secret Message
struct { |
Client Diffie-Hellman Public Value
enum { implicit, explicit } PublicValueEncoding; |
解析后的数据为
16 -- record.type # 内容类型:Handshake = 22 (0x16) |
ChangeCipherSpec
存在改变密码规范协议,以表示加密策略中的转换。该协议由一条消息组成,该消息在当前(而非挂起)连接状态下进行加密和压缩。该消息由一个值为1的字节组成
客户端和服务器都会发送ChangeCipherSpec消息,通知接收方后续记录将受到新协商的CipherSpec和密钥的保护。接收到此消息后,接收器会指示记录层立即将读取的挂起状态复制到读取的当前状态。发送此消息后,发送方必须立即指示记录层将写挂起状态设置为写活动状态。
(见第6.1节。)在安全参数达成一致后,但在发送验证完成消息之前,在握手期间发送ChangeCipherSpec消息。
注意:如果在连接上传输数据时发生重新握手,通信方可能会继续使用旧的CipherSpec发送数据。但是,一旦发送了ChangeCipherSpec,就必须使用新的CipherSpec。发送ChangeCipherSpec的第一方不知道另一方已经完成了新密钥材料的计算(例如,如果它必须执行耗时的公钥操作)。因此,可能存在一个小的时间窗口,在此期间,接收者必须缓冲数据。在实践中,对于现代机器来说,这个间隔可能相当短。
示例数据为
140303000101 |
数据结构为
struct { |
解析为
14 -- record.type # 内容类型:change_cipher_spec(20) |
为什么要发送 ChangeCipherSpec?
它是在 TLS 握手过程中 切换加密模式 的信号:
- 在握手的前半段(ClientHello 到 ServerHelloDone),所有消息都是明文传输。
- 当双方完成密钥交换并派生出
master_secret以及读写密钥后,需要通知对端:“从现在开始,我要用刚协商好的密钥加密后续消息了”。 ChangeCipherSpec就是这个通知,它本身始终以明文发送(不受当前加密状态影响),但它的出现标志着发送方将立即切换到新密钥状态。
因此,它是一道分界线:在这条消息之后,Finished 及所有应用数据都将被加密。
如果不发送会怎样?
- 接收方(服务器)会一直停留在明文模式,继续等待明文握手消息。
- 而发送方(客户端)如果接着发送加密的
Finished,服务器将无法正确解密(因为服务器未切换密钥,会尝试用旧状态或明文解析),导致解密失败或解析错位,最终握手失败并触发警报(如bad_record_mac或unexpected_message)。 - 同样,服务器也必须在发送完
ServerHelloDone后,接收客户端的 ChangeCipherSpec 后再发送自己的 ChangeCipherSpec 和 Finished,否则双方状态不一致。
140303000101 完美对应 ChangeCipherSpec 结构:记录头 + 一个字节的 type=1。它的作用是切换加密状态,确保后续消息受到保护。不发送会导致对方无法正确解密后续消息,握手必然失败
Finished
始终在更改密码规范消息后立即发送完成消息,以验证密钥交换和身份验证过程是否成功。在其他握手消息和完成消息之间接收更改密码规范消息至关重要。
完成消息是第一个使用刚刚协商的算法、密钥和秘密保护的消息。已完成消息的收件人必须验证内容是否正确。一旦一方发送了其完成消息,并从其对等方接收并验证了完成消息,它就可以开始通过连接发送和接收应用程序数据。
示例数据
160303002800000000000000006C1185C0668517CEDDCE7D3D605B29EB1A96A89C89EDE3015A51B44B51248BE5 |
数据结构为
struct { |
哈希表示握手消息的哈希。对于第5节中定义的PRF,哈希必须是用作PRF基础的哈希。任何定义不同PRF的密码套件也必须定义在完成计算中使用的哈希。在以前版本的TLS中,verify_data的长度始终为12个八位字节。在当前版本的TLS中,它取决于密码套件。任何没有明确指定verify_data_length的密码套件的verify_data_length都等于12。这包括所有现有的密码套件。请注意,此表示与以前的版本具有相同的编码。未来的密码套件可以指定其他长度,但此类长度必须至少为12个字节。
此握手中所有消息的所有数据(不包括任何HelloRequest消息),直到但不包括此消息。这只是握手层可见的数据,不包括记录层标头。这是迄今为止交换的第7.4节中定义的所有握手结构的连接。
如果在握手的适当时刻,Finish消息前面没有ChangeCipherSpec消息,则这是一个致命的错误。handshake_messages值包括从ClientHello开始的所有握手消息,包括但不包括此Finish消息。这可能与第7.4.8节中的握手消息不同,因为它将包括CertificateVerify消息(如果发送)。此外,客户端发送的Finish消息的handshake_message将与服务器发送的Finished消息不同,因为第二次发送的消息将包括前一个消息。注意:ChangeCipherSpec消息、警报和任何其他记录类型都不是握手消息,也不包含在哈希计算中。此外,握手哈希中省略了HelloRequest消息。
解密为
16 -- record.type # 内容类型:Handshake = 22 (0x16) |
负载(40 字节)的含义
负载是密文,它包含:
- 显式 IV / Nonce(对于 CBC 是 16 字节 IV,对于 GCM 是 8 字节显式 nonce)
- 密文 + 认证标签(GCM)或 密文 + MAC(CBC)
数据中前 6 个字节是 00 00 00 00 00 00,看起来不像随机 IV,更可能是 GCM 的显式 nonce(序列号),但通常序列号是 8 字节,这里是 6 个 0 后面还有 6C...,可能序列号是 8 字节,即前 8 字节为 00 00 00 00 00 00 6C 11?实际上,我们无法准确判断,因为不知道具体密码套件。
但重点是:这 40 字节不是 Finished 消息本身,而是它的密文。
解密后得到真正的 Finished 握手消息
解密后,我们预期得到一条明文握手消息:
- 类型:
0x14(Finished) - 长度:
0x00000C(12 字节,因为verify_data长度固定为 12) - 消息体:12 字节的
verify_data
根据 RFC 5246,Finished 结构非常简单:
struct { |
它只有 verify_data 数组,没有其他字段。但在握手消息中,它仍然带有握手头(类型+长度),而消息体就是 verify_data。
因此,Finished 消息体的完整二进制就是 12 字节的 verify_data。在握手消息中,它被包装为:
14 00 00 0C [12字节verify_data] |
其中 14 是类型,00 00 0C 是长度。
如何从数据对应到结构?
- 记录层:
16 03 03 00 28指出这是一个 Handshake 记录,负载长度为 40 字节。 - 负载:经过解密后,应得到
14 00 00 0C [12字节]。 - Finished 结构:消息体就是
[12字节],即verify_data。 verify_data的值由 PRF 计算得出,不直接出现在数据中,而是经过加密后发送。
如果不加密,Finished 会是什么样子?
在 ChangeCipherSpec 之前,握手消息都是明文的,但 Finished 必须在加密后发送(因为它的作用是验证加密通道的完整性),所以它总是密文。因此,看到的数据不是明文 Finished,而是加密后的记录。
服务器二次响应
ChangeCipherSpec
示例数据为
14 -- record.type # 内容类型:change_cipher_spec(20) |
Finished
示例数据
16 -- record.type # 内容类型:Handshake = 22 (0x16) |
Application Data Protocol
应用程序数据消息由记录层承载,并根据当前连接状态进行分段、压缩和加密。消息被视为记录层的透明数据。
客户端发送给服务器
示例数据
17030300710000000000000001C5262D872FA87D2D9DB92F44FF019A2EBF1B375C8593DB679F8A34440D4533E6844F0A0308BEE20006408B51636975E990B18E032B502E50DD1888D3F7CB27701F2B04DA864CA73D2986FAB6504BC20678FE4421B047FA373288EF43F8F0D37114C9286880383189FE |
解析为
17 -- record.type # 内容类型:application_data(23) |
服务器发送给客户端
示例数据
1703030231012CAD12A6015C8ADD5995A6BBAC798F1E2C0C1A9D71571A4E7C47793C2D2AEDDA587F5B9B9B1B7AB1FA4D16C9DD10B0E7289F8A7D7C5DBC947D60FF23E7A0952765CFEF0DAE68B7AF1446ABC4E6D8D79319BE8E067BB40E5721BD643770F7243C4FD39643277B3585D0755FD083709C58E14BE663BED27FEACB6B07392CDA9EE7BBFD70012B7926B8F96EDCB410BC2912ED9CE5FB88B2C1A984F609D9D8467E25D30AFB920B378B0244E7E2408C63D31E9F360A59446A33411C22917537BBC8B02E1AFA74D535C6544B2A76DDC46F0C67F3DD60103C6BE931AC0701C887162C6DBBE3CF1AF32B9C6A511984D2F692C1209BEDBEC75F2FBE041F144D04BEC9CC183D55A3E0B9F7E5935A21EF60B8B6F21280A6F175518E9197E21CA5E52A9B88D1866618B6E2C4EC69EB5CC034103A8921432FE96839B4498DF8C60DAF5B9198EA9C40157BA9491723A34F1538B63F22067CEF4C0FAF02AECA811AD254D784BDEF4D6D50347D1A45B6D6054C43B92F82FFF01F0FD460B127658E52F8EF6A6211699AEB3329C72C25925D7F89AFD2B44B3A1A104BCDCF84F831EBE6A15AA240294B0BC768534194FEBB09B692A091B1D66E420CC16E8F2564302A37E666427D41FF285B3C39F921FA881CAF6B0ACA4005D72C516507A9F863587CB9C9F0F531FABF736BB6614980107E49BEA1539ECE18755FE0E9B2A1D53FA5E43AAA8569808E1E2D8725BE67B92FFCEFFEF15B4D74CA836A7CAEC4768688A5E0ECD847710B94C4D8EF413191C58D0C33F0AC8B01C6CF72 |
解析为
17 -- record.type # 内容类型:application_data(23) |
Certificate 和 Application Data 消息超大
从 RFC 规范的角度来看,Certificate 和 Application Data 这两种消息在数据量超大时的处理方式,其核心机制都是 “分片”。
这个机制由 TLS 记录层(Record Layer)统一提供,规定任何消息在通过记录层发送时,都必须被切分成不超过 2^14 字节(即 16 KB) 的数据块。
核心规则:记录层分片
无论是握手消息还是应用数据,在通过记录层发送时都必须遵循一个统一的硬性限制:单个 TLS 记录(TLSPlaintext)的数据长度不得超过 2^14 字节(16 KB)。
因此,所有超过此大小的消息都必须被拆分成多个记录来发送。这是所有处理方式的共同基础。
区分:Certificate 与 Application Data
虽然都基于分片,但由于消息性质不同,两者在处理规则上存在关键差异。
Certificate 消息:有序的握手分片
Certificate 消息属于握手协议,其分片过程受到严格约束,需要保证消息的完整性。
- 必须连续:如果一个握手消息(如
Certificate)被分片,那么承载这些分片的多个 TLS 记录必须是连续的,中间不能夹杂其他类型的记录。 - 不能跨越密钥变更:分片后的握手消息不能跨越密钥变更(key change) 的边界。这意味着在
ChangeCipherSpec消息前后,握手消息必须完整地结束或开始,不能有分片横跨这个边界。 - 实现要求:RFC 明确指出,像
certificate这样的消息可能非常大,以至于需要进行分片。一个健壮的实现必须能正确处理被分片到多个 TLS 记录中的握手消息。
Application Data 消息:灵活的透明数据
与握手消息的严格约束不同,应用数据的分片规则要灵活得多。
- 可以任意分片:
Application Data消息可以被拆分成多个记录进行发送。 - 可以合并发送:多个小的应用数据片段也可以被合并到同一个 TLS 记录中发送。
- 允许零长度分片:允许发送零长度的
Application Data记录,这可以作为流量分析的对抗手段。 - 透明处理:
Application Data的内容对 TLS 记录层是透明的,记录层仅负责分片、压缩和加密。
总结对比
| 特性 | Certificate (握手消息) | Application Data |
|---|---|---|
| 处理方式 | 被分片 (Fragmented) 到多个记录 | 被分片 (Fragmented) 或合并 (Coalesced) |
| 记录约束 | 必须连续,中间不能有其它类型记录 | 无特殊连续性要求 |
| 密钥变更 | 不能跨越密钥变更边界 | 无此限制 |
| 零长度 | 禁止发送零长度分片 | 允许发送零长度分片 |
| 本质 | 结构化的协议消息,需保证完整性 | 透明的上层数据流,处理更灵活 |
总而言之,RFC 规范为 TLS 记录层设计了一套统一的分片机制来处理大消息,但针对 Certificate 这类关键的握手消息和 Application Data 这种灵活的数据流,在具体的连续性、边界对齐等规则上做了细致的区分。
Application Data Protocol消息超大
不能在一个 TLS 记录(包)里无限制发送。TLS 协议对单个记录的有效载荷有明确的长度限制(最大 16KB),更大的应用数据必须拆分成多个记录。
协议规定的硬性限制
根据 RFC 5246(TLS 1.2)第 6.2.3 节:
The record layer fragments information blocks into TLSPlaintext records carrying data in chunks of 2^14 bytes or less.
- 最大值:16,384 字节(即 16KB)。
- 这是 TLSPlaintext(加密前明文)的长度上限。如果应用数据(如 HTTP 响应体)超过 16KB,TLS 层必须自动将其分片为多个 16KB 或更小的记录块。
另外,记录层头部的 length 字段虽然占 2 字节(理论最大 65,535 字节),但标准实现强制限制为 16KB,以保证安全性和缓冲区的合理管理。
为什么不设计成无限大(或更大)
- 缓冲区管理:限制记录大小简化了内存分配,避免为一次性接收巨量数据而申请超大缓冲区(防止 DoS 攻击)。
- 实时性与延迟:16KB 是兼顾网络吞吐和低延迟的折中值。若记录过大,接收端必须收完整条记录才能解密,增加延迟;拆分后可以边收边解。
- 重传效率:TCP 丢包时,只需重传损坏的小记录,而不是整个大块。
- 标准兼容性:所有 TLS 实现(包括 OpenSSL、Go 标准库)都遵循这个上限,保证了互通性。
实际处理方式(在代码中的体现)
- 发送端(如
sendRecord函数): 如果数据超过 16KB,不能直接调用一次sendRecord。上层协议(如 HTTP/1.1 或 HTTP/2)通常会自己分块,或由 TLS 库(标准库)内部循环发送多个记录。 - 接收端(如
recvRecord循环): 读取多个记录,逐个解密并拼接明文。 在主函数中接收 HTTP 响应时,也是用for循环持续读取记录,直到连接关闭(或收到close_notify),这正是为应对多条分片记录而设计的。
需要注意的“包”和“记录”区别
- TLS 记录是协议层的逻辑单元。
- 网络包(IP 包或 TCP 段) 是传输层的物理单元。 由于 TCP 是流协议,一个 TLS 记录可能被拆分到多个 TCP 段发送,反之,一个 TCP 段也可能包含多个小的 TLS 记录。因此,“一个包发完”在 TLS 层面没有意义——即使数据小于 16KB,它也可能在 TCP 层被分包;即使数据很大,TLS 也会通过多个记录承载。
| 情况 | 处理方式 |
|---|---|
| 数据 ≤ 16KB | 封装在 1 个 TLS 记录中发送(但 TCP 层仍可能分包)。 |
| 数据 > 16KB | 拆分成 多个 TLS 记录依次发送(每个记录长度 ≤ 16KB)。 |
所以,不能在单个 TLS 记录里发送超大消息,但可以在一个 TLS 连接中通过多个记录连续发送完整数据。