在零信任网络架构中,双向认证 (Mutual TLS, mTLS) 是服务间通信的标配。与传统的单向认证(仅客户端验证服务端)不同,mTLS 要求服务端也必须验证客户端的身份。
本文将详细介绍如何在 Libevent 中基于 OpenSSL 实现 mTLS。
1. 证书准备
在开始编码之前,我们需要一套测试证书。通常包含: 1. CA (Certificate Authority): 用于签署服务端和客户端证书。 2. Server Cert/Key: 服务端持有。 3. Client Cert/Key: 客户端持有。
快速生成测试证书
# 1. 生成 CA
openssl req -new -x509 -days 365 -nodes -out ca.crt -keyout ca.key -subj "/CN=MyRootCA"
# 2. 生成服务端证书
openssl req -new -nodes -out server.csr -keyout server.key -subj "/CN=localhost"
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 365
# 3. 生成客户端证书
openssl req -new -nodes -out client.csr -keyout client.key -subj "/CN=client-app"
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 3652. 服务端实现
在 Libevent 中,mTLS 的核心在于正确配置
SSL_CTX,然后将其传递给
bufferevent_openssl_socket_new。
2.1 配置 SSL_CTX
关键步骤是设置 SSL_VERIFY_PEER 并加载 CA
证书以验证客户端。
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <event2/bufferevent_ssl.h>
SSL_CTX *create_mtls_context(const char *ca_cert, const char *server_cert, const char *server_key) {
SSL_CTX *ctx = SSL_CTX_new(TLS_server_method());
if (!ctx) return NULL;
// 1. 加载服务端自身的证书和私钥
if (SSL_CTX_use_certificate_chain_file(ctx, server_cert) <= 0 ||
SSL_CTX_use_PrivateKey_file(ctx, server_key, SSL_FILETYPE_PEM) <= 0) {
fprintf(stderr, "Failed to load server cert/key\n");
SSL_CTX_free(ctx);
return NULL;
}
// 2. 加载 CA 证书(用于验证客户端证书)
if (!SSL_CTX_load_verify_locations(ctx, ca_cert, NULL)) {
fprintf(stderr, "Failed to load CA cert\n");
SSL_CTX_free(ctx);
return NULL;
}
// 3. 强制开启客户端验证
// SSL_VERIFY_PEER: 要求客户端发送证书
// SSL_VERIFY_FAIL_IF_NO_PEER_CERT: 如果客户端没发证书,握手失败
SSL_CTX_set_verify(ctx,
SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT,
NULL); // 第三个参数是 verify_callback,可设为 NULL 使用默认验证
return ctx;
}2.2 创建 Bufferevent
在 listener_cb 中,我们使用配置好的
ctx 创建 SSL bufferevent。
void listener_cb(struct evconnlistener *listener, evutil_socket_t fd,
struct sockaddr *sa, int socklen, void *user_data) {
struct event_base *base = user_data;
SSL_CTX *ctx = global_ssl_ctx; // 假设这是全局的或者从 user_data 传入的
SSL *ssl = SSL_new(ctx);
struct bufferevent *bev;
// 创建 SSL bufferevent
bev = bufferevent_openssl_socket_new(base, fd, ssl,
BUFFEREVENT_SSL_ACCEPTING,
BEV_OPT_CLOSE_ON_FREE);
if (!bev) {
fprintf(stderr, "Error constructing bufferevent!\n");
event_base_loopbreak(base);
return;
}
bufferevent_setcb(bev, read_cb, NULL, event_cb, NULL);
bufferevent_enable(bev, EV_READ | EV_WRITE);
}2.3 验证客户端身份 (Authorization)
仅仅验证证书签名是不够的(任何持有该 CA
签发证书的人都能连接)。通常我们还需要检查证书中的
Common Name (CN) 或
Subject Alternative Name (SAN)。
这可以在握手成功后的 event_cb 中进行:
void event_cb(struct bufferevent *bev, short events, void *ctx) {
if (events & BEV_EVENT_CONNECTED) {
// 握手成功
SSL *ssl = bufferevent_openssl_get_ssl(bev);
X509 *cert = SSL_get_peer_certificate(ssl);
if (cert) {
char common_name[256];
X509_NAME_get_text_by_NID(X509_get_subject_name(cert),
NID_commonName,
common_name,
sizeof(common_name));
printf("Client connected with CN: %s\n", common_name);
// 在这里做业务鉴权,例如:
if (strcmp(common_name, "admin-service") != 0) {
printf("Unauthorized client!\n");
bufferevent_free(bev); // 断开连接
X509_free(cert);
return;
}
X509_free(cert);
}
} else if (events & BEV_EVENT_ERROR) {
// 处理错误...
}
}3. 客户端实现
客户端同样需要配置
SSL_CTX,加载客户端证书,并信任 CA。
SSL_CTX *create_client_context(const char *ca_cert, const char *client_cert, const char *client_key) {
SSL_CTX *ctx = SSL_CTX_new(TLS_client_method());
// 加载客户端证书
SSL_CTX_use_certificate_chain_file(ctx, client_cert);
SSL_CTX_use_PrivateKey_file(ctx, client_key, SSL_FILETYPE_PEM);
// 加载 CA 以验证服务端
SSL_CTX_load_verify_locations(ctx, ca_cert, NULL);
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
return ctx;
}4. 测试验证
启动服务端后,使用 curl 进行测试:
# 成功连接
curl --cacert ca.crt --cert client.crt --key client.key https://localhost:8080
# 失败连接(无证书)
curl --cacert ca.crt https://localhost:8080
# 预期报错: alert bad certificate5. 总结
在 Libevent 中实现 mTLS 并不复杂,主要工作量在于 OpenSSL
的配置。 * 服务端: 必须设置
SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT。
* 鉴权: 握手成功后,务必检查证书内容(如
CN),确保持有合法证书的客户端也是你有权访问的客户端。
完整代码: 05-mtls-server.c | 05-gen-certs.sh