TLS 1.3 在安全性和效率上相比 TLS 1.2 有重大改进,其中 CertificateVerify 消息是验证服务器(和客户端)拥有证书私钥的关键机制。本文深入解析其工作原理,特别是握手摘要(Transcript)在其中的作用。

CertificateVerify 的核心作用

在 TLS 握手过程中,服务器需要证明自己拥有与证书对应的私钥。直接发送私钥显然不可能,于是 TLS 协议设计了 CertificateVerify 消息:

服务器使用私钥对握手摘要进行签名,客户端通过公钥验证签名,从而确认服务器拥有对应的私钥。

这一机制确保了:

  1. 身份认证:证书与私钥匹配
  2. 完整性保护:握手消息未被篡改
  3. 不可抵赖:签名无法伪造

握手摘要(Transcript)是什么?

握手摘要是一个 哈希链,包含从 ClientHello 到 CertificateVerify 之前的所有握手消息的哈希值。在 TLS 1.3 中,这个摘要用于:

  1. CertificateVerify 签名:作为签名的输入
  2. Finished 消息验证:验证整个握手的完整性

握手摘要的计算

TLS 1.3 使用 HKDF-Extract 构建握手摘要:

1
Transcript = Hash(Handshake Messages)

这里的 “Handshake Messages” 包含:

  • ClientHello
  • ServerHello
  • EncryptedExtensions
  • Certificate
  • CertificateRequest (如果有)

实际查看握手消息

使用 OpenSSL 可以查看完整的握手消息:

1
2
3
# 查看 TLS 1.3 握手详情
$ openssl s_client -connect cloudflare.com:443 -tls1_3 -debug 2>&1 | \
    grep -E "Handshake message| CertificateVerify" | head -20

输出示例显示握手消息按顺序处理:

1
2
3
4
5
6
Handshake message: client_hello
Handshake message: server_hello
Handshake message: encrypted_extensions
Handshake message: certificate
Handshake message: certificate_verify
Handshake message: finished

CertificateVerify 消息结构

消息格式

1
2
3
4
struct {
    SignatureScheme algorithm;
    opaque signature<0..2^16-1>;
} CertificateVerify;
  • algorithm:签名算法(如 ECDSA+SHA256, RSA+SHA256)
  • signature:对握手摘要的签名

签名内容

CertificateVerify 的签名输入是:

1
Transcript-Hash(Handshake Messages, Certificate)

具体来说,是对以下内容的签名:

1
2
TLS 1.3, CertificateVerify
   Transcript-Hash value

在 RFC 8446 中定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
       const (
           // ... other constants
           TB_CERTIFICATE_VERIFY = 100
       )

       // ...
       
       certificate_verify =
           SignatureScheme algorithm;
           opaque signature<0..2^16-1>;

不同签名算法的处理

TLS 1.3 支持多种签名算法,主要分为两类:

ECDSA 系列

1
2
3
4
5
# 查看服务器支持的签名算法
$ openssl s_client -connect cloudflare.com:443 -tls1_3 2>&1 | grep -E "Peer signing|Signature"

Peer signing digest: SHA256
Peer signature type: ECDSA

对于 ECDSA 证书:

  1. 计算握手摘要的 SHA-256 或 SHA-384 哈希
  2. 使用 ECDSA 私钥对该哈希签名
  3. 签名结果为 (r, s) 对序列化

RSA-PSS

对于 RSA-PSS 证书:

1
2
3
4
# RSA-PSS 签名算法的连接示例
$ openssl s_client -connect example.com:443 -tls1_3 \
    -sigalgs "RSA-PSS+SHA256:RSA-PSS+SHA384" 2>&1 | \
    grep -E "Cipher|Signature"

RSA-PSS 是 TLS 1.3 推荐的 RSA 签名方案,相比传统 PKCS#1 v1.5 更安全。

实际验证过程

查看证书详情

首先查看服务器证书和签名算法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ openssl s_client -connect cloudflare.com:443 -tls1_3 2>&1 | \
    grep -A5 "Server certificate"

Server certificate
-----BEGIN CERTIFICATE-----
MIID+jCCA5+gAwIBAgIRAKScWO9psGAiEyjsqQ14hIUwCgYIKoZIzj0EAwIwOzEL
MAkGA1UEBhMCVVMxHjAcBgNVBAoTFUdvb2dsZSBUcnVzdCBTZXJ2aWNlczEMMAoG
A1UEAxMDV0UxMB4XDTI2MDUxMDIxMTQwNloXDTI2MDgwODIyMTQwMlowGTEXMBUG
A1UEAxMOY2xvdWRmbGFyZS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQy
2/xUV3ysZxMwyNx1cbiFMRfU90PryGBSNpT7byCs+89lVT0dqt7v5qcpycOIc9gS
...
-----END CERTIFICATE-----
subject=CN = cloudflare.com
issuer=C = US, O = Google Trust Services, CN = WE1

