Rust in Everything
基于 rustls-pki-types 1.15.1

no items found / peer sent no certificates

主线位置:校验主线的两头。服务端 with_single_cert 的证书链和私钥、客户端 RootCertStore 里的锚,大多是从 PEM 文件读出来的。锚里存什么见服务器证书,链要发全见 UnknownIssuer。本页讲读文件这一步:哪些文件读不出东西、读出来的东西在哪里被拒绝,报错又分别是什么。

flowchart TB
  f["PEM 文件"] --> it["pem_file_iter"]
  it --> lab{"段的标签"}
  lab -->|CERTIFICATE| c["CertificateDer"]
  lab -->|三种私钥| k["PrivateKeyDer"]
  lab -->|其他| skip["静默跳过"]
  c --> b["with_single_cert"]
  k --> b
  b --> lp["provider 解析私钥"]
  lp --> m["keys_match"]
  m --> ok["ServerConfig"]

实验 crate:exp-rustls/pitb-pem,pem/mk.sh 用 Homebrew 的 OpenSSL 4.1.0 生成各种 PEM 文件,src/main.rs 用 pki-types 的 PemObject 读证书和私钥再调 with_single_cert,另外用 rustls-pemfile 2.2.0 读几个同样的文件。ring provider。M2 Max、macOS 27、rustc 1.99.0,release。

现象

每一行是「证书文件 + 私钥文件」和结果:

== pki-types PemObject + with_single_cert
cert.pem + pkcs8.key: 1 cert(s), key Pkcs8; with_single_cert Ok
cert.pem + sec1.key: 1 cert(s), key Sec1; with_single_cert Ok
ecparam-cert.pem + ecparam.key: 1 cert(s), key Sec1; with_single_cert Ok
rsa1-cert.pem + rsa1.key: 1 cert(s), key Pkcs1; with_single_cert Ok
combined.pem + combined.pem: 1 cert(s), key Pkcs8; with_single_cert Ok
crlf-cert.pem + pkcs8.key: 1 cert(s), key Pkcs8; with_single_cert Ok
cert.pem + encrypted.key: 1 cert(s); key: Err(no items found) / NoItemsFound
cert.pem + decrypted.key: 1 cert(s), key Pkcs8; with_single_cert Ok
cert.pem + cert.pem: 1 cert(s); key: Err(no items found) / NoItemsFound
pkcs8.key + pkcs8.key: 0 cert(s), key Pkcs8; with_single_cert Err(peer sent no certificates) / NoCertificatesPresented
missing.pem + pkcs8.key: pem_file_iter: Err(I/O error: No such file or directory (os error 2))
truncated.pem + pkcs8.key: certificate: Err(missing section end marker: [67, 69, 82, 84, 73, 70, 73, 67, 65, 84, 69])
cert.pem + other.key: 1 cert(s), key Pkcs8; with_single_cert Err(keys may not be consistent: KeyMismatch) / InconsistentKeys(KeyMismatch)
p521-cert.pem + p521.key: 1 cert(s), key Pkcs8; with_single_cert Err(unexpected error: failed to parse private key as RSA, ECDSA, or EdDSA) / General("failed to parse private key as RSA, ECDSA, or EdDSA")

