后端网络【免费下载链接】h2oH2O - the optimized HTTP/1, HTTP/2, HTTP/3 server项目地址https://gitcode.com/gh_mirrors/h2/h2o点击查看免费下载本篇技术指南以 H2O 仓库内deps/ssl-conservatory/openssl/目录下的官方示例代码为核心系统讲解在使用 OpenSSL 编写 HTTPS 客户端时如何正确执行服务端证书链验证与主机名hostname校验覆盖核心 API、验证算法、跨平台编译Linux / OS X / Windows与错误处理并对照 H2O 自身在 lib/common/socket.c 中的真实调用场景帮助你写出安全、可复用的 OpenSSL 客户端代码。一、背景为什么需要专门的证书验证示例代码SSL/TLS 客户端最常见的两个安全缺陷是没有开启证书链验证默认的 OpenSSL 客户端甚至不验证证书以及验证了证书链却忘了校验主机名——攻击者只要能拿到任意一张受信任 CA 签发的证书例如自己注册域名签发的证书就能在中间人攻击中蒙混过关。deps/ssl-conservatory/目录正是为此而设的SSL 样本代码库。项目级说明见 deps/ssl-conservatory/README.md其中明确写道其目标是为正确实现 SSL 客户端的各种语言/框架提供文档完善且安全的示例代码。openssl/子目录对应 C/OpenSSL 实现ios/子目录则提供 iOS 端的证书锁定certificate pinning实现。在openssl/目录中除示例代码外还附有一份配套白皮书《Everything youve always wanted to know about certificate validation with OpenSSL (but were afraid to ask)》PDF 文件位于 deps/ssl-conservatory/openssl/everything-you-wanted-to-know-about-openssl.pdf。源码头文件 openssl_hostname_validation.h 与示例程序 test_client.c 都明确要求读者在使用代码前先阅读该白皮书因为它描述了代码的工作方式、正确用法与局限。该示例已在 Windows 7、OS X 与 Linux 上完成编译与测试见 README.md。二、目录结构与两个核心文件openssl/目录下的文件清单如下文件作用openssl_hostname_validation.c/.h主机名校验核心实现与头文件可被任何 OpenSSL 客户端复用test_client.c完整的 HTTPS 客户端示例连接www.isecpartners.com:443并打印 HTTP GET 响应MakefileUnix 系FreeBSD、Ubuntu、Cygwin构建脚本Makefile_mingwWindows / MinGW 构建脚本DigiCertHighAssuranceEVRootCA.pem示例所用信任锚CA 根证书供SSL_CTX_load_verify_locations()加载everything-you-wanted-to-know-about-openssl.pdf配套白皮书值得注意的一点是这套代码并非仓库里的摆设。H2O 的 TLS 客户端代码在 lib/common/socket.c 中通过#include ../../deps/ssl-conservatory/openssl/openssl_hostname_validation.c直接内联了主机名校验实现并在握手完成后调用validate_hostname()见下文第五节说明该示例代码经受住了真实生产 HTTP 服务器的考验。三、核心 APIvalidate_hostname()与HostnameValidationResult主机名校验的对外接口非常简洁只暴露一个函数见 openssl_hostname_validation.hHostnameValidationResult validate_hostname(const char *hostname, const X509 *server_cert);hostname客户端期望连接的服务端主机名server_cert从 TLS 握手拿到的服务端证书X509 *通常来自SSL_get_peer_certificate()。返回类型HostnameValidationResult是一个枚举共 5 个取值见 openssl_hostname_validation.h枚举值含义MatchFound在证书中找到了与期望主机名匹配的名字MatchNotFound证书中没有任何名字与期望主机名匹配NoSANPresent证书中不存在 Subject Alternative NameSAN扩展仅由 SAN 检查路径返回MalformedCertificate证书中的某个主机名内嵌了 NUL 字符\0视为畸形证书Error入参为空或证书字段提取失败等内部错误校验逻辑遵循 RFC 6125 的推荐顺序见 openssl_hostname_validation.c首先在证书的Subject Alternative Name 扩展中查找匹配若 SAN 扩展不存在返回NoSANPresent降级到 Subject 字段的Common NameCN中查找。这是现代证书校验的标准做法SAN 是签发服务器证书时的法定扩展而 CN 只是历史遗留的降级路径。3.1 SAN 检查matches_subject_alternative_name()该函数见 openssl_hostname_validation.c通过X509_get_ext_d2i(cert, NID_subject_alt_name, ...)提取 SAN 扩展若扩展不存在返回NoSANPresent遍历STACK_OF(GENERAL_NAME)中的每一项只处理type GEN_DNS的 DNS 名称忽略 IP 地址等其他类型只要任一 DNS 名称与期望主机名匹配成功validate_name返回MatchFound立即终止循环并返回遍历结束后若仍无匹配返回MatchNotFound最后用sk_GENERAL_NAME_pop_free()释放栈内存避免泄漏。3.2 CN 兜底matches_common_name()当 SAN 缺失时该函数见 openssl_hostname_validation.c按以下步骤提取 CNX509_NAME_get_index_by_NID(X509_get_subject_name(cert), NID_commonName, -1)定位 CN 字段在 Subject 中的索引找不到返回ErrorX509_NAME_get_entry()取出该条目再经X509_NAME_ENTRY_get_data()拿到 ASN.1 字符串交由validate_name()完成具体比较。3.3 单条名称的比较规则validate_name()这是整个校验中最精细的部分见 openssl_hostname_validation.c它实现了几个关键安全细节NUL 字符防护先用has_nul()检查证书中的名字是否内嵌\0。这是对历史上前缀匹配 NUL 截断攻击如www.attacker.com\0.trusted.com的直接防御发现即返回MalformedCertificate域名末尾点归一化如果期望主机名以.结尾FQDN 写法比较前先去掉末尾点通配符处理仅当证书名字以*.开头时才跳过期望主机名的第一个标签段第一个.之前的部分且要求通配符名字本身长度大于 2即至少是*.x形式。例如证书*.example.com可匹配www.example.com但不能匹配裸域example.com大小写不敏感比较通过memeq_ncase()逐字节转小写后比较DNS 域名本身不区分大小写长度严格相等certname_len ! hostname_len直接判为MatchNotFound杜绝任何前缀/后缀的部分匹配。四、示例客户端test_client.c的完整 HTTPS 流程test_client.c 是一个可独立运行的 HTTPS 客户端演示了正确的 OpenSSL 客户端应具备的完整步骤。其流程可拆解为如下八个阶段1. 初始化与 PRNG 检查SSL_library_init(); SSL_load_error_strings(); if (RAND_status() ! 1) { fprintf(stderr, OpenSSL PRNG not seeded with enough data.); goto error_1; }OpenSSL 的 PRNG伪随机数发生器未获得足够熵时随机数质量不可保证直接影响密钥与随机挑战的强度因此必须显式检查。2. 创建 SSL 上下文并开启证书链验证ssl_ctx SSL_CTX_new(TLSv1_client_method()); SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, NULL);SSL_VERIFY_PEER是强制服务端提供证书并校验其证书链的开关——不设置它下面的所有校验都形同虚设。3. 加载信任库CA 证书if (SSL_CTX_load_verify_locations(ssl_ctx, TRUSTED_CA_PATHNAME, NULL) ! 1) { fprintf(stderr, Couldnt load certificate trust store.\n); goto error_2; }示例使用DigiCertHighAssuranceEVRootCA.pem作为信任锚。注意这是固定信任锚的写法与默认的系统信任库不同属于最小化信任面的安全实践。4. 限制密码套件#define SECURE_CIPHER_LIST RC4-SHA:HIGH:!ADH:!AECDH:!CAMELLIA if (SSL_CTX_set_cipher_list(ssl_ctx, SECURE_CIPHER_LIST) ! 1) goto error_2;该字符串表示启用 RC4-SHA 与所有 HIGH 级套件同时显式排除匿名 DH!ADH匿名套件不提供身份认证、匿名 ECDH!AECDH与 Camellia。需要提醒的是此清单是项目早期的示例产物如今 RC4 已被业界废弃实际生产代码应使用HIGH:!aNULL:!MD5之类更现代的配置。5. 建立 SSL 连接并完成握手sbio BIO_new_ssl_connect(ssl_ctx); BIO_get_ssl(sbio, ssl); BIO_set_conn_hostname(sbio, TARGET_SERVER); // www.isecpartners.com:443 if (SSL_do_handshake(ssl) 0) { long verify_err SSL_get_verify_result(ssl); if (verify_err ! X509_V_OK) fprintf(stderr, Certificate chain validation failed: %s\n, X509_verify_cert_error_string(verify_err)); else ERR_print_errors_fp(stderr); goto error_3; }这里展示了握手失败时区分错误原因的正确姿势优先用SSL_get_verify_result()检查是否为证书链验证失败并通过X509_verify_cert_error_string()输出人类可读的错误描述若验证本身通过才用ERR_print_errors_fp()打印 OpenSSL 错误队列。6. 获取服务端证书server_cert SSL_get_peer_certificate(ssl); if (server_cert NULL) { // 握手成功但服务端未提供证书——极可能使用了不安全的匿名密码套件立即退出 goto error_4; }7. 主机名校验关键一步if (validate_hostname(TARGET_HOST, server_cert) ! MatchFound) { fprintf(stderr, Hostname validation failed.\n); goto error_5; }只有MatchFound才允许继续通信。这一步弥补了SSL_VERIFY_PEER的盲区——链验证只证明证书由可信 CA 签发主机名校验才证明这张证书确实是发给目标服务器的。8. 发送请求并收尾send_http_get_and_print(sbio); // BIO_puts 发送 GET / HTTP/1.0BIO_read 循环读取响应 // 依次释放 X509、关闭 SSL 通道、释放 BIO 链与 SSL_CTX、清理 OpenSSL 全局状态错误处理采用goto级联式清理error_5 → error_4 → error_3 → error_2 → error_1保证任意一步失败时所有已分配资源都被正确释放这是 C 语言中常见的确定性清理模式。五、H2O 项目中的真实调用lib/common/socket.c示例代码并非纸上谈兵。H2O 在 TLS 客户端握手的收尾阶段直接复用了这套实现lib/common/socket.c 通过预处理器直接包含openssl_hostname_validation.c在 lib/common/socket.c 中当SSL_connect成功ret 1且当前角色是客户端!SSL_is_server(...)时取出对端证书并调用validate_hostname()X509 *cert SSL_get_peer_certificate(sock-ssl-ossl); if (cert ! NULL) { switch (validate_hostname(sock-ssl-handshake.client.server_name, cert)) { case MatchFound: /* ok */ break; case MatchNotFound: err h2o_socket_error_ssl_cert_name_mismatch; break; default: err h2o_socket_error_ssl_cert_invalid; break; } X509_free(cert); } else { err h2o_socket_error_ssl_no_cert; }这段代码将枚举结果映射为三类 H2O 内部错误码MatchNotFound对应h2o_socket_error_ssl_cert_name_mismatch主机名不匹配其余异常归为h2o_socket_error_ssl_cert_invalid服务端未提供证书则报h2o_socket_error_ssl_no_cert。由此可以看出这套主机名校验逻辑支撑着 H2O 反向代理等场景下对上游服务器的 TLS 身份验证。六、跨平台编译指南原 README 给出了三套平台的具体编译与测试说明见 deps/ssl-conservatory/openssl/README.md。Linux示例在 Ubuntu 11.04 上编译测试通过需要安装 libssl 与 libcrypto 的开发库及头文件主流发行版中通常位于libssl-dev包使用仓库内的 Makefile 构建CCgcc编译参数-Wall -stdc99 -pedantic链接参数-lcrypto -lssl运行make即可生成test_client可执行文件。OS X示例在 OS X Mountain Lion 上编译测试通过OS X 自带 OpenSSL 开发库但 Apple 对 libssl 做了修改自动使用系统信任库来校验证书链且该行为无法更改。因此在 OS X 上调用SSL_CTX_load_verify_locations()指定信任库永远会被忽略——若你的代码依赖自定义信任锚在 OS X 上必须改用其他机制如证书锁定编译时会输出大量 is deprecated 警告因为 Apple 正从 OpenSSL 迁移到 Common Crypto 框架属正常现象。WindowsMinGW示例使用 MinGW 编译、Windows 7 上测试通过需要安装 MinGW 与 OpenSSL 开发库OpenSSL 官方站点提供 Windows 预编译二进制若使用官方预编译二进制需先把头文件与两个动态库放入 MinGW 目录Copy OpenSSL_Folder/include/ → MinGW_Folder/include/ Copy OpenSSL_Folder/libeay32.dll → MinGW_Folder/lib/libeay32.dll Copy OpenSSL_Folder/libssl32.dll → MinGW_Folder/lib/libssl32.dll随后使用 Windows 专用 Makefile 编译make -f Makefile_mingw对应的 Makefile_mingw 中链接参数为-leay32 -lssl32对应上述两个 DLL生成的可执行文件为test_client.exe。七、使用这套代码的注意事项综合头文件、实现与示例代码中的注释参见 openssl_hostname_validation.c 各函数注释与 openssl_hostname_validation.h使用时应留意以下几点校验顺序不可颠倒必须先完成证书链验证SSL_VERIFY_PEER 信任库再做主机名校验两者缺一不可NUL 注入防护是硬性要求MalformedCertificate状态的引入说明实现者把证书名字中嵌入 NUL视为恶意输入而非普通不匹配调用方应直接中止连接降级路径的局限仅当 SAN 完全缺失时才回退到 CNRFC 6125 的宽松策略对于 SAN 存在但无匹配的情况不应再去 CN 中寻找以避免放宽匹配规则通配符匹配范围*.example.com形式的证书只匹配一个子域层级且不匹配裸域实现中并无对*.com之类非法通配符的特判接入方若面向公网建议在上层补充校验平台差异OS X 上SSL_CTX_load_verify_locations()失效是平台行为移植代码时必须分支处理。八、小结从 deps/ssl-conservatory/openssl/README.md 出发可以看到这套示例代码完整覆盖了 OpenSSL 客户端证书验证的全部关键环节信任库配置、链验证、基于 RFC 6125 的主机名校验SAN 优先、CN 兜底、通配符与 NUL 注入防护以及 Linux / OS X / Windows 三平台的构建方式。它既是学习 OpenSSL 安全客户端编程的最佳入门范本也在 H2O 的 lib/common/socket.c 中被直接复用为生产代码。如果你正在编写需要连接 HTTPS 服务端的 C 程序直接采纳这套模式即可规避绝大多数证书校验方面的经典陷阱。赞分享后端网络【免费下载链接】h2oH2O - the optimized HTTP/1, HTTP/2, HTTP/3 server项目地址https://gitcode.com/gh_mirrors/h2/h2o点击查看免费下载相关推荐H2O 中的 SSL 证书验证与固定实践基于 SSL Conservatory 的 OpenSSL 主机名校验与 iOS 证书固定指南H2O 中的 SSL 证书验证与固定实践基于 SSL Conservatory 的 OpenSSL 主机名校验与 iOS 证书固定指南 正确实现 SSL/TL后端网络requests 测试证书实战用 OpenSSL 自制一张「已过期」TLS 证书来验证 SSL 校验行为requests 测试证书实战用 OpenSSL 自制一张「已过期」TLS 证书来验证 SSL 校验行为 在 requests 的测试体系中有一组专门用来复后端网络通信上一篇如何永久保存微信聊天记录WeChatMsg完整指南助你掌控数字记忆下一篇AMCT 大模型 OFMR 量化实践基于昇腾 AI 处理器的 HiF8/FP8 低比特量化指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
