这篇文章上次修改于 921 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

1 使用自签名证书的目的

本文使用自签名证书的目的:

  • 用于服务端校验客户端是否合法,避免任何一个客户端都可以连上服务端。
  • 基于 TLS,对服务端和客户端之间的传输数据进行加密。

2 自签名证书校验过程

ssl_tls_handshake

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/

https://www.jianshu.com/p/1fc7130eb2c2