Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 88 additions & 28 deletions src/ssl/uvcpp_ssl_context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) 关掉它 ——
Expand All @@ -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<int>(min_version_) > static_cast<int>(tls_version::TLS_1_0))
opts |= SSL_OP_NO_TLSv1;
if (static_cast<int>(min_version_) > static_cast<int>(tls_version::TLS_1_1))
opts |= SSL_OP_NO_TLSv1_1;
if (static_cast<int>(min_version_) > static_cast<int>(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);
}

Expand All @@ -106,7 +130,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;
Expand All @@ -123,12 +162,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<int>(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) {
Expand Down Expand Up @@ -217,23 +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;
long opts = SSL_CTX_get_options(ctx_);
opts |= SSL_OP_NO_TLSv1;
if (static_cast<int>(ver) <= static_cast<int>(tls_version::TLS_1_0))
opts &= ~SSL_OP_NO_TLSv1;
if (static_cast<int>(ver) <= static_cast<int>(tls_version::TLS_1_1))
opts &= ~SSL_OP_NO_TLSv1_1;
if (static_cast<int>(ver) <= static_cast<int>(tls_version::TLS_1_2))
opts &= ~SSL_OP_NO_TLSv1_2;
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;
if (static_cast<int>(ver) < static_cast<int>(tls_version::TLS_1_3))
SSL_CTX_set_options(ctx_, SSL_CTX_get_options(ctx_) | SSL_OP_NO_TLSv1_3);
if (static_cast<int>(ver) < static_cast<int>(tls_version::TLS_1_2))
SSL_CTX_set_options(ctx_, SSL_CTX_get_options(ctx_) | SSL_OP_NO_TLSv1_2);
SSL_CTX_set_max_proto_version(ctx_, openssl_version_of(ver));
}

bool uvcpp_ssl_context::set_cipher_list(const std::string& ciphers) {
Expand Down
147 changes: 147 additions & 0 deletions tests/functional/web_ssl_func.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
#include <ssl/uvcpp_ssl_context.h>
#include <ssl/uvcpp_ssl.h>
#include <openssl/ssl.h> // SSL_ERROR_* 常量:光看返回值分不出「还没完」和「出错」
#include <openssl/pem.h> // PEM_write_bio_X509:回写证书,用于拼多张证书的 PEM
#include <cstdio> // std::remove
using namespace uvcpp;

// =========================================================================
Expand Down Expand Up @@ -356,6 +358,148 @@ 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()` 必须**双向**生效
//
// 原来的实现用 `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)`,清位是白清的。
// 于是 `set_min_version(TLS_1_3)` 想禁掉 1.2 的那一步**没有任何一行代码
// 真正执行**,而构造时给的默认下限恰好就是 TLS_1_2 —— "抬到 1.3" 这个最
// 常见的用法正是失效的那一个。
// 2. OpenSSL 3.0 起这一组选项已废弃,官方指定用 proto version API。
//
// 修法是改用 `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 必须被拒
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()) << ")" << std::endl;
return false;
}

// 放回来: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::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::endl;
return false;
}

// 放宽上限(原实现同样做不到:位一旦置上就再也去不掉)
ctx.set_max_version(tls_version::TLS_1_3);
{
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
<< " —— 上限只能压、不能放" << std::endl;
return false;
}
}
return true;
}

int main() {
bool ok = true;
struct { const char* name; bool (*fn)(); } tests[] = {
Expand All @@ -375,6 +519,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";
Expand Down
Loading