TLS 实战与排错 — 抓包、openssl 与握手失败定位

Wireshark 抓包解密、openssl s_client 实战、证书链检查与握手失败排错 / 表示·安全层 / 随应用端口 / RFC 8446

这篇你能学到

  • 用 Wireshark 把一次 TLS 握手看明白:过滤哪几条、重点看哪几个字段、怎么把加密流量解密成明文。
  • openssl s_clientcurlnmaptestssl.sh 从命令行探测一个站点的协议版本、密码套件、证书链和 OCSP 状态。
  • 遇到"证书过期"“域名不匹配"“handshake failure"这类报错时,按什么顺序定位、看哪个 Alert 编号、改哪一行配置。

抓包观察

Wireshark 显示过滤式

这些过滤式能干什么:在成千上万个包里把 TLS 相关的那部分挑出来——按握手阶段挑、按域名挑、按告警挑,是排错的第一步。

目的过滤表达式
所有 TLS 流量tls
只看握手消息tls.handshake
只看 ClientHellotls.handshake.type == 1
只看 ServerHellotls.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 == 21tls.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
只看某端口的 TLStls && 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_negotiationh2 / http/1.1,决定后续走 HTTP/2 还是 HTTP/1.1。

ServerHello 中看 选定的 Cipher Suitekey_share 的 Named Groupx25519 最常见)。

解密 TLS 流量(调试神器)

自己控制的客户端,可导出会话密钥后让 Wireshark 解密(无需服务器私钥,且对 ECDHE 有效):

这段命令能干什么:让浏览器或 curl 把每次会话用的密钥写到一个日志文件里,再把这个文件喂给 Wireshark,Wireshark 就能把加密流量还原成明文给你看。

1
2
3
4
5
6
7
# 1. 让浏览器/curl 导出密钥日志
export SSLKEYLOGFILE=/tmp/sslkeys.log
curl https://example.com # 或启动 Chrome / Firefox

# 2. Wireshark → 编辑 → 首选项 → Protocols → TLS
# → (Pre)-Master-Secret log filename 填 /tmp/sslkeys.log
# 3. 重新加载抓包文件,即可看到解密后的 HTTP 明文

命令行等价方式:

这条命令能干什么:不开图形界面,直接在终端里用密钥日志解密抓包文件,并只显示 HTTP/2 的内容。

1
tshark -r capture.pcapng -o tls.keylog_file:/tmp/sslkeys.log -Y http2

用服务器 RSA 私钥解密的老办法(tls.keys_list只对 RSA 密钥传输套件有效,对 ECDHE / TLS 1.3 完全无效——这正是前向保密的效果。

常用命令 / 配置

openssl s_client — 最常用的 TLS 探测工具

这组命令能干什么:像一个"手动浏览器"一样去和服务器握一次手,把证书链、协商结果、支持的版本和套件、OCSP 状态全都打印出来——服务端有没有配对,一试便知。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 基本握手,查看证书链与协商结果(-servername 必须带,否则无 SNI)
openssl s_client -connect example.com:443 -servername example.com

# 只输出证书链摘要,不进入交互
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates -ext subjectAltName

# 强制指定版本,用于验证服务器是否还支持老协议
openssl s_client -connect example.com:443 -servername example.com -tls1_2
openssl s_client -connect example.com:443 -servername example.com -tls1_3
openssl s_client -connect example.com:443 -tls1_1 # 应当失败(已弃用)

# 指定密码套件测试
openssl s_client -connect example.com:443 -cipher 'ECDHE-RSA-AES128-GCM-SHA256' # TLS1.2
openssl s_client -connect example.com:443 -ciphersuites TLS_AES_256_GCM_SHA384 # TLS1.3

# 显示完整证书链(含中间证书)并校验
openssl s_client -connect example.com:443 -servername example.com -showcerts -verify_return_error

# 查看 OCSP Stapling 是否开启
openssl s_client -connect example.com:443 -servername example.com -status </dev/null 2>&1 | grep -A3 'OCSP response'

# STARTTLS 类协议(邮件、数据库)
openssl s_client -connect smtp.example.com:587 -starttls smtp
openssl s_client -connect imap.example.com:143 -starttls imap

证书本体检查

这组命令能干什么:不连网络,直接拆开手里的证书文件看内容——它是发给谁的、谁签的、什么时候过期、覆盖哪些域名,以及最容易出错的"证书和私钥是不是一对”。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 查看本地证书文件全部信息
openssl x509 -in server.crt -noout -text

# 只看关键项
openssl x509 -in server.crt -noout -subject -issuer -dates -serial
openssl x509 -in server.crt -noout -ext subjectAltName,keyUsage,extendedKeyUsage

# 校验证书与私钥是否匹配(两个 md5 必须相同)
openssl x509 -noout -modulus -in server.crt | openssl md5
openssl rsa -noout -modulus -in server.key | openssl md5

# 用指定 CA 校验证书链
openssl verify -CAfile ca-chain.crt server.crt

# 生成自签证书(测试用,含 SAN)
openssl req -x509 -newkey rsa:2048 -nodes -keyout test.key -out test.crt -days 365 \
  -subj "/CN=test.local" -addext "subjectAltName=DNS:test.local,DNS:*.test.local"

其他工具

这组命令能干什么:换几个角度做交叉验证——curl 看客户端视角的握手细节,nmap / testssl.sh 批量枚举服务器支持的版本与套件并给安全评级,ss 确认端口有没有人在听,最后一条常用来做证书到期监控。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# curl 查看握手细节
curl -v --tlsv1.3 https://example.com
curl -vI https://example.com 2>&1 | grep -E 'SSL|TLS|subject|issuer|ALPN'
curl --resolve example.com:443:1.2.3.4 -v https://example.com # 绕过 DNS 测指定后端

# nmap 枚举服务器支持的协议版本与套件(含安全评级 A~F)
nmap --script ssl-enum-ciphers -p 443 example.com
nmap --script ssl-cert -p 443 example.com

# 查看谁在监听 443
ss -ltnp | grep ':443'

# testssl.sh:一站式合规扫描(开源脚本)
./testssl.sh --protocols --ciphers --vulnerable https://example.com

# 查看证书有效期还剩多少天(监控脚本常用)
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -checkend 604800 && echo "7天内不会过期"

Nginx 推荐配置片段

这段配置能干什么:把一台 Nginx 调到"现代且安全"的 TLS 基线——只留 1.2/1.3、用 AEAD 套件、开 OCSP Stapling、发全链证书,并强制浏览器以后只走 HTTPS。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
ssl_protocols TLSv1.2 TLSv1.3; # 关闭 1.0/1.1
ssl_prefer_server_ciphers off; # TLS1.3 下应由客户端优选
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off; # 关闭票据以保证前向保密
ssl_stapling on; # OCSP Stapling
ssl_stapling_verify on;
ssl_certificate /etc/nginx/fullchain.pem; # 必须是「叶子+中间」全链
ssl_certificate_key /etc/nginx/privkey.pem;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

常见故障与排错

结论先行:一眼看懂每个现象在说什么

  • 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服务器只发了叶子证书,未下发中间 CAopenssl 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 的版本与套件交集单独校验证书链

与其他协议对比

维度TLSSSL 3.0IPSecSSH
工作层次传输层之上(会话/表示层)同 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)
完整握手 RTT2-RTT1-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

