最近调一个国密SSL的联调问题,页面卡在握手阶段过不去,我习惯性打开Wireshark抓包分析,结果迎面遇到两件让人愣住的事:Cipher Suite那一栏显示Unknown(未知套件),而Certificate消息里竟然躺着两张证书。标准TLS看多了,这套组合拳确实容易让人懵。后来硬是靠着逐字节翻报文、对着标准文档来回比对,才把整套GMTLS握手和国密证书链验证流程彻底跑通。这篇文章就把这套经验完整写出来,从抓包前的认知准备、Wireshark里的过滤和报文定位,到双证书导出与证书链验证,再到实际排查中踩过的坑,一次性讲清楚。适合正在做国密改造、安全测评,或者只是想把国密SSL抓包这件事弄明白的工程师。
1. 抓包前先建立坐标系:GMTLS与普通TLS的差异点在哪
1.1 GMTLS的本质:TLS 1.2框架里的中国商用密码组合
先说结论:国密SSL不是什么从零设计的全新协议,它本质上是沿用TLS 1.2的握手框架,把里面的公钥算法、哈希算法、对称加密算法整体换成国密算法家族。目录编号可以参考GM/T 0024系列以及GB/T 38636-2020《信息安全技术 传输层密码协议》,整套协议在密码学界经常被称作GMTLS。
对应关系很简单:
| 国密算法 | 作用 | 对标国际算法 |
|---|---|---|
| SM2 | 公钥加密、数字签名、密钥交换 | ECDSA/ECDH、RSA |
| SM3 | 消息摘要、签名散列 | SHA-256 |
| SM4 | 分组对称加密 | AES-128 |
在TLS握手层面,国密SSL主要用TLS_ECC_SM4_CBC_SM3这类套件,常见CipherSuite值是0xE013,部分实现里还能看到0xE014。0xE013这个段落在IANA的标准分配表里长期没有正式登记,所以Wireshark拿到手后不认识,显示Unknown是常态,不代表报文有问题。
理解这一点很重要:抓包分析时,整个TLS握手消息结构、状态机、记录层格式都和标准TLS 1.2一致,Wireshark能正常解析大部分握手字段。真正让工具有可能“翻白眼”的,是算法相关的字段,比如套件名、签名算法、证书里的公钥结构。所以分析时要有两手准备——视图能解析就看视图,视图解析不了就直接看十六进制。
1.2 双证书模型:为什么一个Certificate消息里会有两张证
普通TLS里,服务器下发的Certificate消息里通常是一条证书链:服务器证书、中间CA证书,根证书一般不主动下发。但国密SSL不一样,它采用的是“双证书”模型,服务器同时持有两张功能不同的证书:
- 签名证书:用于对ServerKeyExchange等握手消息做数字签名,完成身份认证。
- 加密证书:用于SM2密钥协商,保护后续会话密钥的生成。
这两张证书在TLS握手时一起放在同一个Certificate消息里,通过certificate_list下发。于是从Wireshark里看,就是同一个Handshake消息里连续出现两个Certificate条目。很多初接触的人会把“双证书”和“证书链”搞混,其实这是两个维度的事情:证书链体现的是“信任层级”,双证书体现的是“同一层级、不同用途”。抓包时看到的两个证书可能是同一条信任链上的两张叶子证书,比如由同一个CA签发,而不是“服务器证书+CA证书”的关系。
为什么要知道这个?因为后面导出证书时,你必须知道自己导出的是“签名证”还是“加密证”,否则验证时会把两张证书当作链上的上下级来处理,越验越迷糊。
1.3 先准备一个可复现的测试目标
如果手头没有现成的国密SSL站点,建议本地搭一个最小可复现环境。我常用的方式是借助GmSSL,它原生支持SM2/SM3/SM4集成的SSL栈,比标准OpenSSL方便。
先准备两套SM2密钥和证书。签名证书、加密证书各一份,生成命令大致是:
gmssl ecparam -genkey -name sm2p256v1 -out sign.key gmssl ecparam -genkey -name sm2p256v1 -out enc.key # 生成签名证书和加密证书(需自建CA或用GmSSL提供的样例CA) gmssl req -new -key sign.key -subj "/CN=test-sign" -out sign.csr gmssl req -new -key enc.key -subj "/CN=test-enc" -out enc.csr证书签发好后,用GmSSL自带的服务端直接起一个测试服务:
gmssl s_server -accept 443 -cert sign.crt -key sign.key -dcert enc.crt -dkey enc.key -www这里-dcert和-dkey就是用来指定第二张加密证书的参数。服务起来后,用浏览器或gmssl s_client访问,接下来就可以用Wireshark抓本机回环或者网卡流量了。本地测试环境的好处是能把“抓包—定位—导出—验证”整个闭环走通,不用依赖外部站点,适合反复练习。
2. 让Wireshark“开口说话”:抓取完整GMTLS握手的准备与过滤条件
2.1 选择正确的抓包位置与接口
抓包位置决定了你能看到什么。国密SSL有一个隐蔽的坑:很多单位在生产环境部署的是国密安全网关,网关靠近服务器的一侧可能已经转换成标准TLS,才会和后端应用通信。如果你把Wireshark挂在服务器侧网卡上,抓到的可能根本不是国密流量。一定要在真正承载国密SSL连接的客户端侧抓包,也就是国密浏览器或测试客户端所在的主机上抓。
接口选择上,Windows下用WLAN或以太网,如果浏览器和服务器跑在同一台机器上,就直接选Loopback回环接口。抓包前确认npcap驱动已经正确安装,别和旧版WinPcap混用,不然莫名其妙出现抓不到包、蓝屏一类的问题时,先怀疑驱动层。
抓包时长方面,国密握手本身只有几个报文,几十毫秒就结束了。我习惯先设置一个捕获过滤器,只抓和目标IP相关的443端口流量,避免把无关广播、DNS、后台流量全部塞进来:
host 192.0.2.10 and tcp port 443如果目标站点用了非443端口,把端口号换成实际值即可。这种捕获过滤器在驱动层就把不相关的包丢掉了,比事后用显示过滤器筛更省内存,也避免大pcap文件卡顿。
2.2 显示过滤器:精准捞出握手包
抓包完成后,定位握手包最快的方式是直接用显示过滤器。TLS1.2的握手消息类型里,ClientHello对应1,Certificate对应11,ServerHello对应2。所以这样过滤:
tls.handshake.type == 1 tls.handshake.type == 11老版本Wireshark里字段名可能是ssl.handshake.type,如果你用的版本比较陈旧,把tls换成ssl就能用。想快速看完整握手顺序,可以直接在某个ClientHello上右键,选择“Follow TLS Stream”,然后在弹出的流内容里按横条查看每个握手记录的序号。完整的国密握手,记录顺序大概是:
- ClientHello
- ServerHello
- Certificate(同时带两张证书)
- ServerKeyExchange
- ServerHelloDone
- ClientKeyExchange
- ChangeCipherSpec
- Finished
看到1、2、3基本就能确认抓到了核心部分。不用被Wireshark把TLS记录拆成多个TCP分片吓到,在Packet Details里看到“TLS segment of a reassembled PDU”这类提示时,说明Wireshark已经帮你重组了应用层记录。
2.3 会话复用:为什么抓不到Certificate
很多人在国密站点上抓包,发现只有ClientHello和ServerHello,后面直接跳到ChangeCipherSpec,怎么都找不到Certificate消息。这种情况八成是TLS会话复用(Session Resumption)。
TLS握手在第一次连接完成后,客户端和服务器会协商出一个会话票据或会话ID,后续短连接可以直接复用之前的会话参数,跳过完整握手。这时抓包看到的握手消息被大幅精简,自然没有Certificate和ServerKeyExchange。
对策很简单:
- 使用浏览器的无痕/隐私模式,它们一般不复用旧会话。
- 抓包前先关闭相关标签页,再开新标签页访问。
- 如果还是不行,可以在Wireshark里定位到ClientHello后,右键发送RST复位连接(有些工具支持),或者等服务端会话超时。
- 也可以在ClientHello消息里查看扩展项,如果有session_ticket扩展且内容是空的,说明这是一个全新的会话握手,能抓到完整流程。
会话复用问题在国密SSL联调里出现频率极高,因为很多应用长连接复用同一个TLS会话,导致你抓了几百个包也找不到Certificate消息。记住这个排查顺序,能省下大量时间。
3. 逐包拆解ClientHello与ServerHello:识别“国密身份”的四处特征
3.1 Cipher Suite值:看到0xE013就别慌
定位到ClientHello后,展开协议树里的Secure Sockets Layer,依次看TLSv1.2 Record Layer、Handshake Protocol: Client Hello,往下找Cipher Suites字段。Wireshark会把客户端支持的所有套件列成一个列表,如果目标启用国密,列表里大概率会看到:
Cipher Suite: Unknown (0xe013)这个0xE013就是TLS_ECC_SM4_CBC_SM3,也就是SM2做密钥协商、SM4做对称加密、SM3做PRF哈希的国密套件。部分新一点的洞洞里还会出现0xE014。
看到“Unknown”时先稳住,这不是Wireshark识别失败,而是它没有这个套件的注册信息。你可以直接在Packet Bytes视图里搜索十六进制字节e0 13,确认套件值确实在报文里。ServerHello里服务器最终选中的套件如果也是0xE013,那么基本可以断定这条连接走的就是国密SSL。
3.2 扩展字段里的“身份标记”:SM2曲线与签名算法
除套件之外,国密SSL的ClientHello扩展字段里还有几个固定“身份标记”。重点关注supported_groups和signature_algorithms这两个扩展:
- supported_groups中如果出现了sm2p256v1曲线,其OID是1.2.156.10197.1.301,而标准的NIST P-256曲线OID是1.2.840.10045.3.1.7,两者完全不一样。
- signature_algorithms里如果包含SM2签名算法或相关哈希组合,也可以佐证这是国密握手。
- 不同网关实现可能扩展字段略有差异,但“国密套件+双证书”这两个特征同时出现时,基本不会误判。
3.3 只靠视图不够用时,用十六进制补漏
万一碰上比较老的Wireshark版本,对TLS握手的解析比较粗糙,很多字段没有被单独列出来,这时候就得靠十六进制视图手动定位。我自己常用的锚点是这样的:
- 在Packet Details里点击Handshake Protocol报文头,下方Packet Bytes面板会高亮对应字节。
- TLS握手消息的结构是:第一个字节是Handshake Type,后面3个字节是长度,再往后是消息体。
- Certificate消息的类型值是0x0B,所以看到
0b 00 xx xx开头的握手消息,就是Certificate。 - 证书本身是DER编码的ASN.1结构,所有X.509证书都以字节
30 82开头,看到这个特征就说明从那里开始是一张完整的证书。
摘自实际抓包的一个ServerHello,在十六进制里搜索e0 13,能找到类似:
... 01 00 00 c0 e0 13 00 00 ...这段就表示ServerHello选择的CipherSuite是0xE013。用这种原始字节确认的方式,比依赖工具解析要可靠得多,关键是工具版本和解析器把你带偏时,还能用自己的眼睛兜底。
4. 双证书的分离与导出:把Wireshark里的X.509证书落成文件
4.1 用GUI复制证书字段的十六进制流
抓包分析到这一步,你已经看到Certificate消息里有两张证书了。接下来要做的,是把它们从pcap里“抠”出来,变成可独立验证的.der或.pem文件。
最简单的GUI操作:在Packet Details里展开Certificate消息,找到形如Certificate: 3082...的字段,右键该字段,选择“复制”,再选“作为十六进制流”。Wireshark会把当前字段的原始值以十六进制字符串形式复制到剪贴板,这个值就是从30 82开始的DER编码证书。
把它存成文本文件后,在Linux或Windows的WSL里执行:
xxd -r -p leaf.hex leaf.der然后马上用openssl看一眼是不是有效的DER证书:
openssl x509 -inform DER -in leaf.der -text -noout如果能看到证书的Subject、Issuer、Public Key等字段,说明导出成功了。注意复制时一定要点Certificate字段本身,而不是外层的certificate_list或者整个Handshake消息,否则会把长度前缀一起复制进来,导致openssl解析失败。DER开头必须是30 82,如果不是,基本就是选错字段了。
4.2 用tshark批量提取证书链(多个证书的推荐姿势)
如果Certificate消息里有两张证书,手工一条条复制也不是不行,但效率太低。我一般直接用tshark从pcap里批量提取,一条命令就把两张证书全部导出来:
tshark -r handshake.pcap -Y "tls.handshake.type == 11" -T fields -e tls.handshake.certificate > certs.hex这里-e tls.handshake.certificate会输出Certificate消息里每个证书的DER数据,格式是十六进制字符串,每个证书占一行。接下来把每行转成独立的.der文件:
n=0 while IFS= read -r line; do n=$((n+1)) echo "$line" | xxd -r -p > cert_$n.der done < certs.hex执行完目录下会有cert_1.der、cert_2.der,按服务器下发顺序对应两张证书。如果tshark输出为空,先排查显示过滤器字段名问题,可以运行tshark -G fields | grep -i "certificate"看看你手里这个版本里到底有哪些可用字段。Wireshark升级频繁,字段名偶尔会调整,不要死记硬背,学会自己查字段列表才是真技能。
4.3 确认导出结果:从字节特征到证书内容
导出后建议做一遍快速检查,避免后面验证时被脏数据浪费半天:
file cert_1.der openssl x509 -inform DER -in cert_1.der -noout -subject -issuerOpenSSL能正常输出subject和issuer,说明这个文件内容完整。再检查一下文件大小,一张SM2证书大概在700到1600字节之间,如果某个文件特别大,比如几十KB,那很可能把整个TLS记录或者多个证书连体导出来了。
到这一步,你已经拥有两张真实的国密证书,接下来才进入整篇文章的核心价值区:证书链验证。
5. 证书链验证:从根到叶查SM2签名与扩展项
5.1 先把DER转成PEM并检查关键字段
证书验证的第一步,是确认这张证书到底是什么证书。先把DER转成PEM格式,方便后续所有命令统一处理:
openssl x509 -inform DER -in cert_1.der -out cert_1.pem openssl x509 -in cert_1.pem -text -noout输出内容里有几个关键位置要特别看:
- Signature Algorithm这一栏,正常国密证书会显示SM2-with-SM3,对应OID是1.2.156.10197.1.501。
- Public Key Algorithm这一栏,如果是SM2公钥,通常能看到id-ecPublicKey,并附带sm2p256v1曲线参数,曲线OID是1.2.156.10197.1.301。
- Subject和Issuer字段,用来建立证书链关系。
- X509v3 extensions里的Key Usage和Basic Constraints,用来判断这张证书的用途和CA属性。
如果openssl版本较老或者编译时没带SM2支持,解析国密证书可能直接报unsupported signature algorithm。这时候别怀疑证书坏了,换GmSSL命令行工具再试:
gmssl x509 -in cert_1.pem -text -nooutGmSSL对国密算法的支持是最完整的,实测下来验证国密证书链的体验比标准OpenSSL顺畅很多。
5.2 手工确认信任路径并构建verify命令
证书链验证的本质,是沿着“叶子证书 → 中间CA证书 → 根证书”这条路径,逐级验证签名关系。手工确认时,最容易用的判断就是检查每张证书的Subject和Issuer字段能否首尾相接:
- 服务器证书的Issuer必须等于中间CA证书的Subject。
- 中间CA证书的Issuer必须等于根证书的Subject。
- 根证书的Subject和Issuer完全一致,因为它是自签的信任锚。
确认路径没有问题后,再执行实际的链验证命令:
openssl verify -CAfile root.pem -untrusted intermediate.pem leaf.pem注意几个细节:
-CAfile放的是根证书,也就是你的信任锚。-untrusted放的是中间CA证书和叶子证书之外的所有辅助证书,通常就是中间CA。- 叶子证书直接作为最后一个参数传进去。
- 不要把根证书塞进
-untrusted,否则openssl会报self-signed certificate in certificate chain,让你白折腾很久。
国密环境下建议用GmSSL执行同款命令:
gmssl verify -CAfile root.pem -untrusted intermediate.pem leaf.pem看到输出leaf.pem: OK,说明这条链走到根都是可信的。这里有个容易踩的坑:国密SSL的Certificate消息里下发的是“签名证书+加密证书”两张同级证书,而不是“服务器证书+中间CA”。如果你一上来就把第一张证书当叶子、第二张当中间CA,verify命令大概率会报错,因为它们的Subject、Issuer根本不是上下级关系。判断方法很简单:看两张证书的Issuer,如果相同,它们就是同一条信任链上的兄弟节点,不是父子节点。
5.3 国密证书特有的扩展项检查
除了OpenSSL/GmSSL自动验证的签名链之外,证书链验证还应该人工检查几个影响实际信任的扩展项。我每次测评都会按这个表过一遍:
| 检查项 | 要求 | 典型问题 |
|---|---|---|
| Key Usage | 签名证书要含digitalSignature,加密证书要含keyEncipherment或keyAgreement | 证书用途与套件不匹配,握手会在密钥协商阶段失败 |
| Basic Constraints | CA证书必须是CA:TRUE,叶子证书必须是CA:FALSE | 一张叶子证书被误做成CA证书,可能被接受为中间CA |
| Subject Alternative Name | 必须包含站点域名或IP | 浏览器报域名不匹配 |
| Validity | 证书有效期包含当前时间 | 服务器时间异常或证书过期 |
| CRL Distribution Points | 应有可访问的CRL地址 | 无法正常吊销检查 |
| Authority Information Access | 通常包含OCSP服务地址 | OCSP访问不通,客户端可能拒绝信任 |
国密证书的具体格式和扩展项定义,配套标准里规定得比较细。实际验证时建议把下载下来的根证书、中间CA证书和两张服务器证书一起保存到固定目录,方便反复验证和审计。
5.4 验证失败时的判断方向
证书链验证失败时,不要一头扎进去反复重跑命令,先看报错类型再对症下药。常见的几类报错和对应原因,我整理成一个速查表:
| 报错信息 | 含义 | 解决办法 |
|---|---|---|
| self-signed certificate in certificate chain | 根证书被当成非信任证书处理 | 把根证书移到-CAfile,从-untrusted中去掉 |
| unable to get local issuer certificate | 缺少中间CA或根证书 | 抓包导出完整证书链,确认中间CA也导出来了 |
| unsupported signature algorithm | 当前openssl/GmSSL不支持SM2签名 | 换用GmSSL或确认openssl版本和编译选项 |
| SM2 signature verification failed | 证书签名验不过 | 重新导出证书,检查DER数据是否完整,排除导出时截断问题 |
| certificate verify failed | 通用失败 | 结合详细-verbose输出确认具体原因 |
最后还有一招:如果实在卡住,直接用openssl asn1parse -inform DER -in cert_1.der查看证书的ASN.1结构,逐层看签名算法OID和签名值。这能帮你确认是不是工具解析问题。
6. 实操中踩过的坑,按出现频率排序
6.1 Wireshark把国密套件显示为unknown,不等于报文有问题
这是新人最容易被吓到的一步。看到Unknown CipherSuite就以为抓错包,或者怀疑服务器配置有问题。实际原因是Wireshark的TLS dissector依赖一份套件注册表,而0xE013这系列的国密套件没有正式注册进Wireshark的默认表里,所以只能显示Unknown。
判断方法是组合看其他特征:ClientHello里有没有SM2曲线、ServerHello是不是同样选了0xE013、Certificate消息里是不是出现两张证书。这几点都满足,就可以自信地认定这就是一条国密SSL流量,无需为了“让Wireshark认识国密套件”去折腾自定义插件——虽然理论上能做,但对接下来的分析没有实质帮助。
6.2 不完整握手的迷惑性
第二个高频坑是抓了半天只有ClientHello和ServerHello,没有Certificate。大多数时候不是抓包姿势错了,而是TLS会话复用。遇到这种情况,先看ClientHello里有没有session ticket扩展,再看ServerHello是否返回了新的会话票据。如果两者都是复用,就意味着当前连接复用了一个老会话,证书和密钥协商步骤都被跳过了。
处理方式我前面提过:开无痕窗口、关掉旧标签页重新访问,或者在Wireshark里定位到ClientHello后,用客户端工具断开连接,强制走一次全新握手。调试时还可以在客户端代码里显式禁用会话缓存,比如用golang的配置把ClientSessionCache设为nil,或者用curl时加上--no-sessionid。这样能稳定复现完整握手。
6.3 标准s_client拉不到国密证书
很多人习惯用openssl s_client -connect host:443 -showcerts来拉证书链。这个命令在标准TLS站点上没问题,但遇到国密SSL站点时经常直接报错,连接还没建立就结束了。原因很简单:openssl默认的s_client只携带国际标准套件,客户端Hello里根本没带0xE013,服务器自然直接给你一个handshake_failure,证书链根本不会下发到客户端。
处理办法是改用GmSSL的s_client:
gmssl s_client -connect 127.0.0.1:443 -state -showcertsGmSSL原生支持国密套件,能完成握手并把服务器下发的证书链打印出来。不过更通用、可追溯性的做法还是回到Wireshark抓包导出证书,因为pcap文件里的数据是原始报文,不依赖客户端工具对证书链的解析逻辑,审计时更可靠。
6.4 误把“签名证书+加密证书”当成CA链
最后这个坑属于国密特有的概念混淆。很多人在Certificate消息里看到两张证书,第一反应是“第一张是服务器证书,第二张是中间CA证书”,然后直接拿第二张去验证第一张的签名,结果自然失败。
正确的理解是,国密SSL下发的两张证书通常是同一信任层级下的两个不同功能实体:一张签名、一张加密。它们的Issuer可能完全相同,都是同一个CA,所以它们之间没有签名与被签名的关系。验证时要分别把每张证书归到同一条根路径下,而不是把两张证书串成一个链。
识别方法我反复用过:先分别看两张证书的Subject和Issuer。如果Subject不同但Issuer相同,说明是兄弟证书,走同一条CA路径;如果Issuer和Subject能上下衔接,才是父子证书链。另外,KeyUsage也很关键:签名证书往往带digitalSignature,加密证书带keyEncipherment或keyAgreement,用途区分一目了然。
写在后面
个人经验是,排查国密SSL问题最稳的路径永远是:抓完整握手 → 确认套件和双证书特征 → 把证书导出成文件 → 逐张验证签名链和扩展项 → 再回到握手日志看密钥协商是否成功。这套流程熟练之后,大多数联调问题半小时内就能定位到具体环节,无论是工具链识别、证书链不完整还是算法套件不匹配,都有清晰的判断依据。
最后分享一个操作细节:Wireshark里双击任意字段后按Ctrl+C,复制出来的值就是当前字段的原始值。比如双击Cipher Suite字段再Ctrl+C,出来的是Unknown (0xe013);双击Certificate字段再Ctrl+C,出来是DER证书完整的十六进制流。这个小技巧在写分析报告、比对报文时特别顺手,希望对你也有用。