前六行都能用:PRIVATE KEY(PKCS#8)、EC PRIVATE KEY(SEC1)、openssl ecparam -genkey 那种前面多一段 EC PARAMETERS 的、RSA PRIVATE KEY(PKCS#1)、证书和私钥放在同一个文件里、Windows 换行,都没问题。出问题的是后面几行:

  • openssl pkcs8 -topk8 加了密码的 ENCRYPTED PRIVATE KEY:no items found,没有一个字提到「加密」。
  • 把证书文件当私钥读:同样是 no items found。
  • 反过来把私钥文件当证书读:迭代器什么也不吐,得到一条空链,错误要到 with_single_cert 才出现,而且文案是 peer sent no certificates,读起来像握手里对方没发证书。
  • 少了 -----END CERTIFICATE----- 的文件:报错里的标签打印成了字节数组,[67, 69, 82, ...] 就是 CERTIFICATE。
  • 证书和私钥不是一对:KeyMismatch,启动时就报,不用等到握手。
  • P-521 的 ECDSA 私钥:ring provider 不认。

rustls-pemfile 2 读同样的文件:

== rustls-pemfile 2
cert.pem + pkcs8.key: certs: 1 item(s) [Ok("Ok")]; private_key: Ok(Some("Pkcs8"))
cert.pem + encrypted.key: certs: 1 item(s) [Ok("Ok")]; private_key: Ok(None)
pkcs8.key + cert.pem: certs: 0 item(s) []; private_key: Ok(None)
truncated.pem + pkcs8.key: certs: 1 item(s) [Err("section end \"CERTIFICATE\" missing")]; private_key: Ok(Some("Pkcs8"))

找不到私钥时 private_key 返回的是 Ok(None),不是错误;? 一路传上去不会停,常见的写法是接着 .unwrap(),在别处 panic。

原因

不认识的段被跳过,不报错。 pki-types 的 PEM 解析在 read 里逐行走,遇到 -----BEGIN xxx----- 就记下标签。段结束时,标签不在已知列表里就清空缓冲,继续找下一段:

    if let Some(label) = section.as_ref() {
        if label.is_end(line) {
            let kind = match label {
                SectionLabel::Known(kind) => *kind,
                // unhandled section: have caller try again
                SectionLabel::Unknown(_) => {
                    *section = None;
                    b64buf.clear();
                    return Ok(ControlFlow::Continue(()));
                }
            };

ENCRYPTED PRIVATE KEY 和 EC PARAMETERS 都不在已知列表里,都走这条路。所以 ecparam.key 能读(跳过参数段,读到后面的 EC PRIVATE KEY),encrypted.key 读到文件尾也没有一段能用。

每种类型只要自己那种标签。 PemObject 的 from_pem_file 取第一个匹配的段,一个都没有就是 NoItemsFound;pem_file_iter 只吐匹配的段,没有就是空迭代器,不报错。CertificateDer 只要 CERTIFICATE(PemObjectFilter),PrivateKeyDer 收三种:

impl PemObject for PrivateKeyDer<'static> {
    fn from_pem(kind: SectionKind, value: Vec<u8>) -> Option<Self> {
        match kind {
            SectionKind::RsaPrivateKey => Some(Self::Pkcs1(value.into())),
            SectionKind::EcPrivateKey => Some(Self::Sec1(value.into())),
            SectionKind::PrivateKey => Some(Self::Pkcs8(value.into())),
            _ => None,
        }
    }
}

空链在 rustls 里才被发现。 with_single_cert 调 CertifiedKey::from_der:先让 provider 的 key_provider 解析私钥,再用 keys_match 比私钥的公钥和链上第一张证书的 SPKI。取第一张用的是 end_entity_cert:

    pub fn end_entity_cert(&self) -> Result<&CertificateDer<'_>, Error> {
        self.cert
            .first()
            .ok_or(Error::NoCertificatesPresented)
    }

NoCertificatesPresented 本来是给握手用的(对方的 Certificate 消息是空的),它的 Display 写死成 peer sent no certificates。这里借用了它,所以配置阶段的空链也打印成「对方没发证书」。

P-521 是 provider 的事。 私钥解析交给 provider:ring 的 any_supported_type 依次试 RSA、ECDSA(只有 P-256、P-384)、EdDSA,都不行就是那句 failed to parse private key as RSA, ECDSA, or EdDSA。aws-lc-rs 的 any_ecdsa_type 多一个 P-521。同一对文件在 pitb-lab 的 p11_p521 里换两个 provider:

ring: Err(General("failed to parse private key as RSA, ECDSA, or EdDSA"))
aws-lc-rs: Ok("Ok")

rustls-pemfile 2 只是一层壳。 它的 README 写着主要功能已经并入 pki-types,2.2.0 在新实现上保留旧 API。底下是同一个解析器,区别在返回值:private_key 把「没找到」表达成 Ok(None),而 pki-types 的 from_pem_* 返回 Err(NoItemsFound)。

📝NOTE
冷知识:PEM 段的大小上限,报错文案和代码对不上

pki-types 的 b796d3d(「pem: error for sections that are too large」,首次发布在 1.14.1)之前,一个 PEM 段多长都照单全收。这个提交加了 SectionTooLarge:base64 缓冲超过 MAX_PEM_SECTION_SIZE 就停。上限最初是 64 MB,同一版本的 bb3c1da(「Adjust PEM size limit to account for huge CRLs」)把它改成 256 MB,注释写着「We've seen CRLs of 100MB (DER) / ~135MB (PEM) in the wild.」。Error 上的文档注释跟着改成了 256 MB,Display 却从第一天起就写着 10 MB。pitb-pem huge 喂给 CertificateDer::from_pem_reader 一个只有开头、base64 永远不结束的读取器:

huge section: Err("PEM section exceeds maximum allowed size of 10 MB"); read 268443648 bytes
读了 2.68 亿字节才停,报错说的却是 10 MB。

修法

加密私钥先解密。 pki-types 和 rustls 都不解密 PEM 私钥(rustls-pemfile 的 README 解释过:这类加密「typically very poor」,也不防什么有意义的威胁)。部署前用 OpenSSL 去掉密码:openssl pkcs8 -in encrypted.key -passin pass:... -out decrypted.key,得到 PRIVATE KEY,上面的 cert.pem + decrypted.key 一行就是它。私钥文件的保护交给文件权限或密钥管理服务。

读完立刻检查,别把空的东西往下传。 证书链读出来先看长度,私钥用返回 Result 的 from_pem_file:

use rustls::pki_types::pem::PemObject;
use rustls::pki_types::{CertificateDer, PrivateKeyDer};

fn load(cert_path: &str, key_path: &str) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>), String> {
  let certs = CertificateDer::pem_file_iter(cert_path)
    .map_err(|e| format!("{cert_path}: {e}"))?
    .collect::<Result<Vec<_>, _>>()
    .map_err(|e| format!("{cert_path}: {e}"))?;
  if certs.is_empty() {
    return Err(format!("{cert_path}: no CERTIFICATE section"));
  }
  let key = PrivateKeyDer::from_pem_file(key_path)
    .map_err(|e| format!("{key_path}: {e} (encrypted keys are not supported)"))?;
  Ok((certs, key))
}

