如何启用 TLS 双向认证?
解读
在国内互联网、金融、物联网及云原生场景下,双向 TLS(mTLS) 已成为“零信任”架构的标配:不仅服务端要向客户端出示证书,客户端也必须回传合法证书,否则连接直接终止。面试官问“如何启用”,既考察候选人是否理解 TLS 握手流程,也考察能否用 Rust 生态落地生产级配置,包括国密、证书热更新、SNI、ALPN、中间人降级防护等细节。回答时要把“为什么做”和“怎么做”拆开,先讲原理,再给代码骨架,最后补充踩坑点,体现“编译通过即正确”的 Rust 文化。
知识点
- TLS 1.3 握手与 CertificateRequest:服务端在 ServerHello 后发送 CertificateRequest,客户端必须回证书+签名。
- rustls 与 OpenSSL 的权衡:rustls 纯 Rust、FIPS 待审、无国密;OpenSSL 有国密、需 unsafe,国内合规项目常双栈支持。
- 证书链校验:系统根库、自定义 CA、CRL/OCSP、国密双证(签名+加密)。
- tokio-rustls / hyper-rustls / tonic:异步生态统一接口,ServerConfig::with_client_cert_verifier 是关键扩展点。
- 客户端证书选择回调:当客户端有多张证书时,需实现 ResolvesClientCert trait,按 SNI/算法/国密策略动态挑选。
- ALPN 协商:http/1.1、h2、grpc-exp,防止协议降级。
- 热更新:证书文件走 notify crate 监听 inotify,reload 后原子替换 Arc<ServerConfig>,服务零中断。
- 错误诊断:rustls 报错仅给 AlertDescription,需要开启 secretly extract peer cert chain 日志,结合 Wireshark 抓包,否则现场排障抓瞎。
答案
以下示例基于 tokio + rustls 0.21,覆盖“服务端强制校验客户端证书”与“客户端自动送证”两端,可直接放进简历项目。
- 准备 CA 与双证
# 自建根 CA
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out ca.key
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt -subj "/CN=example-ca"
# 服务端证书
openssl req -new -nodes -out server.csr -keyout server.key -subj "/CN=server.example"
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 365
# 客户端证书
openssl req -new -nodes -out client.csr -keyout client.key -subj "/CN=client.example"
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 365
- 服务端代码(强制 mTLS)
use std::sync::Arc;
use tokio::net::TcpListener;
use tokio_rustls::{TlsAcceptor, server::TlsStream};
use rustls::{ServerConfig, RootCertStore, AllowAnyAuthenticatedClient};
use rustls_pemfile::{certs, pkcs8_private_keys};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. 加载 CA 根证书
let mut root_store = RootCertStore::empty();
let ca_file = &mut std::io::BufReader::new(std::fs::File::open("ca.crt")?);
for cert in certs(ca_file)? {
root_store.add(&rustls::Certificate(cert))?;
}
// 2. 加载服务端证书与私钥
let cert_file = &mut std::io::BufReader::new(std::fs::File::open("server.crt")?);
let key_file = &mut std::io::BufReader::new(std::fs::File::open("server.key")?);
let cert_chain = certs(cert_file)?.into_iter().map(rustls::Certificate).collect();
let mut keys = pkcs8_private_keys(key_file)?;
let key = rustls::PrivateKey(keys.remove(0));
// 3. 构造 ServerConfig:强制客户端出示证书
let config = ServerConfig::builder()
.with_safe_defaults()
.with_client_cert_verifier(AllowAnyAuthenticatedClient::new(root_store))
.with_single_cert(cert_chain, key)?;
let acceptor = TlsAcceptor::from(Arc::new(config));
let listener = TcpListener::bind("0.0.0.0:443").await?;
loop {
let (stream, peer_addr) = listener.accept().await?;
let acceptor = acceptor.clone();
tokio::spawn(async move {
match acceptor.accept(stream).await {
Ok(tls) => handle(tls, peer_addr).await,
Err(e) => eprintln!("TLS handshake failed from {}: {}", peer_addr, e),
}
});
}
}
async fn handle(stream: TlsStream<tokio::net::TcpStream>, peer: std::net::SocketAddr) {
// 4. 提取客户端证书链,做二次业务鉴权
let (_, session) = stream.get_ref();
if let Some(chain) = session.peer_certificates() {
println!("client cert count: {}", chain.len());
// TODO: 解析 CN/O 做细粒度 RBAC
}
// 5. 业务逻辑
use tokio::io::{AsyncWriteExt, AsyncReadExt};
let (mut rd, mut wr) = tokio::io::split(stream);
let n = rd.copy(&mut wr).await.unwrap();
println!("echo {} bytes from {}", n, peer);
}
- 客户端代码(自动送证)
use tokio::net::TcpStream;
use tokio_rustls::{TlsConnector, client::TlsStream};
use rustls::{ClientConfig, RootCertStore};
use rustls_pemfile::{certs, pkcs8_private_keys};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. 信任同一 CA
let mut root_store = RootCertStore::empty();
let ca_file = &mut std::io::BufReader::new(std::fs::File::open("ca.crt")?);
for cert in certs(ca_file)? {
root_store.add(&rustls::Certificate(cert))?;
}
// 2. 加载客户端证书与私钥
let cert_file = &mut std::io::BufReader::new(std::fs::File::open("client.crt")?);
let key_file = &mut std::io::BufReader::new(std::fs::File::open("client.key")?);
let cert_chain = certs(cert_file)?.into_iter().map(rustls::Certificate).collect();
let mut keys = pkcs8_private_keys(key_file)?;
let key = rustls::PrivateKey(keys.remove(0));
let config = ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(root_store)
.with_single_cert(cert_chain, key)?; // 关键:送客户端证书
let connector = TlsConnector::from(Arc::new(config));
let stream = TcpStream::connect("server.example:443").await?;
let domain = rustls::ServerName::try_from("server.example")?;
let mut tls = connector.connect(domain, stream).await?;
tls.write_all(b"GET / HTTP/1.0\r\n\r\n").await?;
let mut buf = vec![0; 1024];
let n = tls.read(&mut buf).await?;
println!("{}", String::from_utf8_lossy(&buf[..n]));
Ok(())
}
- 验证
# 服务端日志应出现 “client cert count: 1”
# 若客户端未送证,rustls 会返回 AlertDescription::CertificateRequired,连接直接断开
拓展思考
- 国密场景:rustls 目前无 SM2/SM3/SM4,需回退到 openssl + tokio-openssl,并启用 GmSSL 引擎;同时注意双证体系,签名证书与加密证书需分开发送。
- 证书热更新:把
Arc<ServerConfig>包进ArcSwap,结合 notify 监听证书目录,原子替换无需重启进程;线上灰度时可通过 Envoy/NGINX Rust 插件 做软负载兜底。 - 多租户 SNI:在
ResolvesServerCert中按 SNI 选择不同证书,实现 单端口万域名;客户端同样可在ResolvesClientCert里按服务端 SNI 回传不同证书链,防止证书滥发。 - 性能调优:rustls 默认启用 TLS 1.3 0-RTT,但 0-RTT 有重放风险,金融支付类业务需关闭;可开启 boringssl 兼容的 SSLKEYLOGFILE 抓包,结合 eBPF 分析握手时延。
- 合规审计:国内等保 2.0 要求“通信完整性校验”,mTLS 满足该条款,但需把客户端证书序列号、算法、有效期落盘到日志,对接 ELK/ClickHouse 做事后溯源。