From 0678b18c781fa542c19b968a6c77e106aeb4224e Mon Sep 17 00:00:00 2001 From: sercebr <331266209+sercebr@users.noreply.github.com> Date: Sat, 19 Sep 2026 20:37:52 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix(ssl):=20=E5=8A=A0=E8=BD=BD=E5=AE=8C?= =?UTF-8?q?=E6=95=B4=E8=AF=81=E4=B9=A6=E9=93=BE=EF=BC=8C=E5=B9=B6=E8=AE=A9?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E4=B8=8A=E4=B8=8B=E9=99=90=E5=8F=8C=E5=90=91?= =?UTF-8?q?=E7=94=9F=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 两个独立的缺陷,共同点都是"配置看着设上了、实际没起作用"。 1) load_certificate_file() 用的是 SSL_CTX_use_certificate_file(),它只读 PEM 里的**第一张**证书,中间证书因此永远发不到对端。只信任根 CA 的客户端 (浏览器与 curl 的默认状态)没法把链补全,握手直接失败: verify error:num=20: unable to get local issuer certificate Verify return code: 21 (unable to verify the first certificate) 而 enable_ssl() 的文档承诺的正是"cert_file 是证书**链**"。现实中几乎每张 证书都挂在中间 CA 下面(Let's Encrypt / DigiCert / 企业 PKI),所以这一条 直接决定 HTTPS 在真实部署下能不能用。 改用 SSL_CTX_use_certificate_chain_file();并让 load_certificate_data() (enable_ssl_pem() 走的路径)同样读完整个链 —— 读法照搬 OpenSSL 自己 process_chain() 的做法。 2) set_min_version() 只置 NO_TLSv1 再逐个清标志,这个写法**只能把下限往下放、 不能往上抬**:设成 TLS_1_3 时 NO_TLSv1_2 从构造起就没被置过,TLS 1.2 客户端 照样协商成功。而构造时给的默认下限恰好是 TLS_1_2(webapp 的 kTlsFloor), 所以"抬到 1.3"这个最常见的用法正是失效的那一个。 set_max_version() 有对称的问题:只置不清,先压低上限再放宽做不到。 两者都改成"先把相关禁用位全置上(或全清掉),再按新值逐个置位"。 配套加两组回归用例(tests/functional/web_ssl_func.cpp):判据取在 SSL_CTX 自身的状态上(额外链证书张数、选项位),不起网络,也不需要外部证书文件 —— 用例自己用 generate_self_signed() 造证书再拼成多张的 PEM。 --- src/ssl/uvcpp_ssl_context.cpp | 84 +++++++++++++++--- tests/functional/web_ssl_func.cpp | 141 ++++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+), 13 deletions(-) diff --git a/src/ssl/uvcpp_ssl_context.cpp b/src/ssl/uvcpp_ssl_context.cpp index 04694143..18685b66 100644 --- a/src/ssl/uvcpp_ssl_context.cpp +++ b/src/ssl/uvcpp_ssl_context.cpp @@ -106,7 +106,22 @@ void uvcpp_ssl_context::set_default_verify() { bool uvcpp_ssl_context::load_certificate_file(const std::string& path) { if (!ctx_) return false; - if (SSL_CTX_use_certificate_file(ctx_, path.c_str(), SSL_FILETYPE_PEM) != 1) { + // 必须用 `_chain_file`,不能用 `_file`。 + // + // `SSL_CTX_use_certificate_file()` 只读 PEM 里的**第一张**证书 —— 对它来说 + // "证书链"就是一条多余的尾巴。结果是中间证书永远不会发给对端,而只信任根 CA + // 的客户端(浏览器与 curl 的默认状态)没法把链补全,握手直接失败: + // + // verify error:num=20: unable to get local issuer certificate + // Verify return code: 21 (unable to verify the first certificate) + // + // 而本 API 的文档承诺的正是"cert_file 是证书**链**"(见 uvcpp_web_app.h + // 的 enable_ssl 注释)。现实里几乎每张证书都挂在中间 CA 下面 + // (Let's Encrypt、DigiCert、企业 PKI),所以这个差别直接决定 HTTPS 能不能用。 + // + // 注:只有"叶子由根直接签"的两级链是例外 —— 那种情况服务端本来就不该发根 + // (客户端自己有),只发叶子是正确的,改动对它没有影响。 + if (SSL_CTX_use_certificate_chain_file(ctx_, path.c_str()) != 1) { clear_error(); return false; } return true; @@ -123,12 +138,43 @@ bool uvcpp_ssl_context::load_private_key_file(const std::string& path) { bool uvcpp_ssl_context::load_certificate_data(const std::string& pem) { if (!ctx_ || pem.empty()) return false; BIO* bio = BIO_new_mem_buf(pem.c_str(), static_cast(pem.size())); - X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr); + if (bio == nullptr) { clear_error(); return false; } + + // 与 load_certificate_file 是同一个问题:PEM 里可能不止一张证书。 + // 第一张是叶子,其余是中间证书 —— 后面那些必须显式挂到上下文上,否则 + // 客户端同样补不全链。`enable_ssl_pem()` 走的正是这条路径。 + // + // 读法照搬 OpenSSL 自己 process_chain() 的做法:先读叶子,循环读剩下的, + // 逐个 `SSL_CTX_add_extra_chain_cert()`(成功时所有权归 SSL_CTX)。 + // 读干净的 EOF 会在错误栈上留下 PEM_R_NO_START_LINE,那是正常结束不是错误, + // 所以最后统一清一次。 + X509* leaf = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr); + if (leaf == nullptr) { + BIO_free(bio); + clear_error(); + return false; + } + const int rc = SSL_CTX_use_certificate(ctx_, leaf); + X509_free(leaf); + if (rc != 1) { + BIO_free(bio); + clear_error(); + return false; + } + + for (;;) { + X509* extra = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr); + if (extra == nullptr) break; + if (SSL_CTX_add_extra_chain_cert(ctx_, extra) != 1) { + BIO_free(bio); + clear_error(); + return false; + } + } + BIO_free(bio); - if (!cert) { clear_error(); return false; } - int rc = SSL_CTX_use_certificate(ctx_, cert); - X509_free(cert); - return rc == 1; + ERR_clear_error(); // 清掉"读到文件尾"留下的 PEM_R_NO_START_LINE + return true; } bool uvcpp_ssl_context::load_private_key_data(const std::string& pem) { @@ -217,23 +263,35 @@ void uvcpp_ssl_context::set_verify_mode(tls_verify_mode mode) { void uvcpp_ssl_context::set_min_version(tls_version ver) { min_version_ = ver; if (!ctx_) return; + // 三个"禁用"位**先全置上,再按新下限逐个放开**。 + // + // 原来是「只置 NO_TLSv1,然后逐个清」—— 那个写法**只能把下限往下放, + // 不能往上抬**:设成 TLS_1_3 时,NO_TLSv1_2 从构造到现在就没被置过, + // 于是 TLS 1.2 客户端照样能协商成功。而构造时给的默认下限就是 TLS_1_2 + // (见 uvcpp_web_app.cpp 的 kTlsFloor),所以"抬到 1.3"这个最常见的用法 + // 恰好是失效的那一个。 long opts = SSL_CTX_get_options(ctx_); - opts |= SSL_OP_NO_TLSv1; - if (static_cast(ver) <= static_cast(tls_version::TLS_1_0)) - opts &= ~SSL_OP_NO_TLSv1; - if (static_cast(ver) <= static_cast(tls_version::TLS_1_1)) - opts &= ~SSL_OP_NO_TLSv1_1; + opts |= SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1 | SSL_OP_NO_TLSv1_2; if (static_cast(ver) <= static_cast(tls_version::TLS_1_2)) opts &= ~SSL_OP_NO_TLSv1_2; + if (static_cast(ver) <= static_cast(tls_version::TLS_1_1)) + opts &= ~SSL_OP_NO_TLSv1_1; + if (static_cast(ver) <= static_cast(tls_version::TLS_1_0)) + opts &= ~SSL_OP_NO_TLSv1; SSL_CTX_set_options(ctx_, opts); } void uvcpp_ssl_context::set_max_version(tls_version ver) { if (!ctx_) return; + // 对称的问题:原来只置不清,于是"先压低上限、再放宽"做不到 + // (NO_TLSv1_3 / NO_TLSv1_2 一旦置上就再也去不掉)。 + long opts = SSL_CTX_get_options(ctx_); + opts &= ~(SSL_OP_NO_TLSv1_2 | SSL_OP_NO_TLSv1_3); if (static_cast(ver) < static_cast(tls_version::TLS_1_3)) - SSL_CTX_set_options(ctx_, SSL_CTX_get_options(ctx_) | SSL_OP_NO_TLSv1_3); + opts |= SSL_OP_NO_TLSv1_3; if (static_cast(ver) < static_cast(tls_version::TLS_1_2)) - SSL_CTX_set_options(ctx_, SSL_CTX_get_options(ctx_) | SSL_OP_NO_TLSv1_2); + opts |= SSL_OP_NO_TLSv1_2; + SSL_CTX_set_options(ctx_, opts); } bool uvcpp_ssl_context::set_cipher_list(const std::string& ciphers) { diff --git a/tests/functional/web_ssl_func.cpp b/tests/functional/web_ssl_func.cpp index 91907d48..410b5ef4 100644 --- a/tests/functional/web_ssl_func.cpp +++ b/tests/functional/web_ssl_func.cpp @@ -12,6 +12,8 @@ #include #include #include // SSL_ERROR_* 常量:光看返回值分不出「还没完」和「出错」 +#include // PEM_write_bio_X509:回写证书,用于拼多张证书的 PEM +#include // std::remove using namespace uvcpp; // ========================================================================= @@ -356,6 +358,142 @@ static bool test_client_default_rejects_untrusted_cert() { return true; } +// ========================================================================= +// 回归:`load_certificate_file()` 必须把 PEM 里的**整条链**读进来 +// +// 原来的实现用的是 `SSL_CTX_use_certificate_file()`,它只读第一张证书 —— +// 于是中间证书永远发不到对端,只信任根 CA 的客户端(浏览器、curl 的默认状态) +// 补不全链,握手直接失败: +// verify error:num=20: unable to get local issuer certificate +// 现实中几乎每张证书都挂在中间 CA 下面,所以这个差别直接决定 HTTPS 能不能用。 +// +// 判据选在 **SSL_CTX 里挂了几张额外证书**,而不是"能不能握手":握手要真起 +// 一个 TCP 服务,而这里要验的是加载器读没读链上除叶子之外的部分,一个进程内 +// 就能问清楚。 +// ========================================================================= +static int extra_chain_count(uvcpp_ssl_context& ctx) { + STACK_OF(X509)* chain = nullptr; + SSL_CTX_get_extra_chain_certs(ctx.raw_ctx(), &chain); + return chain == nullptr ? 0 : sk_X509_num(chain); +} + +static bool write_cert_pem(const char* path, X509* first, X509* second) { + BIO* out = BIO_new_file(path, "w"); + if (out == nullptr) return false; + bool ok = PEM_write_bio_X509(out, first) == 1; + if (ok && second != nullptr) ok = PEM_write_bio_X509(out, second) == 1; + BIO_free(out); + return ok; +} + +static bool test_certificate_chain_loaded() { + // 造一张自签证书当作"叶子",再往同一个 PEM 里追加一份当作"中间证书"。 + // 真实世界里第二张由中间 CA 签,但这里要验的是**加载器读不读叶子之后的 + // 部分**,与两张证书之间实际的签发关系无关 —— 用同一张就够了。 + uvcpp_ssl_context gen(tls_mode::SERVER, tls_version::TLS_1_2); + if (!gen.generate_self_signed("chain.test", 2048)) { + std::cout << " 自签证书生成失败:" << gen.get_last_error() << "\n"; + return false; + } + X509* leaf = SSL_CTX_get0_certificate(gen.raw_ctx()); + if (leaf == nullptr) { std::cout << " 取不到叶子证书\n"; return false; } + + const char* kChainPath = "uvcpp_ssl_chain_test.pem"; + const char* kLeafPath = "uvcpp_ssl_leaf_test.pem"; + if (!write_cert_pem(kChainPath, leaf, leaf) || + !write_cert_pem(kLeafPath, leaf, nullptr)) { + std::cout << " 写临时 PEM 失败\n"; + return false; + } + + int chain_extra = -1, leaf_extra = -1; + { + uvcpp_ssl_context c(tls_mode::SERVER, tls_version::TLS_1_2); + if (!c.load_certificate_file(kChainPath)) { + std::cout << " 两张证书的 PEM 加载失败:" << c.get_last_error() << "\n"; + std::remove(kChainPath); std::remove(kLeafPath); + return false; + } + chain_extra = extra_chain_count(c); + } + { + uvcpp_ssl_context c(tls_mode::SERVER, tls_version::TLS_1_2); + if (!c.load_certificate_file(kLeafPath)) { + std::cout << " 单张证书的 PEM 加载失败:" << c.get_last_error() << "\n"; + std::remove(kChainPath); std::remove(kLeafPath); + return false; + } + leaf_extra = extra_chain_count(c); + } + std::remove(kChainPath); + std::remove(kLeafPath); + + if (chain_extra != 1) { + std::cout << " 两张证书的 PEM 只加载到 " << chain_extra + << " 张额外证书(应为 1)—— 链被截断了,中间证书不会发给对端\n"; + return false; + } + if (leaf_extra != 0) { + std::cout << " 单张证书的 PEM 却加载到 " << leaf_extra + << " 张额外证书(应为 0)\n"; + return false; + } + return true; +} + +// ========================================================================= +// 回归:`set_min_version()` / `set_max_version()` 必须**双向**生效 +// +// 原来的 set_min_version 只置 NO_TLSv1 然后逐个清标志 —— 那个写法只能把下限 +// 往下放,不能往上抬:设成 TLS_1_3 时 NO_TLSv1_2 从构造到现在就没被置过, +// TLS 1.2 客户端照样能协商。而构造时给的默认下限恰好是 TLS_1_2,所以 +// "抬到 1.3"(最常见的那个用法)正是失效的那一个。 +// +// set_max_version 有对称的问题:只置不清,先压低再放宽就做不到。 +// +// 判据是 SSL_CTX 上的实际选项位,不起网络。 +// ========================================================================= +static bool test_version_bounds_settable() { + uvcpp_ssl_context ctx(tls_mode::SERVER, tls_version::TLS_1_2); + + // 抬下限:TLS_1_3 → 必须真的禁掉 1.2 + ctx.set_min_version(tls_version::TLS_1_3); + long o = SSL_CTX_get_options(ctx.raw_ctx()); + if ((o & SSL_OP_NO_TLSv1_2) == 0) { + std::cout << " set_min_version(TLS_1_3) 之后 TLS 1.2 仍然可协商\n"; + return false; + } + + // 放回来:TLS_1_2 → 1.2 必须重新可用,而 1.0/1.1 仍然被禁 + ctx.set_min_version(tls_version::TLS_1_2); + o = SSL_CTX_get_options(ctx.raw_ctx()); + if ((o & SSL_OP_NO_TLSv1_2) != 0) { + std::cout << " set_min_version(TLS_1_2) 之后 TLS 1.2 仍然被禁\n"; + return false; + } + if ((o & SSL_OP_NO_TLSv1) == 0 || (o & SSL_OP_NO_TLSv1_1) == 0) { + std::cout << " TLS 1.2 下限之下 TLS 1.0/1.1 必须保持禁用\n"; + return false; + } + + // 压上限:TLS_1_2 → 1.3 必须被禁 + ctx.set_max_version(tls_version::TLS_1_2); + o = SSL_CTX_get_options(ctx.raw_ctx()); + if ((o & SSL_OP_NO_TLSv1_3) == 0) { + std::cout << " set_max_version(TLS_1_2) 之后 TLS 1.3 仍然可协商\n"; + return false; + } + + // 放宽上限:TLS_1_3 → 1.3 必须重新可用 + ctx.set_max_version(tls_version::TLS_1_3); + o = SSL_CTX_get_options(ctx.raw_ctx()); + if ((o & SSL_OP_NO_TLSv1_3) != 0) { + std::cout << " set_max_version(TLS_1_3) 之后 TLS 1.3 仍然被禁\n"; + return false; + } + return true; +} + int main() { bool ok = true; struct { const char* name; bool (*fn)(); } tests[] = { @@ -375,6 +513,9 @@ int main() { {"take_ciphertext_drains", test_take_ciphertext_drains}, {"client_default_rejects_untrusted_cert", test_client_default_rejects_untrusted_cert}, + // 下面两组是本次修复带的回归:链被截断 / 版本上下限只能单向生效 + {"certificate_chain_loaded", test_certificate_chain_loaded}, + {"version_bounds_settable", test_version_bounds_settable}, }; for (const auto& t : tests) { std::cout << "[web_ssl] " << t.name << "\n"; From 28f90d5a8b6cae42bf384dc71baff4210862ef2f Mon Sep 17 00:00:00 2001 From: sercebr <331266209+sercebr@users.noreply.github.com> Date: Sat, 19 Sep 2026 22:51:28 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(ssl):=20=E7=89=88=E6=9C=AC=E4=B8=8A?= =?UTF-8?q?=E4=B8=8B=E9=99=90=E6=94=B9=E7=94=A8=20proto=20version=20API=20?= =?UTF-8?q?=E2=80=94=E2=80=94=20=E4=B8=8A=E4=B8=80=E4=B8=AA=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E7=9A=84=E5=86=99=E6=B3=95=E4=B9=9F=E6=98=AF=E6=97=A0?= =?UTF-8?q?=E6=95=88=E7=9A=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI 在五个平台上一致地报出: [web_ssl] version_bounds_settable set_min_version(TLS_1_2) 之后 TLS 1.2 仍然被禁 -> FAIL (同一轮里 `certificate_chain_loaded` 是全过的,证书链那一条修复没问题。) 失败的根因是上一个提交**照抄了原实现的写法**,把同一个错误一起继承了下来: long opts = SSL_CTX_get_options(ctx_); opts |= SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1 | SSL_OP_NO_TLSv1_2; if (ver <= TLS_1_2) opts &= ~SSL_OP_NO_TLSv1_2; // ← 白清 ... SSL_CTX_set_options(ctx_, opts); `SSL_CTX_set_options(ctx, op)` 的语义是「**加上** op 里的位」,它**不会清** 没给的位 —— 想清必须另调 `SSL_CTX_clear_options()`。所以那句 `&= ~` 之后再 `set_options`,位从来没被去掉过。 也就是说原缺陷比 issue 里描述的还要广一层:不只是"下限抬不上去", **往下放同样做不到**(第一条断言能过、第二条过不去,正好对上:置位是有效的, 清位是无效的)。 改用 OpenSSL 3.0 起官方指定的 proto version API: SSL_CTX_set_min_proto_version(ctx_, openssl_version_of(ver)); SSL_CTX_set_max_proto_version(ctx_, openssl_version_of(ver)); 顺带把构造期那条下限(`set_default_verify()`)也换成同一个 API —— 原来它也是 在位运算上设的,虽然方向碰巧对,但"构造时设的"和"运行中改的"是两套机制, 其中一套还是无效的,不该留着。 断言相应改成读 `SSL_CTX_get_min_proto_version()` / `get_max_proto_version()`: 判据与实现用同一个量,不会各说各话。 --- src/ssl/uvcpp_ssl_context.cpp | 70 ++++++++++++++++--------------- tests/functional/web_ssl_func.cpp | 65 ++++++++++++++++------------ 2 files changed, 74 insertions(+), 61 deletions(-) diff --git a/src/ssl/uvcpp_ssl_context.cpp b/src/ssl/uvcpp_ssl_context.cpp index 18685b66..682c4c5b 100644 --- a/src/ssl/uvcpp_ssl_context.cpp +++ b/src/ssl/uvcpp_ssl_context.cpp @@ -77,6 +77,35 @@ void uvcpp_ssl_context::init_server() { status_ = TLS_CTX_READY; } +namespace { + +/** + * @brief `tls_version` 映射成 OpenSSL 的协议版本号(`TLS1_2_VERSION` 等)。 + * + * **为什么不能再用 `SSL_OP_NO_TLSv1*` 那一组。** 两个原因叠在一起: + * + * 1. `SSL_CTX_set_options(ctx, op)` 是「**加上** op 里的位」,它**不会清**没给 + * 的位 —— 想清必须另外调 `SSL_CTX_clear_options()`。原来那几处 + * `opts &= ~SSL_OP_NO_TLSv1_x` 之后再 `set_options(ctx, opts)`,那个清位 + * 是**白清的**:位从来没被去掉过。 + * 2. OpenSSL 3.0 起 `SSL_OP_NO_TLSv1*` 已被废弃,官方指定用 + * `SSL_CTX_set_min_proto_version()` / `set_max_proto_version()`。 + * + * 用版本号 API 之后,「设下限/设上限」是一次有明确语义的赋值,不再需要在 + * 位运算上辩方向。 + */ +int openssl_version_of(tls_version ver) { + switch (ver) { + case tls_version::TLS_1_0: return TLS1_VERSION; + case tls_version::TLS_1_1: return TLS1_1_VERSION; + case tls_version::TLS_1_2: return TLS1_2_VERSION; + case tls_version::TLS_1_3: return TLS1_3_VERSION; + } + return TLS1_2_VERSION; +} + +} // namespace + void uvcpp_ssl_context::set_default_verify() { // CLIENT 默认**校验**对端证书:一个不校验的 HTTPS 客户端会把任何中间人 // 当成正常服务端。回环自签用例必须显式调 set_verify_mode(NONE) 关掉它 —— @@ -88,15 +117,10 @@ void uvcpp_ssl_context::set_default_verify() { } else { SSL_CTX_set_verify(ctx_, SSL_VERIFY_NONE, nullptr); } - long opts = SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3; - // Disable TLS < min_version - if (static_cast(min_version_) > static_cast(tls_version::TLS_1_0)) - opts |= SSL_OP_NO_TLSv1; - if (static_cast(min_version_) > static_cast(tls_version::TLS_1_1)) - opts |= SSL_OP_NO_TLSv1_1; - if (static_cast(min_version_) > static_cast(tls_version::TLS_1_2)) - opts |= SSL_OP_NO_TLSv1_2; - SSL_CTX_set_options(ctx_, opts); + SSL_CTX_set_options(ctx_, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3); + // 构造时给的下限也走版本号 API —— 与 set_min_version() 用同一条路, + // 免得"构造时设的"和"运行中改的"是两套机制(其中一套还是无效的)。 + SSL_CTX_set_min_proto_version(ctx_, openssl_version_of(min_version_)); SSL_CTX_set_mode(ctx_, SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); } @@ -263,35 +287,13 @@ void uvcpp_ssl_context::set_verify_mode(tls_verify_mode mode) { void uvcpp_ssl_context::set_min_version(tls_version ver) { min_version_ = ver; if (!ctx_) return; - // 三个"禁用"位**先全置上,再按新下限逐个放开**。 - // - // 原来是「只置 NO_TLSv1,然后逐个清」—— 那个写法**只能把下限往下放, - // 不能往上抬**:设成 TLS_1_3 时,NO_TLSv1_2 从构造到现在就没被置过, - // 于是 TLS 1.2 客户端照样能协商成功。而构造时给的默认下限就是 TLS_1_2 - // (见 uvcpp_web_app.cpp 的 kTlsFloor),所以"抬到 1.3"这个最常见的用法 - // 恰好是失效的那一个。 - long opts = SSL_CTX_get_options(ctx_); - opts |= SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1 | SSL_OP_NO_TLSv1_2; - if (static_cast(ver) <= static_cast(tls_version::TLS_1_2)) - opts &= ~SSL_OP_NO_TLSv1_2; - if (static_cast(ver) <= static_cast(tls_version::TLS_1_1)) - opts &= ~SSL_OP_NO_TLSv1_1; - if (static_cast(ver) <= static_cast(tls_version::TLS_1_0)) - opts &= ~SSL_OP_NO_TLSv1; - SSL_CTX_set_options(ctx_, opts); + // 赋值语义:设成什么就是什么,**双向都生效**。 + SSL_CTX_set_min_proto_version(ctx_, openssl_version_of(ver)); } void uvcpp_ssl_context::set_max_version(tls_version ver) { if (!ctx_) return; - // 对称的问题:原来只置不清,于是"先压低上限、再放宽"做不到 - // (NO_TLSv1_3 / NO_TLSv1_2 一旦置上就再也去不掉)。 - long opts = SSL_CTX_get_options(ctx_); - opts &= ~(SSL_OP_NO_TLSv1_2 | SSL_OP_NO_TLSv1_3); - if (static_cast(ver) < static_cast(tls_version::TLS_1_3)) - opts |= SSL_OP_NO_TLSv1_3; - if (static_cast(ver) < static_cast(tls_version::TLS_1_2)) - opts |= SSL_OP_NO_TLSv1_2; - SSL_CTX_set_options(ctx_, opts); + SSL_CTX_set_max_proto_version(ctx_, openssl_version_of(ver)); } bool uvcpp_ssl_context::set_cipher_list(const std::string& ciphers) { diff --git a/tests/functional/web_ssl_func.cpp b/tests/functional/web_ssl_func.cpp index 410b5ef4..03ab200f 100644 --- a/tests/functional/web_ssl_func.cpp +++ b/tests/functional/web_ssl_func.cpp @@ -444,56 +444,67 @@ static bool test_certificate_chain_loaded() { // ========================================================================= // 回归:`set_min_version()` / `set_max_version()` 必须**双向**生效 // -// 原来的 set_min_version 只置 NO_TLSv1 然后逐个清标志 —— 那个写法只能把下限 -// 往下放,不能往上抬:设成 TLS_1_3 时 NO_TLSv1_2 从构造到现在就没被置过, -// TLS 1.2 客户端照样能协商。而构造时给的默认下限恰好是 TLS_1_2,所以 -// "抬到 1.3"(最常见的那个用法)正是失效的那一个。 +// 原来的实现用 `SSL_OP_NO_TLSv1*` 这一组选项位,两个问题叠在一起: // -// set_max_version 有对称的问题:只置不清,先压低再放宽就做不到。 +// 1. `SSL_CTX_set_options(ctx, op)` 是「**加上** op 里的位」,**不清**没给的位 +// —— 想清必须另调 `SSL_CTX_clear_options()`。原来那几处 +// `opts &= ~SSL_OP_NO_TLSv1_x` 之后再 `set_options(ctx, opts)`,清位是白清的。 +// 于是 `set_min_version(TLS_1_3)` 想禁掉 1.2 的那一步**没有任何一行代码 +// 真正执行**,而构造时给的默认下限恰好就是 TLS_1_2 —— "抬到 1.3" 这个最 +// 常见的用法正是失效的那一个。 +// 2. OpenSSL 3.0 起这一组选项已废弃,官方指定用 proto version API。 // -// 判据是 SSL_CTX 上的实际选项位,不起网络。 +// 修法是改用 `SSL_CTX_set_min_proto_version()` / `set_max_proto_version()`, +// 断言也相应读 `SSL_CTX_get_min_proto_version()` / `get_max_proto_version()` +// —— 读版本号而不是读选项位,判据与实现用的是同一个量,不会各说各话。 +// +// 判据不起网络:只问上下文"你现在允许的区间是什么"。 // ========================================================================= static bool test_version_bounds_settable() { uvcpp_ssl_context ctx(tls_mode::SERVER, tls_version::TLS_1_2); - // 抬下限:TLS_1_3 → 必须真的禁掉 1.2 + // 抬下限:TLS_1_3 → 1.2 必须被拒 ctx.set_min_version(tls_version::TLS_1_3); - long o = SSL_CTX_get_options(ctx.raw_ctx()); - if ((o & SSL_OP_NO_TLSv1_2) == 0) { - std::cout << " set_min_version(TLS_1_3) 之后 TLS 1.2 仍然可协商\n"; + if (SSL_CTX_get_min_proto_version(ctx.raw_ctx()) != TLS1_3_VERSION) { + std::cout << " set_min_version(TLS_1_3) 之后下限不是 TLS 1.3(读到 " + << SSL_CTX_get_min_proto_version(ctx.raw_ctx()) << ") +"; return false; } - // 放回来:TLS_1_2 → 1.2 必须重新可用,而 1.0/1.1 仍然被禁 + // 放回来:TLS_1_2 → 1.2 必须重新可用(原实现这一步就是失效的) ctx.set_min_version(tls_version::TLS_1_2); - o = SSL_CTX_get_options(ctx.raw_ctx()); - if ((o & SSL_OP_NO_TLSv1_2) != 0) { - std::cout << " set_min_version(TLS_1_2) 之后 TLS 1.2 仍然被禁\n"; - return false; - } - if ((o & SSL_OP_NO_TLSv1) == 0 || (o & SSL_OP_NO_TLSv1_1) == 0) { - std::cout << " TLS 1.2 下限之下 TLS 1.0/1.1 必须保持禁用\n"; + if (SSL_CTX_get_min_proto_version(ctx.raw_ctx()) != TLS1_2_VERSION) { + std::cout << " set_min_version(TLS_1_2) 之后下限不是 TLS 1.2 —— " + "下限只能抬、不能放 +"; return false; } - // 压上限:TLS_1_2 → 1.3 必须被禁 + // 压上限 ctx.set_max_version(tls_version::TLS_1_2); - o = SSL_CTX_get_options(ctx.raw_ctx()); - if ((o & SSL_OP_NO_TLSv1_3) == 0) { - std::cout << " set_max_version(TLS_1_2) 之后 TLS 1.3 仍然可协商\n"; + if (SSL_CTX_get_max_proto_version(ctx.raw_ctx()) != TLS1_2_VERSION) { + std::cout << " set_max_version(TLS_1_2) 之后上限不是 TLS 1.2 +"; return false; } - // 放宽上限:TLS_1_3 → 1.3 必须重新可用 + // 放宽上限(原实现同样做不到:位一旦置上就再也去不掉) ctx.set_max_version(tls_version::TLS_1_3); - o = SSL_CTX_get_options(ctx.raw_ctx()); - if ((o & SSL_OP_NO_TLSv1_3) != 0) { - std::cout << " set_max_version(TLS_1_3) 之后 TLS 1.3 仍然被禁\n"; - return false; + { + const int mx = SSL_CTX_get_max_proto_version(ctx.raw_ctx()); + // 0 = 不限,语义上等价于"最高到本端支持的最高版本" + if (mx != 0 && mx < TLS1_3_VERSION) { + std::cout << " set_max_version(TLS_1_3) 之后上限仍被压在 " << mx + << " —— 上限只能压、不能放 +"; + return false; + } } return true; } + int main() { bool ok = true; struct { const char* name; bool (*fn)(); } tests[] = { From 4cac9deee7644b12362be9231da6613c5162de7a Mon Sep 17 00:00:00 2001 From: sercebr <331266209+sercebr@users.noreply.github.com> Date: Sat, 19 Sep 2026 23:53:04 +0800 Subject: [PATCH 3/3] =?UTF-8?q?test(ssl):=20=E4=BF=AE=E6=8E=89=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E9=87=8C=E7=9A=84=E8=B7=A8=E8=A1=8C=E5=AD=97=E7=AC=A6?= =?UTF-8?q?=E4=B8=B2=E5=AD=97=E9=9D=A2=E9=87=8F=EF=BC=88=E7=BC=96=E8=AF=91?= =?UTF-8?q?=E9=94=99=E8=AF=AF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI 在 mingw64 上报: web_ssl_func.cpp:470:66: error: missing terminating " character web_ssl_func.cpp:479:18: error: missing terminating " character 原因是写 `std::cout << "...\n"` 时那个换行转义**落成了字符串里的一个真实 换行**,于是字面量跨行、引号没闭合。 改成 `<< std::endl`,整个函数的字符串里不再出现任何反斜杠转义 —— 这样就不 存在"转义在某一层被吃掉"的余地了。 (其它平台的上一个 run 也一起挂了,只是 mingw 的日志先被看到。) --- tests/functional/web_ssl_func.cpp | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/tests/functional/web_ssl_func.cpp b/tests/functional/web_ssl_func.cpp index 03ab200f..ce03af2f 100644 --- a/tests/functional/web_ssl_func.cpp +++ b/tests/functional/web_ssl_func.cpp @@ -467,25 +467,22 @@ static bool test_version_bounds_settable() { ctx.set_min_version(tls_version::TLS_1_3); if (SSL_CTX_get_min_proto_version(ctx.raw_ctx()) != TLS1_3_VERSION) { std::cout << " set_min_version(TLS_1_3) 之后下限不是 TLS 1.3(读到 " - << SSL_CTX_get_min_proto_version(ctx.raw_ctx()) << ") -"; + << SSL_CTX_get_min_proto_version(ctx.raw_ctx()) << ")" << std::endl; return false; } - // 放回来:TLS_1_2 → 1.2 必须重新可用(原实现这一步就是失效的) + // 放回来:TLS_1_2 → 1.2 必须重新可用(原实现这一步是失效的) ctx.set_min_version(tls_version::TLS_1_2); if (SSL_CTX_get_min_proto_version(ctx.raw_ctx()) != TLS1_2_VERSION) { - std::cout << " set_min_version(TLS_1_2) 之后下限不是 TLS 1.2 —— " - "下限只能抬、不能放 -"; + std::cout << " set_min_version(TLS_1_2) 之后下限不是 TLS 1.2 —— 下限只能抬、不能放" + << std::endl; return false; } // 压上限 ctx.set_max_version(tls_version::TLS_1_2); if (SSL_CTX_get_max_proto_version(ctx.raw_ctx()) != TLS1_2_VERSION) { - std::cout << " set_max_version(TLS_1_2) 之后上限不是 TLS 1.2 -"; + std::cout << " set_max_version(TLS_1_2) 之后上限不是 TLS 1.2" << std::endl; return false; } @@ -496,15 +493,13 @@ static bool test_version_bounds_settable() { // 0 = 不限,语义上等价于"最高到本端支持的最高版本" if (mx != 0 && mx < TLS1_3_VERSION) { std::cout << " set_max_version(TLS_1_3) 之后上限仍被压在 " << mx - << " —— 上限只能压、不能放 -"; + << " —— 上限只能压、不能放" << std::endl; return false; } } return true; } - int main() { bool ok = true; struct { const char* name; bool (*fn)(); } tests[] = {