SSL\TLS协议与数字证书

SSL\TLS协议与证书链

reference:

https://blog.csdn.net/qq_51789211/article/details/127778352
https://www.runoob.com/np/tls-protocol.html

概述

SSL(Socket Secure Layer)是TLS(Transport Layer Security)的前身。TLS1.0在SSL3.0的基础上提出,当前已不推荐使用,推荐使用较新版本的TLS。

TLS(Transport Layer Security,传输层安全)是一种用于在网络中加密数据传输的协议,旨在保护数据的机密性、完整性和身份验证。

TLS基于TCP,建立两个进程之间的安全传输连接。

工作原理

TLS通过在应用层和传输层之间插入加密层,保护数据的安全。核心功能是建立加密通道和验证身份。TLS握手过程如下。

image

图中流程为客户端验证服务器证书的单向验证流程,实际情况下,TLS支持双向验证,此时不仅客户端验证服务器证书链,服务器也要验证客户端的证书链。

数字证书

数字证书是基于非对称加密(公钥 + 私钥)的 “网络身份凭证”,核心作用是证明 “公钥的真实归属”,解决 “公钥伪造” 和 “身份冒充” 问题,是 HTTPS、电子签名、安全登录等场景的信任基石。简单说,数字证书就像 “网络世界的身份证”,由权威机构背书,确保双方通信时 “对方是可信的”。

一、 为什么需要数字证书?

非对称加密中,公钥1是公开分发的,但存在一个致命漏洞:​公钥可能被伪造(即 “中间人攻击”)。

举个例子:你想给银行网站发敏感信息(如密码),银行公开了公钥 A,你用 A 加密信息后发送。但如果黑客拦截了你的请求,伪造了一个 “假公钥 A’”,并冒充银行把 A’发给你,你用 A’加密的信息,黑客就能用自己的私钥解密(获取敏感信息),再用真实的公钥 A 加密后发给银行 —— 你和银行都无法发现异常。

而数字证书的作用就是 “防伪” :银行的公钥会由 CA 签署成证书,你收到证书后,会先验证 CA 的签名(用 CA 的公钥),确认证书是真实的,进而确认证书里的公钥是银行的,避免被中间人欺骗。

二、核心定义和本质

数字证书是一份​结构化的电子文件,包含以下关键信息(由权威机构加密签署,不可篡改):

  • 证书持有者的身份信息(如网站域名、企业名称、个人姓名);
  • 证书持有者的​公钥(用于加密通信或验证签名);
  • 签发机构(CA,Certificate Authority,即 “证书认证中心”)的名称;
  • 证书的有效期(超出期限则失效,需重新申请);
  • 签发机构用自身私钥生成的​数字签名(用于验证证书本身的真实性)。

其本质是:由权威第三方(CA)为 “公钥 + 身份” 做担保,告诉接收方 “这份公钥确实属于该身份,可放心使用”。

三、数字证书的核心工作流程(以 HTTPS 为例)

  1. 证书申请(CSR 提交) :网站运营者生成自己的公钥 + 私钥,然后向 CA 提交 “证书申请文件(CSR)”,包含网站域名、公钥等信息,并证明自己是域名的合法所有者(如验证域名解析、企业资质等)。

  2. CA 审核与签发​:CA 验证申请者的身份和域名所有权后,用 CA 自身的私钥对申请者的公钥、身份信息等进行数字签名,生成正式的数字证书,发给申请者。

  3. 证书部署与使用:网站将数字证书部署在服务器上,当用户通过 HTTPS 访问时,服务器会把证书发送给用户的浏览器。

  4. 证书验证(关键步骤)

    • 浏览器先获取 CA 的公钥(主流 CA 的公钥已预装在浏览器 / 操作系统中,如 VeriSign、Let’s Encrypt 等);
    • 用 CA 的公钥验证证书上的 “CA 数字签名”—— 若验证通过,说明证书未被篡改,且公钥确实是该网站的;
    • 同时检查证书的有效期、域名是否匹配(如证书是www.abc.com,不能用于www.def.com),全部通过后,才能建立安全加密通信。

四、核心技术:CA 与信任链

数字证书的信任基础是 “CA 的权威性”,而 CA 体系是分层的(避免单一 CA 风险),形成 “信任链”,信任链是保障 CA 体系安全、解决 “如何信任 CA” 的关键机制 —— 两者共同构成了非对称加密的信任基础,确保数字证书不被伪造、身份可追溯:

  • 根 CA:顶级 CA,其公钥预装在浏览器 / 操作系统中(如 Windows、Chrome 的根证书库),无需再验证(默认可信);
  • 中间 CA:由根 CA 授权签发的二级 CA,实际用于给用户签发证书(根 CA 不直接面向用户,避免私钥泄露风险);
  • 信任链验证:当验证中间 CA 签发的证书时,会先验证中间 CA 的证书(由根 CA 签名),再验证用户证书(由中间 CA 签名),层层追溯到根 CA,形成完整信任链。

1. CA 的核心定位与作用

CA 是具备权威资质的第三方机构(需符合国际 / 国家安全标准),核心职责是 “验证身份 + 签发证书 + 管理证书生命周期”,相当于 “网络世界的公证处”:

  1. 身份验证:接收用户(个人 / 企业 / 服务器)的证书申请后,验证申请者的真实身份(如域名所有权、企业资质、个人信息),确保 “公钥与身份对应”;
  2. 签发证书:验证通过后,用 CA 自身的私钥对申请者的公钥、身份信息、有效期等数据进行数字签名,生成合法的数字证书;
  3. 证书管理:负责证书的更新、吊销(如私钥泄露时)、查询等,发布吊销列表(CRL)或提供在线验证接口(OCSP),确保失效证书不被滥用;
  4. 信任背书:CA 的权威性来自其合规资质(如国际 WebTrust 认证、国内《电子认证服务许可证》),其公钥预装在浏览器、操作系统、手机系统中,默认为 “可信”。

