前言
当你在浏览器地址栏输入 https:// 开头的网址时,TCP 三次握手完成后,紧接着就是 TLS 握手。这一过程在几百毫秒内完成,却承担着身份验证、密钥协商、加密通信三重任务。本文深入剖析 TLS 1.2 和 TLS 1.3 的握手细节,揭开从明文到加密通道的全过程。
TLS 握手概览
一、TLS 协议栈
1.1 TLS 在网络协议中的位置
+-----------------------------------+| Application | HTTP, FTP, SMTP+-----------------------------------+| TLS | 安全层+-----------------+-----------------+| Handshake | Record || Protocol | Protocol |+-----------------+-----------------+| TCP | 传输层+-----------------------------------+| IP | 网络层+-----------------------------------+1.2 TLS 协议组成
| 协议 | 用途 | 端口号 |
|---|---|---|
| Handshake | 协商密钥和参数 | - |
| ChangeCipherSpec | 通知启用加密 | - |
| Alert | 错误和警告通知 | - |
| Record | 数据分片和加密传输 | - |
| Application Data | 加密的应用数据 | 443 |
1.3 TLS Record 结构
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| ContentType | Version |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| Length |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| Fragment (plaintext) |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ContentType 取值:
| 值 | 类型 |
|---|---|
| 20 | ChangeCipherSpec |
| 21 | Alert |
| 22 | Handshake |
| 23 | Application Data |
二、TLS 1.2 完整握手流程
2.1 第一次往返:协商参数
2.2 ClientHello 详解
// ClientHello 消息结构const clientHello = { clientVersion: 0x0303, // TLS 1.2 random: { gmt_unix_time: 1711065600, random_bytes: "28字节的随机数", }, session_id: "", // 首次连接为空 cipher_suites: [ 0xC02F, // TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 0xC030, // TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 0xC013, // TLS_RSA_WITH_AES_128_CBC_SHA 0xC014, // TLS_RSA_WITH_AES_256_CBC_SHA ], compression_methods: [0x00], // 无压缩 extensions: { server_name: "example.com", ec_point_formats: [0x00], // uncompressed elliptic_curves: [0x001D, 0x0017, 0x0018], // x25519, secp256r1, secp384r1 signature_algorithms: [ { hash: "SHA256", sign: "RSA" }, { hash: "SHA384", sign: "RSA" }, { hash: "SHA256", sign: "ECDSA" }, ], session_ticket: "", // 空表示支持但不持有 },};ClientHello 抓包示例:
TLSv1.2 ClientHello Version: TLS 1.2 (0x0303) Random: gmt_unix_time: Mar 22, 2026 08:00:00.000000000 CST random_bytes: a3f2b8c1d4e5... Session ID Length: 0 Cipher Suites (4 suites) TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_RSA_WITH_AES_128_CBC_SHA TLS_RSA_WITH_AES_256_CBC_SHA Extensions: server_name: example.com supported_groups: x25519, secp256r1, secp384r1 signature_algorithms: rsa_pkcs1_sha256, ecdsa_secp256r1_sha2562.3 ServerHello 详解
ServerHello 消息:
TLSv1.2 ServerHello Version: TLS 1.2 (0x0303) Random: gmt_unix_time: Mar 22, 2026 08:00:00.100000000 CST random_bytes: 7e3a1b9c2d8f... Session ID Length: 32 Session ID: 5a6b7c8d9e0f... Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xC02F) Compression Method: null (0x00) Extensions: renegotiation_info: (empty) ec_point_formats: uncompressed2.4 证书与密钥交换
证书验证链:
根 CA 证书(预装在操作系统/浏览器) └── 中间 CA 证书 └── 服务器证书(example.com)
验证步骤:1. 服务器证书的签发者 == 中间 CA 的主题2. 中间 CA 证书的签发者 == 根 CA 的主题3. 根 CA 证书在信任库中4. 所有证书在有效期内5. 证书未被吊销(CRL/OCSP)2.5 密钥生成过程
// TLS 1.2 ECDHE 密钥生成流程// 1. 密钥交换const serverECDH = generateECDHKeyPair("x25519");// 服务端发送公钥 serverECDH.pubKey
const clientECDH = generateECDHKeyPair("x25519");// 客户端发送公钥 clientECDH.pubKey
const preMasterSecret = computeSharedSecret( clientECDH.privKey, serverECDH.pubKey);
// 2. 生成 Master Secretconst masterSecret = PRF( preMasterSecret, "master secret", clientRandom + serverRandom)[0..47]; // 48 字节
// 3. 扩展为密钥材料const keyBlock = PRF( masterSecret, "key expansion", serverRandom + clientRandom);
// 4. 切分密钥材料const clientWriteMACKey = keyBlock[0..19]; // 20 字节 HMAC-SHA1const serverWriteMACKey = keyBlock[20..39];const clientWriteKey = keyBlock[40..55]; // 16 字节 AES-128const serverWriteKey = keyBlock[56..71];const clientWriteIV = keyBlock[72..79]; // 8 字节(GCM 则为 4 字节 nonce)const serverWriteIV = keyBlock[80..87];2.6 Finished 消息
verify_data 的计算:
// verify_data 验证握手完整性const verifyData = PRF( masterSecret, "client finished", Hash(handshakeMessages))[0..11]; // 12 字节
// Hash 包含所有握手消息(不含 ChangeCipherSpec)// 任何中间人篡改都会导致 verify_data 不匹配三、密码套件详解
3.1 密码套件命名规则
TLS_<密钥交换算法>_<身份验证算法>_WITH_<加密算法>_<MAC算法>
示例:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 ├─────┤ ├───┤ ├──┤ ├───┤ │ │ │ │ 密钥交换 认证 加密 哈希/AEAD3.2 各组件详解
| 组件 | 可选值 | 说明 |
|---|---|---|
| 密钥交换 | RSA, DHE, ECDHE, PSK | 如何协商预主密钥 |
| 身份验证 | RSA, ECDSA, DSS | 证书签名算法 |
| 加密算法 | AES-128, AES-256, CHACHA20 | 对称加密 |
| MAC/AEAD | SHA256, SHA384, GCM, POLY1305 | 消息认证 |
3.3 常见密码套件对比
| 密码套件 | 安全等级 | 性能 | PFS |
|---|---|---|---|
| TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 | 推荐 | 高 | 是 |
| TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 | 推荐 | 中 | 是 |
| TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 | 推荐 | 高 | 是 |
| TLS_RSA_WITH_AES_128_CBC_SHA | 不安全 | 高 | 否 |
| TLS_RSA_WITH_AES_256_CBC_SHA | 不安全 | 中 | 否 |
四、密钥交换算法
4.1 RSA 密钥交换
RSA 密钥交换的致命缺陷:没有前向保密。服务器私钥一旦泄露,所有历史流量都可被解密。
攻击场景:1. 攻击者录制所有加密流量2. 一年后服务器私钥泄露3. 攻击者用私钥解密所有历史流量中的预主密钥4. 由预主密钥推导出会话密钥5. 解密所有历史通信内容4.2 DHE 密钥交换
// Diffie-Hellman 密钥交换// 公共参数:p(大素数), g(生成元)
// 服务端const serverPrivate = randomBigInt(); // aconst serverPublic = pow(g, serverPrivate) % p; // g^a mod p
// 客户端const clientPrivate = randomBigInt(); // bconst clientPublic = pow(g, clientPrivate) % p; // g^b mod p
// 双方计算共享密钥const sharedSecret_server = pow(clientPublic, serverPrivate) % p; // (g^b)^a mod pconst sharedSecret_client = pow(serverPublic, clientPrivate) % p; // (g^a)^b mod p// g^(ab) mod p == g^(ba) mod pDHE 参数交换:
ServerKeyExchange 消息(DHE): dh_p: 大素数 p(2048 bits) dh_g: 生成元 g(通常为 2) dh_Ys: 服务端公钥 g^a mod p signature: 用服务器私钥对上述参数的签名4.3 ECDHE 密钥交换
常用椭圆曲线:
| 曲线名称 | 位长 | 安全等级 | 备注 |
|---|---|---|---|
| x25519 | 255 | 128-bit | 推荐,速度快 |
| secp256r1 | 256 | 128-bit | 也称 P-256 |
| secp384r1 | 384 | 192-bit | 也称 P-384 |
| secp521r1 | 521 | 256-bit | 也称 P-521 |
// Go 标准库 ECDHE 密钥交换// 参考: https://github.com/golang/go/blob/go1.25.0/src/crypto/tls/key_agreement.go
func (ka *ecdheKeyAgreement) generateServerKeyShare(config *Config, version uint16) error { // 选择曲线 curveID := ka.curveID // 生成临时密钥对 key, err := generateECDHEKey(curveID) if err != nil { return err } ka.key = key return nil}
func (ka *ecdheKeyAgreement) processClientKeyShare(curveID CurveID, clientPublic []byte) ([]byte, error) { // 计算共享密钥 sharedKey, err := ka.key.ECDH(clientPublic) if err != nil { return nil, err } return sharedKey, nil}五、TLS 1.3 握手改进
5.1 TLS 1.3 vs TLS 1.2
5.2 TLS 1.3 1-RTT 握手
关键改进:
- ClientHello 携带 KeyShare:客户端提前猜测服务端支持的密钥交换参数
- ServerHello 后立即加密:服务端响应第一个报文后,后续全部加密
- 简化密码套件:只保留 AEAD 加密(GCM、CHACHA20-POLY1305)
5.3 TLS 1.3 0-RTT 握手
0-RTT 的安全限制:
| 特性 | 1-RTT | 0-RTT |
|---|---|---|
| 重放保护 | 是 | 否 |
| 前向保密 | 是 | 仅 Early Data |
| 适用请求 | 所有 | 幂等请求 |
| 连接唯一性 | 每次独立 | 依赖 PSK |
5.4 TLS 1.3 密码套件
TLS 1.3 大幅简化了密码套件,移除了不安全的算法:
TLS 1.3 支持的密码套件: TLS_AES_128_GCM_SHA256 TLS_AES_256_GCM_SHA384 TLS_CHACHA20_POLY1305_SHA256 TLS_AES_128_CCM_SHA256 TLS_AES_128_CCM_8_SHA256
移除的算法: RC4(已破解) 3DES(过时) CBC 模式(易受填充 oracle 攻击) RSA 密钥交换(无 PFS) MD5/SHA-1(碰撞攻击) DHE(性能差,1024-bit 不安全) 静态 ECDH(无 PFS)六、完美前向保密(PFS)
6.1 PFS 原理
6.2 临时密钥的生命周期
连接 1: ECDHE 密钥对 A → 共享密钥 K1(会话结束后销毁 A)连接 2: ECDHE 密钥对 B → 共享密钥 K2(会话结束后销毁 B)连接 3: ECDHE 密钥对 C → 共享密钥 K3(会话结束后销毁 C)
即使 K1 泄露,K2 和 K3 不受影响。即使服务器长期私钥泄露,K1/K2/K3 均无法推导。七、会话恢复
7.1 Session ID
Session ID 的限制:
- 服务端必须存储所有会话状态
- 集群环境下需要共享会话缓存
- 内存开销随并发连接数线性增长
7.2 Session Ticket
// Go 标准库 Session Ticket 处理// 参考: https://github.com/golang/go/blob/go1.25.0/src/crypto/tls/ticket.go
type sessionState struct { version uint16 cipherSuite uint16 createdAt uint64 masterSecret []byte certificates [][]byte}
func (c *Conn) encryptTicket(state []byte) ([]byte, error) { // 使用服务端配置的密钥加密 Ticket encrypted := aesGCMSeal(c.config.SessionTicketKey, state) return encrypted, nil}
func (c *Conn) decryptTicket(encrypted []byte) ([]byte, error) { // 解密 Ticket 恢复会话状态 state, err := aesGCMOpen(c.config.SessionTicketKey, encrypted) if err != nil { return nil, err // 解密失败,回退到完整握手 } return state, nil}7.3 TLS 1.3 PSK
// TLS 1.3 Pre-Shared Key// 服务端发送 NewSessionTicket 消息
const newSessionTicket = { ticket_lifetime: 7200, // 2 小时 ticket_age_add: 0x3A7B2C1D, // 随机偏移量(防客户端指纹) ticket_nonce: Buffer.from("01"), ticket: Buffer.from("加密的会话数据"), extensions: {},};
// 客户端恢复时发送 pre_shared_key 扩展const clientHello = { // ... 其他字段 extensions: { pre_shared_key: { identities: [ { identity: newSessionTicket.ticket, obfuscated_ticket_age: (Date.now() - ticketReceivedTime + newSessionTicket.ticket_age_add) % 0xFFFFFFFF, }, ], binders: [ hmac(psk, "ClientHello 的哈希前缀"), // 证明拥有 PSK ], }, },};7.4 会话恢复方式对比
| 方式 | 服务端状态 | 集群支持 | 安全性 | TLS 版本 |
|---|---|---|---|---|
| Session ID | 有状态 | 需要共享 | 中 | 1.2 |
| Session Ticket | 无状态 | 天然支持 | 高 | 1.2 |
| PSK (TLS 1.3) | 无状态 | 天然支持 | 最高 | 1.3 |
八、安全性分析
8.1 常见攻击与防御
| 攻击名称 | 目标 | 原理 | 防御 |
|---|---|---|---|
| BEAST | TLS 1.0 | CBC 模式 IV 可预测 | 使用 TLS 1.1+ |
| POODLE | SSL 3.0 | CBC 填充 oracle | 禁用 SSL 3.0 |
| Heartbleed | OpenSSL | 心跳扩展长度校验缺失 | 修复 OpenSSL |
| Logjam | DHE 512-bit | 强制降级到弱 DHE 参数 | 使用 2048+ bit DHE |
| Sweet32 | 3DES/CBC | 64-bit 分组碰撞 | 禁用 3DES |
| ROBOT | RSA PKCS#1 v1.5 | RSA 填充 oracle 攻击 | 禁用 RSA 密钥交换 |
| DROWN | SSLv2 | 跨协议攻击 RSA | 禁用 SSLv2 |
8.2 BEAST 攻击原理
8.3 降级攻击防御
// TLS 1.3 降级保护机制// ServerHello.random 的最后 8 字节会被特殊标记
function detectDowngrade(serverHello, supportedVersion) { const last8Bytes = serverHello.random.slice(24, 32);
if (supportedVersion === 0x0304) { // 客户端支持 TLS 1.3 // 但服务端协商了 TLS 1.2 if (serverHello.version === 0x0303) { // 检查是否有降级标记 const downgradeSentinel = Buffer.from("44 4F 57 4E 47 52 44 01", "hex"); if (last8Bytes.equals(downgradeSentinel)) { // 服务端主动降级,合法 return; } // 攻击者强制降级,断开连接 throw new Error("降级攻击检测"); } }}九、性能优化
9.1 OCSP Stapling
OCSP Stapling 的好处:
- 客户端不需要额外请求 OCSP 服务器
- 减少握手延迟(节省一次网络往返)
- 避免隐私泄露(CA 不知道客户端访问了哪些站点)
# Nginx 启用 OCSP Staplingssl_stapling on;ssl_stapling_verify on;ssl_trusted_certificate /path/to/chain.pem;resolver 8.8.8.8 8.8.4.4 valid=300s;9.2 False Start
False Start 条件:
- 密码套件必须支持前向保密(ECDHE)
- 必须使用 AEAD 加密(GCM/CHACHA20)
- 服务端证书必须可信
9.3 优化建议
# Nginx TLS 优化配置
# 使用 TLS 1.2 和 1.3ssl_protocols TLSv1.2 TLSv1.3;
# 优先使用服务端密码套件顺序ssl_prefer_server_ciphers on;
# 推荐密码套件ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305';
# 启用 Session Ticketssl_session_tickets on;ssl_session_timeout 1d;ssl_session_cache shared:SSL:50m;
# 启用 OCSP Staplingssl_stapling on;ssl_stapling_verify on;
# 启用 0-RTT (TLS 1.3)ssl_early_data on;十、调试与诊断
10.1 使用 OpenSSL 测试
# 测试 TLS 握手openssl s_client -connect example.com:443 -tls1_2
# 查看 TLS 1.3 握手openssl s_client -connect example.com:443 -tls1_3
# 查看证书链openssl s_client -connect example.com:443 -showcerts
# 测试特定密码套件openssl s_client -connect example.com:443 \ -cipher 'ECDHE-RSA-AES128-GCM-SHA256'
# 测试 OCSP Staplingopenssl s_client -connect example.com:443 -status10.2 使用 tcpdump 抓包
# 抓取 TLS 握手tcpdump -i eth0 'tcp port 443' -w tls_handshake.pcap
# 只看 ClientHello 和 ServerHellotshark -r tls_handshake.pcap \ -Y "tls.handshake.type == 1 || tls.handshake.type == 2"
# 查看协商的密码套件tshark -r tls_handshake.pcap \ -Y "tls.handshake.type == 2" \ -T fields -e tls.cipher_suite10.3 使用 ssldiagnose
# 检查服务器 TLS 配置nmap --script ssl-enum-ciphers -p 443 example.com
# 使用 testssl.shtestssl.sh example.com
# 在线检测# https://www.ssllabs.com/ssltest/FAQ
Q1: TLS 和 SSL 的关系是什么?
SSL(Secure Sockets Layer)是 TLS 的前身。SSL 3.0 之后,协议被 IETF 接管并重命名为 TLS 1.0。目前 SSL 已完全废弃,所有现代浏览器都使用 TLS。但由于历史原因,很多人仍习惯性地将 TLS 称为 SSL。
Q2: HTTPS 一定安全吗?
HTTPS 只保证传输过程加密和服务器身份验证,不保证以下内容:
- 服务器本身是否可信(钓鱼网站也可以有合法证书)
- 客户端是否安全(恶意软件可能绕过 TLS)
- 数据存储是否安全(服务器收到明文后如何处理)
Q3: TLS 1.3 为什么移除了 RSA 密钥交换?
RSA 密钥交换没有前向保密(PFS)。如果服务器私钥泄露,攻击者可以解密所有用该密钥交换的历史流量。ECDHE 每次连接使用临时密钥对,即使长期私钥泄露也不会影响历史会话。
Q4: 自签名证书和 CA 签名证书有什么区别?
加密能力相同,区别在于信任链。CA 签名证书由受信任的证书颁发机构签名,浏览器会自动信任。自签名证书需要用户手动确认信任,适合内部使用,不适合公开服务。
Q5: 0-RTT 有什么安全风险?
0-RTT 的 Early Data 可能遭受重放攻击。攻击者截获 0-RTT 数据后可以重新发送,导致服务端重复执行非幂等操作(如支付请求)。因此 0-RTT 只适合幂等请求,或需要应用层额外做防重放处理。
Q6: 证书链验证为什么很重要?
证书链从服务器证书追溯到根 CA。如果验证不完整,攻击者可以伪造证书冒充目标服务器。2011 年 DigiNotar CA 被入侵后签发了大量伪造证书,导致 Google、Yahoo 等网站的流量被中间人截获。
参考资料
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






