土法炼钢兴趣小组的算法知识备份

双向认证 (mTLS) 实战指南

目录

在零信任网络架构中,双向认证 (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 365

2. 服务端实现

在 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 certificate

5. 总结

在 Libevent 中实现 mTLS 并不复杂,主要工作量在于 OpenSSL 的配置。 * 服务端: 必须设置 SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT。 * 鉴权: 握手成功后,务必检查证书内容(如 CN),确保持有合法证书的客户端也是你有权访问的客户端。

完整代码: 05-mtls-server.c | 05-gen-certs.sh

返回 Libevent 专题索引


By .