把根证书放进目录,curl 还是报 unable to get local issuer certificate;改成 --cacert 指向同一张证书却能连通。这类问题可能不是证书缺了,而是 OpenSSL 找不到目录里的"索引"。本文只讨论使用 OpenSSL 后端的 curl,把 CA 文件、哈希目录和真实 HTTPS 验收分开。
一、先确认你传的是文件还是目录
--cacert 接受 CA 文件,--capath 接受证书目录。两者不是换个路径就等价:文件可装多张 PEM 证书,OpenSSL 的目录查找则依赖按 subject 名称计算的哈希文件名或链接。把 root.pem 复制进去,只是入库,还没有索引。
bash
curl -V
openssl version
openssl x509 -in /srv/app/ca.pem -noout -subject -issuer -fingerprint -sha256
先记录 curl -V 的 TLS 后端,再核对证书的 subject、issuer 和 SHA-256 指纹。指纹用于确认具体证书;subject_hash 用于定位目录入口,两者不能互换。其他后端需查对应文档。
二、目录里的 hash.0 到底是什么
OpenSSL 查找 CA 时,不会把目录内每个任意文件名都当作待验证证书遍历。常见入口形如 00ceea4b.0,链接目标才是 root.pem。这个名字来自证书 subject 的哈希,不是网站域名哈希,更不是证书的完整指纹。
同一 subject 哈希可对应多个对象,后缀递增,不能据后缀判断新旧。删除原文件却留下软链接,目录看着热闹,读取时仍会落空。

三、在独立目录重建索引,不直接改系统库
以下 /srv/app/trust-new 是示例候选目录,需要先创建,并放入经过来源核验的 root.pem;/srv/app/ca.pem 是第一步检查的同一张 CA。请替换为自己的测试路径,确认写权限,避免把服务器私钥复制进信任目录。
bash
TRUST=/srv/app/trust-new
openssl rehash -v "$TRUST"
HASH=$(openssl x509 -in "$TRUST/root.pem" -noout -subject_hash) || exit 1
readlink "$TRUST/$HASH.0"
openssl x509 -in "$TRUST/$HASH.0" -noout -subject -fingerprint -sha256
rehash 扫描 .pem、.crt、.cer、.crl 文件。它可能移除旧的哈希形式链接再生成新链接,所以必须显式传入候选目录,不要对不明用途目录直接执行。本例单张 CA 只检查 .0,多对象需检查全部目标。
命令退出 0 只是流程结束,不是"全部证书已建索引"的证明。检查标准错误中的 skipping、重复对象等警告,再读取实际链接目标的指纹。多张证书合并的 bundle 适合 CAfile,但 rehash 要求单文件恰好一个证书或 CRL 对象,不能把 bundle 改个扩展名就当索引修好了。
四、先离线验证,再判断是不是目录问题
准备待检查的 leaf.pem,下例使用示例域名 service.example.test。它是叶证书而不是 CA 文件;真实站点若有中间 CA,还需用 -untrusted 指向收集到的中间证书文件,不要为了补链就把未知中间 CA 直接加入信任根集合。
bash
openssl verify -no-CAfile -CApath /srv/app/trust-new \
-purpose sslserver -verify_hostname service.example.test leaf.pem
rc=$?
printf 'verify exit=%s\n' "$rc"
test "$rc" -eq 0
-no-CAfile 排除默认 CA 文件带来的旁路成功,-CApath 指向候选目录;主机名与 sslserver 用途仍要单独检查。若同一批材料用 CAfile 成功、CApath 失败,再重点看索引和路径。若两边都失败,则继续检查签发关系、有效期、扩展和中间链,不要把所有 error 20 都归因于 rehash。
五、隔离实验:有证书不等于有可用索引
本次在临时目录和回环 TLS 服务做了实际验证:curl 7.61.1,OpenSSL 1.1.1k FIPS。使用独立 CA 签发测试叶证书,不修改系统信任库和生产服务。curl 显式指定另一张无关 CA 作为 CAfile,避免默认信任文件掩盖目录效果;这不是生产配置建议。
只有 root.pem、没有哈希入口时,离线 verify 报 error 20 并退出 2,curl 报 60。执行 rehash 后,同一目录验证通过,curl 退出 0 且收到 HTTP 200。随后只删除链接目标,保留入口,两个测试重新失败;恢复材料与索引后可再次验证。
另一个反例是两张 CA 拼成 bundle.pem:本机 rehash 退出 0,但打印"不恰好包含一个证书或 CRL"的跳过警告,没有建立入口;同一 bundle 用作 CAfile 能验证。索引修好后改用错误主机名仍报 error 62。这些结果只证明本次夹具,不代表所有版本、浏览器或生产链路。
六、常见现象与下一步
| 现象 | 优先检查 | 不要直接下的结论 |
|---|---|---|
| CAfile 成功,CApath 失败 | 哈希入口、路径、读取权限 | 服务端一定缺证书 |
| rehash 退出 0 但有 skipping | 多对象 PEM 或格式问题 | 目录已全部修复 |
| 链接存在却打不开 | 悬空目标、容器内路径 | 看到文件名就能读取 |
| 目录验证通过,域名仍不匹配 | SAN 与请求主机名 | 加更多 CA 能修复身份 |
权限检查要站在实际服务用户的视角:管理员能读,不代表应用用户能穿过父目录。容器中的目录与宿主机不是同一视角,尤其注意链接目标是否也随挂载进入容器。先确认材料可达,再谈证书可信。
七、真实请求复验,保留失败退出码
bash
curl --noproxy '*' --capath /srv/app/trust-new \
--show-error --silent --output response.txt \
--write-out 'HTTP=%{http_code}\n' https://service.example.test/
rc=$?
printf 'curl exit=%s\n' "$rc"
test "$rc" -eq 0
这里 --noproxy 用来明确做直连诊断,域名和候选目录必须换成获授权的目标;需要代理的环境应按实际网络路径另验。curl 仍可能使用默认 CA 文件,因此这条命令验证的是实际请求能否通过,不能独自证明成功只来自 CApath;归因要结合第四节的隔离对照。
response.txt 是本地响应文件,执行前确认不会覆盖需要保留的数据。不要加 -k 让问题"消失",也不要只看 HTTP 状态:TLS 失败时可能显示 000,HTTP 非 2xx 则要另查应用。长驻进程是否重新加载信任库仍需按应用文档另验。
八、交付前检查清单
确认后端和版本;核对 CA 来源与指纹;候选目录内不含私钥;检查 rehash 警告、哈希入口和实际目标;以应用身份验证路径权限;保留正确链与错误主机名的正反例;在真实网络路径复验并记录退出码。切换目录前保留旧版本,切换后观察应用日志,失败能够回退。
临时私钥、证书和监听已清理,未执行生产切换或浏览器信任验证。目录问题的关键不是"再塞一张证书",而是验证器能否读到正确对象。