TLS扩展入门学习笔记

每一滴眼泪都承载着无尽的哀伤,如同秋叶飘落般无奈

Posted by yishuifengxiao on 2026-07-22

https://datatracker.ietf.org/doc/html/rfc6066#autoid-5 TLS协议本身并未提供某些场景所需的功能,RFC 6066针对以下六个核心需求定义了相应扩展:

需求场景 对应扩展
客户端需告知服务器其正在访问的服务器名称(虚拟主机场景) server_name(SNI)
受内存/带宽限制的客户端希望协商更小的分片长度 max_fragment_length
受约束的客户端希望用URL代替完整证书链 client_certificate_url
客户端希望告知服务器自己信任的CA根密钥 trusted_ca_keys
为节省带宽,希望将HMAC输出截断至80位 truncated_hmac
客户端希望在握手期间获取证书状态(如OCSP响应) status_request

Handshake Protocol协议如下

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), certificate_url(21), certificate_status(22),
(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;
case certificate_url: CertificateURL;
case certificate_status: CertificateStatus;
} body;
} Handshake;

核心概念

SNI(Server Name Indication)

目的:TLS握手发生在HTTP请求之前,服务器无法知道客户端访问的是哪个域名。SNI允许客户端在ClientHello中告知服务器目标服务器名称,使虚拟主机(virtual hosting) 场景下能正确选择证书。

机制

  • 客户端在ClientHello中包含server_name扩展,数据为服务器名称列表(当前仅支持host_name类型)。
  • 服务器收到后,据此选择合适的证书返回给客户端。
  • 若服务器使用了该信息,必须在ServerHello中回显server_name扩展(数据为空)。
  • 会话恢复时,若server_name与原始会话不同,服务器不得恢复原会话,而应执行完整握手建立新会话。


TLS并未提供一种机制让客户端能够告知服务器其正在联系的服务器名称。在某些情况下,客户端可能需要提供此信息,以便与那些在单个底层网络地址上托管多个“虚拟”服务器的服务器建立安全连接。为了提供任何服务器名称,客户端可以在(扩展的)客户端问候中包含一个类型为“server_name”的扩展。此扩展的“extension_data”字段应包含“ServerNameList”,其中:

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>;

struct {
ServerName server_name_list<1..2^16-1>
} ServerNameList;


ServerNameList中不得包含多个相同name_type的名称。如果服务器理解ClientHello扩展但无法识别服务器名称,则服务器应采取以下两种操作之一:要么通过发送致命级别的unrecognized_name(112)警报来中止握手,要么继续握手。不建议发送警告级别的unrecognized_name(112)警报,因为客户端对警告级别警报的反应行为是不可预测的。如果客户端应用程序使用的服务器名称与服务器选择的凭据的服务器名称不匹配,则当客户端应用程序执行服务器端点识别时,这种不匹配将变得明显,此时客户端应用程序必须决定是否继续通信。鼓励TLS实现向应用程序调用者提供关于在TLS握手期间接收或发送的警告级别警报的信息。此类信息对于诊断目的非常有用。


注:本规范的早期版本允许同一name_type有多个名称。但实际上,当前的客户端实现仅发送一个名称,且客户端无法确定服务器选择了哪个名称。因此,现在禁止同一name_type有多个名称。


目前,仅支持的服务器名称是DNS主机名;但这并不意味着TLS对DNS有任何依赖,未来可能会添加其他名称类型(通过更新本文档的RFC)。与host_name NameType相关的数据结构是一个以16位长度开头的可变长度向量。为了向后兼容,所有未来与新NameType相关的数据结构都必须以16位长度字段开头。TLS可以将提供的服务器名称视为不透明数据,并将名称和类型传递给应用程序。


“HostName”包含客户端可识别的服务器的完全限定DNS主机名。主机名以ASCII编码的字节字符串形式表示,且末尾不带点。这样可以通过使用[RFC5890]中定义的A标签来支持国际化域名。DNS主机名不区分大小写。比较主机名的算法在[RFC5890]的第2.3.2.4节中进行了描述。


“主机名”中不允许使用字面IPv4和IPv6地址。


建议客户端在通过受支持的名字类型找到服务器时,在客户端问候中包含一个类型为“server_name”的扩展。


