Rust in Everything
基于 rustls-webpki 0.103.16

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。

💬 关联讨论

发起讨论
加载中...