2. 为什么需要信任链?(CA 分层的核心原因)

如果只有一个 “顶级 CA” 直接给所有用户签发证书,会存在两个致命问题:

  1. 私钥泄露风险:顶级 CA 的私钥一旦泄露,所有由它签发的证书都会失效,整个信任体系崩塌;
  2. 管理效率低:全球亿级用户 / 服务器直接向顶级 CA 申请证书,审核、签发压力无法承载。

因此,CA 体系采用​分层架构,通过 “信任链” 层层背书,既分散风险,又提升管理效率 —— 信任链的本质是 “从可信的顶级 CA,逐级验证到最终用户证书的链条”。

3. 信任链的结构与层级(以 HTTPS 为例)

信任链是自上而下的层级关系,核心分为 3 层(部分场景可能有更多中间层):

3.1. 根 CA(Root CA):信任的起点
  • 地位:CA 体系的 “顶级权威”,是信任链的源头,无需其他机构验证(默认可信);

  • 关键特点:

    • 私钥高度保密(通常存储在离线硬件安全模块 HSM (Hardware Security Module)中,永不联网);
    • 公钥预装在主流浏览器、操作系统、手机系统的 “根证书库” 中;
    • 不直接面向终端用户签发证书,仅用于授权 “中间 CA”。
3.2. 中间 CA(Intermediate CA):信任的传递者
  • 地位:由根 CA 签发证书(即 “中间 CA 证书”)的二级 CA,是实际面向用户的 “签发者”;

  • 关键特点:

    • 根 CA 通过数字签名,将自己的信任 “传递” 给中间 CA;
    • 可根据地域、行业、用途细分(如 “金融行业中间 CA”“国内某地区中间 CA”);
    • 即使某一个中间 CA 的私钥泄露,仅影响其签发的证书,不会波及整个根 CA 体系,风险可控。
3.3. 终端实体证书(End-Entity Certificate):用户 / 服务器的证书
  • 地位:信任链的最底层,即普通用户、企业、网站服务器使用的证书(如你访问淘宝时,淘宝服务器出示的证书);
  • 关键特点:由中间 CA 签发,其合法性依赖于 “中间 CA 证书的有效性”,最终追溯到根 CA。

4. 信任链的验证流程

当你用浏览器访问 HTTPS 网站时,浏览器会自动完成信任链验证,步骤如下(以 “根 CA→中间 CA→淘宝证书” 为例):

  1. 获取终端证书:网站服务器向浏览器发送自己的证书(终端实体证书);
  2. 检查终端证书的签发者:浏览器读取证书中的 “签发者信息”,发现该证书由 “某中间 CA” 签发;
  3. 获取中间 CA 证书:浏览器从网站服务器获取该中间 CA 的证书(或从本地缓存、CA 服务器下载);
  4. 验证中间 CA 证书:用 “根 CA 的公钥” 验证中间 CA 证书上的 “根 CA 数字签名”—— 若签名验证通过,说明中间 CA 是根 CA 授权的,可信;
  5. 验证终端证书:用 “中间 CA 的公钥” 验证终端证书上的 “中间 CA 数字签名”—— 若通过,说明终端证书未被篡改,且公钥属于该网站;
  6. 确认信任链完整:从终端证书→中间 CA→根 CA,层层验证无断裂,且证书有效期、域名匹配,浏览器才判定 “证书可信”,建立安全连接。

私钥验证机制详解

1. 基本原理

私钥验证的核心是基于非对称加密的特性:

  • 公钥加密的数据只能用对应的私钥解密
  • 私钥签名的数据只能用对应的公钥验证

这种特性确保了只有私钥的持有者才能生成有效的签名。

2. TLS握手过程中的私钥验证

在TLS握手过程中,私钥验证的具体步骤如下:

以客户端向服务器证明身份为例:

  1. 客户端发送证书

    • 客户端发送自己的证书链(包含终端证书和中间证书)
    • 证书中包含客户端的公钥
  2. 服务器发送挑战数据

    • 服务器生成一些随机数据作为"挑战"
    • 这些数据是唯一的,每次握手都不同
  3. 客户端使用私钥签名挑战

    // 这部分在OpenSSL内部自动完成
    SSL_connect(ssl);  // 握手过程中会自动进行私钥签名
    
    • 客户端使用自己的私钥(client.key)对挑战数据进行签名
    • 签名过程是:用私钥对挑战数据的哈希值进行加密
  4. 服务器验证签名

    • 服务器收到签名后,使用客户端证书中的公钥进行验证
    • 验证过程是:用公钥解密签名,得
    • 到哈希值
    • 同时计算原始挑战数据的哈希值
    • 比较两个哈希值,如果相同则验证成功

3. 为什么这能证明身份?

这个机制能证明身份的原因是:

  1. 只有私钥持有者才能生成有效签名

    • 即使攻击者知道公钥和挑战数据,也无法生成有效签名
    • 因为签名需要私钥,而私钥只有合法的客户端拥有
  2. 每次挑战都不同

    • 使用随机挑战数据防止重放攻击
    • 即使攻击者截获了之前的签名,也无法在新会话中使用
  3. 证书绑定身份

    • 证书中的公钥与私钥是数学上的一对
    • 证书本身由CA签名,证明了公钥的所有者身份

4. 实际代码中的体现

在代码中,这个过程是自动进行的:

// 客户端代码
// 加载客户端证书和私钥
SSL_CTX_use_certificate_chain_file(ctx, CLIENT_CERT_CHAIN.c_str());
SSL_CTX_use_PrivateKey_file(ctx, CLIENT_PRIVATE_KEY.c_str(), SSL_FILETYPE_PEM);

// 握手过程中自动进行私钥验证
if (SSL_connect(ssl) <= 0) {
// 如果验证失败,这里会返回错误
}

// 服务器代码
// 要求客户端提供证书
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nullptr);

// 握手过程中自动验证客户端证书和私钥
if (SSL_accept(ssl) <= 0) {
// 如果验证失败,这里会返回错误
}

5. 简化的示例

为了更好地理解,这里有一个简化的示例说明这个过程:

// 假设的简化过程(实际TLS握手更复杂)

// 1. 服务器发送挑战
std::string challenge = "random_data_12345";

// 2. 客户端使用私钥签名(实际OpenSSL内部完成)
std::string signature = rsa_sign_with_private_key(challenge, client_private_key);

// 3. 客户端发送证书和签名
send_to_server(client_certificate, signature);

// 4. 服务器验证签名
bool verify_result = rsa_verify_signature(challenge, signature, public_key_from_certificate);
if (verify_result) {
// 验证成功,客户端确实拥有对应私钥
}

代码实例

git仓库

https://gitcode.com/KindleDawn/TLS_verify_demo

证书生成步骤

根CA证书 (ca.crt)
    ↓ (签名)
中间CA证书 (intermediate.crt)
    ↓ (签名)
终端证书 (server.crt 或 client.crt)
根CA证书:生成根CA私钥-》通过私钥生成自签名根CA证书
中间CA证书:生成中间CA私钥-》通过私钥生成中间CA CSR-》用根CA证书和私钥签名生成中间CA证书
服务器终端证书:生成服务器终端私钥-》通过私钥生成服务器CSR-》用中间CA证书和私钥签名生成服务器终端证书
客户端终端证书:生成客户端终端私钥-》通过私钥生成客户端CSR-》用中间CA证书和私钥签名生成客户端终端证书
  • CSR 是 Certificate Signing Request(证书签名请求)的缩写,本质是「向 CA(证书颁发机构)申请数字证书时提交的 “标准化申请表”」—— 由证书申请者(比如你生成服务器 / 客户端证书时)用自己的私钥生成,包含申请者的公钥 + 身份信息(如国家、公司、域名等),且会用申请者的私钥对这些信息签名,确保内容不可篡改。

    • 身份信息(Subject) ​:你在server.conf​里配置的C/ST/L/O/CN等,CA 会核验这些信息(自签名 CA 可跳过核验,商用 CA 会严格审核);
    • 公钥(Subject Public Key Info) ​:申请者自己生成的私钥对应的公钥(比如server.key对应的公钥)—— 这是 CSR 的核心,CA 签发的证书会直接嵌入这个公钥;
    • 签名(Signature) :申请者用自己的私钥(server.key)对 “身份信息 + 公钥” 做的签名 ——CA 验证这个签名,就能确认 “CSR 中的公钥确实属于申请者”(因为只有私钥持有者能生成这个签名)。

详细的证书生成配置文件和脚本见git仓库。

基于TCP的服务器和客户端TLS验证代码

基本流程

  1. 初始化openssl库
  2. 创建并配置openssl上下文(其中会导入证书链)
  3. 创建tcp socket
  4. 创建ssl对象绑定tcp socket
  5. 使用ssl对象进行tcp通信,其会自动使用之前配置的openssl上下文进行证书验证和加密通信

server.cpp

#include <iostream>
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <openssl/ssl.h>
#include <openssl/err.h>

using namespace std;

const int PORT = 8443;
const string CERT_PATH = "crt/mtls_certs/";
const string SERVER_CERT_CHAIN = CERT_PATH + "server_chain.crt";
const string SERVER_PRIVATE_KEY = CERT_PATH + "server.key";
const string CA_CERT = CERT_PATH + "ca.crt"; // 用于验证客户端证书

// 初始化OpenSSL库
void init_openssl() {
OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_LOAD_CRYPTO_STRINGS, nullptr);
SSL_load_error_strings();
OpenSSL_add_all_algorithms();
}

// 创建并配置SSL上下文
SSL_CTX* create_context() {
const SSL_METHOD* method = TLS_server_method();
SSL_CTX* ctx = SSL_CTX_new(method);
if (!ctx) {
ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}

// 加载服务器证书链和私钥
if (SSL_CTX_use_certificate_chain_file(ctx, SERVER_CERT_CHAIN.c_str()) &lt;= 0) {
    ERR_print_errors_fp(stderr);
    exit(EXIT_FAILURE);
}
if (SSL_CTX_use_PrivateKey_file(ctx, SERVER_PRIVATE_KEY.c_str(), SSL_FILETYPE_PEM) &lt;= 0) {
    ERR_print_errors_fp(stderr);
    exit(EXIT_FAILURE);
}

// 加载根CA证书(用于验证客户端证书)
if (SSL_CTX_load_verify_locations(ctx, CA_CERT.c_str(), nullptr) &lt;= 0) {
    ERR_print_errors_fp(stderr);
    exit(EXIT_FAILURE);
}

// 要求客户端提供证书
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nullptr);

return ctx;

}

// 创建TCP服务器套接字
int create_socket() {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}

struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = INADDR_ANY;

if (bind(sockfd, (struct sockaddr*)&amp;addr, sizeof(addr)) &lt; 0) {
    perror(&quot;bind failed&quot;);
    exit(EXIT_FAILURE);
}

if (listen(sockfd, 10) &lt; 0) {
    perror(&quot;listen failed&quot;);
    exit(EXIT_FAILURE);
}