当服务器接收到包含“server_name”扩展的客户端问候时,该服务器可使用该扩展中包含的信息来指导其选择合适的证书返回给客户端,以及/或其他安全策略方面。在这种情况下,服务器应在(扩展的)服务器问候中包含一个“server_name”类型的扩展。此扩展的“extension_data”字段应为空。


当服务器决定是否接受恢复会话的请求时,可以在会话缓存中查找会话时使用server_name扩展的内容。客户端应在会话恢复请求中包含与建立会话的完整握手相同的server_name扩展。如果server_name扩展包含不同的名称,则实现此扩展的服务器不得接受恢复会话的请求。相反,它应继续进行完整握手以建立新会话。在恢复会话时,服务器不得在服务器问候中包含server_name扩展。


如果应用程序使用应用协议协商服务器名称,然后升级到TLS,并且如果发送了server_name扩展,则该扩展应包含在应用协议中协商的相同名称。如果在TLS会话握手过程中建立了server_name,则客户端不应尝试在应用层请求不同的服务器名称。

Maximum Fragment Length Negotiation - max_fragment_length

目的:TLS默认明文分片最大长度为2^14(16384)字节。内存有限的嵌入式设备可能无法处理这么大的分片,此扩展允许协商更小的分片长度。

机制

  • 客户端在ClientHello中请求期望的最大分片长度,可选值为:2^9(512)、2^10(1024)、2^11(2048)、2^12(4096)字节
  • 服务器接受后,在ServerHello中回显相同值。
  • 协商成功后,双方立即开始分片所有消息,确保不超过协商长度。
  • 若服务器收到非允许值,必须发送illegal_parameter警报并中止握手。
enum{
2^9(1), 2^10(2), 2^11(3), 2^12(4), (255)
} MaxFragmentLength;

Client Certificate URLs - client_certificate_url

目的:在需要客户端证书认证时,允许客户端发送证书URL代替完整的证书链,适用于存储空间有限的客户端。

机制

  • 客户端在CertificateURL消息中发送一个或多个URL及其SHA-1哈希值。
  • 服务器从URL获取证书后,必须验证SHA-1哈希是否匹配,否则发送bad_certificate_hash_value(114)警报并中止握手。
  • 若服务器无法获取证书,发送certificate_unobtainable(111)警报。
  • 引入新的手写消息类型certificate_url(21)


若不启用此扩展,则根据TLS协议,在进行客户端身份验证时,客户端会在TLS握手过程中向服务器发送客户端证书。对于资源受限的客户端而言,可能更希望发送证书URL而非证书本身,这样它们就不需要存储证书,从而节省内存。


为了协商向服务器发送证书URL,客户端可在(扩展的)客户端问候中包含一个类型为“client_certificate_url”的扩展。此扩展的“extension_data”字段应留空。


(请注意,为避免“破坏”现有的TLS服务器,有必要协商客户端证书URL的使用。)


接收到包含“client_certificate_url”扩展的扩展客户端问候的服务器,可通过在(扩展的)服务器问候中包含“client_certificate_url”类型的扩展,来表明它们愿意接受证书URL。“extension_data”字段在此扩展中应保持为空。


在成功完成客户端证书URL使用协商(通过交换包含“client_certificate_url”扩展的hello消息)后,客户端可以按照以下方式发送“CertificateURL”消息来代替“Certificate”消息:

enum {
individual_certs(0), pkipath(1), (255)
} CertChainType;

struct {
CertChainType type;
URLAndHash url_and_hash_list<1..2^16-1>;
} CertificateURL;

struct {
opaque url<1..2^16-1>;
unint8 padding;
opaque SHA1Hash[20];
} URLAndHash;

在这里,“url_and_hash_list”包含一系列URL和哈希值。根据[RFC3986]的规定,每个“url”必须是绝对URI引用,可以直接用于获取证书。

当使用X.509证书时,存在两种可能的情况:

  • 如果CertificateURL.type为“individual_certs”,则每个URL都指向一个单独的DER编码的X.509v3证书,且客户端证书的URL位于首位。

    • 如果CertificateURL.type为“pkipath”,则列表中包含一个引用DER编码证书链的URL,该证书链使用第10.1节中描述的PkiPath类型


    当使用任何其他证书格式时,描述该格式在TLS中使用的规范应定义证书或证书链的编码格式,以及对其排序的任何约束。