编号名称常见含义
0close_notify正常优雅关闭
20bad_record_mac解密/完整性校验失败
40handshake_failure无共同参数(版本/套件/曲线)
42bad_certificate证书损坏或不可用
44certificate_revoked证书已吊销
45certificate_expired证书过期
46certificate_unknown证书状态无法确定
48unknown_ca签发 CA 不受信任
49access_denied证书有效但被拒绝访问
51decrypt_error签名验证失败
70protocol_version版本不支持
80internal_error服务端内部错误
86inappropriate_fallback检测到不当降级
112unrecognized_nameSNI 指定的域名服务器不认识

常见面试题

  1. TLS 握手为什么既要非对称又要对称加密? 非对称能在无预共享秘密的情况下解决身份认证与密钥协商,但运算慢(RSA 2048 签名比 AES 慢几个数量级);对称加密快但需要先有共享密钥。故用非对称"一次性"建立对称密钥,即混合密码体系。

  2. 什么是前向保密?TLS 1.3 如何保证? 长期私钥泄露后,历史流量仍不可解密。TLS 1.3 删除 RSA 密钥传输,强制 (EC)DHE:每次握手用一次性临时密钥对协商共享秘密,服务器长期私钥只用于签名,不参与密钥计算。

  3. 中间人(MITM)为什么无法伪造 HTTPS? 中间人可以转发 ClientHello、可以给出一张证书,但它无法为目标域名提供受信任 CA 签发的证书,也无法伪造 CertificateVerify 签名(没有对应私钥)。企业 SSL 中间盒能"合法"解密,是因为在客户端信任库里预置了企业根 CA。

  4. SNI 是什么?为什么它是明文?有什么隐私问题? SNI 让客户端在握手最开始告诉服务器要访问哪个域名,从而实现一个 IP 托管多域名。它必须在服务器选证书之前发送,所以只能明文。这会泄露访问目标,ECH(Encrypted Client Hello)正是为解决此问题而设计。

  5. 0-RTT 为什么不安全? 0-RTT 数据用上一次会话派生的 PSK 加密,不具备前向保密,且服务端在完成握手前无法判断新鲜度,攻击者可重放该数据包。因此只能用于幂等请求。

  6. TLS 1.3 的 ChangeCipherSpec 还有用吗? 协议层面无用,仅为"中间设备兼容模式"保留——很多老防火墙看到握手后立刻出现加密数据会误判丢包,发一个假的 CCS 能让它们放行。

  7. fullchain 与 cert 的区别? cert.pem 只含叶子证书;fullchain.pem = 叶子 + 中间 CA。服务器必须发送中间 CA,否则不含该中间证书的客户端会报 unable to get local issuer certificate。根证书无需发送(客户端本地已有)。

  8. 为什么 TLS 1.3 只剩 5 个密码套件? 1.2 的套件把"密钥交换+认证+加密+MAC"四件事编在一个名字里,组合爆炸且含大量弱算法。1.3 把密钥交换与签名算法拆到独立扩展协商,套件只描述 AEAD + 哈希,并只保留经过审查的 AEAD 算法。