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)。
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
修法
加密私钥先解密。 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。 - 读完就检查长度、错误里带文件名;加密私钥部署前解密。