“填充”字节必须为0x01。它的存在是为了使结构向后兼容。


每个URL对应的哈希值是证书或证书链的SHA-1哈希值(对于X.509证书,则是DER编码的证书或DER编码的PkiPath)。


请注意,当使用X.509证书的URL列表时,URL的排序与TLS证书消息中使用的排序相同(见[RFC5246],第7.4.2节),但与证书在PkiPath中的编码顺序相反。在任何情况下,如果假设服务器必须已经拥有自签名根证书才能对其进行验证,则该证书可以从链中省略。

Trusted CA Indication - trusted_ca_keys

目的:内存有限的客户端可能只存有少量CA根密钥,此扩展允许客户端告知服务器自己信任哪些CA,避免因服务器返回客户端不信任的证书链而导致握手失败。

机制

  • 客户端在ClientHello中携带TrustedAuthorities列表,标识方式包括:预协商(pre_agreed)、密钥SHA-1哈希(key_sha1_hash)、X.509 Distinguished Name(x509_name)、证书SHA-1哈希(cert_sha1_hash)。
  • 服务器收到后,可据此选择合适的证书链返回给客户端。
  • 若服务器使用了该信息,必须在ServerHello中回显trusted_ca_keys扩展(数据为空)
struct {
TrustedAuthority trusted_authorities_list<0..2^16-1>;
} TrustedAuthorities;

struct {
IdentifierType identifier_type;
select (identifier_type) {
case pre_agreed: struct {};
case key_sha1_hash: SHA1Hash;
case x509_name: DistinguishedName;
case cert_sha1_hash: SHA1Hash;
} identifier;
} TrustedAuthority;

enum {
pre_agreed(0), key_sha1_hash(1), x509_name(2),
cert_sha1_hash(3), (255)
} IdentifierType;

opaque DistinguishedName<1..2^16-1>;


由于内存限制,仅拥有少量证书颁发机构(CA)根密钥的受限客户端可能希望向服务器表明它们拥有哪些根密钥,以避免反复的握手失败。


为了表明其拥有的CA根密钥,客户端可在(扩展的)客户端问候中包含一个类型为“trusted_ca_keys”的扩展。此扩展的“extension_data”字段应包含“TrustedAuthorities”.


在此,“TrustedAuthorities”提供了一份客户端所拥有的CA根密钥标识符列表。每个CA根密钥都通过以下任一方式进行标识:

  • “pre_agreed”:未提供CA根密钥标识。
  • “key_sha1_hash”:包含证书颁发机构(CA)根密钥的SHA-1哈希值。对于数字签名算法(DSA)和椭圆曲线数字签名算法(ECDSA)密钥,这是“subjectPublicKey”值的哈希值。对于RSA密钥,哈希值是模数的大端字节字符串表示形式,且不包含任何初始值为零的字节。(这复制了其他环境中部署的密钥哈希格式。)
  • “x509_name”:包含证书颁发机构(CA)的DER编码的X.509 DistinguishedName。
  • “cert_sha1_hash”:包含一个DER编码证书的SHA-1哈希值,该证书包含CA根密钥。

Truncated HMAC - truncated_hmac

目的:TLS使用HMAC进行记录层认证,完整HMAC输出可能较大。为节省带宽,此扩展允许将HMAC截断至80位(10字节)

机制

  • 客户端在ClientHello中包含空的truncated_hmac扩展。
  • 服务器接受后在ServerHello中回显空扩展。
  • 协商成功后,SecurityParameters.mac_length设为10字节,仅传输和校验HMAC输出的前10字节。
  • 不影响PRF计算和密钥衍生。
  • 若协商的密码套件不使用HMAC,此扩展无效


当前定义的传输层安全(TLS)密码套件使用消息认证码(MAC)构造HMAC [RFC2104]来对记录层通信进行身份验证。在TLS中,哈希函数的整个输出被用作MAC标签。然而,在受限环境中,可能希望通过在形成MAC标签时将哈希函数的输出截断为80位来节省带宽。