cout &lt;&lt; &quot;Server listening on port &quot; &lt;&lt; PORT &lt;&lt; endl;
return sockfd;

}

int main() {
init_openssl();
SSL_CTX* ctx = create_context();
int sockfd = create_socket();

while (true) {
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);
    int client_sockfd = accept(sockfd, (struct sockaddr*)&amp;client_addr, &amp;client_len);
    if (client_sockfd &lt; 0) {
        perror(&quot;accept failed&quot;);
        continue;
    }

    cout &lt;&lt; &quot;Client connected: &quot; &lt;&lt; inet_ntoa(client_addr.sin_addr) &lt;&lt; &quot;:&quot; &lt;&lt; ntohs(client_addr.sin_port) &lt;&lt; endl;

    // 创建SSL对象并绑定客户端套接字
    SSL* ssl = SSL_new(ctx);
    SSL_set_fd(ssl, client_sockfd);

    // 执行TLS握手(会验证客户端证书)
    if (SSL_accept(ssl) &lt;= 0) {
        ERR_print_errors_fp(stderr);
        cout &lt;&lt; &quot;TLS handshake failed (client certificate invalid?)&quot; &lt;&lt; endl;
    } else {
        cout &lt;&lt; &quot;TLS handshake succeeded (mutual authentication passed)&quot; &lt;&lt; endl;

        // 发送数据给客户端
        const string response = &quot;Hello from mTLS server!&quot;;
        SSL_write(ssl, response.c_str(), response.length());
        cout &lt;&lt; &quot;Sent to client: &quot; &lt;&lt; response &lt;&lt; endl;

        // 接收客户端数据
        char buffer[1024] = {0};
        int bytes_read = SSL_read(ssl, buffer, sizeof(buffer));
        if (bytes_read &gt; 0) {
            cout &lt;&lt; &quot;Received from client: &quot; &lt;&lt; string(buffer, bytes_read) &lt;&lt; endl;
        }
    }

    // 关闭连接
    SSL_shutdown(ssl);
    SSL_free(ssl);
    close(client_sockfd);
    cout &lt;&lt; &quot;Client disconnected&quot; &lt;&lt; endl;
}

close(sockfd);
SSL_CTX_free(ctx);
return 0;

}

client.cpp

#include <iostream>
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <openssl/ssl.h>
#include <openssl/err.h>

using namespace std;

const string SERVER_IP = "127.0.0.1";
const int PORT = 8443;
const string CERT_PATH = "crt/mtls_certs/";
const string CLIENT_CERT_CHAIN = CERT_PATH + "client_chain.crt";
const string CLIENT_PRIVATE_KEY = CERT_PATH + "client.key";
const string CA_CERT = CERT_PATH + "ca.crt"; // 用于验证服务器证书

// 初始化OpenSSL库
void init_openssl() {
OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_LOAD_CRYPTO_STRINGS, nullptr);
SSL_load_error_strings();
OpenSSL_add_all_algorithms();
}

// 创建并配置SSL上下文
SSL_CTX* create_context() {
const SSL_METHOD* method = TLS_client_method();
SSL_CTX* ctx = SSL_CTX_new(method);
if (!ctx) {
ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}

// 加载客户端证书链和私钥
if (SSL_CTX_use_certificate_chain_file(ctx, CLIENT_CERT_CHAIN.c_str()) &lt;= 0) {
    ERR_print_errors_fp(stderr);
    exit(EXIT_FAILURE);
}
if (SSL_CTX_use_PrivateKey_file(ctx, CLIENT_PRIVATE_KEY.c_str(), SSL_FILETYPE_PEM) &lt;= 0) {
    ERR_print_errors_fp(stderr);
    exit(EXIT_FAILURE);
}

// 加载根CA证书(用于验证服务器证书)
if (SSL_CTX_load_verify_locations(ctx, CA_CERT.c_str(), nullptr) &lt;= 0) {
    ERR_print_errors_fp(stderr);
    exit(EXIT_FAILURE);
}

return ctx;

}

// 连接到服务器
int connect_to_server() {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}

struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
if (inet_pton(AF_INET, SERVER_IP.c_str(), &amp;server_addr.sin_addr) &lt;= 0) {
    perror(&quot;invalid server address&quot;);
    exit(EXIT_FAILURE);
}

if (connect(sockfd, (struct sockaddr*)&amp;server_addr, sizeof(server_addr)) &lt; 0) {
    perror(&quot;connection failed&quot;);
    exit(EXIT_FAILURE);
}

cout &lt;&lt; &quot;Connected to server: &quot; &lt;&lt; SERVER_IP &lt;&lt; &quot;:&quot; &lt;&lt; PORT &lt;&lt; endl;
return sockfd;

}

int main() {
init_openssl();
SSL_CTX* ctx = create_context();
int sockfd = connect_to_server();

// 创建SSL对象并绑定套接字
SSL* ssl = SSL_new(ctx);
SSL_set_fd(ssl, sockfd);

// 执行TLS握手(会验证服务器证书)
if (SSL_connect(ssl) &lt;= 0) {
    ERR_print_errors_fp(stderr);
    cout &lt;&lt; &quot;TLS handshake failed (server certificate invalid?)&quot; &lt;&lt; endl;
} else {
    cout &lt;&lt; &quot;TLS handshake succeeded (mutual authentication passed)&quot; &lt;&lt; endl;

    // 发送数据给服务器
    const string request = &quot;Hello from mTLS client!&quot;;
    SSL_write(ssl, request.c_str(), request.length());
    cout &lt;&lt; &quot;Sent to server: &quot; &lt;&lt; request &lt;&lt; endl;

    // 接收服务器数据
    char buffer[1024] = {0};
    int bytes_read = SSL_read(ssl, buffer, sizeof(buffer));
    if (bytes_read &gt; 0) {
        cout &lt;&lt; &quot;Received from server: &quot; &lt;&lt; string(buffer, bytes_read) &lt;&lt; endl;
    }
}

// 关闭连接
SSL_shutdown(ssl);
SSL_free(ssl);
close(sockfd);
SSL_CTX_free(ctx);
return 0;

}

