TLS 实战与排错 — 抓包、openssl 与握手失败定位
Wireshark 抓包解密、openssl s_client 实战、证书链检查与握手失败排错 / 表示·安全层 / 随应用端口 / RFC 8446
这篇你能学到
- 用 Wireshark 把一次 TLS 握手看明白:过滤哪几条、重点看哪几个字段、怎么把加密流量解密成明文。
- 用
openssl s_client、curl、nmap、testssl.sh从命令行探测一个站点的协议版本、密码套件、证书链和 OCSP 状态。 - 遇到"证书过期"“域名不匹配"“handshake failure"这类报错时,按什么顺序定位、看哪个 Alert 编号、改哪一行配置。
抓包观察
Wireshark 显示过滤式
这些过滤式能干什么:在成千上万个包里把 TLS 相关的那部分挑出来——按握手阶段挑、按域名挑、按告警挑,是排错的第一步。
| 目的 | 过滤表达式 |
|---|---|
| 所有 TLS 流量 | tls |
| 只看握手消息 | tls.handshake |
| 只看 ClientHello | tls.handshake.type == 1 |
| 只看 ServerHello | tls.handshake.type == 2 |
| 只看证书消息 | tls.handshake.type == 11 |
| 按 SNI 过滤域名 | tls.handshake.extensions_server_name == "example.com" |
| 含 SNI 扩展的包 | tls.handshake.extensions_server_name |
| 看 ALPN 协商结果 | tls.handshake.extensions_alpn_str |
| 告警(握手失败必看) | tls.record.content_type == 21 或 tls.alert_message |
| 致命告警 | tls.alert_message.level == 2 |
| 应用数据记录 | tls.record.content_type == 23 |
| 指定 TLS 版本的记录 | tls.record.version == 0x0303 |
| 真实协商版本(1.3) | tls.handshake.extensions.supported_version == 0x0304 |
| 只看某端口的 TLS | tls && tcp.port == 8443 |
注意:Wireshark 3.0 起过滤器前缀由
ssl改为tls,老教程里的ssl.handshake需替换。
典型字段说明
抓到 ClientHello 后重点看四处:
- Version / supported_versions 扩展:
Record Layer里的0x0303是伪装值,展开Extension: supported_versions才能看到客户端真正支持TLS 1.3 (0x0304)。 - Cipher Suites:客户端提供的套件列表。若列表里全是
TLS_RSA_*(无 ECDHE),说明客户端很老且不支持前向保密。 - Extension: server_name (SNI):明文可见,是 CDN / Nginx 虚拟主机分流的依据。若这里为空(用 IP 直连),服务器只能返回默认站点证书,极易导致域名不匹配报错。
- Extension: application_layer_protocol_negotiation:
h2/http/1.1,决定后续走 HTTP/2 还是 HTTP/1.1。
ServerHello 中看 选定的 Cipher Suite 与 key_share 的 Named Group(x25519 最常见)。
解密 TLS 流量(调试神器)
对自己控制的客户端,可导出会话密钥后让 Wireshark 解密(无需服务器私钥,且对 ECDHE 有效):
这段命令能干什么:让浏览器或 curl 把每次会话用的密钥写到一个日志文件里,再把这个文件喂给 Wireshark,Wireshark 就能把加密流量还原成明文给你看。
命令行等价方式:
这条命令能干什么:不开图形界面,直接在终端里用密钥日志解密抓包文件,并只显示 HTTP/2 的内容。
| |
用服务器 RSA 私钥解密的老办法(
tls.keys_list)只对 RSA 密钥传输套件有效,对 ECDHE / TLS 1.3 完全无效——这正是前向保密的效果。
常用命令 / 配置
openssl s_client — 最常用的 TLS 探测工具
这组命令能干什么:像一个"手动浏览器"一样去和服务器握一次手,把证书链、协商结果、支持的版本和套件、OCSP 状态全都打印出来——服务端有没有配对,一试便知。
| |
证书本体检查
这组命令能干什么:不连网络,直接拆开手里的证书文件看内容——它是发给谁的、谁签的、什么时候过期、覆盖哪些域名,以及最容易出错的"证书和私钥是不是一对”。
| |
其他工具
这组命令能干什么:换几个角度做交叉验证——
curl看客户端视角的握手细节,nmap/testssl.sh批量枚举服务器支持的版本与套件并给安全评级,ss确认端口有没有人在听,最后一条常用来做证书到期监控。
| |
Nginx 推荐配置片段
这段配置能干什么:把一台 Nginx 调到"现代且安全"的 TLS 基线——只留 1.2/1.3、用 AEAD 套件、开 OCSP Stapling、发全链证书,并强制浏览器以后只走 HTTPS。
| |
常见故障与排错
结论先行:一眼看懂每个现象在说什么
certificate has expired—— 先记住结论:要么证书真过期了,要么某一端的系统时钟不对,两种可能都要查。unable to get local issuer certificate—— 先记住结论:服务器少发了中间 CA 证书,多半是配成了cert.pem而不是fullchain.pem。Hostname mismatch/NET::ERR_CERT_COMMON_NAME_INVALID—— 先记住结论:你访问的域名不在证书 SAN 里,或者客户端压根没发 SNI,服务器只好回默认站点证书。handshake failure (40)—— 先记住结论:双方没有共同的版本或密码套件,谈不拢就直接散场。protocol_version (70)—— 先记住结论:版本对不上,一端要求的最高/最低版本另一端不支持。unknown_ca (48)—— 先记住结论:是客户端不认这个根 CA,不是服务器证书本身坏了。bad_certificate (42)/ mTLS 失败 —— 先记住结论:问题出在客户端证书这一侧(缺失、过期或不被服务端 CA 链信任)。bad_record_mac (20)—— 先记住结论:加密后的数据在路上"对不上号"了,密钥不同步或中间有人动过手脚。inappropriate_fallback (86)—— 先记住结论:是客户端自己主动降级被拦下了,不是服务器的问题。- 首字节延迟高、TTFB 大 —— 先记住结论:每次都在做完整握手,会话复用或 OCSP Stapling 没生效。
close_notify缺失 / 连接被截断 —— 先记住结论:对端粗暴地 RST 断开了,没走优雅关闭流程。- 用 IP 访问报错,用域名正常 —— 先记住结论:这是预期行为,证书只对域名签发,IP 不在 SAN 中。
详细排查表(现象 / 可能原因 / 排查方法)
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
certificate has expired | 证书过期,或服务器/客户端系统时间不对 | openssl s_client ... | openssl x509 -noout -dates;同时 date -u 核对两端时间 |
unable to get local issuer certificate | 服务器只发了叶子证书,未下发中间 CA | openssl s_client -showcerts 看返回几张证书;Nginx 应配置 fullchain.pem 而非 cert.pem |
Hostname mismatch / NET::ERR_CERT_COMMON_NAME_INVALID | 访问的域名不在证书 SAN 里;或客户端未发 SNI,服务器返回了默认站点证书 | openssl x509 -ext subjectAltName;抓包确认 tls.handshake.extensions_server_name 是否存在 |
handshake failure (40) | 双方无共同的 TLS 版本或密码套件;服务器关了 1.0/1.1 而客户端太老 | nmap --script ssl-enum-ciphers;用 -tls1_2 / -tls1_3 逐个版本试 |
protocol_version (70) | 客户端提议的最高版本服务器不支持(或反之) | 检查 ssl_protocols 配置与客户端 OpenSSL 版本 |
unknown_ca (48) | 客户端信任库里没有该根 CA(内网自签 / 老系统缺 ISRG Root X1) | openssl verify -CAfile;更新 ca-certificates 包或显式 --cacert |
bad_certificate (42) / mTLS 失败 | 双向认证时客户端证书缺失、过期或不被服务端 CA 链信任 | 抓包看是否有 CertificateRequest;检查服务端 ssl_client_certificate |
bad_record_mac (20) | 密钥不同步、中间设备篡改、极少数为网卡 offload bug | 换网络路径复测;关闭中间盒 SSL 检查;ethtool -K eth0 tso off gso off 试排除 |
inappropriate_fallback (86) | 客户端主动降级重连,被 TLS_FALLBACK_SCSV 拦截 | 检查客户端是否实现了不必要的降级重试逻辑 |
| 首字节延迟高、TTFB 大 | 每次都完整握手,会话复用未生效;或 OCSP 未 Stapling 导致客户端另发查询 | 抓包看是否出现 NewSessionTicket 与 PSK 复用;-status 检查 Stapling |
close_notify 缺失 / 连接被截断 | 服务端直接 RST 关闭,未发优雅关闭告警 | 抓包确认最后是否为 TCP RST;多见于负载均衡超时踢连接 |
| 用 IP 访问报错,用域名正常 | 证书只对域名签发,IP 不在 SAN 中 | 属预期行为;确需 IP 访问要签发含 IP:x.x.x.x 的 SAN |
通用排查顺序:能否 TCP 连通(ss/telnet) → openssl s_client 看握手在哪一步断 → 看 Alert 编号定位类别 → 抓包对照 ClientHello / ServerHello 的版本与套件交集 → 单独校验证书链。
与其他协议对比
| 维度 | TLS | SSL 3.0 | IPSec | SSH |
|---|---|---|---|---|
| 工作层次 | 传输层之上(会话/表示层) | 同 TLS | 网络层(IP 层) | 应用层 |
| 保护范围 | 单条 TCP 连接的载荷 | 同 TLS | 整个 IP 包(含 IP 头,隧道模式) | 单条 SSH 会话及其转发通道 |
| 身份认证 | X.509 证书(PKI 体系) | X.509 证书 | 预共享密钥 / 证书 / EAP | 主机公钥指纹 + 用户公钥/口令 |
| 是否需应用改造 | 需要(库调用或 STARTTLS) | 需要 | 不需要,对应用透明 | 应用本身就是 SSH |
| 典型场景 | HTTPS、加密邮件、API | 已废弃(RFC 7568 禁用) | 站点到站点 VPN、远程接入 | 远程登录、端口转发、SFTP |
| NAT 友好性 | 好(就是普通 TCP) | 好 | 差(AH 不可用,ESP 需 NAT-T) | 好 |
| 现状 | TLS 1.3 为主流 | 禁用 | 广泛用于企业 VPN | 广泛用于运维 |
TLS 1.2 vs TLS 1.3 差异速览
| 维度 | TLS 1.2 (RFC 5246) | TLS 1.3 (RFC 8446) |
|---|---|---|
| 完整握手 RTT | 2-RTT | 1-RTT;恢复时 0-RTT |
| 密钥交换 | RSA 传输 / DHE / ECDHE 均可 | 仅 (EC)DHE 或 PSK,强制前向保密 |
| 密码套件数量 | 300+ 个,大量弱套件 | 仅 5 个,全为 AEAD |
| 已删除算法 | — | RSA 密钥传输、静态 DH、RC4、3DES、CBC、SHA-1、MD5、压缩、重协商、自定义 DH 组 |
| 证书是否加密 | 明文传输 | 加密传输(EncryptedExtensions 之后) |
| 密钥派生 | 自定义 PRF | 标准化 HKDF |
| 会话恢复 | Session ID + Session Ticket | 统一为 PSK + NewSessionTicket |
| ChangeCipherSpec | 真实使用 | 仅作中间设备兼容的"假消息” |
速查表 / 常见面试题
Alert 编号速查(tls.alert_message.desc)
| 编号 | 名称 | 常见含义 |
|---|---|---|
| 0 | close_notify | 正常优雅关闭 |
| 20 | bad_record_mac | 解密/完整性校验失败 |
| 40 | handshake_failure | 无共同参数(版本/套件/曲线) |
| 42 | bad_certificate | 证书损坏或不可用 |
| 44 | certificate_revoked | 证书已吊销 |
| 45 | certificate_expired | 证书过期 |
| 46 | certificate_unknown | 证书状态无法确定 |
| 48 | unknown_ca | 签发 CA 不受信任 |
| 49 | access_denied | 证书有效但被拒绝访问 |
| 51 | decrypt_error | 签名验证失败 |
| 70 | protocol_version | 版本不支持 |
| 80 | internal_error | 服务端内部错误 |
| 86 | inappropriate_fallback | 检测到不当降级 |
| 112 | unrecognized_name | SNI 指定的域名服务器不认识 |
常见面试题
TLS 握手为什么既要非对称又要对称加密? 非对称能在无预共享秘密的情况下解决身份认证与密钥协商,但运算慢(RSA 2048 签名比 AES 慢几个数量级);对称加密快但需要先有共享密钥。故用非对称"一次性"建立对称密钥,即混合密码体系。
什么是前向保密?TLS 1.3 如何保证? 长期私钥泄露后,历史流量仍不可解密。TLS 1.3 删除 RSA 密钥传输,强制 (EC)DHE:每次握手用一次性临时密钥对协商共享秘密,服务器长期私钥只用于签名,不参与密钥计算。
中间人(MITM)为什么无法伪造 HTTPS? 中间人可以转发 ClientHello、可以给出一张证书,但它无法为目标域名提供受信任 CA 签发的证书,也无法伪造
CertificateVerify签名(没有对应私钥)。企业 SSL 中间盒能"合法"解密,是因为在客户端信任库里预置了企业根 CA。SNI 是什么?为什么它是明文?有什么隐私问题? SNI 让客户端在握手最开始告诉服务器要访问哪个域名,从而实现一个 IP 托管多域名。它必须在服务器选证书之前发送,所以只能明文。这会泄露访问目标,ECH(Encrypted Client Hello)正是为解决此问题而设计。
0-RTT 为什么不安全? 0-RTT 数据用上一次会话派生的 PSK 加密,不具备前向保密,且服务端在完成握手前无法判断新鲜度,攻击者可重放该数据包。因此只能用于幂等请求。
TLS 1.3 的 ChangeCipherSpec 还有用吗? 协议层面无用,仅为"中间设备兼容模式"保留——很多老防火墙看到握手后立刻出现加密数据会误判丢包,发一个假的 CCS 能让它们放行。
fullchain 与 cert 的区别?
cert.pem只含叶子证书;fullchain.pem= 叶子 + 中间 CA。服务器必须发送中间 CA,否则不含该中间证书的客户端会报unable to get local issuer certificate。根证书无需发送(客户端本地已有)。为什么 TLS 1.3 只剩 5 个密码套件? 1.2 的套件把"密钥交换+认证+加密+MAC"四件事编在一个名字里,组合爆炸且含大量弱算法。1.3 把密钥交换与签名算法拆到独立扩展协商,套件只描述 AEAD + 哈希,并只保留经过审查的 AEAD 算法。