libcurl 证书主机名校验指南:深入解析 CURLOPT_SSL_VERIFYHOST
【免费下载链接】curlA command line tool and library for transferring data with URL syntax, supporting DICT, FILE, FTP, FTPS, GOPHER, GOPHERS, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, MQTT, MQTTS, POP3, POP3S, RTSP, SCP, SFTP, SMB, SMBS, SMTP, SMTPS, TELNET, TFTP, WS and WSS. libcurl offers a myriad of powerful features项目地址: https://gitcode.com/GitHub_Trending/cu/curl
本指南围绕 libcurl 的CURLOPT_SSL_VERIFYHOST选项展开,讲解如何通过该选项控制 TLS 连接中对服务器证书主机名的校验行为,涵盖取值语义、通配符与 IP 地址匹配规则、与CURLOPT_SSL_VERIFYPEER的分工,以及相关安全风险与历史演进。结合当前仓库源码(lib/vtls/hostcheck.c、lib/vtls/openssl.c、lib/setopt.c等)与单元测试,读者可掌握该选项的底层实现原理,并能在自己的 C 程序或命令行场景中正确配置证书校验。
选项总览
CURLOPT_SSL_VERIFYHOST用于设置 libcurl 在与服务器进行 TLS 协商时,是否校验服务器证书中的主机名(hostname)是否与请求的 URL 主机名一致。
- 类型:
long(见 easyoptions.c 中登记为CURLOT_LONG) - 协议:TLS(HTTPS、FTPS、IMAPS、SMTPS、WSS 等所有基于 TLS 的协议)
- TLS 后端:全部(OpenSSL、Schannel、GnuTLS、mbedTLS、WolfSSL、Secure Transport 等)
- 引入版本:7.8.1
- 默认值:
2(严格校验) - 设置方式:
#include <curl/curl.h> CURLcode curl_easy_setopt(CURL *handle, CURLOPT_SSL_VERIFYHOST, long verify);取值语义与行为
传入的值含义如下:
| 取值 | 行为 |
|---|---|
2L | 校验服务器证书中的主机名必须与连接的主机名/地址匹配,否则连接失败(默认值) |
1L | 与2L等效(自 7.66.0 起统一处理,见下文「历史演进」) |
0L | 跳过主机名校验,无论证书中的名称是什么,连接都成功 |
警告:将值设为0会关闭主机名校验,攻击者可以对通信实施中间人攻击(man-in-the-middle)而不被察觉。仅依赖传输加密是不够的——你无法保证正在与正确的端点通信。此外,libcurl 在使用安全协议时会信任服务器返回的信息并存储、使用诸如 HSTS 与 Alt-Svc 等数据,关闭证书校验可能使 libcurl 信任来自恶意服务器的这类信息(详见原文档「DESCRIPTION」部分)。因此默认值2是强烈建议保持的配置。
注意:
CURLOPT_SSL_VERIFYHOST控制的是“证书声称的身份”是否匹配主机名;而证书是否由受信任的 CA 签发,则由独立的 CURLOPT_SSL_VERIFYPEER 选项控制。两者配合使用才能完成完整的 TLS 证书信任校验。
匹配规则:主机名、通配符与 IP 地址
常规主机名匹配
当verify为1或2时,服务器证书必须表明它是为 curl 连接的主机名或地址签发的,否则连接失败。证书中的 Common Name(CN)字段或 Subject Alternative Name(SAN)字段必须与 URL 中用于连接的主机名一致。libcurl 正是通过Curl_cert_hostcheck()(定义于 hostcheck.c)执行这一比较。
通配符(Wildcard)匹配规则
证书名称可以包含通配符,但约束严格:
- 唯一的星号
*必须是最左侧字符,且其后必须紧跟一个点号(.),即形如*.example.com; - 通配符必须包含多于一个点号,因此不能为顶级域名设置通配符(例如
*.com是无效的); *只能用于最左侧的标签,a*、a*b、*b这类形式都不允许。
这些规则在 hostcheck.c 的hostmatch()中逐一落实:模式不以*.开头则退化为精确比较;主机名若是 IP 地址或以点号开头则直接失败;模式中至少需要两个点号才能启用通配匹配(避免过宽匹配)。实现遵循 RFC 6125 第 6.4.3 节 的匹配规则,并额外忽略主机名与通配符末尾的尾随点号(与浏览器行为一致)。例如foo.host.com可以匹配*.host.com,但bar.foo.host.com不能匹配*.host.com(星号只覆盖一个域名标签)。
IP 地址匹配
证书也可以为数字 IP 地址(IPv4 或 IPv6)签发,但此时该名称必须是Subject Alternative Name类型,并且其类型必须正确标识该字段为 IP 地址(即 SAN 中的iPAddress类型,而非dNSName)。在 OpenSSL 后端中,ossl_verifyhost()(openssl.c)会根据peer->type(CURL_SSL_PEER_IPV4/CURL_SSL_PEER_IPV6/CURL_SSL_PEER_DNS)决定目标类型是GEN_IPADD还是GEN_DNS,并只遍历证书中同类型的 SAN 条目进行比较;同时hostcheck.c也会检测主机名为 IP 地址时拒绝通配匹配(hostcheck.c)。IP 地址要求精确相等,不允许通配。
完整示例
以下代码展示了如何在 C 程序中显式设置默认的严格校验:
#include <curl/curl.h> int main(void) { CURL *curl = curl_easy_init(); if(curl) { CURLcode result; curl_easy_setopt(curl, CURLOPT_URL, "https://example.com"); /* 设置默认值:开启严格的主机名校验 */ curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); result = curl_easy_perform(curl); curl_easy_cleanup(curl); } return 0; }运行curl_easy_perform()后,若证书主机名与example.com不匹配,调用将返回CURLE_PEER_FAILED_VERIFICATION(该错误码在 openssl.c 等多处用于主机名校验失败路径)。
源码实现细节
参数解析
在 setopt.c 中,CURLOPT_SSL_VERIFYHOST的解析逻辑如下:
case CURLOPT_SSL_VERIFYHOST: /* 显然人们没有阅读文档,太多人以为该参数是布尔值而误用。 将 1 和 2 等同对待 */ s->ssl.primary.verifyhost = enabled; ok = 2; /* 更新当前连接的 ssl_config。 */ Curl_ssl_conn_config_update(data, FALSE); break;配置被写入ssl.primary.verifyhost,并同步到当前连接的 SSL 配置中,供各 TLS 后端读取。同时 vtls_config.c 中的Curl_ssl_config_init()将verifypeer与verifyhost均默认置为TRUE,印证了默认严格校验的行为;match_ssl_primary_config()(vtls_config.c)还会将verifyhost纳入连接复用(connection reuse)的匹配条件,避免复用了校验策略不同的连接。
各 TLS 后端的校验流程
- OpenSSL:
ossl_verifyhost()(openssl.c)提取证书的subjectAltName列表,仅对与目标类型相同的 SAN 条目调用Curl_cert_hostcheck()进行比较;失败时返回CURLE_PEER_FAILED_VERIFICATION。 - GnuTLS:gtls.c 中
rc与verifyhost共同决定结果,并打印证书校验消息。 - mbedTLS:mbedtls.c 中
verifypeer与verifyhost相互独立——仅关闭被禁用的检查;若verifyhost为真,则始终让 mbedTLS 校验证书(mbedtls.c)。 - Schannel:schannel.c 在关闭
verifyhost时阻止 Schannel 自动发送名称,并依据verifypeer/verifyhost组合决定是否进行主机名校验(schannel.c)。 - WolfSSL:wolfssl.c 在
verifyhost为真且存在 SNI 时参与主机名验证流程。
单元测试验证
仓库中的 unit1397.c 针对Curl_cert_hostcheck()进行了全面测试:包含大量“主机/模式/是否匹配”用例(unit1397.c),并验证函数不依赖模式的尾随 NUL 字节(模拟底层 TLS 库不复制 NUL 结尾的模式),同时检查嵌入 NUL 的模式不会被误判为匹配(unit1397.c)。这些用例直接验证了上文所述的通配符与精确匹配规则。
历史演进
- 7.28.0 及更早:值
1曾被当作某种调试选项使用,因频繁导致程序员误用而不再支持; - 7.28.1 到 7.65.3:设置为
1会使curl_easy_setopt()返回错误并保持标志不变; - 7.66.0 起:libcurl 将
1和2等同对待,均表示严格校验。这正是 setopt.c 注释中所说的“将 1 和 2 等同处理”的由来。
返回值与错误处理
curl_easy_setopt()返回CURLcode指示成功或错误:
CURLE_OK(0)表示一切正常;- 非零值表示发生了错误,具体错误码参见
libcurl-errors文档。
主机名校验失败时,执行请求会得到CURLE_PEER_FAILED_VERIFICATION,而不是设置选项时立即报错。
关联选项
- CURLOPT_CAINFO:指定 CA 证书文件,供
CURLOPT_SSL_VERIFYPEER使用; - CURLOPT_PINNEDPUBLICKEY:将服务器公钥固定到指定值,提供额外的身份校验;
- CURLOPT_SSL_VERIFYPEER:控制是否验证证书由受信任的 CA 签发;
- CURLOPT_PROXY_SSL_VERIFYHOST:针对代理连接的等价选项;
- CURLOPT_DOH_SSL_VERIFYHOST:针对 DoH(DNS over HTTPS)连接的等价选项。
命令行工具 curl 默认也保持严格的主机名校验(src/tool_setopt.c中CURLOPT_SSL_VERIFYHOST默认值为 1,src/config2setopts.c中同样强调 libcurl 默认严格校验),并使用-k/--insecure临时关闭证书校验。在实际开发中,建议始终保留默认的严格校验,仅在受控的测试环境(如自签名证书的本地调试)中临时关闭,且需充分理解中间人攻击风险。
【免费下载链接】curlA command line tool and library for transferring data with URL syntax, supporting DICT, FILE, FTP, FTPS, GOPHER, GOPHERS, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, MQTT, MQTTS, POP3, POP3S, RTSP, SCP, SFTP, SMB, SMBS, SMTP, SMTPS, TELNET, TFTP, WS and WSS. libcurl offers a myriad of powerful features项目地址: https://gitcode.com/GitHub_Trending/cu/curl
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考