验证证书链过程中存在的坑

openssl命令验证证书
openssl verify -CAfile ca.crt client_chain.crt
  • ca.crt就是根ca证书

  • client_chain.crt​是中间ca证书+终端证书,但是要想上面这条命令验证通过,client_chain.crt​的顺序必须是中间ca证书在前,终端证书在后。即

    cat intermddiate.crt client.crt > client_chain.crt
    

我们也可以选择下面这条证书验证命令,这样就可以确保在证书正确的情况下,验证通过

openssl verify -CAfile ca.crt -untrusted intermediate.crt client.crt
openssl函数验证证书

然而,cat intermediate.crt client.crt > client_chain.crt​生成的证书链可以通过openssl verify -CAfile ca.crt client_chain.crt​的验证,却不能通过openssl库函数的验证,该函数如下:

SSL_CTX_use_certificate_chain_file(ctx, CLIENT_CERT_CHAIN.c_str())

该函数接受的证书链,要求是终端证书在前,中间ca证书在后,即

cat client.crt intermediate.crt > client_chain.crt

TLS在MQTT中的应用

以下例子基于mosquitto22.0.18,openssl3.0.13-0ubuntu3.6

mosquitto 命令行(验证 TLS 双向认证)

前提条件:

  • 证书和私钥,包括“ca证书,中间ca证书,服务器和客户端终端证书,服务器和客户端私钥”
  • mtls.conf​,启动mosquitto服务器的配置项,文件名自取,但是路径固定在/etc/mosquitto/conf.d/
  • acl.conf​,ACL(access control list),配置哪些用户能访问哪些主题(topic),文件名和路径都不固定,但是在mtls.conf中要指定该文件路径

证书

  • ca.crt, intermediate.crt, client.crt, server.crt, client.key, server.key​的创建如上一节证书生成步骤所示,但是这里需要创建一个ca_chain.crt,原因是mosquitto命令中并不支持“根ca+中间ca+终端证书”三个参数,因此要将“根ca+中间ca”合为一个根证书链。对该根证书链中两个证书的顺序有严格要求,生成命令如下:

    cat intermediate.crt ca.crt > ca_chain.crt
    
  • 可以使用openssl verify -CAfile ca_chain.crt /etc/mosquitto/certs/client.crt验证证书链

  • 我们这里把证书和私钥都放到/etc/mosquitto/certs/

配置文件

mtls.conf

我们把mtls.conf放在/etc/mosquitto/conf.d/

# 核心:开启listener配置独立(2.0+必须加,否则allow_anonymous不生效)
per_listener_settings true

设置监听地址和端口

listener 8883 0.0.0.0

=ACL配置===

允许匿名:不允许。不允许匿名,则必须配置acl

allow_anonymous false

使用证书CN作为MQTT用户名

use_identity_as_username true

指定ACL配置文件路径

acl_file /etc/mosquitto/acl.conf

=TLS配置===

证书路径

cafile /etc/mosquitto/certs/ca_chain.crt
certfile /etc/mosquitto/certs/server.crt
keyfile /etc/mosquitto/certs/server.key

强制证书(关闭则单向认证,否则双向认证)

require_certificate true

日志全开,便于确认配置生效

log_dest stdout
log_type all

  • 层面 作用 配置载体
    TLS 双向认证 验证 “客户端是不是合法身份”(身份验证) cafile​/certfile
    ACL 授权 验证 “合法身份能操作哪些主题”(权限管控) ACL 文件
  • 查看证书CN(也就是用户名)命令

    # 命令
    openssl x509 -in client.crt -noout -subject
    # 正常输出
    subject=C = CN, ST = Beijing, L = Beijing, O = My Company, OU = Client Department, CN = client.user
    # 根据use_identity_as_username true,使用client.crt的客户端用户名为client.user,来自于CN
    
acl.conf

放在/etc/mosquitto/

# 客户端用户名(根据上面配置,就是客户端证书的CN)
user client.user
# 允许订阅+发布所有主题,#是通配符
topic readwrite #

兜底规则,拒绝所有其他用户

user *

