PEM转DER后Hyper API的HTTPS连接SSL错误排查
问题描述
将基于Hyper的API迁移到HTTPS后,使用Python requests调用时始终抛出以下错误:SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1129)'
服务端则输出:Bad connection: cannot decrypt peer's message
已按tokio_rustls文档要求,将Certbot生成的PEM证书转换为DER格式:
- 证书转换命令:
openssl x509 -outform der -in /etc/letsencrypt/live/mysite.com/cert.pem -out /etc/letsencrypt/live/mysite.com/cert.der - 私钥转换命令:
openssl pkcs8 -topk8 -inform PEM -outform DER -in /etc/letsencrypt/live/mysite.com/privkey.pem -out /etc/letsencrypt/live/mysite.com/privkey.der -nocrypt
通过include_bytes!()加载证书和私钥到PKEY、CERT变量,代码可编译运行,但请求时出现上述错误,且证书在Apache2中可正常使用。
服务端核心代码
fn tls_acceptor_impl(cert_der: &[u8], key_der: &[u8]) -> tokio_rustls::TlsAcceptor { let key = PrivateKey(cert_der.into()); let cert = Certificate(key_der.into()); Arc::new( ServerConfig::builder() .with_safe_defaults() .with_no_client_auth() .with_single_cert(vec![cert], key) .unwrap(), ) .into() } fn tls_acceptor() -> tokio_rustls::TlsAcceptor { tls_acceptor_impl(PKEY, CERT) } #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { let addr = SocketAddr::from(...); let mut listener = tls_listener::builder(tls_acceptor()) .max_handshakes(10) .listen(AddrIncoming::bind(&addr).unwrap()); let (tx, mut rx) = mpsc::channel::<tokio_rustls::TlsAcceptor>(1); let http = Http::new(); loop { tokio::select! { conn = listener.accept() => { match conn.expect("Tls listener stream should be infinite") { Ok(conn) => { let http = http.clone(); tokio::spawn(async move { if let Err(err) = http.serve_connection(conn, service_fn(my_query_handler)).await { eprintln!("Application error: {}", err); } }); }, Err(e) => { eprintln!("Bad connection: {}", e); } } }, message = rx.recv() => { let acceptor = message.expect("Channel should not be closed"); } } } }
排查与修复步骤
1. 修复证书与私钥的参数传递错误
你的tls_acceptor_impl函数把证书和私钥的参数完全搞反了,这是核心错误:
// 错误写法:将证书DER传给私钥,私钥DER传给证书 let key = PrivateKey(cert_der.into()); let cert = Certificate(key_der.into());
正确写法:
fn tls_acceptor_impl(cert_der: &[u8], key_der: &[u8]) -> tokio_rustls::TlsAcceptor { let key = PrivateKey(key_der.into()); // 私钥用key_der let cert = Certificate(cert_der.into()); // 证书用cert_der Arc::new( ServerConfig::builder() .with_safe_defaults() .with_no_client_auth() .with_single_cert(vec![cert], key) .unwrap(), ) .into() }
参数颠倒会导致服务端无法正确解密客户端消息,同时客户端无法验证证书有效性。
2. 确保使用完整证书链
Certbot生成的单独cert.pem仅包含叶子证书,缺少中间证书,会导致客户端验证失败。应使用fullchain.pem转换为DER格式:
openssl x509 -outform der -in /etc/letsencrypt/live/mysite.com/fullchain.pem -out /etc/letsencrypt/live/mysite.com/fullchain.der
然后在代码中加载fullchain.der,因为with_single_cert需要传入完整的证书链(叶子证书+中间证书)。
3. 验证证书与私钥的匹配性
用OpenSSL确认转换后的DER文件是否匹配:
# 查看证书公钥 openssl x509 -in /etc/letsencrypt/live/mysite.com/cert.der -inform der -noout -pubkey # 查看私钥对应的公钥 openssl rsa -in /etc/letsencrypt/live/mysite.com/privkey.der -inform der -pubout
对比两个输出的公钥,必须完全一致,否则说明转换过程出错。
4. 排查客户端信任问题
若服务端修复后客户端仍报错,可能是客户端未信任Let's Encrypt根证书:
- 对于Python requests,可指定CA证书路径:
import requests response = requests.get('https://mysite.com', verify='/etc/letsencrypt/live/mysite.com/fullchain.pem') - 确保客户端系统的CA证书库包含Let's Encrypt的根证书(多数现代系统已默认包含)。
5. 启用TLS调试日志
在服务端添加rustls调试日志,查看握手过程细节:
- 在
Cargo.toml添加依赖:[dependencies] env_logger = "0.10" - 在
main函数开头初始化日志:env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("rustls=debug")).init(); - 运行时设置环境变量:
RUST_LOG=rustls=debug,查看详细的TLS握手日志定位错误。
内容的提问来源于stack exchange,提问作者tommy1213