为了协商使用80位截断的HMAC,客户端可以在扩展客户端问候中包含一个类型为“truncated_hmac”的扩展。此扩展的“extension_data”字段应为空。
接收到包含“truncated_hmac”扩展的扩展问候消息的服务器,可通过在扩展服务器问候消息中包含类型为“truncated_hmac”且“extension_data”为空的扩展,来同意使用截断HMAC。


请注意,如果添加了不使用HMAC的新密码套件,并且会话协商了这些密码套件中的某一个,则此扩展将不会产生任何效果。强烈建议任何使用其他MAC的新密码套件将MAC大小视为密码套件定义中不可或缺的一部分,同时考虑到安全性和带宽因素。


如果在TLS握手过程中成功协商了HMAC截断,并且协商的密码套件使用HMAC,则客户端和服务器都会将这一事实以及其他协商的安全参数传递给TLS记录层。随后在会话期间,客户端和服务器必须使用截断的HMAC,其计算方式如[RFC2104]所述。即,SecurityParameters.mac_length为10个字节,并且只传输和检查HMAC输出的前10个字节。请注意,此扩展不影响作为握手或密钥推导一部分的伪随机函数(PRF)的计算。


协商后的HMAC截断大小适用于会话期间,包括会话恢复。

Certificate Status Request - status_request

目的:允许客户端在TLS握手期间请求服务器证书的在线状态信息(如OCSP响应),避免单独获取CRL,节省带宽和往返时间。

机制

  • 客户端在ClientHello中包含status_request扩展。
  • 服务器可在CertificateStatus消息中返回证书状态信息。
  • 引入新的手写消息类型certificate_status(22)

与后续TLS版本的关系

