src/ssl/ 是最小的一个模块:三个头、三个公开类型。使用者真正要构造的只有一个
——uvcpp_ssl_context,它包一个 SSL_CTX(证书、私钥、CA、协议版本、校验模式、
ALPN 名单)。另一个 uvcpp_ssl 是每连接的包装,由 uvcpp_tcp_client 内部持有,
不是给一般使用者用的门面。
TLS 在这库里是过滤层,不是独立的传输实现:装到 uvcpp_tcp_client 之后,
write() 收明文、读回调交明文,密文只在内部与 socket 之间流动
(src/net/uvcpp_tcp_client.h:193-205)。所以 web/ / webapp/ 那条线一行都不用改。
- 打开方式:
-DUVCPP_ENABLE_OPENSSL=ON - 包含方式:
<ssl/uvcpp_ssl_context.h>(构造上下文必须);只声明变量的话<net/uvcpp_tcp_server.h>就够,它只前置声明 - 不需要 OpenSSL 头:三个头一律前置声明 + PIMPL(
src/ssl/uvcpp_ssl_context.h:26那句<openssl/…>在注释里)
本指南里的签名、默认值、行为都对着当前源码核过。凡是"这一层没做"的地方都明确 标出来,头文件里也写明了 —— 所以下面给的是当前的行为,不是"注释说 A、实现是 B" 的对照表。
| 类型 | 头 | 角色 |
|---|---|---|
uvcpp_ssl_context |
src/ssl/uvcpp_ssl_context.h:32 |
主类型。一个上下文可以共享给多条连接(:8-9) |
uvcpp_ssl |
src/ssl/uvcpp_ssl.h:33 |
每连接包装(一个 SSL*),uvcpp_tcp_client 内部持有 |
ssl_detail::alpn_wire_format |
src/ssl/uvcpp_ssl_common.h:98 |
把 ALPN 名单编成线格式的自由函数,实现细节 |
值类型在 ssl/uvcpp_ssl_common.h:tls_version、tls_mode、tls_verify_mode、
tls_ctx_status、tls_cert_info。
没有 uvcpp_ssl_server 这种类。 服务端 TLS 的落点是
uvcpp_tcp_server::set_ssl_context()。
三个头整段套在 #if UVCPP_OPENSSL_ENABLE 里(src/ssl/uvcpp_ssl.h:17、
src/ssl/uvcpp_ssl_common.h:14、src/ssl/uvcpp_ssl_context.h:18)。
UVCPP_OPENSSL_ENABLE 由 CMake 选项 UVCPP_ENABLE_OPENSSL 决定,最终值写进生成头
uvcpp/uvcpp_config.h。
不要自己 define 这个宏。 外部定义且与本次构建不一致的值是硬 #error
(cmake/uvcpp_config.h.in:56-61)——理由是类布局会变,而不一致的布局没有任何编译
或链接错误,只会在运行时读到错位的垃圾。
关掉这个选项时:src/ssl/ 会被从源文件与头文件列表里排除,而且根本不安装 ssl 头
(CMakeLists.txt:3135-3136)——使用者连 #include <ssl/uvcpp_ssl_context.h> 都会
找不到文件。同理,uvcpp_tcp_server::set_ssl_context() 在关掉时连声明都不存在。
运行时"关掉 TLS"不是宏,是 set_ssl_context(nullptr)。
#include <net/uvcpp_net_read.h>
#include <net/uvcpp_tcp_client.h>
#include <net/uvcpp_tcp_server.h>
#include <ssl/uvcpp_ssl_context.h>
int main() {
// 一个服务端一个上下文。TLS 版本显式给:枚举那几档是**协议下界**,构造函数默认
// TLS_1_2(见 §10),别靠默认值。
uvcpp::uvcpp_ssl_context ssl_ctx(uvcpp::tls_mode::SERVER,
uvcpp::tls_version::TLS_1_2);
if (!ssl_ctx.is_ready()) return 1;
// 测试/内网用自签;真证书换成 §4 那两行
if (!ssl_ctx.generate_self_signed("localhost", 2048)) return 1;
if (!ssl_ctx.check_private_key()) return 1;
uvcpp::uvcpp_tcp_server server;
// 必须在 listen() 之前;裸指针、非拥有,ssl_ctx 必须活得更久
server.set_ssl_context(&ssl_ctx);
server.set_read_callback([](uvcpp::uvcpp_tcp_client& c,
const uvcpp::net_read_result& r) {
if (r.is_data()) {
// r.data / r.size 收到的已经是明文
c.write(r.data, r.size, [](int) {});
}
});
int rc = server.bind("127.0.0.1", 8443);
if (rc != 0) return 1;
// 连接回调只在**握手成功之后**才被调用
rc = server.listen([](uvcpp::uvcpp_tcp_client* client) {
const char kHello[] = "hello over TLS\n";
client->write(kHello, sizeof(kHello) - 1, [](int) {});
}, 128);
if (rc != 0) return 1;
server.run();
return 0;
}客户端侧要能连自签证书,必须显式关掉校验——这是有意的设计,让"关掉校验"在调用点
看得见(src/ssl/uvcpp_ssl_context.cpp:110-112):
#include <memory>
#include <ssl/uvcpp_ssl_context.h>
std::unique_ptr<uvcpp::uvcpp_ssl_context> doc_make_loopback_client() {
// 上下文删了拷贝构造(src/ssl/uvcpp_ssl_context.h:35),而用户声明了拷贝构造就会
// 抑制移动构造 —— 所以它既不能拷贝也不能按值返回,交给 unique_ptr 持有。
std::unique_ptr<uvcpp::uvcpp_ssl_context> ctx(
new uvcpp::uvcpp_ssl_context(uvcpp::tls_mode::CLIENT,
uvcpp::tls_version::TLS_1_2));
// 自签证书要连得上就必须关校验。让"关掉校验"在调用点看得见,是有意的设计。
ctx->set_verify_mode(uvcpp::tls_verify_mode::NONE);
return ctx;
}| 方法 | 返回 | 要点 |
|---|---|---|
load_certificate_file(path) |
bool |
内部用 SSL_CTX_use_certificate_chain_file —— 必须是链,否则中间证书不发,只信根 CA 的客户端握手失败(src/ssl/uvcpp_ssl_context.cpp:135-149) |
load_private_key_file(path) |
bool |
见下面的顺序陷阱 |
load_certificate_data(pem) |
bool |
支持多证书 PEM:叶子 + 逐个 extra_chain_cert |
load_private_key_data(pem) |
bool |
同样附带配对检查 |
load_ca_file(path) |
bool |
SSL_CTX_load_verify_locations |
generate_self_signed(cn, bits = 2048) |
bool |
现场生成 RSA + 自签 X509,有效期 365 天 |
check_private_key() |
bool |
显式配对检查 |
顺序陷阱:先装证书,再装私钥。 load_private_key_file / load_private_key_data
内部都会调 SSL_CTX_check_private_key(src/ssl/uvcpp_ssl_context.cpp:161、:214)。
OpenSSL 在"尚无证书"时该调用返回 0,于是先装私钥会返回 false——而私钥其实已经
装进上下文了,属于"报了错但状态已改"——补装证书之后不用重装私钥。这个顺序头注释
现在也写明了(src/ssl/uvcpp_ssl_context.h:44-50)。
为什么值得单独调 check_private_key():证书和私钥不配对在 OpenSSL 里既不影响
SSL_CTX_new、也不影响两个 load_* 的返回值——它只在每一次握手时才失败。
表现是"服务起来了、端口在听、每条连接建完就被拒",很难往配置上想
(src/ssl/uvcpp_ssl_context.h:74-77)。
#include <ssl/uvcpp_ssl_context.h>
bool doc_load_server_cert(uvcpp::uvcpp_ssl_context& ctx,
const char* cert_chain_pem,
const char* private_key_pem) {
// 顺序不能反:私钥装载内部会做配对检查,那时证书还没进去就会返回 false
if (!ctx.load_certificate_file(cert_chain_pem)) return false;
if (!ctx.load_private_key_file(private_key_pem)) return false;
// 把"不配对"提前到启动期报错,而不是留到每次握手
return ctx.check_private_key();
}generate_self_signed 只用于测试和受控内网(src/webapp/uvcpp_web_app.h:500-503)。
// doc-snippet: fragment — 枚举定义摘录,本页要讲的是后面那句"服务端上两个值仍
// 等价",摘的是真注释,不能挪走。
enum class tls_verify_mode : uint8_t {
NONE = 0, // 不校验对端证书
PEER = 1, // 校验对端证书(**不含主机名**)
PEER_STRICT = 2, // 校验对端 + 主机名(服务端上等价于 PEER,见下)
};客户端上 PEER_STRICT 真的会校验主机名。 建立连接时,uvcpp_tcp_client 与
uvcpp_http_client 把 connect() 收到的那个主机名钉给这张证书的校验
(src/ssl/uvcpp_ssl.cpp:192-215):数字 IP 字面量走 iPAddress 匹配,其余走
dNSName 匹配。于是「证书链可信、但签发给别的域名」的对端建立不起来 ——
这正是 PEER 挡不住的那一类。头注释见 src/ssl/uvcpp_ssl_common.h:50-63。
服务端上 PEER_STRICT 仍与 PEER 等价。 本库的服务端不发 SNI、也不要求客户端
证书,没有可校验的名字,所以那一半只对客户端有意义。
只有 PEER_STRICT 才钉主机名,判据是 uvcpp_ssl_context::verify_mode()
(src/ssl/uvcpp_ssl_context.h:107)。设了主机名之后,无论 SSL_CTX 上的 verify 位
是什么,OpenSSL 都会在握手末尾做这次检查,而 SSL_VERIFY_NONE 下检查失败也不中止
握手 —— 对 NONE / PEER 也钉的话,PEER 会凭空多出一层从没承诺过的校验,
NONE 则静默什么都不发生。所以 NONE / PEER 两条路的行为逐字节不变。
绕开 uvcpp_tcp_client / uvcpp_http_client 直接用 uvcpp_ssl 的调用方拿不到这层
校验 —— 自动通路只覆盖那两个入口,得自己调 uvcpp_ssl::set_verify_hostname()。
默认值(src/ssl/uvcpp_ssl_context.cpp:109-127):
| 模式 | 默认 |
|---|---|
CLIENT |
SSL_VERIFY_PEER + SSL_CTX_set_default_verify_paths()(装系统信任库) |
SERVER |
SSL_VERIFY_NONE |
服务端默认没有信任库。 set_default_verify_paths() 只在 CLIENT 分支调
(:117)。所以服务端想做客户端证书认证时,必须自己 load_ca_file(...)。而且代码
只设 SSL_VERIFY_PEER,从不设 SSL_VERIFY_FAIL_IF_NO_PEER_CERT
(:277-288)——服务端是"请求但非强制"要证书,要强制得在应用层用
uvcpp_ssl::verify_peer() 兜。
// doc-snippet: fragment — 接口签名摘录,与 §4、§7 那几张表同类的东西。
bool set_alpn_protos(const std::vector<std::string>& protos); // 上下文级
void set_alpn_select_protos(const std::vector<std::string>& protos); // 服务端
bool has_alpn_select() const;这里有一个反过来的返回值约定:SSL_CTX_set_alpn_protos 成功返回 0,与 OpenSSL
大多数接口相反(src/ssl/uvcpp_ssl_context.cpp:361 专门写了注释)。本库的包装把它
翻正成 bool,但如果绕到 raw_ctx() 自己调,记住这条。
其他要点:
- 名单里空串或长度超过 255 字节的项会被静默丢掉,不报错;全部丢完导致编码为空时
set_alpn_protos返回false(src/ssl/uvcpp_ssl_common.h:92-95)。 - 服务端挑不中时返回
SSL_TLSEXT_ERR_NOACK,不是 fatal (src/ssl/uvcpp_ssl_context.h:155-157)——写成 fatal 会让所有老客户端连握手都完不成。 has_alpn_select()存在的理由是框架默认值与用户策略的冲突:uvcpp_web_app默认要 替使用者宣告 h2,但不会覆盖已经显式设过的名单(:166-172)。- 每连接的覆盖在
uvcpp_ssl::set_alpn_protos,必须在握手前调。
上下文是裸指针、非拥有,每个消费点都这么写:
| 落点 | 声明 | 所有权说明 |
|---|---|---|
uvcpp_tcp_server::set_ssl_context |
src/net/uvcpp_tcp_server.h:696 |
"生命周期必须覆盖整个服务端,本服务端不持有它的所有权,也不负责释放"(:689-691) |
uvcpp_tcp_client::enable_tls |
src/net/uvcpp_tcp_client.h:221-221 |
"生命周期必须覆盖整条连接"(:217) |
uvcpp_web_app |
src/webapp/uvcpp_web_app.cpp:2011 |
全库唯一"有人拥有"的一处:std::shared_ptr<uvcpp_ssl_context> |
顺序:set_ssl_context 必须在 listen() 之前(:346-347),清空用
set_ssl_context(nullptr)。而且它是在 loop 线程调用的。
上下文对象本身的地址被 OpenSSL 长期持有——ALPN 选择回调通过 arg 拿到的就是成员
alpn_select_wire_ 的地址(src/ssl/uvcpp_ssl_context.h:209,实现
SSL_CTX_set_alpn_select_cb(ctx_, alpn_select_cb, &alpn_select_wire_))。
所以不要移动它、不要提前析构它。
uvcpp_ssl::use_memory_bio() 必须在任何握手/IO 之前调(src/ssl/uvcpp_ssl.h:100)。
装载类方法(load_* / set_cipher_list / set_alpn_protos / check_private_key)
返回 bool,失败原因在 get_last_error()(里面先把错误队列清空再把 ERR_get_error()
串起来,"; " 分隔)。set_verify_mode / set_min_version / set_max_version /
set_alpn_select_protos 返回 void——没有错误通道,只可能静默无效。
握手返回值契约(最易踩,src/ssl/uvcpp_ssl.h:90-98 原文):
handshake(): 1 = 握手完成,0 = 需要更多 I/O,< 0 = 真出错
read/write(): > 0 = 处理的字节数,0 = 需要更多 I/O,< 0 = 真出错
所以判失败要用 < 0,不能用 != 1:0 是"还没完",是非阻塞下的常态。
这不是假想 bug——tests/functional/web_ssl_func.cpp:152-156 记录了
uvcpp_ws_client 的 wss:// 路径曾经就是这么写错的,"于是它在非阻塞 socket 上
永远握不上手"。
另外两条同类的:
uvcpp_ssl::read()对SSL_ERROR_ZERO_RETURN(对端干净关闭)也返回 0,与 "需要更多数据"同值——只看返回值分不出来,要配last_ssl_error()。uvcpp_ssl::write(data, 0)返回 -1:零长度写在别处常是合法 no-op,这里算错误。
握手失败怎么知道:
- 服务端:握手成功才
deliver_connection();失败只记账,on_connection一次都不被调用(src/net/uvcpp_tcp_server.h:680-688)。 - 客户端:
set_tls_ready_callback(cb),status == 0成功;只触发一次,失败时在 关闭回调之前触发(那时对象还活着)。 - 握手期连接不在上层登记表里,
idle_timeout_ms覆盖不到——所以有set_tls_handshake_timeout_ms(),默认 10000,0= 不设。
uvcpp_ssl_context ctx; 编得过、链接不过。 UVCPP_DEFINE_FUNC 会声明一个默认
构造函数,但 .cpp 里从未定义它——唯一定义的是
uvcpp_ssl_context(tls_mode, tls_version)(src/ssl/uvcpp_ssl_context.cpp:54)。
必须传 tls_mode。uvcpp_ssl 同理。
is_ready() 不反映证书装载失败。 status_ 只在构造时赋值
(src/ssl/uvcpp_ssl_context.cpp:68,70,75,77),之后任何 load_* 失败都不改它。而
uvcpp_tcp_client::enable_tls() 恰好只查 ctx->is_ready()
(src/net/uvcpp_tcp_client.cpp:2577-2577)——所以必须看各 load_* 的返回值,
不能只看 is_ready()。
先装私钥后装证书会返回 false,见 §4。
服务端上 PEER_STRICT 不校验主机名(客户端上会),见 §5。
TLS 上 uvcpp_buf* 的零拷贝不成立:调用之后那个 buf 仍然是满的,
(src/net/uvcpp_tcp_client.h:399-402)。
握手完成前写必然失败(UV_ENOTCONN),SSL_write 要求握手已完成。
同步/异步混用返回 UV_ENOTSUP,而不是把明文写进一条已加密的连接
(src/web/uvcpp_http_client.h:236-245)。
怎么确认过滤器真的接上了: 在连接回调里断言 is_tls_handshake_done(),并对收到的
字节按预期明文内容比对。测试就是这么做的——tests/functional/web_ssl_server_func.cpp:131-135
的注释写着"收到的是明文才作数:过滤器没接上的话这里会是密文,逐字节比对会失败"。
- 主机名校验只覆盖客户端,且只覆盖两个自动入口。
PEER_STRICT的主机名那一半 在uvcpp_tcp_client/uvcpp_http_client上生效,见 §5;服务端上没有可校验的 名字,仍是PEER;直接用uvcpp_ssl的调用方要自己调uvcpp_ssl::set_verify_hostname()。正向的数字 IP 用例未覆盖 ——generate_self_signed()只产 CN、不产subjectAltName,造不出带iPAddressSAN 的证书;反向(无 IP SAN 的证书连 IP 必须失败)已覆盖。 - 完全没有 SNI。 全仓 grep
SSL_set_tlsext_host_name/servername零命中: 客户端不发 SNI,服务端也不读。一个 IP 上挂多张证书的虚拟主机式 TLS 服务端服务 不了,服务端也无法据 SNI 选证书。 - 服务端默认没有信任库,且不强制客户端证书,见 §5。
- 没有 OCSP、没有 session ticket 配置、没有会话复用开关。
tls_version的值是"下限",不是"默认版本"。 枚举(src/ssl/uvcpp_ssl_common.h:30-35) 的这些值都只被当作协议下界;构造函数的默认值是tls_version::TLS_1_2(src/ssl/uvcpp_ssl_context.h:38),它只被送进SSL_CTX_set_min_proto_version(),上限不设。实际是"min = TLS1.2,max = OpenSSL 默认"。 建议显式传版本。- 线程安全没有承诺。 上下文可以共享给多条连接(
SSL_CTX本身线程安全),但 setter 的线程安全头里没有说明。按"配置阶段调完再 listen/connect"写。 uvcpp_ssl不是给使用者的门面。 它的handshake()/feed_ciphertext()那一套 只在手写 TLS(内存 BIO)时需要,正常用法走uvcpp_tcp_client的 TLS 过滤层。
相关文档:net 网络层指南、低层指南、 webapp 应用框架开发者指南、项目 README。
本页用到的头文件:<ssl/uvcpp_ssl_context.h>、<ssl/uvcpp_ssl.h>、
<ssl/uvcpp_ssl_common.h>、<net/uvcpp_tcp_server.h>、<net/uvcpp_tcp_client.h>、
<net/uvcpp_net_read.h>。