验证签名算法

1
2
3
4
5
6
7
8
9
# 详细查看证书信息
$ openssl x509 -in /dev/stdin -noout -text <<< "$(openssl s_client -connect cloudflare.com:443 -tls1_3 2>/dev/null | openssl x509)" | grep -A2 "Subject Public Key"

Subject Public Key Info:
    Public Key Algorithm: id-ecPublicKey
        Public-Key: (256 bit)
       ASN.1 OID: prime256v1
       NIST Curve: P-256
    Signature Algorithm: ecdsa-with-SHA256

手动模拟验证

虽然无法直接获取服务器的私钥进行验证,但可以用私钥模拟 CertificateVerify 的生成过程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 生成测试私钥和证书
$ openssl ecparam -genkey -prime256v1 -out test.key

# 生成自签名证书
$ openssl req -new -x509 -key test.key -out test.crt -days 365 \
    -subj "/CN=Test Server"

# 提取证书公钥
$ openssl x509 -in test.crt -noout -pubkey > test.pubkey

# 模拟签名过程(仅为示例)
$ echo "test transcript hash" | openssl dgst -sha256 -sign test.key

安全性分析

握手摘要的安全作用

  1. 防止重放攻击:每次握手生成不同的摘要,无法复用
  2. 防止中间人篡改:任何握手消息的修改都会导致摘要变化
  3. 前向安全:即使长期私钥泄露,历史握手仍安全

常见攻击向量

1. 证书私钥泄露

如果服务器私钥泄露,攻击者可以伪造 CertificateVerify。但现代证书通常有较短的有效期,降低风险。

2. 弱签名算法

TLS 1.3 已废弃弱算法:

不推荐使用 推荐使用
RSA-PKCS1 v1.5 RSA-PSS
ECDSA (SHA-1) ECDSA (SHA-256/384)
RSA (1024-bit) RSA (2048-bit+)

验证服务器配置

检查服务器是否使用安全的签名算法:

1
2
3
4
5
# 检查支持的签名算法
$ openssl s_client -connect cloudflare.com:443 -tls1_3 -showcerts 2>&1 | \
    openssl x509 -noout -text | grep "Signature Algorithm"

    Signature Algorithm: ecdsa-with-SHA256

排查 CertificateVerify 失败

常见错误

使用 Wireshark 抓包时,可能遇到以下错误:

1
2
CertificateVerify: Failed
Signature verification error

排查步骤

  1. 确认证书链完整
1
$ openssl s_client -connect example.com:443 -showcerts 2>&1
  1. 检查握手摘要

使用 Wireshark 过滤器:

1
tls.handshake.type == 15  // CertificateVerify
  1. 验证签名算法兼容性
1
2
3
# 指定签名算法连接
$ openssl s_client -connect example.com:443 -tls1_3 \
    -sigalgs "ECDSA+SHA256" 2>&1

与 TLS 1.2 的对比

特性 TLS 1.2 TLS 1.3
CertificateVerify 时机 证书之后、Finished 之前 证书之后、Finished 之前
签名输入 握手消息特定字段 完整握手摘要
传输加密 明文 加密(ServerHello 后已建立密钥)
支持的算法 RSA, DSA, ECDSA 仅强签名算法
0-RTT 支持 不支持 支持

实际配置建议

Nginx 配置

确保服务器使用强签名算法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
server {
    listen 443 ssl http2;
    server_name example.com;

    # 启用 TLS 1.3
    ssl_protocols TLSv1.3;

    # 优先使用强密码套件
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers on;

    # 证书和密钥
    ssl_certificate /path/to/server.crt;
    ssl_certificate_key /path/to/server.key;
}

验证配置

1
2
3
4
5
6
7
# 测试服务器签名算法
$ openssl s_client -connect example.com:443 -tls1_3 2>&1 | \
    grep "Peer signing"

# 使用指定算法测试
$ openssl s_client -connect example.com:443 -tls1_3 \
    -sigalgs "ECDSA+SHA256" 2>&1 | grep "Cipher"

总结

TLS 1.3 的 CertificateVerify 机制是握手中关键的安全环节:

  1. 握手摘要确保签名的上下文完整,防止篡改
  2. 加密传输CertificateVerify 消息,提升安全性
  3. 强签名算法要求(RSA-PSS、ECDSA)提升抗攻击能力

理解这一机制有助于:

  • 排查 TLS 握手故障
  • 正确配置服务器
  • 评估证书安全性

参考来源