CaUsedAsEndEntity / UnknownRevocationStatus
主线位置:校验主线的路径构建。每张进入路径的证书都要过基本约束、扩展密钥用途(EKU)和有效期三项检查,签名验过之后再按 CRL 查吊销。三项检查的代码见路径构建,CRL 的四个开关见客户端证书与自定义校验。本页从「OpenSSL 说没问题、rustls 却拒绝」这个场景出发,把三种报错各对照一次
openssl verify。
flowchart TB
p["路径上每张证书"] --> bc{"基本约束"}
bc -->|叶子是 CA| e1["CaUsedAsEndEntity"]
bc --> eku{"EKU"}
eku -->|中间证书只有 clientAuth| e2["InvalidPurpose"]
eku --> sig["从锚往下验签名"]
sig --> crl{"这一级有 CRL"}
crl -->|没有 默认拒绝| e3["UnknownRevocationStatus"]
crl -->|有且列出| e4["Revoked"]
实验 crate:exp-rustls/pitb-lab。certs16/mk.sh 用 Homebrew 的 OpenSSL 4.1.0 造证书,src/bin/p16_ca.rs 用 pki-types 读这些 PEM,在内存里握手;CRL 部分用 rcgen 造链和 CRL,再导出给 openssl verify 对照。ring provider,TLS 1.3。M2 Max、macOS 27、rustc 1.99.0,release。
现象
OpenSSL 造的证书。 A 是最常见的一行命令:openssl req -x509 -newkey ec ... -subj /CN=localhost -addext subjectAltName=DNS:localhost;B 在 A 的基础上加 -addext basicConstraints=critical,CA:FALSE;后两条是「根 → 中间 → 叶子」,区别只在中间证书有没有写 extendedKeyUsage=clientAuth。客户端信任 A、B 本身或者根:
== OpenSSL-made certificates
A: req -x509 one-liner (CA:TRUE), trusted directly: client error: invalid peer certificate: Other(OtherError(CaUsedAsEndEntity))
debug: InvalidCertificate(Other(OtherError(CaUsedAsEndEntity)))
(server then sees: received fatal alert: CertificateUnknown)
B: req -x509 with CA:FALSE, trusted directly: Ok
root -> intermediate (no EKU) -> leaf serverAuth: Ok
root -> intermediate (EKU clientAuth) -> leaf serverAuth: client error: invalid peer certificate: certificate does not allow extended key usage for server authentication, allows client authentication
debug: InvalidCertificate(InvalidPurposeContext { required: ServerAuth, presented: [ClientAuth] })
(server then sees: received fatal alert: UnsupportedCertificate)
同样四份证书交给 openssl verify -purpose sslserver:
== A self-signed CA:TRUE
a.pem: OK
== B self-signed CA:FALSE
b.pem: OK
== plain intermediate
leaf-plain.pem: OK
== clientAuth intermediate
CN=Corp int-client
error 26 at 1 depth lookup: unsuitable certificate purpose
error leaf-client.pem: verification failed
A 是分歧所在:OpenSSL 接受,rustls 拒绝。EKU 那条两边都拒绝,但 OpenSSL 指出了是深度 1 的 CN=Corp int-client,rustls 的报错没说是哪一张。
部分 CRL。 链是「CRL Root → CRL Intermediate → leaf」,中间 CA 签了一份 CRL,里面列着叶子的序列号。服务器证书校验器(WebPkiServerVerifier)只拿到这一份:
== CRLs on the server-certificate verifier (root -> intermediate -> leaf)
intermediate's CRL (lists leaf) only, defaults: client error: invalid peer certificate: UnknownRevocationStatus
debug: InvalidCertificate(UnknownRevocationStatus)
(server then sees: received fatal alert: UnknownCA)
叶子确实被吊销了,报错却是「吊销状态未知」。同样的证书和 CRL 交给 OpenSSL:
== -crl_check, intermediate's CRL only
CN=leaf
error 23 at 0 depth lookup: certificate revoked
error crl-leaf.pem: verification failed
== -crl_check_all, intermediate's CRL only
CN=leaf
error 23 at 0 depth lookup: certificate revoked
error crl-leaf.pem: verification failed
原因
openssl req -x509 默认造的是 CA 证书。 A 的扩展里是 X509v3 Basic Constraints: critical / CA:TRUE(openssl x509 -text 看到的):命令行里没写,是本机 OpenSSL 4.1.0 的 req -x509 自己加上的。rustls-webpki 的 check_basic_constraints 对叶子的角色要求「不是 CA」:
match (role, is_ca, path_len_constraint) {
(Role::EndEntity, true, _) => Err(Error::CaUsedAsEndEntity),
(Role::Issuer, false, _) => Err(Error::EndEntityUsedAsCa),
// ...
_ => Ok(()),
}
把 A 放进 RootCertStore 并不改变这一点:锚只留下 subject 和公钥(服务器证书),握手里发来的 A 仍然是以叶子的身份被检查的。OpenSSL 对这张既是信任锚、又是被验证对象的证书给了 OK,没有拦下 CA:TRUE。CaUsedAsEndEntity 不在 rustls 的 CertificateError 专门变体里,经 pki_error 落进 Other(OtherError(...)),所以长成那个样子。
EKU 也查中间证书。 check_eku 和基本约束一样作用于路径上的每张非锚证书,KeyUsage::server_auth() 是「有 EKU 扩展就必须含 serverAuth」。中间证书写了 EKU 却只有 clientAuth,整条路径就不能用于服务器认证。OpenSSL 在这一点上和 rustls 一致。
CRL 从锚往下查,第一级就卡住。 吊销检查跟着签名检查走,顺序是从锚往下(路径构建)。第一张被查的是中间证书,它的签发者是根;RevocationOptions::check 找不到根签的 CRL,默认策略 UnknownStatusPolicy::Deny 当场返回 UnknownRevocationStatus,根本没走到叶子。OpenSSL 的报错说 at 0 depth,它先看到了叶子被吊销;openssl verify -help 对两个选项的说明是 check leaf certificate revocation 和 check full chain revocation,查整条链的 -crl_check_all 这一次也报了叶子被吊销,没有因为缺根的 CRL 而改口。客户端证书那一侧完全相同,客户端证书与自定义校验的动脑筋题就是这个例子。
修法
自签证书:明确写 CA:FALSE,或者造一个真正的 CA。 上面的 B 只多了一个 -addext basicConstraints=critical,CA:FALSE,直接信任它就能握手。正式一点的做法是像「根 → 中间 → 叶子」那样分开:根是 CA,叶子不是。rcgen 的 generate_simple_self_signed 造的证书没有基本约束扩展,按「不是 CA」处理,所以本卷其他实验没撞上这个坑。
EKU:重签中间证书。 去掉中间证书的 EKU,或者把 serverAuth 加进去。上面不带 EKU 的那条链(intermediate (no EKU))就是修好的样子。
CRL:给链上每一级签发者都配一份,或者明确降低要求。 同一个场景再跑三种配置:
+ root's CRL (empty): client error: invalid peer certificate: Revoked
debug: InvalidCertificate(Revoked)
(server then sees: received fatal alert: CertificateRevoked)
intermediate's CRL only, only_check_end_entity_revocation: client error: invalid peer certificate: Revoked
debug: InvalidCertificate(Revoked)
(server then sees: received fatal alert: CertificateRevoked)
intermediate's CRL only, allow_unknown_revocation_status: client error: invalid peer certificate: Revoked
debug: InvalidCertificate(Revoked)
(server then sees: received fatal alert: CertificateRevoked)
- 补上根签的 CRL(哪怕是空的),中间证书查得到、没被吊销,检查继续往下,叶子报出真正的
Revoked。 only_check_end_entity_revocation跳过中间证书,只查叶子。allow_unknown_revocation_status把「找不到 CRL」当作没被吊销。它对所有层级生效:一张本该有 CRL 的证书缺了 CRL,也会被放过。
怎么检查
- 逐张看扩展: 对链上每一张跑
openssl x509 -in cert.pem -noout -text,看Basic Constraints(叶子必须没有或是CA:FALSE,中间证书必须是CA:TRUE)和Extended Key Usage(中间证书要么没有,要么含TLS Web Server Authentication)。 - 别只用
openssl verify当判据: 它接受CA:TRUE的自签叶子,rustls 不接受。拿 rustls 自己跑一次握手(或者直接调WebPkiServerVerifier::verify_server_cert)更可靠。 - 看到
UnknownRevocationStatus: 先数一数with_crls给了几份 CRL、各是谁签的;链上每一级签发者(锚除外)都要有一份签发者名字对得上的 CRL。
小结
openssl req -x509一行命令造的自签证书默认带CA:TRUE,OpenSSL 照样接受,rustls 报CaUsedAsEndEntity(在 rustls 里长成Other(OtherError(...)));加basicConstraints=critical,CA:FALSE就好。- EKU 检查作用于中间证书:中间证书只写 clientAuth,服务器证书就失败,报错不指明是哪一张。
- CRL 从锚往下逐级查,默认找不到 CRL 就失败:只给中间 CA 的 CRL 时报
UnknownRevocationStatus,盖住了叶子被吊销的事实。补齐每一级的 CRL,或者显式用only_check_end_entity_revocation。