HTTPS叶证书没过期,SAN也包含访问域名,OpenSSL却报 permitted subtree violation。别急着重装根证书:这可能是CA证书的名称约束拒绝了下级名称。下面区分"访问名能否匹配"和"这条链能否为这些名称背书",再用error 47、48和离线命令定位。
一、SAN匹配,不代表名称约束通过
主机名验证检查访问名是否匹配叶证书;nameConstraints限制CA下级证书的名称范围。前者通过,后者仍可失败。好比姓名对得上,签发机构的授权范围却不覆盖这张证件。
中间CA只允许example.test,叶证书却含api.example.test与api.other.test。只访问前者,多余DNS SAN仍可让路径被拒绝。

二、从error 47和48定位,不把depth当约束来源
本次OpenSSL 1.1.1k FIPS离线实验中,47对应permitted subtree violation,48对应excluded subtree violation。前者是名称未落在允许范围,后者是命中排除范围。错误日志还会给出subject与depth,需要完整保留。
depth 0是待验证叶证书,1是签发它的CA,以此类推。但名称约束失败时,depth可以指向被检查名称所在的证书,并不保证指向写着nameConstraints的CA。本例约束放在中间CA,两类失败都报depth 0;别因此只修改叶证书配置。
hostname mismatch走主机名分支;unable to get local issuer certificate先处理构链。错误可不止一条,不用最后一行代替诊断。
三、逐张检查最终证书,不只看CSR
bash
openssl version
# 每个文件只放一张证书;先确认文件角色
for f in root.pem issuing.pem leaf.pem; do
printf "\nfile=%s\n" "$f"
openssl x509 -in "$f" -noout -subject -issuer \
-serial -fingerprint -sha256
openssl x509 -in "$f" -noout \
-ext subjectAltName,nameConstraints,basicConstraints
done
重点读取每张CA的Name Constraints和叶证书全部SAN。x509只读首张证书,fullchain.pem也不例外;多级链先拆分并编号。
RFC 5280规定名称约束扩展只用于CA证书,并应标为critical。permittedSubtrees表示允许范围,excludedSubtrees表示排除范围,命中排除范围仍应拒绝。存在多个受约束CA时,要满足路径上适用的各级限制,不是让下级写一个更宽范围就覆盖上级。
四、固定信任材料,分开验证名称与路径
bash
openssl verify -no-CApath -CAfile root.pem \
-untrusted issuing.pem -purpose sslserver \
-verify_hostname api.example.test -show_chain leaf.pem
rc=$?
printf "verify_exit=%s\n" "$rc"
命令只验本地文件,不连接网站。CAfile固定根,untrusted提供中间链,leaf.pem是目标。名称、用途、路径一起检查,OK且退出0才通过;show_chain成功时显示路径。
为定位可另做不带verify_hostname的对照,但它只是不执行这项访问名匹配,并不会解除CA名称约束。本次多SAN越界样本去掉该参数仍报47。不能把这种诊断对照写进生产验收脚本,宣称域名也已验证。
不要提升中间CA为信任锚或关闭验证来消除错误;这改变了信任边界,并非修复原路径。
五、配置片段表达什么,修复就改什么
ini
[issuing_ca]
basicConstraints = critical,CA:TRUE,pathlen:0
keyUsage = critical,keyCertSign,cRLSign
subjectKeyIdentifier = hash
nameConstraints = critical,permitted;DNS:example.test,excluded;DNS:admin.example.test
这是CA扩展片段,不是完整生产CA配置:允许example.test及其DNS子域,排除admin.example.test及其子域。它不是DNS解析规则,也不是网站访问控制。pathlen:0描述中间CA层级,不能替代名称范围检查;两种约束要分别满足。
DNS、IP、URI、邮件地址是不同名称类型,不能把permitted;DNS当作"只允许所有类型身份属于这个域"的总开关。本文只测试DNS SAN,不把结果推广到IP范围、通配符或其他客户端实现;涉及前导点、国际化域名等边界时应另建准确样本验证。
如果多余SAN是申请错误,由授权CA按正确名称集合重新签发叶证书。若业务确需范围外名称,应选择政策允许的签发路径,或由CA管理员审批后调整并重签适用CA证书。只改CSR、配置文件或服务端server_name,不会改变已经签名的扩展。
六、用正反例确认,不只测一个能通的名字
本次用临时根、中间CA和serverAuth叶证书离线验证,未改系统信任、时钟或生产服务,材料已清理。不冒充浏览器或线上握手实测。
| 叶证书或条件 | 退出码与错误 | 结论 |
|---|---|---|
| 仅api.example.test | 0;OK | 允许范围内 |
| 仅api.other.test | 2;47,depth 0 | 超出允许范围 |
| api.example.test加api.other.test | 2;47,depth 0 | 访问名匹配仍不足 |
| 仅admin.example.test | 2;48,depth 0 | 允许范围内但被排除 |
| 合规证书,验证wrong.example.test | 2;62,depth 0 | 主机名不匹配 |
bash
# 在隔离夹具中比较四份证书的路径约束,不检查访问名
for f in good.pem outside.pem mixed.pem excluded.pem; do
openssl verify -no-CApath -CAfile root.pem \
-untrusted issuing.pem -purpose sslserver "$f"
rc=$?
printf "%s exit=%s\n" "$f" "$rc"
done
四份文件对应表中前四行,并非内置样本。循环不验访问名,需另用verify_hostname。总体退出码可能来自末尾printf;自动化应逐项判断rc。
七、上线前还差一次真实客户端验证
离线通过不说明服务已加载新证书。部署后从实际客户端读取线上指纹、SAN与链,以正确主机名完成握手和业务请求。不同TLS库支持可能不同,本机OK不能替所有客户端背书。
八、验收清单与官方依据
- 记录错误原文、退出码、depth与证书指纹,定位名称及约束来源。
- 检查全部DNS SAN和路径上适用的名称限制,而非只找一个匹配名。
- 合规正例通过,越界及排除负例仍失败;主机名另作明确校验。
- 签发修改经过授权,最终证书重新读取,线上版本另行验收。
依据:OpenSSL扩展配置、verify手册、RFC 5280名称约束。先分清"名称匹配"和"名称授权范围",再决定重签哪一层。