s_client -showcerts只看到两张证书,verify -show_chain却显示三层,是服务器漏发了根证书吗?先别往fullchain里不断追加文件。这两个选项看的不是同一件事:一个列出对端发来的材料,一个展示本地验证构建出的路径。本文用隔离三层证书链,把"收到""构链""信任"拆开。
一、showcerts不是验证通过的证书链
OpenSSL官方手册明确:-showcerts展示服务端发送的证书列表,保留发送顺序;它不是已经验证的链。看到PEM块,只能证明本次握手里收到了这些证书,不能证明名称匹配、未过期或可信。
记录版本。本文为OpenSSL 1.1.1k FIPS回环实验,不改系统信任、时间或生产配置。输出可能随版本变化。
bash
openssl version -a

图为三层链示例,不承诺任意材料都能构链成功。配图由AI辅助生成,经技术与可读性复核。
二、根不在发送列表里,通常不是漏链
常见链路是叶证书由中间CA签发,中间CA再由根CA签发。服务端发送叶证书和所需中间证书,客户端从自己的信任库找到根,于是发送列表两张、本地路径三层,完全可以同时成立。
RFC 8446第4.4.2节要求发送者证书在列表首位,后续证书应直接签发前一张;因为信任锚独立分发,已知对端持有时可省略其证书。这里不能反过来理解成"任何最后一张都可省略"。客户端缺少构链必需的中间证书,仍可能验证失败。
信任来自预先授权,不是对端自报家门。多发根不会使其自动受信;别把陌生站点发来的根直接加入信任库。
三、保存同一次握手的完整证据
以下是Bash诊断脚本,保存后单独执行,不要直接粘贴到不想退出的交互Shell。localhost、8443及trusted-root.pem是回环示例;实际使用须替换成有权检测的终点、预期DNS名称与独立取得的可信CA文件。只写入当前专用诊断目录,不操作生产证书。
bash
HOST=localhost
CONNECT=127.0.0.1:8443
ROOT=trusted-root.pem
openssl s_client -connect "$CONNECT" -servername "$HOST" \
-verify_hostname "$HOST" -verify_return_error \
-CAfile "$ROOT" -no-CApath -showcerts \
</dev/null >handshake.txt 2>handshake.err
rc=$?
printf 's_client exit=%s\n' "$rc"
exit "$rc"
-connect选择网络终点,-servername发送SNI,-verify_hostname才提出预期名称。-verify_return_error让验证错误阻止握手继续;-no-CApath排除默认目录,显式CAfile提供本次信任材料。不要用抓到多少PEM块替代退出码与完整错误日志。
失败时可能来不及输出PEM,不能据此判断服务端未发证书;本例错误根使严格握手提前中止。拆分仅作诊断,不是成功门禁。
四、把发送列表拆开,不自动升级为信任
在同一诊断目录运行下面代码,需要Python 3。它只拆分s_client文本中的完整PEM块,拒绝覆盖已有captured目录。若前一步失败,仍可把已有证书作诊断材料,但必须保留失败结论;这个解析器不是ASN.1校验器,也不是成功门禁。
bash
python3 - <<'PY'
from pathlib import Path
import re
text = Path("handshake.txt").read_text()
certs = re.findall(r"-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----", text, re.S)
if not certs:
raise SystemExit("no PEM certificate captured")
out = Path("captured")
out.mkdir(exist_ok=False)
for i, pem in enumerate(certs):
(out / f"peer-{i}.pem").write_text(pem + "\n")
print("received certificates:", len(certs))
PY
peer-0是本例叶证书,peer-1是中间CA;真实环境应逐张查看subject、issuer与签名关系,不能只凭文件序号把所有后续项都当根。多出来的交叉签名证书也可能改变候选路径,数量不是通用健康分。
五、show_chain回答本地怎么走到信任锚
下面仅适用于已经确认"叶+一张中间CA"的本例。-trusted限定信任锚来源并排除默认库,-untrusted只给构链候选,不表示把中间证书授信。-show_chain自OpenSSL 1.1.0加入;低版本不能照抄此展示选项。
bash
openssl x509 -in captured/peer-0.pem -noout -subject -issuer
openssl verify -show_chain -purpose sslserver \
-verify_hostname localhost -trusted trusted-root.pem \
-untrusted captured/peer-1.pem captured/peer-0.pem
rc=$?
printf 'verify exit=%s\n' "$rc"
exit "$rc"
成功时本例显示depth 0叶、depth 1中间CA、depth 2根。中间项标记untrusted表示来自非信任材料列表,不等于它最终验证失败。这一行很容易让人紧张:标签说的是材料来源,不是最终判决。
离线通过不证明生产入口发链正确,仍须用实际客户端复验。
六、四组对照比数证书更可靠
我用同一叶证书和中间CA切换服务端发送列表,并以独立根做反例。最终HTML中的四个代码块也提取执行,避免文章看着正确、复制出来却变样。
| 服务端发送 | 客户端信任 | 本例结果 |
|---|---|---|
| 叶+中间 | 正确根 | 收到2张,验证成功 |
| 只有叶 | 正确根 | 缺中间,严格验证失败 |
| 叶+中间+根 | 无关根 | 发送3张仍验证失败 |
| 叶+中间+根 | 正确根 | 收到3张,验证成功 |
额外负例把预期名称换错,完整链仍失败;离线去掉-untrusted,中间材料不足也失败。以上是本机回环与文件实验,不冒充公网浏览器、生产代理或所有TLS实现的测试。临时私钥和监听在实验结束后清理。
七、定位问题时别做三种"修复"
第一,不为凑齐三张就追加根。单根直签、多个中间层和交叉签名都会改变数量。第二,不把-untrusted改成信任配置来消除报错;材料来源与授权信任是两回事。第三,不关闭名称或链验证,让错误消失并不等于修好。
客户端结果不一致时,对照终点、SNI、实际发链和信任库。某客户端能补齐中间证书,不代表服务器可以漏发。
八、验收清单与参考资料
先确认检测的是正确终点与名称;保存本次发送列表;核实叶与中间证书关系;从授权渠道确认信任根;执行严格链与名称验证;最后由实际应用检查HTTP和业务结果。分别保留证据,不把"看到根""数到三张"或离线OK当作完整HTTPS验收。