这篇文章上次修改于 921 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
1 使用自签名证书的目的
本文使用自签名证书的目的:
- 用于服务端校验客户端是否合法,避免任何一个客户端都可以连上服务端。
- 基于 TLS,对服务端和客户端之间的传输数据进行加密。
2 自签名证书校验过程
3 原理
3.1 数字证书
服务端使用自己的域名向 CA(Certificate Authority,证书颁发机构)申请证书。
CA 颁发的证书中含有公钥、证书所有者、有效期、CA 利用自己的私钥生成的签名等信息。
如果客户端想要验证服务端的证书是否合法,可以看能否用 CA 的公钥解开证书上的签名,如果能解开,说明服务端的证书合法。但是怎么确认 CA 的公钥是合法的呢,万一有人冒充 CA 怎么办,这时候可以通过 CA 的 CA 来验证,一直向上追溯,直到追溯到著名的 root CA。
本文中使用的是自签名证书,给人的感觉是“我就是我,你爱信不信”。
3.2 TLS
TLS(Transport Layer Security,安全传输层),TLS是 建立在传输层 TCP 协议之上的协议,服务于应用层,它的前身是 SSL(Secure Socket Layer,安全套接字层),它实现了将应用层的报文进行加密后再交由 TCP 进行传输的功能。
TLS 传输过程大致分为两阶段:
第一阶段:客户端和服务端使用非对称加密交换信息,用于生成对称加密传输所需的 key。
- 该过程中,使用私钥对数据加密,使用对方证书中的公钥对数据解密。
- 第二阶段:使用生成的 key 对通信数据进行加密和解密。
4 自签名证书生成
参考 rustls 给出的示例进行了修改。
- 自己作为 CA,生成 CA cert,后续该 CA cert 可被服务端和客户端所信任。
利用 CA cert 颁发服务端和客户端的 SSL 证书。服务端和客户端的 SSL 证书生成步骤是一样的,具体如下:
- 生成私钥。
- 利用私钥生成证书请求。
- 利用证书请求和 CA cert 生成证书,CA 会利用自己的私钥生成证书签名。
build-a-pki.sh:
#!/bin/sh
set -xe
work_dir=$(cd $(dirname $0); pwd)
dir='dev'
if [ $# -eq 1 ]; then
while getopts ":r" opt
do
case $opt in
r)
dir='release'
;;
?)
echo "Unknow input"
exit 1
;;
esac
done
fi
dir=$work_dir/$dir
rm -rf $dir
mkdir $dir
openssl req -nodes \
-x509 \
-days 3650 \
-newkey rsa:4096 \
-keyout $dir/ca.key \
-out $dir/ca.cert \
-sha256 \
-batch \
-subj "/CN=ponytown RSA CA"
openssl req -nodes \
-newkey rsa:2048 \
-keyout $dir/server.key \
-out $dir/server.req \
-sha256 \
-batch \
-subj "/CN=testserver.com"
openssl rsa \
-in $dir/server.key \
-out $dir/server.rsa
openssl req -nodes \
-newkey rsa:2048 \
-keyout $dir/client.key \
-out $dir/client.req \
-sha256 \
-batch \
-subj "/CN=ponytown client"
for kt in $dir ; do
openssl x509 -req \
-in $kt/server.req \
-out $kt/server.cert \
-CA $kt/ca.cert \
-CAkey $kt/ca.key \
-sha256 \
-days 2000 \
-set_serial 456 \
-extensions v3_server -extfile openssl.cnf
openssl x509 -req \
-in $kt/client.req \
-out $kt/client.cert \
-CA $kt/ca.cert \
-CAkey $kt/ca.key \
-sha256 \
-days 2000 \
-set_serial 789 \
-extensions v3_client -extfile openssl.cnf
cat $kt/server.cert $kt/ca.cert > $kt/server.fullchain
done
rm $dir/*.req
rm $dir/ca.key
rm $dir/server.cert $dir/server.key
openssl.cnf:
[ v3_server ]
basicConstraints = critical,CA:false
keyUsage = nonRepudiation, digitalSignature
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer:always
subjectAltName = @alt_names
[ v3_client ]
basicConstraints = critical,CA:false
keyUsage = nonRepudiation, digitalSignature
extendedKeyUsage = critical, clientAuth
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer:always
[ alt_names ]
DNS.1 = testserver.com
DNS.2 = second.testserver.com
DNS.3 = localhost
4 使用自签名证书示例
本文基于 rust 中 tokio_rustls 库实现 TLS。TLS是 建立在 TCP 之上的,服务于应用层,它将应用层的报文进行加密后再交由 TCP 进行传输。
客户端:
- 加载证书,生成 rustls::ClientConfig
- 利用 rustls::ClientConfig 生成 rustls::TlsConnector
- 利用 rustls::TlsConnector 将 tokio::net::TcpStream 转为 tokio_rustls::client::TlsStream,后续可用 TlsStream 收发数据
服务端:
- 加载证书,生成 rustls::ServerConfig
- 利用 rustls::ServerConfig 生成 rustls::TlsAcceptor
- 利用 rustls::TlsAcceptor 将 tokio::net::TcpStream 转为 tokio_rustls::server::TlsStream,后续可用 TlsStream 收发数据
use std::convert::TryFrom;
use std::fs::File;
use std::io;
use std::io::BufReader;
use std::sync::Arc;
use std::net::{
SocketAddr,
ToSocketAddrs,
};
use rustls::{
RootCertStore,
server::{AllowAnyAuthenticatedClient},
};
use tokio::net::{TcpStream};
use tokio_rustls::{
TlsAcceptor,
TlsConnector,
rustls::{self},
client::TlsStream as ClientTlsStream,
};
pub fn load_certs(filename: &str) -> Vec<rustls::Certificate> {
let certfile = File::open(filename).expect("cannot open certificate file");
let mut reader = BufReader::new(certfile);
rustls_pemfile::certs(&mut reader)
.unwrap()
.iter()
.map(|v| rustls::Certificate(v.clone()))
.collect()
}
pub fn load_private_key(filename: &str) -> rustls::PrivateKey {
let keyfile = File::open(filename).expect("cannot open private key file");
let mut reader = BufReader::new(keyfile);
loop {
match rustls_pemfile::read_one(&mut reader).expect("cannot parse private key .pem file") {
Some(rustls_pemfile::Item::RSAKey(key)) => return rustls::PrivateKey(key),
Some(rustls_pemfile::Item::PKCS8Key(key)) => return rustls::PrivateKey(key),
None => break,
_ => {}
}
}
panic!(
"no keys found in {:?} (encrypted keys not supported)",
filename
);
}
pub fn lookup_ipv4(host: &str, port: u16) -> SocketAddr {
let addrs = (host, port).to_socket_addrs().unwrap();
for addr in addrs {
if let SocketAddr::V4(_) = addr {
return addr;
}
}
unreachable!("Cannot lookup address");
}
fn make_client_config(ca_file: &str, certs_file: &str, key_file: &str) -> Arc<rustls::ClientConfig> {
let cert_file = File::open(&ca_file).expect("Cannot open CA file");
let mut reader = BufReader::new(cert_file);
let mut root_store = RootCertStore::empty();
root_store.add_parsable_certificates(&rustls_pemfile::certs(&mut reader).unwrap());
let suites = rustls::DEFAULT_CIPHER_SUITES.to_vec();
let versions = rustls::DEFAULT_VERSIONS.to_vec();
let certs = load_certs(certs_file);
let key = load_private_key(key_file);
let config = rustls::ClientConfig::builder()
.with_cipher_suites(&suites)
.with_safe_default_kx_groups()
.with_protocol_versions(&versions)
.expect("inconsistent cipher-suite/versions selected")
.with_root_certificates(root_store)
.with_single_cert(certs, key)
.expect("invalid client auth certs/key");
Arc::new(config)
}
fn make_server_config(certs: &str, key_file: &str) -> Arc<rustls::ServerConfig> {
let roots = load_certs(certs);
let certs = roots.clone();
let mut client_auth_roots = RootCertStore::empty();
for root in roots {
client_auth_roots.add(&root).unwrap();
}
let client_auth = AllowAnyAuthenticatedClient::new(client_auth_roots);
let privkey = load_private_key(key_file);
let suites = rustls::ALL_CIPHER_SUITES.to_vec();
let versions = rustls::ALL_VERSIONS.to_vec();
let mut config = rustls::ServerConfig::builder()
.with_cipher_suites(&suites)
.with_safe_default_kx_groups()
.with_protocol_versions(&versions)
.expect("inconsistent cipher-suites/versions specified")
.with_client_cert_verifier(client_auth)
.with_single_cert_with_ocsp_and_sct(certs, privkey, vec![], vec![])
.expect("bad certificates/private key");
config.key_log = Arc::new(rustls::KeyLogFile::new());
config.session_storage = rustls::server::ServerSessionMemoryCache::new(256);
Arc::new(config)
}
pub async fn new_tls_stream(domain: &str, addr: std::net::SocketAddr,
ca_file: &str, cert_file: &str, key_file: &str) -> ClientTlsStream<TcpStream> {
let config = make_client_config(&ca_file, &cert_file, &key_file);
let connector = TlsConnector::from(config);
let stream = TcpStream::connect(&addr).await.unwrap();
let domain = rustls::ServerName::try_from(domain)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid dnsname")).unwrap();
let stream = connector.connect(domain, stream).await.unwrap();
stream
}
pub fn new_tls_acceptor(cert_file: &str, key_file: &str) -> TlsAcceptor {
let config = make_server_config(&cert_file, &key_file);
let acceptor = TlsAcceptor::from(config);
acceptor
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::net::TcpListener;
use tokio::io::{AsyncWriteExt, AsyncReadExt};
const CA_FILE: &str = "cert/dev/ca.cert";
const CLIENT_CERT_FILE: &str = "cert/dev/client.cert";
const CLIENT_KEY_FILE: &str = "cert/dev/client.key";
const SERVER_CERT_FILE: &str = "cert/dev/server.fullchain";
const SERVER_KEY_FILE: &str = "cert/dev/server.rsa";
#[tokio::test]
async fn tls() {
let msg = b"Hello world\n";
let mut buf = [0; 12];
start_server().await;
start_client(msg, &mut buf).await;
assert_eq!(&buf, msg);
}
async fn start_server() {
let tls_acceptor = new_tls_acceptor(SERVER_CERT_FILE, SERVER_KEY_FILE);
let listener = TcpListener::bind("0.0.0.0:5002").await.unwrap();
tokio::spawn(async move {
let (stream, _peer_addr) = listener.accept().await.unwrap();
let mut tls_stream = tls_acceptor.accept(stream).await.unwrap();
println!("server: Accepted client conn with TLS");
let mut buf = [0; 12];
tls_stream.read(&mut buf).await.unwrap();
println!("server: got data: {:?}", buf);
tls_stream.write(&buf).await.unwrap();
println!("server: flush the data out");
});
}
async fn start_client(msg: &[u8], buf: &mut [u8]) {
let addr = lookup_ipv4("127.0.0.1", 5002);
let mut tls_stream =
new_tls_stream("localhost", addr, CA_FILE, CLIENT_CERT_FILE, CLIENT_KEY_FILE).await;
tls_stream.write(msg).await.unwrap();
println!("client: send data");
tls_stream.read(buf).await.unwrap();
println!("client: read echoed data");
}
}
参考
刘超. HTTPS协议:点外卖的过程原来这么复杂.
https://www.jscape.com/blog/client-certificate-authentication
https://www.makethenmakeinstall.com/2014/05/ssl-client-authentication-step-by-step/
没有评论