首页 / 资讯中心 / 文章详情

libwebsockets TLS 证书热更新实战:基于 `lws_tls_cert_updated()` 保持 h2 与客户端证书策略不丢失

libwebsockets TLS 证书热更新实战:基于 `lws_tls_cert_updated()` 保持 h2 与客户端证书策略不丢失 ★ FEATURED ARTICLE
后端网络通信【免费下载链接】libwebsocketscanonical libwebsockets.org networking library项目地址https://gitcode.com/gh_mirrors/li/libwebsockets点击查看免费下载导读api-test-tls-renew是 libwebsockets 仓库中的一个 API 自测程序selftest它在一个进程内同时扮演 lws 客户端与 lws TLS 服务器用来验证一个关键行为服务器 vhost 经由lws_tls_cert_updated()重建出来的 TLS ctxSSL_CTX与 vhost 首次创建时得到的 ctx 具备完全一致的配置——因为 OpenSSL、mbedTLS 等 TLS 后端把 ALPN 协议协商、客户端证书mTLS策略等状态保存在 ctx 上而不是 vhost 上证书热更新后如果只换证书不重设 ctx就会丢掉 h2 协商能力甚至不再索要客户端证书。读完本文你将掌握该自测程序的运行方式、两个测试用例的判定逻辑、lws_tls_cert_updated()的完整调用链以及它在 ACME 自动证书场景中的工程价值。这个测试程序在验证什么按照 api-test-tls-renew 的 README 的说明本程序模拟的是一个正在等待首个 ACME 证书的 vhost服务器 vhost 创建时使用LWS_SERVER_OPTION_IGNORE_MISSING_CERT并把证书路径指向不存在的位置因此 vhost 以「完全没有 ctx」的状态启动。随后程序按顺序执行两个 casecase预期结果证书到达lws_tls_cert_updated()为 vhost 创建第一个 ctx向该 vhost 发出的 h2 请求由 h2 协议提供服务证书续期lws_tls_cert_updated()创建替换用的新 ctx再次向该 vhost 发出的 h2 请求仍然由 h2 提供服务第二个 case 正是本测试的核心价值如果后端在续期时只重载证书而丢掉 ctx 上的其他配置那么 h2 将无法再协商成功测试就会失败。在 api-tests/README.md 的总览表中这一测试被概括为一个没有证书就启动的服务器 vhostIGNORE_MISSING_CERT通过lws_tls_cert_updated()先后获得第一个 ctx 和续期 ctx且每次都与 vhost 首次 ctx 配置一致h2 依然能够协商。为什么 ALPN 和客户端证书策略必须挂在 ctx 上要理解这个测试需要先弄清 lws 的层次划分一个struct lws_context下可以有多个struct lws_vhost每个 vhost 对应一个监听端口 服务名组合而 TLS 后端OpenSSL 的SSL_CTX、mbedTLS 的mbedtls_ssl_config则是 vhost 持有的具体会话配置对象。在 lib/tls/tls-server.c 中lws_tls_server_vhost_ctx_setup()的函数注释直接点明了设计约束What a vhosts server tls ctx needs on top of its certs: the client-cert policy, whatever extra verification certs user code loads into it, and the alpn. The backends keep all of it on the ctx, so every ctx the vhost gets needs it... without it, on openssl, an mTLS vhost would ask no client for a cert, and h2 would no longer be negotiated.也就是说后端把三类状态都存在 ctx 上客户端证书验证策略authmode / verify 配置用户代码通过回调额外加载的验证证书ALPN 协议列表如h2,http/1.1。因此 vhost 拿到的每一个ctx 都必须补齐这些配置——无论是首次创建时的 ctx还是lws_tls_cert_updated()重建的 ctx。否则在 OpenSSL 上一个 mTLS vhost 续期后就再也不会向客户端索要证书h2 也不再被协商出来。mbedTLS 后端同样如此。在 lib/tls/mbedtls/mbedtls-server.c 的lws_tls_vhost_backend_create_ctx()中cipher 列表、SNI 回调、mbedtls_ssl_conf_authmode()客户端证书模式、lws_mbedtls_set_alpn()ALPN全部在创建 ctx这一步统一应用注释明确说明Everything the vhosts config asks of its ctx is applied here rather than in ... init, so a ctx remade later bylws_tls_cert_updated()also gets it: a renewed ctx without the SNI callback would serve every name this vhosts cert and never refuse an unknown one (C-424), and one without the authmode or ALPN would stop asking for client certs or offering h2.运行方式与命令行参数前置条件构建时启用的特性从 CMakeLists.txt 可以看到该测试的构建依赖编译 libwebsockets 时需要满足以下配置项LWS_ROLE_H1HTTP/1.1 角色LWS_WITH_HTTP2HTTP/2 支持测试的判定依赖 h2LWS_WITH_NETWORKLWS_WITH_CLIENT服务器角色LWS_WITHOUT_SERVER必须为 0LWS_WITH_TLSLWS_WITH_BEARSSL必须为 0原因见下文内存交付证书一节启动命令在原文档给定的命令基础上默认端口为 7681$ lws-api-test-tls-renew -p 7681 --certs this dir参数含义如下表选项含义-p portTLS 服务器 vhost 的监听端口默认7681-s address客户端连接的目标地址默认localhost--certs dir存放证书与密钥的目录默认.程序启动后会依次打印两个 case 的结果例如case 0 (the first cert arrived): PASS最终汇总为Completed: PASS (0 of 2 cases failed)并以返回值 0/1 表达整体成败。每个 case 有 20 秒的超时保护源码中#define CASE_TIMEOUT_S 20超时未完成会判定为失败同时它也是注册在 CTest 下的正式测试add_test(NAME api-test-tls-renew ...)超时上限 60 秒端口由lws_get_free_port()动态分配可通过ctest -R api-test-tls-renew一键运行。测试用 PKI 与目录结构目录minimal-examples-lowlevel/api-tests/api-test-tls-renew/下共有 5 个文件文件用途main.c测试程序源码客户端 服务器在同一进程README.md本文所依据的说明文档CMakeLists.txt构建与 CTest 注册ca.crt一次性throwawayCA 证书私钥未保留客户端信任该 CAserver.crt / server.keyshort.example的服务器证书与私钥由上面的 CA 签发从证书内容可以看到server.crt的 CN 与 SAN 为short.exampleSAN 中还包含一组*.example通配域名由ca.crtCN 为lws tls sessions test CA签发而ca.crt是自签名的测试根证书。客户端 vhost 通过info.client_ssl_ca_filepath certs/ca.crt信任它从而在测试中完成对short.example服务端证书的校验。源码级拆解程序是如何工作的1. 初始化先建 context再建两个 vhost在 main.c 的main()中用lws_context_info_defaults()初始化info并通过lws_cmdline_option()解析-p、-s、--certs三个选项load_pem(server.crt, ...)与load_pem(server.key, ...)把证书和私钥提前读入内存缓冲区上限PEM_MAX 8192字节这是后续内存交付证书的前提context 以LWS_SERVER_OPTION_EXPLICIT_VHOSTS | LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT创建其中EXPLICIT_VHOSTS表示 vhost 全部由应用显式创建context 不自动建默认 vhostDO_SSL_GLOBAL_INIT触发 TLS 全局初始化服务器 vhostinfo.ssl_cert_filepath certs/not-yet/server.crt、info.ssl_private_key_filepath certs/not-yet/server.key——这两个路径下什么都没有配合info.options | LWS_SERVER_OPTION_IGNORE_MISSING_CERTvhost 得以无证书启动、没有任何 ctx客户端 vhost清掉服务器相关字段后info.port CONTEXT_PORT_NO_LISTEN不监听、info.client_ssl_ca_filepath certs/ca.crt创建用于发起连接的clivhost。LWS_SERVER_OPTION_IGNORE_MISSING_CERT的官方语义见 include/libwebsockets/lws-context-vhost.h证书或密钥缺失时不报错继续运行vhost 此时无法提供任何服务但例如 ACME 插件配置了取证书时这个选项允许你先从一个没有证书的 vhost 启动。在 lib/tls/tls-network.c 的lws_tls_generic_cert_checks()中证书缺失且设置了该选项时返回LWS_TLS_EXTANT_NO并记录vhost-tls.skipped_certs 1随后后端释放 ctx 并将其置 NULL——这正是no ctx at all的来源。2. 每个 case先热更新证书再发起 h2 请求next_case()main.c是每个 case 的驱动器打印 case N: ... 重置case_over与on_h2并安排一个 20 秒 watchdog调用核心 APIif (lws_tls_cert_updated(context, cert_path, key_path, pem_cert, pem_cert_len, pem_key, pem_key_len)) { case_finish(lws_tls_cert_updated() failed); return; }其中cert_path/key_path是not-yet的路径只用来定位使用该路径的 vhostpem_cert/pem_key是第一步读入内存的真实证书与私钥然后以i.alpn h2、i.ssl_connection LCCSCF_USE_SSL、i.host i.origin short.example发起 HTTPS 连接i.opaque_user_data cur 1用于区分当前 case 的连接。3. 判定逻辑请求必须由 h2 服务且应答 200客户端回调callback_cli()main.c负责判定在LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP中执行on_h2 lws_get_network_wsi(wsi) ! wsi;——在 h2 下一次请求是某个网络连接上的一条流因此流 wsi不同于网络 wsi据此判断是否真的协商上了 h2若服务器没有提供 ALPN连接会退化为 h1on_h2为假在LWS_CALLBACK_COMPLETED_CLIENT_HTTP中检查响应码是否为 200同时确认on_h2为真两者都满足才算 PASS连接错误、提前关闭、超时均判定失败。服务器回调callback_srv()main.c则简单返回 HTTP 200、text/plain、正文ok。两次 case 中请求能持续以 h2 被服务就证明了续期后的新 ctx 完整继承了 ALPN 配置。lws_tls_cert_updated()的底层实现该 API 的声明位于 include/libwebsockets/lws-x509.h六个参数依次是 context、证书路径、私钥路径、内存证书副本及其长度、内存私钥副本及其长度语义为为所有使用给定证书路径的 vhost 更新证书。其实现位于 lib/tls/tls-network.cint lws_tls_cert_updated(struct lws_context *context, const char *certpath, const char *keypath, const char *mem_cert, size_t len_mem_cert, const char *mem_privkey, size_t len_mem_privkey) { lws_start_foreach_vhost(v, context) { if (v-tls.cfg_alloc_cert_path v-tls.cfg_key_path !strcmp(v-tls.cfg_alloc_cert_path, certpath) !strcmp(v-tls.cfg_key_path, keypath)) lws_tls_vhost_cert_reload(v, mem_cert, len_mem_cert, mem_privkey, len_mem_privkey); } lws_end_foreach_vhost(v); return 0; }它遍历 context 下的全部 vhost用路径匹配找出所有使用该证书的 vhost 并逐一重载。这正是测试中路径只用于选择 vhost、证书走内存的设计由来。真正的重载逻辑在lws_tls_vhost_cert_reload()lib/tls/tls-network.c其流程是记录旧 ctxold_ctx与旧的active_ctx_ref构造一个假 wsi只填充 context 与 vhost 字段用于向应用协议回调传递上下文调用lws_tls_vhost_backend_create_ctx(v)创建全新 ctx——这一步把 cipher、SNI、authmode、ALPN 等 vhost 配置全部应用到新 ctx 上创建新 ctx 的引用new_ref调用lws_tls_server_certs_load()加载证书——优先使用传入的内存副本mem_cert/mem_privkey文件路径仅作兜底调用lws_tls_server_vhost_ctx_setup(v, wsi)补齐客户端证书验证策略、额外验证证书与 ALPN见上文 tls-server.c 的注释提交新 ctxv-tls.active_ctx_ref new_ref旧 ref 被加入v-tls.retired_ctx_listprivate-network.h 定义了这个退休 ctx 链表并解除一次引用由仍然持有旧连接的应用方继续使用直到最后一个连接关闭对应 C-669 的引用计数语义若任一步失败则释放新 ctx、把v-tls.ssl_ctx恢复为old_ctx保证失败的更新不会破坏当前服务。内存交付证书与 BearSSL 的限制原文档特别强调证书是直接以内存形式交给lws_tls_cert_updated()的路径只用于选择 vhost且该测试不基于 BearSSL 构建。原因有两层测试场景中服务器 vhost 配置的证书路径not-yet/server.crt根本不存在后端在加载证书时如果坚持先读文件就无从获取证书——只有内存副本可依赖BearSSL 的证书加载实现只在给定了路径时才去读文件无法从内存加载因此在 CMakeLists.txt 中明确要求require_lws_config(LWS_WITH_BEARSSL 0 requirements)。这也从侧面说明lws_tls_cert_updated()的内存优先参数设计正是为了配合 ACME 自动续期这类证书由代码生成、未必落盘的流程。在真实项目中的工程启示ACME 引导bootstrap模式LWS_SERVER_OPTION_IGNORE_MISSING_CERTlws_tls_cert_updated()组合正是官方为 ACME 场景设计的从无证书到有证书路径vhost 先以无 ctx 状态启动ACME 插件拿到首个证书后调用lws_tls_cert_updated()补齐第一个 ctx此后每次续期再调用同一 API 替换 ctx。本次测试的 case 0 验证的就是这段补第一个 ctx的过程。无中断续期从retired_ctx_list的设计可以推断旧 ctx 不会立刻销毁而是保留到仍在使用它的连接全部结束从而支持证书滚动期间旧连接不中断而新连接从一开始就使用新证书。磁盘监视的另一种做法值得一提的是仓库还提供了磁盘监视方案在 lib/tls/private-lib-tls.h 附近定义了LWS_TLS_CERT_WATCHlws_tls_vhost_cert_reload()在加载成功后记录证书文件的指纹lws_tls_cert_file_id_get后台定期比对发现文件变化即重新加载。与之配套的另一个测试 api-test-tls-cert-rotate 专门验证证书在磁盘上被替换、lws 主动发现的路径而api-test-tls-renew则聚焦应用主动告知的路径两者互补。迁移到你的 mTLS h2 服务时的检查清单确认后端把 ALPN / 客户端证书策略放在 ctx 上OpenSSL、mbedTLS、GnuTLS、Schannel 均如此因此续期后必须重设这些配置——lws 在lws_tls_server_vhost_ctx_setup()中统一处理用户无需手动干预如果服务配置了info.alpn如h2,http/1.1续期后应像本测试一样实际验证 h2 是否仍能协商避免静默退化到 h1若使用 BearSSL 后端内存交付路径不可用应确保证书文件真实存在且路径可读证书更新失败时 lws 会自动回退到旧 ctx业务侧无需自行做保留旧配置的兜底。总结api-test-tls-renew以最精简的形态单进程、双 vhost、两轮 case覆盖了 libwebsockets TLS 证书热更新的关键契约由lws_tls_cert_updated()重建的 ctx必须与 vhost 首次创建时的 ctx 配置完全一致ALPN 协商与客户端证书策略都不能丢失。理解它的运行方式与实现链路能帮助你在实际部署中安全地落地 ACME 自动续期与 mTLS/h2 服务并借助 CTest 注册的自动化测试持续回归验证这一行为。赞分享后端网络通信【免费下载链接】libwebsocketscanonical libwebsockets.org networking library项目地址https://gitcode.com/gh_mirrors/li/libwebsockets点击查看免费下载相关推荐BFE 基于 TLS 客户端证书认证mTLS的 HTTPS 双向认证配置实战BFE 基于 TLS 客户端证书认证mTLS的 HTTPS 双向认证配置实战 BFE 是一款基于 Go 实现的现代化七层负载均衡器支持在 TLS 握手阶段后端网络/通信云原生3步完成将老旧电视盒子变身全能Linux服务器的终极指南3步完成将老旧电视盒子变身全能Linux服务器的终极指南 你是否曾想过那些被遗忘在角落的电视盒子其实蕴藏着强大的潜力今天我要告诉你一个秘密只需几个简单的嵌入式开发工具构建工具操作系统ingress-nginx 客户端证书认证mTLS实战基于 auth-tls 注解实现双向 TLS 身份验证ingress nginx 客户端证书认证mTLS实战基于 auth tls 注解实现双向 TLS 身份验证 导读 本文以 ingress nginx 官后端API网关负载均衡云原生上一篇OBMM预导入技术3步优化远程内存访问时延提升分布式系统性能下一篇witty-ub-diag-tool深度测评超节点故障定界从未如此简单创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?
咨询建站