libcurl CURLINFO_CERTINFO 详解:从 TLS 握手后提取服务器证书链信息的完整指南
【免费下载链接】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
导读
CURLINFO_CERTINFO 是 libcurl 提供的 TLS 证书链信息查询接口,配合 CURLOPT_CERTINFO 选项,可以在 HTTPS 等 TLS 连接建立后,按证书链顺序提取每一张证书的 Subject、Issuer、序列号、有效期、公钥参数乃至完整 PEM 内容等结构化文本信息。本文以 curl 项目官方文档 CURLINFO_CERTINFO 为骨架,结合 curl.h、getinfo.c、vtls.c 与各 TLS 后端实现源码,讲清接口用法、数据结构、字段含义、后端差异与底层实现原理。读完本文,你将能够用一段可运行的 C 代码打印出目标站点的完整证书链,并理解其背后的数据流。
一、接口速览:函数签名与数据结构
CURLINFO_CERTINFO 是 curl_easy_getinfo(3) 的查询选项,用于获取服务器证书链的信息。其调用形式为:
#include <curl/curl.h> CURLcode curl_easy_getinfo(CURL *handle, CURLINFO_CERTINFO, struct curl_certinfo **chainp);调用时传入一个struct curl_certinfo *指针的地址,返回后该指针被设置为指向一个保存服务器证书链信息的结构体。前提是发起请求时已经通过CURLOPT_CERTINFO开启了证书信息收集(详见下文)。
返回的数据结构在 include/curl/curl.h 中定义如下:
/* info about the certificate chain, for SSL backends that support it. Asked for with CURLOPT_CERTINFO / CURLINFO_CERTINFO */ struct curl_certinfo { int num_of_certs; /* number of certificates with information */ struct curl_slist **certinfo; /* for each index in this array, there is a linked list with textual information for a certificate in the format "name:content". eg "Subject:foo", "Issuer:bar", etc. */ };num_of_certs:证书链中证书的数量,即certinfo数组的元素个数。链中证书通常按从叶证书(服务器证书)到根证书的顺序排列,具体顺序与 TLS 后端及服务器配置有关。certinfo:一个指针数组,数组长度为num_of_certs。每个元素指向一个struct curl_slist链表,链表中的每一项是一条文本信息,格式统一为"name:content",例如"Subject:CN=www.example.com"、"Issuer:C=US, O=Let's Encrypt"等。每张证书的条目内容因 TLS 后端和证书本身而异。
在 include/curl/curl.h 中,CURLINFO_CERTINFO被定义为CURLINFO_PTR + 34,属于指针类型的信息项——这也解释了为什么返回值是一个指针而非普通数值。
相关的配套选项
- CURLOPT_CERTINFO(docs/libcurl/opts/CURLOPT_CERTINFO.md):传输前通过
curl_easy_setopt传入1L开启证书信息收集,默认值为0(关闭)。必须开启它,curl_easy_getinfo查询CURLINFO_CERTINFO才有数据可读。 - 该选项在 TLS 协议(HTTPS、FTPS、IMAPS、SMTPS 等)下有效,并依赖具体 TLS 后端的支持能力。
二、使用前提:先通过 CURLOPT_CERTINFO 开启收集
CURLINFO_CERTINFO本身只负责“读取”,数据收集需要靠CURLOPT_CERTINFO选项提前开启。官方文档 CURLOPT_CERTINFO 说明:传入一个值为1的 long 即可启用 libcurl 的证书链信息收集器,之后 libcurl 会在 TLS 握手阶段提取证书链中每张证书的大量信息,供传输结束后通过curl_easy_getinfo与CURLINFO_CERTINFO取回。
在源码层面,lib/setopt.c 处理CURLOPT_CERTINFO时会先检测当前 TLS 后端是否支持证书信息收集能力(SSLSUPP_CERTINFO):
case CURLOPT_CERTINFO: #ifdef USE_SSL if(Curl_ssl_supports(data, SSLSUPP_CERTINFO)) s->ssl.certinfo = enabled; else #endif return CURLE_NOT_BUILT_IN; break;这意味着:如果 libcurl 编译时使用的 TLS 后端不支持该特性,curl_easy_setopt(curl, CURLOPT_CERTINFO, 1L)会直接返回CURLE_NOT_BUILT_IN,而不是静默忽略。开发者应当检查该调用的返回值,以确认功能可用。
三、完整示例:打印目标站点的证书链
官方文档 CURLINFO_CERTINFO 给出了核心示例;仓库中的 docs/examples/certinfo.c 则提供了一个更完整的可编译版本(额外包含curl_global_init/curl_global_cleanup与写回调,避免响应体干扰输出)。下面是在官方示例基础上补充了错误检查与初始化清理的完整代码:
#include <stdio.h> #include <curl/curl.h> /* 丢弃响应体,仅关注证书信息 */ static size_t write_cb(char *ptr, size_t size, size_t nmemb, void *stream) { (void)stream; (void)ptr; return size * nmemb; } int main(void) { CURL *curl; CURLcode result; result = curl_global_init(CURL_GLOBAL_ALL); if(result != CURLE_OK) return (int)result; curl = curl_easy_init(); if(curl) { curl_easy_setopt(curl, CURLOPT_URL, "https://www.example.com/"); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb); /* 连接到任意 HTTPS 站点,无论证书是否受信任 */ curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); /* 开启证书链信息收集(关键步骤) */ curl_easy_setopt(curl, CURLOPT_CERTINFO, 1L); result = curl_easy_perform(curl); if(result == CURLE_OK) { struct curl_certinfo *ci = NULL; result = curl_easy_getinfo(curl, CURLINFO_CERTINFO, &ci); if(result == CURLE_OK && ci) { int i; printf("%d certs!\n", ci->num_of_certs); for(i = 0; i < ci->num_of_certs; i++) { struct curl_slist *slist; printf("-- cert[%d] --\n", i); for(slist = ci->certinfo[i]; slist; slist = slist->next) printf("%s\n", slist->data); } } } curl_easy_cleanup(curl); } curl_global_cleanup(); return (int)result; }代码要点:
- 必须设置
CURLOPT_CERTINFO为1L,且放在curl_easy_perform之前;否则查询返回的结构中num_of_certs为 0,取不到任何数据。 - 示例中关闭了
CURLOPT_SSL_VERIFYPEER与CURLOPT_SSL_VERIFYHOST,目的是允许连接到自签名或不受信任证书的站点也能完成握手并提取链信息;生产环境中通常保留默认的证书校验。 curl_easy_getinfo返回的结构体指针指向 libcurl 内部管理的内存(data->info.certs),无需、也不应手动 free;libcurl 会在合适时机自行释放(详见下文实现原理)。示例代码不调用curl_slist_free_all,正是因为这个原因。- 结果结构体在
curl_easy_cleanup之后即失效,请确保在同一 handle 的生命周期内完成读取。
编译方式(以 gcc 为例):
gcc -o certinfo certinfo.c -lcurl ./certinfo输出形如(字段随 TLS 后端与证书内容变化):
3 certs! -- cert[0] -- Subject:CN=www.example.com Issuer:C=US, O=Let's Encrypt, CN=R10 Version:2 Serial Number:04:1e:... Signature Algorithm:sha256WithRSAEncryption Public Key Algorithm:rsaEncryption ... Start date:Sep 8 00:00:00 2024 GMT Expire date:Dec 7 00:00:00 2024 GMT RSA Public Key:(2048 bits) ... Cert: -----BEGIN CERTIFICATE----- ... -----END CERTIFICATE----- -- cert[1] -- Subject:C=US, O=Let's Encrypt, CN=R10 Issuer:C=US, O=Internet Security Research Group, CN=ISRG Root X1 ...四、字段清单:一张证书会包含哪些 "name:content" 条目
文档明确指出,每条信息的格式是"name:content"(如"Subject:Foo"、"Issuer:Bar"),并且条目内容因 SSL 后端和证书本身而异。以 OpenSSL 后端为例,lib/vtls/openssl.c 中的ossl_certchain()函数逐张提取了以下字段:
| 字段名 | 含义 | 提取方式(OpenSSL 后端) |
|---|---|---|
Subject | 证书主体(持有者)DN | X509_NAME_print_ex(..., X509_get_subject_name(...)) |
Issuer | 证书颁发者 DN | X509_NAME_print_ex(..., X509_get_issuer_name(...)) |
Version | X.509 版本号(十六进制) | X509_get_version() |
Serial Number | 证书序列号(十六进制) | X509_get_serialNumber() |
Signature Algorithm | 签名算法 OID | X509_get0_signature()/i2a_ASN1_OBJECT() |
Public Key Algorithm | 公钥算法 OID | X509_PUBKEY_get0_param() |
| X.509 v3 扩展 | 如Subject Alternative Name、Basic Constraints等 | X509V3_ext()(遍历X509_get0_extensions()) |
Start date | 生效时间 | ASN1_TIME_print(X509_get0_notBefore()) |
Expire date | 到期时间 | ASN1_TIME_print(X509_get0_notAfter()) |
| RSA/DSA/DH 公钥参数 | 如RSA Public Key:(2048 bits)、RSA Public-Key:及模数/指数 | pubkey_show()/print_pubkey_BN()宏 |
Signature | 签名值(十六进制,字节以:分隔) | 遍历ASN1_STRING_get0_data(psig) |
Cert | 该证书的完整 PEM 文本 | PEM_write_bio_X509() |
其中公钥参数的提取值得展开:OpenSSL 后端会根据EVP_PKEY_id(pubkey)的结果,对 RSA(get_pkey_rsa)、DSA(get_pkey_dsa)、DH(get_pkey_dh)分别调用pubkey_show(),将模数n、指数e等大整数以BN_print形式写入条目,例如:
RSA Public Key:(2048 bits) RSA Public-Key:(2048 bit) Modulus: 00:ab:cd:... Exponent:65537 (0x10001)注意,其它后端(GnuTLS、Schannel、Rustls、mbedTLS 等)产出的字段集合并不完全相同——这正是文档强调“items vary depending on the SSL backend and the certificate”的原因。编写依赖具体字段的代码时,应对未知字段保持容错。
五、支持的 TLS 后端与版本历史
根据文档头部的元信息与 HISTORY 章节:
- Added-in: 7.19.1——
CURLINFO_CERTINFO自 libcurl 7.19.1 起加入。 - 文档声明支持的后端:OpenSSL、GnuTLS、Schannel、Rustls。
- 历史补充:
- GnuTLS 支持自7.42.0加入;
- Schannel(Windows 原生 TLS)支持自7.50.0加入;
- mbedTLS 支持自8.9.0加入。
在源码中,lib/vtls/目录下openssl.c、gtls.c、schannel.c、rustls.c、mbedtls.c都实现了证书信息收集,且多个后端都做了证书数量上限保护(MAX_ALLOWED_CERT_AMOUNT)。以 OpenSSL 后端为例,openssl.c 会先获取对端证书链,若数量超过上限则返回CURLE_SSL_CONNECT_ERROR:
numcerts = sk_X509_num(sk); if(numcerts > MAX_ALLOWED_CERT_AMOUNT) { failf(data, "%d certificates is more than allowed (%d)", (int)numcerts, MAX_ALLOWED_CERT_AMOUNT); return CURLE_SSL_CONNECT_ERROR; }CURLOPT_CERTINFO的启用同样依赖后端能力:在 lib/setopt.c 中,若后端不支持SSLSUPP_CERTINFO,设置选项会返回CURLE_NOT_BUILT_IN。因此跨平台程序应先检查curl_easy_setopt的返回值,并对“不支持”的情况做降级处理。
六、底层实现:从握手到取回的数据流
理解CURLINFO_CERTINFO的内部数据流,有助于正确使用它。整条链路涉及三处关键代码:
1. 存储位置:data->info.certs
证书信息存放在每个 easy handle 的struct Curl_easy中。见 lib/urldata.h:
struct curl_certinfo certs; /* info about the certs. Asked for with */2. 收集:握手阶段由 TLS 后端填充
各 TLS 后端在握手完成后把证书信息逐条写入data->info.certs,其公共操作封装在 lib/vtls/vtls.c 中:
Curl_ssl_init_certinfo(data, num):按链中证书数量num分配struct curl_slist **数组,先释放旧数据(Curl_ssl_free_certinfo),再curlx_calloc分配数组并设置num_of_certs。见 vtls.c。Curl_ssl_push_certinfo_len(data, certnum, label, value, valuelen):把一条"label:value"文本追加到第certnum张证书对应的 slist 链表中。它内部用dynbuf拼接label、:与定长value,再通过Curl_slist_append_nodup挂到链表尾部;失败时释放整个链表并返回CURLE_OUT_OF_MEMORY。见 vtls.c。Curl_ssl_free_certinfo(data):遍历链表逐个curl_slist_free_all,再释放数组本身、重置num_of_certs = 0。见 vtls.c。
以 OpenSSL 后端为例,ossl_certchain()(openssl.c)的流程为:
SSL_get_peer_cert_chain(ssl)取对端证书链;- 检查数量上限后
Curl_ssl_init_certinfo分配数组; - 循环每张证书,用 BIO 内存缓冲配合
X509_NAME_print_ex、ASN1_INTEGER、ASN1_TIME_print等 OpenSSL API 格式化各字段,逐一push_certinfo写入; - 任一步出错则调用
Curl_ssl_free_certinfo清理残留数据并返回错误。
3. 取回:getinfo 返回内部指针
curl_easy_getinfo(curl, CURLINFO_CERTINFO, &ci)的处理位于 lib/getinfo.c 的getinfo_slist()分支:
case CURLINFO_CERTINFO: /* Return the a pointer to the certinfo struct. Not really an slist pointer but we can pretend it is here */ ptr.to_certinfo = &data->info.certs; *param_slistp = ptr.to_slist; break;可见它只是把data->info.certs的地址“伪装”成 slist 指针返回给调用者,没有发生数据拷贝。这带来两个重要结论:
- 返回的结构体由 libcurl 内部持有,生命周期与 easy handle 绑定,不要手动释放其中的链表;
- 在
curl_easy_cleanup之前完成读取;多次调用curl_easy_getinfo返回的是同一块内部数据。
七、在 curl 命令行工具中的使用:%{certs}变量
CURLINFO_CERTINFO不仅面向 C 程序,curl 命令行工具也通过它实现了-w(write-out)输出变量%{certs}。在 src/tool_writeout.c 中,工具会惰性获取证书信息:
static void certinfo(struct per_transfer *per) { if(!per->certinfo) { const struct curl_certinfo *certinfo; CURLcode result = curl_easy_getinfo(per->curl, CURLINFO_CERTINFO, &certinfo); per->certinfo = (!result && certinfo) ? certinfo : NULL; } }随后在输出%{certs}时(tool_writeout.c),它会遍历每张证书的 slist,跳过"cert:"前缀字段并把其余"name:content"条目逐行拼接。命令行用法示例:
curl -k --certinfo -w '\n%{certs}\n' -o /dev/null https://www.example.com/(--certinfo对应CURLOPT_CERTINFO选项;%{certs}的输出内容即curl_certinfo结构的文本化呈现。)注意:%{certs}需要CURLINFO_CERTINFO底层能力支持,且仅在 TLS 连接成功建立后才有数据。
八、测试与验证:证书链顺序检查
仓库的单元测试 tests/libtest/lib3102.c 展示了curl_certinfo的典型消费方式,也说明了字段格式的稳定性:它读取每张证书的Subject与Issuer条目,验证链中证书顺序是否正确(前一张证书的 Subject 应等于后一张证书的 Issuer):
static bool is_chain_in_order(struct curl_certinfo *cert_info) { const char *last_issuer = NULL; int cert; /* Chains with only a single certificate are always in order */ if(cert_info->num_of_certs <= 1) return TRUE; /* Enumerate each certificate in the chain */ for(cert = 0; cert < cert_info->num_of_certs; cert++) { const struct curl_slist *slist = cert_info->certinfo[cert]; const char *issuer = NULL; const char *subject = NULL; /* Find the certificate issuer and subject by enumerating each field */ for(; slist && (!issuer || !subject); slist = slist->next) { static const char issuer_prefix[] = "Issuer:"; static const char subject_prefix[] = "Subject:"; ... } } ... }该测试的写法可以借鉴为生产代码的通用模式:遍历 slist,用strncmp匹配"Issuer:"、"Subject:"等前缀来定位字段,而不是假设条目顺序。
九、返回值与错误处理
curl_easy_getinfo返回CURLcode,文档明确:
CURLE_OK (0):查询成功;- 非零:发生错误,具体含义参见 libcurl-errors 文档(如
CURLE_UNKNOWN_OPTION、内存分配失败等)。
同样,curl_easy_setopt(curl, CURLOPT_CERTINFO, 1L)的返回值也需要检查,尤其是当目标平台 TLS 后端不支持该特性时,会得到CURLE_NOT_BUILT_IN。健壮的程序应:
- 检查
CURLOPT_CERTINFO设置是否返回CURLE_OK; - 检查
curl_easy_perform返回CURLE_OK(握手成功)后再查询证书信息; - 查询后判空(
ci可能为NULL); - 读取时对字段前缀做容错匹配,不依赖固定条目顺序。
十、注意事项小结
- 必须先开
CURLOPT_CERTINFO再 perform,否则num_of_certs为 0、无数据可读;该选项默认关闭。 - 返回指针归 libcurl 所有:不要手动释放 slist,也不要跨过
curl_easy_cleanup之后继续使用。 - 字段内容与后端相关:OpenSSL/GnuTLS/Schannel/Rustls/mbedTLS 的字段集合不完全一致;文档与源码均强调这一点,解析代码须容错。
- 仅 TLS 协议有效:非 TLS(如纯 HTTP、FTP)连接不会有证书信息。
- 适用于审计类场景:证书链检查、指纹采集、证书有效期监控、链顺序校验(如 lib3102.c 所示)都是
CURLINFO_CERTINFO的典型应用;更底层的 SSL 句柄访问可参考CURLINFO_TLS_SSL_PTR系列接口。
【免费下载链接】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),仅供参考