pitb-pem 里编译了同一个函数,喂给它上面两个出问题的组合(路径前缀已去掉):

load(pkcs8.key, pkcs8.key): Err("pkcs8.key: no CERTIFICATE section")
load(cert.pem, encrypted.key): Err("encrypted.key: no items found (encrypted keys are not supported)")

错误里带上文件名,no items found 和 peer sent no certificates 就不再是谜。

从 rustls-pemfile 迁过来。 一一对应:rustls_pemfile::certs(&mut BufRead) 换成 CertificateDer::pem_reader_iter(Read) 或 pem_file_iter(path);rustls_pemfile::private_key 换成 PrivateKeyDer::from_pem_reader 或 from_pem_file。唯一要改逻辑的是私钥:原来判断 Ok(None) 的地方,现在是 Err(pem::Error::NoItemsFound)。rustls 已经通过 rustls::pki_types 重新导出了 pki-types,不用另加依赖。

P-521:换 aws-lc-rs,或者换一把 P-256 / P-384 的私钥。

怎么检查

  • 看文件里有哪些段: grep -- '-----BEGIN' file。ENCRYPTED PRIVATE KEY 要先解密;只有 CERTIFICATE 的文件当私钥读一定是 no items found。
  • 看证书和私钥是不是一对: with_single_cert 已经替你比过 SPKI,KeyMismatch 就是答案;部署前也可以分别用 openssl x509 -pubkey -noout 和 openssl pkey -pubout 打出公钥对比。
  • 看私钥算法: openssl pkey -in key -noout -text 的第一行。P-521 只有 aws-lc-rs 能用。

小结

  • pki-types 只认 CERTIFICATE 和三种明文私钥标签,其余段(包括 ENCRYPTED PRIVATE KEY)静默跳过,所以加密私钥报的是 no items found。
  • pem_file_iter 读不到东西时给空迭代器;空链要到 with_single_cert 才报,文案借用了握手里的 peer sent no certificates。
  • 私钥由 provider 解析:ring 不支持 P-521,aws-lc-rs 支持;证书和私钥不配对在启动时就报 KeyMismatch。
  • rustls-pemfile 2 是 pki-types 上的一层壳,找不到私钥返回 Ok(None);迁移时把这一处改成处理 NoItemsFound。
  • 读完就检查长度、错误里带文件名;加密私钥部署前解密。

💬 关联讨论

发起讨论
加载中...