拒绝访问所有主题(#是通配符,匹配所有)

topic deny #

  • #在行开头,表示注释符号,在行内,则为通配符。

命令

# 启动mosquitto服务,指定配置文件,并且打印日志,mosquitto.conf包含了所有conf.d路径下的配置文件
sudo mosquitto -c /etc/mosquitto/mosquitto.conf -v

启动订阅客户端,订阅指定主题,并且指定使用的客户端证书和私钥

mosquitto_sub -h 127.0.0.1 -p 8883 -t "test/topic" --cafile /etc/mosquitto/certs/ca_chain.crt --cert /etc/mosquitto/certs/client.crt --key /etc/mosquitto/certs/client.key -v

启动发布客户端,向指定主题发布信息,并且指定使用的客户端证书和私钥

mosquitto_pub -h 127.0.0.1 -p 8883 -t "test/topic" -m "试试就试试" --cafile /etc/mosquitto/certs/ca_chain.crt --cert /etc/mosquitto/certs/client.crt --key /etc/mosquitto/certs/client.key

查看服务是否启动

sudo netstat -tulpn | grep mosquitto

输出

tcp 0 0 0.0.0.0:8883 0.0.0.0:* LISTEN 611122/mosquitto

程序中使用mqsquitto+tls

注:编码只是实现订阅和发布客户端,mqtt服务配置及启动如上一节所示。其中客户端和服务端的证书需要匹配。【证书生成脚本会生成匹配的服务端和客户端的证书及私钥,服务端配置使用生成的服务端证书和密钥,客户端中使用生成的客户端证书及密钥】

https://gitcode.com/KindleDawn/MqttTls


  1. 公钥和私钥

    公钥和私钥是非对称加密算法的核心,也是现代网络安全(如加密通信、数字签名、区块链等)的基础,核心特点是 “一对密钥、分工明确、无法互推”。

    一、核心定义与本质

    • 密钥对:公钥和私钥是通过加密算法(如 RSA、ECC)生成的一对数学相关的字符串,生成时绑定,无法单独存在。
    • 私钥​:又称 “秘密密钥”,是用户绝对保密的核心密钥,需存储在安全设备(如本地硬盘、加密芯片、硬件钱包)中,绝不对外泄露。
    • 公钥:又称 “公开密钥”,可自由对外分发(如发布在网站、区块链地址、密钥服务器),任何人都可获取,无保密要求。

    二、核心工作原理:“加密 - 解密” 与 “签名 - 验证”

    非对称加密的核心价值是解决 “对称加密需共享密钥” 的安全隐患,主要有两大核心用途:

    1. 加密通信(公钥加密,私钥解密)

    用于传递敏感信息(如密码、文件),确保只有接收方才能解密:

    • 发送方:获取接收方的​公钥,用公钥对明文(原始信息)加密,生成无法直接读取的密文;
    • 接收方:用自己唯一的私钥对密文解密,还原为明文;
    • 关键:即使密文和公钥被第三方截取,没有私钥也无法解密(破解难度相当于分解极大的质数,目前算力无法实现)。

    2. 数字签名(私钥签名,公钥验证)

    用于确认信息完整性和发送方身份(防篡改、防伪造),常见于合同签署、软件校验、区块链交易:

    • 发送方:用自己的私钥对信息(或信息的哈希值)进行 “签名”,生成签名数据,与明文一起发送;
    • 接收方:获取发送方的​公钥,用公钥验证签名 —— 若验证通过,说明信息未被篡改,且确实是该私钥持有者发送;
    • 关键:私钥唯一对应发送方,他人无法伪造签名;信息一旦篡改,签名验证会直接失败。

    三、公钥与私钥的核心区别

    维度 公钥 私钥
    保密性 公开可见,无需保密 绝对保密,严禁泄露
    用途 加密信息、验证签名 解密信息、生成签名
    生成与推导 由私钥通过算法生成 算法直接生成,无法由公钥反推
    持有对象 多人持有(接收方 / 验证方) 唯一持有(发送方 / 签名方)
    安全要求 低(公开无风险) 极高(泄露则信息 / 身份不安全)
  2. mosquitto【MQTT消息代理软件】

    mosquitto简述

    概述

    mosquitto是一款开源的MQTT消息代理(服务器)软件,实现了MQTT协议版本3.1和3.1.1,提供轻量级的,支持可发布/可订阅的的消息推送模式。

    官网:Eclipse Mosquitto

    API:mosquitto.h

    安装

    sudo apt install mosquitto  # 安装服务端
    sudo apt install mosquitto-clients  # 安装客户端
    sudo apt-get install libmosquitto-dev # C风格开发依赖包
    sudo apt-get install libmosquittopp-dev  # C++风格封装的libmosquitto开发包
    

    服务端指令

    • 查看服务状态

      sudo service mosquitto status
      
    • 启动服务器

      sudo service mosquitto start
      
    • 关闭服务器

      sudo service mosquitto stop
      
    • 启动服务器并实时显示所有日志

      mosquitto -v
      
    • 根据指定的配置文件启动服务器

      mosquitto -c /etc/mosquitto/mosquitto.conf -d
      
      • -c : 指定配置文件
      • -d : 后台运行
    • 指定端口启动服务器,默认端口是1883,最多指定10次

      mosquitto -p 1884
      

    配置文件

    /etc/mosquitto/mosquitto.conf

    # 消息持久存储
    persistence true
    persistence_location /var/lib/mosquitto/
    

    日志文件

    log_dest file /var/log/mosquitto/mosquitto.log

    其他配置

    include_dir /etc/mosquitto/conf.d

    禁止匿名访问

    allow_anonymous false

    认证配置,即登录账号信息的文件

    password_file /etc/mosquitto/pwfile

    权限配置

    acl_file /etc/mosquitto/aclfile

    监听的端口

    listener 1883

    客户端指令

    订阅主题

    mosquitto_sub -t topic
    

    发布主题

    mosquitto_pub -t topic -m '消息'
    

    Mosquitto库编程(C风格)

    发布信息客户端

    mqtt_pub.c

    #include <stdio.h>
    #include <stdlib.h>
    #include <mosquitto.h>
    #include <string.h>
    

    define HOST "127.0.0.1"

    define PORT 1883

    define KEEP_ALIVE_TIME 60

    define MSG_MAX_SIZE 512

    bool session = true;

    int main(void)
    {
    int err = 0;
    printf("mqtt publish init ...\n");
    struct mosquitto* mosq = NULL;
    char buff[MSG_MAX_SIZE];

    // 初始化
    err = mosquitto_lib_init();
    if(err&lt;0)
    {
        printf(&quot;mosquitto lib int fail...&quot;);
        return -1;
    }
    
    // 创建客户端
    mosq = mosquitto_new(NULL, session, NULL);
    if(mosq==NULL)
    {
        printf(&quot;create client failed...\n&quot;);
        err = -1;
        mosquitto_lib_cleanup();
        return -1;
    }
    
    // 客户端连接broker
    err = mosquitto_connect(mosq, HOST, PORT, KEEP_ALIVE_TIME);
    if(err&lt;0)
    {
        printf(&quot;connect fail&quot;);
        mosquitto_destroy(mosq);
        return -1;
    }
    
    // 启动事件循环(启动独立线程处理mosquitto事件)
    err = mosquitto_loop_start(mosq);
    if(err!=MOSQ_ERR_SUCCESS)
    {
        printf(&quot;mosquitto loop error\n&quot;);
        mosquitto_disconnect(mosq);
        return -1;
    }
    
    // 发布信息到test主题
    strncpy(buff, &quot;hello world!&quot;, 13);
    mosquitto_publish(mosq, NULL, &quot;test&quot;, strlen(buff)+1, buff, 0, 0);
    
    mosquitto_disconnect(mosq);
    mosquitto_loop_stop(mosq, true);
    mosquitto_destroy(mosq);
    mosquitto_lib_cleanup();
    
    return 0;
    

    }

    订阅信息客户端

    mqtt_sub.c

    #include <stdio.h>
    #include <stdlib.h>
    #include <mosquitto.h>
    #include <string.h>
    

    define HOST "127.0.0.1"

    define PORT 1883

    define KEEP_ALIVE 60

    bool session = true;

    // 订阅主题成功时回调
    void mqtt_subscribe_callback(struct mosquitto *mosq,
    void *userdata, int mid, int qos_count, const int *granted_qos)
    {
    int i;
    printf("subscribed (mid: %d): %d", mid, granted_qos[0]);
    for(i=1; i < qos_count; i++){
    printf(", %d", granted_qos[i]);
    }
    printf("\n");
    }

    //消息回调函数,收到订阅的消息后调用
    void mqtt_message_callback(struct mosquitto *mosq, void *userdata, const struct mosquitto_message *message)
    {
    if (message->payloadlen){
    printf("%s: %s \n", message->topic, (char *)message->payload);
    }else{
    printf("%s (null)\n",message->topic);
    }
    }

    //mqtt连接回调
    void mqtt_connect_callback(struct mosquitto *mosq, void *userdata, int result)
    {
    int ret;
    if (!result){
    ret = mosquitto_subscribe(mosq, NULL, "test", 2);
    if(ret < 0){
    printf("Subscription failed\n");
    }else{
    printf("Subscription succeeded\n");
    }
    }else{
    printf("connect failed\n");
    }
    }

    //日志回调函数
    void mqtt_log_callback(struct mosquitto *mosq, void *userdata, int level, const char *str)
    {
    printf("log__ %s\n", str);
    }

    int main(void)
    {

    int err = 0;
    printf(&quot;mqtt client init...\n&quot;);
     
    struct mosquitto *mosq = NULL;
     
    //libmosquitto 库初始化
    err = mosquitto_lib_init();
    if (err &lt; 0){
        printf(&quot;mosquitto lib int fail...&quot;);
        return err;
    }
     
    //创建mosquitto客户端
    mosq = mosquitto_new(NULL,session,NULL);
    if (mosq == NULL){
        printf(&quot;create client failed...\n&quot;);
        err = -1;
        mosquitto_lib_cleanup();
        return err;
    }
     
    //设置回调函数
    mosquitto_log_callback_set(mosq, mqtt_log_callback);
    mosquitto_connect_callback_set(mosq, mqtt_connect_callback);
    mosquitto_message_callback_set(mosq, mqtt_message_callback);
    mosquitto_subscribe_callback_set(mosq, mqtt_subscribe_callback);
     
    //客户端连接服务器
    err = mosquitto_connect(mosq, HOST, PORT, KEEP_ALIVE);
    if (err &lt; 0){
        printf(&quot;connect fail&quot;);    
        mosquitto_destroy(mosq);
        return err;
    }
        
    //启动事件循环,永久阻塞
    err = mosquitto_loop_forever(mosq, -1, 1);
    if (err &lt; 0){
        printf(&quot;mosquitto loop fail&quot;);
        mosquitto_disconnect(mosq);
        return err;
    }
     
    mosquitto_disconnect(mosq);
    mosquitto_loop_stop(mosq, false);
    mosquitto_destroy(mosq);
    mosquitto_lib_cleanup();
     
    return 0;
    

    }

    编译

    CMakeLists.txt

    cmake_minimum_required(VERSION 3.16)
    

    project(mosquitto_demo)

    find_package(PkgConfig REQUIRED)

    pkg_check_modules(MOSQ libmosquitto)

    set(PUB_SOURCES mqtt_pub.c)
    set(SUB_SOURCES mqtt_sub.c)

    add_executable(subclient ${SUB_SOURCES})
    add_executable(pubclient ${PUB_SOURCES})

    target_include_directories(subclient PRIVATE ${MOSQ_INCLUDE_DIRS})
    target_link_libraries(subclient PRIVATE ${MOSQ_LIBRARIES})

    target_include_directories(pubclient PRIVATE ${MOSQ_INCLUDE_DIRS})
    target_link_libraries(pubclient PRIVATE ${MOSQ_LIBRARIES})

    Mosquittopp库编程(C++风格)

    订阅信息客户端

    subclient.cc

    #include<mosquittopp.h>
    #include<iostream>
    #include<string>
    

    const char* topic = "topic1";
    const char* host = "127.0.0.1";
    const int port = 1883;
    const int alivetime = 60;

    class MqttSubClient:public mosqpp::mosquittopp
    {
    public:
    MqttSubClient(const char* id):mosquittopp(id){}
    void on_connect(int rc) override;
    void on_disconnect(int rc) override;
    void on_subscribe(int mid, int qos_count, const int* granted_qos) override;
    void on_message(const struct mosquitto_message* message) override;
    };

    // 连接回调函数
    void MqttSubClient::on_connect(int rc)
    {
    if(rc == MOSQ_ERR_SUCCESS)
    {
    std::cout<<"connect success!"<<std::endl;
    // 订阅主题
    subscribe(nullptr, topic, 1);
    }
    else
    {
    std::cerr<<"connect failed!"<<std::endl;
    }
    }

    // 断开连接回调函数
    void MqttSubClient::on_disconnect(int rc)
    {
    std::cout<<"disconnect success!"<<std::endl;
    }

    // 订阅成功回调函数
    void MqttSubClient::on_subscribe(int mid, int qos_count, const int* granted_qos)
    {
    std::cout<<"订阅 mid: "<<mid<<" success!"<<std::endl;
    }

    // 消息处理回调函数
    void MqttSubClient::on_message(const struct mosquitto_message* message)
    {
    bool match = false;
    mosqpp::topic_matches_sub(topic, message->topic, &match);
    if(match)
    {
    std::string recv(static_cast<char*>(message->payload), message->payloadlen);
    std::cout<<"来自"<<message->topic<<"的消息:"<<recv<<" (mid: "<<message->mid<<")"<<std::endl;
    }
    }

    int main()
    {
    // 初始化
    mosqpp::lib_init();
    MqttSubClient subclient("subclient1");
    int rc;
    rc = subclient.connect(host, port, alivetime);
    if(rc == MOSQ_ERR_ERRNO)
    {
    std::cout<<"连接错误: "<<mosqpp::strerror(rc)<<std::endl;
    }
    else if(MOSQ_ERR_SUCCESS == rc)
    {
    // 启动事件循环,并阻塞
    rc = subclient.loop_forever();
    }

    subclient.disconnect();
    mosqpp::lib_cleanup();
    
    
    return 0;
    

    }

    发布信息客户端

    pubclient.cc

    #include<mosquittopp.h>
    #include<string>
    #include<iostream>
    #include<thread>
    

    const char* topic = "topic1";
    const char* host = "127.0.0.1";
    const int port = 1883;
    const int alivetime = 60;

    class MqttPubClient:public mosqpp::mosquittopp
    {
    public:
    MqttPubClient(const char* id):mosquittopp(id){}
    /// @brief 连接broker时回调
    /// @param rc 返回码
    void on_connect(int rc) override;
    /// @brief 断开连接时回调
    /// @param rc 返回码
    void on_disconnect(int rc) override;
    /// @brief 消息发布成功时回调
    /// @param mid
    void on_publish(int mid) override;
    /// @brief 发布消息
    /// @param message 要发布的消息
    /// @param qos 定义消息传输可靠性 0 1 2
    void publish_message(std::string message, int qos);
    };

    void MqttPubClient::on_connect(int rc)
    {
    if(rc == MOSQ_ERR_SUCCESS)
    {
    std::cout<<"connect success!\n";
    }
    else
    {
    std::cout<<"connect error!";
    }
    }

    void MqttPubClient::on_disconnect(int rc)
    {
    std::cout<<"disconeect!\n";
    }

    void MqttPubClient::on_publish(int mid)
    {
    std::cout<<"消息发布成功 (mid: "<<mid<<" ), topic: "<<topic<<std::endl;
    }

    void MqttPubClient::publish_message(std::string message, int qos)
    {
    int ret = publish(nullptr, topic, message.size(), message.c_str(), qos, false);
    if(ret != MOSQ_ERR_SUCCESS)
    {
    std::cerr<<"publish error!"<<mosqpp::strerror(ret)<<"\n";
    }
    }

    int main()
    {
    // 初始化
    mosqpp::lib_init();
    MqttPubClient publisher("cpp_publisher");

    int rc = publisher.connect(host, port, alivetime);
    if(rc != MOSQ_ERR_SUCCESS)
    {
        std::cerr&lt;&lt;&quot;connect error!&quot;&lt;&lt;mosqpp::strerror(rc)&lt;&lt;std::endl;
        return -1;
    }
    
    // 启动异步事件循环
    publisher.loop_start();
    
    // 消息发布
    std::string msg;
    std::this_thread::sleep_for(std::chrono::milliseconds(5));  // 两个sleep是为了确保主线程的输出在后台线程的回调函数输出之后
    std::cout&lt;&lt;&quot;请输入要发布的消息:&quot;;
    while(std::cin&gt;&gt;msg)
    {
        publisher.publish_message(msg, 1);
        std::this_thread::sleep_for(std::chrono::milliseconds(5));
        std::cout&lt;&lt;&quot;请输入要发布的消息:&quot;;
    }
    
    // 清理资源
    publisher.loop_stop(true);
    publisher.disconnect();
    mosqpp::lib_cleanup();
    
    return 0;
    

    }

    编译

    CMakeLists.txt

    cmake_minimum_required(VERSION 3.16)
    

    project(mqtt_cpp_style)

    find_package(PkgConfig REQUIRED)

    pkg_check_modules(MOSQ libmosquittopp)

    set(SUBSRC subclient.cc)
    set(PUBSRC pubclient.cc)

    add_executable(subclient ${SUBSRC})
    add_executable(pubclient ${PUBSRC})

    target_include_directories(subclient PRIVATE ${MOSQ_INCLUDE_DIRS})
    target_link_libraries(subclient PRIVATE ${MOSQ_LIBRARIES})

    target_include_directories(pubclient PRIVATE ${MOSQ_INCLUDE_DIRS})
    target_link_libraries(pubclient PRIVATE ${MOSQ_LIBRARIES})

posted @ 2025-12-04 16:27  重光拾  阅读(3)  评论(0)    收藏  举报