libhv是一个国产的网络库,在v1.2.5版本提供了SSL客户端支持,可以便携地实现双向认证。
实现
客户端
废话不多说,先show the code:
#include "hv/http_client.h"
using namespace hv;
int main() {
int ret;
http_client_t *cli = http_client_new(NULL, 8080, 1);
if(cli == NULL) {
printf("Error: cli is null\n");
return 0;
}
hssl_ctx_opt_t *ssl_opt = new hssl_ctx_opt_t;
ssl_opt->verify_peer = 1;
ssl_opt->endpoint = HSSL_CLIENT;
ssl_opt->ca_path = NULL;
ssl_opt->ca_file = "cert/ca.crt";
ssl_opt->crt_file = "cert/client.crt";
ssl_opt->key_file = "cert/client_rsa_private.pem";
hssl_ctx_t ctx = hssl_ctx_new(ssl_opt);
if(ctx == NULL) {
printf("Error: ctx is null\n");
return 0;
}
ret = http_client_set_ssl_ctx(cli, ctx);
if (ret != 0) {
printf("Cert Error: %s : %d\n", http_client_strerror(ret), ret);
return 0;
}
HttpRequest req;
req.method = HTTP_POST;
req.url = "https://127.0.0.1:8080/echo";
req.headers["Connection"] = "keep-alive";
req.body = "This is a test request.";
req.timeout = 10;
HttpResponse resp;
ret = http_client_send(cli, &req, &resp);
if (ret != 0) {
printf("Request Failed: %s : %d\n", http_client_strerror(ret), ret);
} else {
printf("%d %s\r\n", resp.status_code, resp.status_message());
printf("%s\n", resp.body.c_str());
}
http_client_del(cli);
}
Https服务端
服务端的实现就更简单了,也不是本文的重点,所以用Go来实现一下:
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"log"
"net/http"
)
const (
CACertPath = "cert/ca.crt"
ServerCertPath = "cert/server.crt"
ServerKeyPath = "cert/server_rsa_private.pem"
)
func handler(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
return
}
fmt.Fprintf(w, "Get From Client: %s", string(body))
}
func main() {
pool := x509.NewCertPool()
crt, err := ioutil.ReadFile(CACertPath)
if err != nil {
log.Fatalln("读取证书失败!", err.Error())
}
pool.AppendCertsFromPEM(crt)
http.HandleFunc("/echo", handler)
s := &http.Server{
Addr: ":8080",
TLSConfig: &tls.Config{
ClientCAs: pool,
ClientAuth: tls.RequireAndVerifyClientCert,
},
}
log.Fatal(s.ListenAndServeTLS(ServerCertPath, ServerKeyPath))
}
原理
双向认证基本原理
如果已经清楚双向认证原理的同学,建议直接跳过这一节。
双向认证,顾名思义,客户端和服务器端都需要验证对方的身份,可以使得连接更加安全,在建立Https连接的过程中,握手的流程比单向认证多了几步。 单向认证的过程:客户端从服务器端下载服务器端公钥证书进行验证,然后生成对称密钥用以加密数据、建立安全通信通道。 双向通信的过程:除了客户端需要从服务器端下载服务器的公钥证书进行验证外,服务端还会要求客户端提供证书,因此客户端还需要把客户端的公钥证书上传到服务器端给服务器端进行验证,等双方都认证通过了,才开始建立安全通信通道进行数据传输。
因此最基本的一点:服务端要保存服务端的证书和私钥,并在建立连接时将证书发给客户端;同样的,客户端也要保存客户端的证书和私钥,并在建立连接时将证书发给服务端。
libhv客户端实现原理
那么libhv是咋做到将一个hssl_ctx绑定到http_client便可以实现双向认证(其实也就是向服务端提供自己的证书认证)的呢?
以下内容皆基于libhv v1.2.5发布版
首先,我们使用hssl_ctx_new 函数来初始化了一个hssl_ctx,而在libhv中,hssl_ctx是一个携带了证书、私钥等的SSL上下文。
对着源码,在ssl/openssl.c:16行 ,声明了其初始化函数hssl_ctx_new :可以看到,该函数执行了Openssl的SSL初始化工作,并且,若初始化时填入了param,也就是hssl_ctx_opt_t ,则hssl_ctx会使用你所设置的CA证书来执行SSL_CTX_load_verify_locations 、使用证书文件来执行SSL_CTX_use_certificate_file 、使用私钥文件来执行SSL_CTX_use_PrivateKey_file ,并随后校验证书及私钥,同时,若设置hssl_ctx_opt_t->verify_peer != 0 则会使SSL验证模式设为SSL_VERIFY_PEER 。
熟悉OpenSSL的同学应该已经发现了,上述过程,正是客户端设置己方证书、私钥并要求对方提供证书,也就是双向认证的初始化流程。
随后,我们使用http_client_set_ssl_ctx 函数来将该hssl_ctx与之前创建的HttpClient绑定,在该函数(http/client/http_client.cpp:114)中,将HttpClient的ssl_ctx字段设为了该hssl_ctx,而该字段可以在http_client_connect (http/client/http_client.cpp:429)等函数中发现:若该字段不为NULL且为https请求,则会使用此前设置的hssl_ctx来接管整个客户端连接、通信过程,也就是使用hssl_read、hssl_write等函数来代替原本的recv、send等函数,使得整个通信信道被hssl_ctx所管理的SSL连接接管。
而由于此前已经在hssl_ctx_new时设置了己方的证书、私钥,并且SSL连接已接管,因此,在服务端要求客户端提供证书认证时,自然而然地,由OpenSSL处理了相关逻辑,正确地提供了己方证书,完成双向认证。
附加说明
这里着重说明一下,为了正常使用libhv的SSL认证功能,需要在安装libhv时附加Openssl选项,这是我的编译安装命令:
sudo apt install openssl libssl-dev
git clone https://github.com/ithewei/libhv.git
cd libhv
./configure --with-openssl
make
sudo make install
如何编译引用libhv库的c++代码,想必看这篇文章的同学应该都会吧?我就不附上了。
生成证书及私钥:
openssl req -newkey rsa:2048 -nodes -keyout ca_rsa_private.pem -x509 -days 365 -out ca.crt -subj "/C=CN/ST=GD/L=SZ/O=COM/OU=NSP/CN=CA/emailAddress=youremail@qq.com"
openssl req -newkey rsa:2048 -nodes -keyout server_rsa_private.pem -out server.csr -subj "/C=CN/ST=GD/L=SZ/O=COM/OU=NSP/CN=SERVER/emailAddress=youremail@qq.com"
openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca_rsa_private.pem -CAcreateserial -out server.crt
openssl req -newkey rsa:2048 -nodes -keyout client_rsa_private.pem -out client.csr -subj "/C=CN/ST=GD/L=SZ/O=COM/OU=NSP/CN=CLIENT/emailAddress=youremail@qq.com"
openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca_rsa_private.pem -CAcreateserial -out client.crt
|