RFC 6066最初作为TLS 1.2(RFC 5246)的配套文档发布。其定义的扩展在后续TLS版本中继续沿用:

  • TLS 1.3中,这些扩展的ExtensionType编号被保留,但部分扩展(如truncated_hmac)已不再推荐使用。
  • SNI(server_name 在TLS 1.3中仍是核心扩展。
  • status_request 在TLS 1.3中演变为status_requeststatus_request_v2等机制。

实现与生态

RFC 6066的扩展已被广泛实现:

  • OpenSSL 1.1.1开始支持max_fragment_length扩展
  • Mbed TLS实现了max_fragment_lengthtruncated_hmac扩展
  • OpenJDK的TLS实现包含RFC 6066定义的扩展类
  • Go的crypto/tls遵循RFC 6066的SNI规范

安全考虑

RFC 6066为每个扩展单独列出了安全考虑:

扩展 主要安全考量
server_name 域名信息泄露隐私;服务器必须验证收到的名称
max_fragment_length 较小分片可能增加管理开销
client_certificate_url URL获取可能引入额外攻击面;必须验证哈希
trusted_ca_keys 泄露客户端信任的CA信息
truncated_hmac 截断降低MAC安全性,80位提供约2^80暴力破解难度
status_request OCSP响应必须验证签名

SNI(Server Name Indication,服务器名称指示)

SNI(Server Name Indication,服务器名称指示) 是 TLS 协议的一个扩展(定义于 RFC 6066),它允许客户端在 TLS 握手阶段(ClientHello) 就告诉服务器自己正在访问的具体域名。


在你的代码中,它对应常量 EXT_SERVER_NAME = 0x0000,并在 buildClientHello 函数中被构建和发送。


为什么需要 SNI?(核心作用)

在传统的 HTTP 中,域名信息是在 TLS 握手完成后的 HTTP Host 头 中传递的。但服务器在握手阶段就需要选择正确的数字证书(因为证书是明文发给客户端的),这导致了一个“鸡生蛋”问题

  • 一台物理服务器(一个 IP 地址)上可能托管着成百上千个不同域名的网站(虚拟主机)。
  • 如果每个网站都使用独立的 HTTPS 证书,服务器在收到 ClientHello 时,不知道该返回哪个网站的证书
  • SNI 解决了这个问题:客户端在握手的第一步就把目标域名(如 www.yishuifengxiao.com)告诉服务器,服务器根据这个域名选出对应的证书返回给客户端。

SNI 是如何构造的

buildClientHello 函数中,代码按照 TLS 标准格式手动打包了 SNI 扩展:

// 1. 获取域名
sniDomain := []byte(domain) // domain = "www.yishuifengxiao.com"

// 2. 构造 SNI 内容:类型(0x00=主机名) + 长度(2字节) + 域名数据
sniSN.WriteByte(0x00)
sniSN.Write([]byte{byte(len(sniDomain) >> 8), byte(len(sniDomain))})
sniSN.Write(sniDomain)

// 3. 封装进扩展数据:扩展类型(0x0000) + 数据长度 + 上述内容
sniE.Write([]byte{0x00, 0x00}) // SNI 扩展类型
sniE.Write([]byte{byte(sniED.Len() >> 8), byte(sniED.Len())}) // 扩展长度
sniE.Write(sniED.Bytes()) // 域名数据

最终,这段数据会被塞进 ClientHello 消息的“扩展字段”中,随 TCP 连接发送给服务器。


如果不发 SNI 或发错 SNI,会发生什么?

  • 如果服务器要求 SNI 但客户端没发:服务器可能返回一个默认证书(通常是主站证书),客户端验证证书时发现域名不匹配,从而报错(虽然你的代码只验证了 ServerKeyExchange 的签名,未严格校验证书 CommonName/SAN,但标准浏览器会直接阻断访问)。
  • 如果发了错误的 SNI:服务器会返回该错误域名对应的证书,但如果你的 HTTP Host 头指向另一个域名,握手虽然能成功(因为签名验证只证明私钥持有),但服务器返回的 HTTP 内容可能不是你想要的,或者服务器直接返回 400/404。

需要注意的安全与隐私问题

  • 明文传输:SNI 是在 ClientHello 中以 明文 形式发送的(因为此时加密通道尚未建立)。这意味着网络中的中间人(如 ISP、防火墙)可以清楚地看到你在访问哪个网站。
  • 应对方案:为了应对 SNI 泄露,现代互联网正在推广 ESNI(加密 SNI)ECH(Encrypted ClientHello,TLS 1.3 标准),但你的代码实现的是基础 TLS 1.2,SNI 是明文的。

domain := "www.yishuifengxiao.com" 这一变量不仅用于构建 HTTP 请求的 Host 头,更重要的是在 TLS 握手之初,通过 SNI 扩展告知服务器:“请把 www.yishuifengxiao.com 这个域名的证书发给我”,这是完成后续证书校验和密钥交换的前提。

构造ClientHello中的SNI 扩展

SNI 扩展在协议中不是扁平结构,而是分层的“盒中盒”; >> 8(右移8位)则是为了将整数长度转换为网络传输要求的大端序(Big-Endian)双字节

为什么嵌套这么写?(格式要求)

根据 RFC 6066,SNI 扩展在 ClientHello 中的二进制结构如下:

Extension (总扩展块)
+-- ExtensionType (2字节) = 0x0000
+-- ExtensionData Length (2字节) = len(ServerNameList)
+-- ServerNameList (数据块)
+-- List Length (2字节) = len(ServerNameEntry)
+-- ServerNameEntry (条目)
+-- NameType (1字节) = 0x00 (主机名)
+-- NameLength (2字节) = len(域名)
+-- Name (变长) = "rsp.esim.whty.com.cn"

你的代码完美对应了这个三层结构:

  1. sniSN:构造最内层 条目(类型 + 域名长度 + 域名)。
  2. sniED:构造中间层 列表(列表总长度 + 上面的条目)。
  3. sniE:构造最外层 扩展(扩展类型 + 扩展长度 + 上面的列表)。

如果少包一层(比如直接写域名,不写列表长度),服务器解析时就会错位,握手会直接失败。


长度为什么要 >> 8?(字节序转换)

因为 TLS 协议规定所有整数字段使用大端序(网络字节序),即高位字节在前。

  • len(sniDomain) 返回的是 int 类型(比如域名长度 21,十六进制是 0x0015)。
  • 我们需要把这个 2 字节(16位)的数值,拆成两个单独的 byte 塞进报文:
    • 高位字节(第 15~8 位):len >> 8(右移8位)。例如 0x0015 >> 8 = 0x00
    • 低位字节(第 7~0 位):byte(len)(直接取低8位)。例如 byte(0x0015) = 0x15

如果你不进行 >> 8byte() 转换,而是直接塞一个 int,Go 会报错(类型不匹配);如果你只写 byte(len),那就只发了低位字节(0x15),丢掉了高位 0x00,服务器收到的长度就会变成 0x15(21)而不是 0x0015(21),虽然数值碰巧一样(因为长度小于255),但一旦域名长度超过255字节,高位不为0时,不写 >> 8 就会导致服务器认为长度只有低8位,从而解析完全错乱


为什么不用 binary.BigEndian.PutUint16

代码里其实导入了 encoding/binary,完全可以用:

buf := make([]byte, 2)
binary.BigEndian.PutUint16(buf, uint16(len(sniDomain)))
sniSN.Write(buf)

位移 + 字节切片字面量 的方式 []byte{byte(len >> 8), byte(len)},这在 Go 中是一种非常常见的惯用法,优点是不需要额外分配临时切片,直接在构造缓冲区时一步到位,代码更紧凑高效。

  • 嵌套结构 是协议强制规定的 “扩展 → 列表 → 条目” 三层封装。
  • >> 8 是为了把整数长度拆成 大端序的双字节,确保服务器能正确读取长度字段。

其他一些重要的扩展点

这几个 TLS 扩展的定义分散在不同的 RFC 文档中。下表汇总了你所关心的五个扩展及其核心定义文档:

扩展名称 (Extension Name) 类型值 (Type Value) 主要定义规范 (RFC) 数据结构定义章节
SNI (server_name) 0x0000 RFC 6066 Section 3
签名算法 (signature_algorithms) 0x000D RFC 5246 (TLS 1.2) Section 7.4.1.4.1
支持的组 (supported_groups) 0x000A RFC 7919 Section 4
EC点格式 (ec_point_formats) 0x000B RFC 4492 Section 5.1.2
重协商信息 (renegotiation_info) 0xFF01 RFC 5746 Section 3.4

这五个扩展在 TLS 握手过程中各司其职,确保客户端与服务器能安全、正确地协商出双方都支持的加密参数。下面依次说明它们的作用:

扩展的作用


SNI(server_name,类型 0x0000

  • 作用:让客户端在握手第一阶段(ClientHello)就告知服务器它要访问的具体域名。
  • 解决的问题:一台服务器(一个 IP)可能托管多个虚拟主机,每个主机有自己的证书。如果没有 SNI,服务器无法在发送证书前知道该返回哪个证书,只能发默认证书,导致证书域名不匹配。
  • 流程位置:客户端在 ClientHello 中明文发送,服务器据此选择正确证书。

签名算法(signature_algorithms,类型 0x000D

  • 作用:客户端告知服务器它支持哪些 哈希算法 + 签名算法 组合(例如 SHA256 + ECDSASHA384 + RSA)。
  • 解决的问题:服务器在发送 ServerKeyExchange 消息时,需要用证书私钥对消息进行签名。如果签名算法客户端不支持,客户端就无法验证签名,导致握手失败。这个扩展让服务器挑选一个双方都支持的算法。
  • 流程位置:客户端在 ClientHello 中发送;服务器在构造 ServerKeyExchange 时参考此列表,并在消息中明确使用的哈希和签名算法(代码中 skxHashAlgskxSigAlg 即来自此协商)。

支持的组(supported_groups,旧称 elliptic_curves,类型 0x000A

  • 作用:客户端列出它支持的椭圆曲线(如 secp256r1X25519),用于 ECDHE 密钥交换。
  • 解决的问题:ECDHE 需要双方选择同一条曲线才能计算共享密钥。客户端通过此扩展告知服务器它支持哪些曲线,服务器从中选择一条用于后续密钥交换。
  • 流程位置:客户端在 ClientHello 中发送;服务器在 ServerKeyExchange 中返回选择的曲线(代码中 st.skxCurve 即服务器选择的曲线)。

EC 点格式(ec_point_formats,类型 0x000B

  • 作用:客户端告知服务器它支持哪些椭圆曲线点编码格式(主要是 uncompressed(未压缩),也可能支持压缩格式)。
  • 解决的问题:如果服务器使用客户端不支持的编码格式发送 ECDHE 公钥,客户端无法正确解析。当前几乎所有实现都支持未压缩格式(即 0x04 前缀 + X + Y),所以此扩展在实践中重要性下降,但为兼容旧实现仍保留。
  • 流程位置:客户端在 ClientHello 中发送;服务器在 ServerKeyExchange 中使用客户端支持的格式编码公钥。

重协商信息(renegotiation_info,类型 0xFF01

  • 作用:用于安全地支持 TLS 重协商(即在已有连接上重新握手)。
  • 解决的问题:早期 TLS 存在重协商攻击(如截获并注入数据),RFC 5746 引入此扩展,让客户端和服务器在初始握手中交换一个“安全绑定”信息(renegotiated_connection 字段)。在初始握手中,该字段为空,但表明双方都支持安全重协商;后续重协商时,会携带之前连接的验证数据,防止中间人攻击。
  • 流程位置:客户端在 ClientHello 中发送空内容(长度 1,值为 0x00),服务器若支持会同样回复。在后续重协商中才会携带真实数据。
扩展 核心目的 发送方 关键作用对象
SNI 传递目标域名,选择证书 客户端 服务器证书选择
签名算法 协商签名算法 客户端 服务器签名验证
支持的组 协商椭圆曲线 客户端 ECDHE 密钥交换曲线选择
点格式 协商点编码格式 客户端 ECDHE 公钥解析
重协商信息 安全重协商 客户端(初始为空) 防止重协商攻击

这些扩展共同确保了 TLS 握手能顺利协商出符合双方能力且安全的参数,从而建立加密通道。

SNI (server_name)

  • 规范:RFC 6066 定义了 server_name 扩展。
  • 数据结构:其数据结构是一个名为 ServerNameList 的列表。
    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>;

    struct {
    ServerName server_name_list<1..2^16-1>
    } ServerNameList;

    该定义位于 RFC 6066 的 Section 3

签名算法 (signature_algorithms)

  • 规范:RFC 5246 (TLS 1.2) 引入了此扩展。
  • 数据结构:核心是 SignatureAndHashAlgorithm 结构体列表。
    enum {
    none(0), md5(1), sha1(2), sha224(3), sha256(4), sha384(5),
    sha512(6), (255)
    } HashAlgorithm;

    enum { anonymous(0), rsa(1), dsa(2), ecdsa(3), (255) } SignatureAlgorithm;

    struct {
    HashAlgorithm hash;
    SignatureAlgorithm signature;
    } SignatureAndHashAlgorithm;

    SignatureAndHashAlgorithm
    supported_signature_algorithms<2..2^16-2>;

    该定义位于 RFC 5246 的 Section 7.4.1.4.1

支持的组 (supported_groups)

  • 规范:RFC 7919 将此扩展重命名并泛化,以支持 FFDHE。
  • 数据结构:一个 NamedGroup 的列表。
    enum {
    secp256r1(0x0017), secp384r1(0x0018), secp521r1(0x0019),
    x25519(0x001D), x448(0x001E),
    ffdhe2048(0x0100), ffdhe3072(0x0101),
    ffdhe4096(0x0102), ffdhe6144(0x0103), ffdhe8192(0x0104),
    (0xFFFF)
    } NamedGroup;

    struct {
    NamedGroup named_group_list<2..2^16-1>;
    } NamedGroupList;

    该定义位于 RFC 7919 的 Section 4

EC点格式 (ec_point_formats)

  • 规范:RFC 4492 为 ECC 密码套件定义了此扩展。
  • 数据结构:一个 ECPointFormat 的列表。
    enum { uncompressed (0), ansiX962_compressed_prime (1),
    ansiX962_compressed_char2 (2), reserved (248..255) } ECPointFormat;

    struct {
    ECPointFormat ec_point_format_list<1..2^8-1>;
    } ECPointFormatList;

    该定义位于 RFC 4492 的 Section 5.1.2

重协商信息 (renegotiation_info)

  • 规范:RFC 5746 定义了此扩展以解决TLS重协商漏洞。
  • 数据结构:包含一个 RenegotiationInfo 结构。
    struct {
    opaque renegotiated_connection<0..255>;
    } RenegotiationInfo;

    在初始握手中,renegotiated_connection 字段长度为零。该定义位于 RFC 5746 的 Section 3.4