服务器证书被换了?用curl的公钥钉扎把这道防线焊死
【免费下载链接】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
HTTPS 默认信任"证书颁发机构",但如果 CA 被攻破或签发系统出问题,一张"合法"的假证书照样能拿到你的数据。curl 提供了一条更硬的底线:证书钉扎——在客户端预先记下服务器公钥的指纹,TLS 握手阶段一旦发现指纹对不上,curl 直接在收发任何数据之前就掐断连接。这条机制对应 libcurl 的CURLOPT_PINNEDPUBLICKEY选项和命令行的--pinnedpubkey参数,自 7.39.0 起可用,目前被 OpenSSL、GnuTLS、wolfSSL、mbedTLS、Schannel 五个 TLS 后端支持。
钉的是公钥,不是证书
先把一个容易混淆的点说清楚:很多人以为钉扎的对象是整张证书。不是。
TLS 握手时服务器会出示证书,curl 从证书里抽出公钥,再和你预存的指纹比对。这样做的好处是解耦:
- 证书可以到期、续期、换 CA,只要密钥对不变,钉扎就继续有效;
- 攻击者即使用同一域名骗到一张新证书,只要公钥不是当初那把,照样被拒。
类比一下:证书核验像核对"身份证",身份证会换发、可能被伪造补办;公钥钉扎更像比对"指纹",指纹换不了。
命令行的入口文档在 docs/cmdline-opts/pinnedpubkey.md,库选项的完整说明在 docs/libcurl/opts/CURLOPT_PINNEDPUBLICKEY.md。
三种钉法:一个哈希、一串哈希、一个文件
--pinnedpubkey接受两类输入,用分号还能并列多个:
- 单个 SHA256 指纹:
sha256//前缀加一段 base64 编码的公钥摘要curl --pinnedpubkey 'sha256//ce118b51897f4452dc' https://example.com - 多个指纹并列:服务器轮换密钥时,把新旧两把都列上,避免"换钥即断线"
curl --pinnedpubkey 'sha256//AAA=;sha256//BBB=' https://example.com - 公钥文件:直接传一个 PEM 或 DER 格式的公钥文件路径
curl --pinnedpubkey /etc/keys/server.pub https://example.com
在程序里用 libcurl,只需多设一个选项:
curl_easy_setopt(curl, CURLOPT_PINNEDPUBLICKEY, "sha256//AAA=;sha256//BBB=");多个指纹是"任一命中即放行"的关系,所以第 2 种写法是密钥轮换期的标准姿势。
源码里发生了什么
整条校验链的公共入口是Curl_pin_peer_pubkey,位于 lib/vtls/vtls.c,各 TLS 后端在握手完成后各自把证书公钥交进来,然后走同一段逻辑:
| 步骤 | 做的事 | 不通过时 |
|---|---|---|
| 1 | 判断输入是不是sha256//开头 | 不是则改走文件比对分支 |
| 2 | 对服务器公钥算 SHA256,再做 base64 编码 | 后端缺 SHA256 能力直接失败 |
| 3 | 逐个切分预置指纹串,做逐字节精确比较(长度+memcmp) | 全部不中 |
| 4 | 任一命中即返回成功 | 返回错误码 90CURLE_SSL_PINNEDPUBKEYNOTMATCH |
几个值得注意的工程细节:
- 走文件分支时,curl 先
ftell文件大小,公钥长度大于文件直接判不匹配,不浪费 IO; - 摘要比较用的是
memcmp,不做模糊容错——指纹错一个字符就是失败; - 失败时的错误信息由 lib/strerror.c 统一给出文案,方便上层程序向用户透传。
OpenSSL 后端的调用点在 lib/vtls/openssl.c 的ossl_pkp_pin_peer_pubkey里,GnuTLS、Schannel、mbedTLS、wolfSSL 各自在对应文件中实现同一条契约,接口原型声明在 lib/vtls/vtls.h。
三个最容易踩的坑
坑一:钉扎不依赖证书验证开关。--pinnedpubkey和--insecure(即CURLOPT_SSL_VERIFYPEER关闭)互相独立。即使你为了跳过证书校验关掉了验证,公钥比对依然会执行——这是设计出来的"最后防线",而不是被--insecure一起禁用的可选项。
坑二:重定向到新域名时不查。钉扎只校验传输的初始源站。如果请求跟随了跳转到别的域名的重定向,新域名不会拿这把指纹去验。如果你的流程涉及跨域重定向,需要单独评估。
坑三:LDAP 有个例外。使用传统 LDAP 后端时,TLS 不走 curl 自己的 TLS 层,钉扎对它无效;用 OpenLDAP 后端编译的 libcurl 才会走统一层并生效。
另外记一下后端支持的时间线,排查"为什么我这版本不生效"时很有用:
- PEM/DER 文件:7.39.0(OpenSSL、GnuTLS)→ 7.43.0(wolfSSL)→ 7.47.0(mbedTLS)→ 7.58.1(Schannel)
- sha256 指纹:7.44.0(OpenSSL、GnuTLS、wolfSSL)→ 7.47.0 → 7.58.1
手上没有公钥?用 openssl 现算指纹
官方文档给出的最短路径:先拿证书,再抽公钥,转 DER,最后算摘要。一条链子跑完:
# 1. 抓服务器证书(记得肉眼核对是不是预期的那张) openssl s_client -servername www.example.com -connect www.example.com:443 \ < /dev/null | sed -n "/-----BEGIN/,/-----END/p" > www.example.com.pem # 2. 从证书抽出公钥 openssl x509 -in www.example.com.pem -pubkey -noout > server.pubkey.pem # 3. PEM 转 DER openssl asn1parse -noout -inform pem -in server.pubkey.pem -out server.pubkey.der # 4. 算 sha256 并 base64,拼出可粘贴的指纹 openssl dgst -sha256 -binary server.pubkey.der | openssl base64把第 4 步输出的字符串前面加上sha256//,就是--pinnedpubkey要的值。注意第 1 步"肉眼核对证书"不是客套话——如果抓证书时你正被中间人,后面算出来的指纹本身就是错的。
上线前自查清单
- 预置当前 + 备用两把指纹,密钥轮换时先加新、后删旧,中间不断连
- 指纹获取全程在受控网络上执行,并核对证书主体与预期一致
- 程序捕获错误码 90(
CURLE_SSL_PINNEDPUBKEYNOTMATCH)并给用户明确的失败原因,而不是笼统的"网络错误" - 跨域重定向、LDAP 旧后端等特殊路径单独评估
- 把"重新获取指纹"的动作纳入证书/密钥轮换的运维流程,写进 checklist
curl 的公钥钉扎不是替代 CA 校验,而是叠在 CA 校验之上的一道独立检查:证书可以换,公钥必须是你认识的那把。两者同时在线,中间人手里至少要多一把你无法伪造的东西。图示:证书公钥提取与 SHA256 指纹比对流程(服务器出示证书 → 抽取公钥 → 摘要比对 → 命中放行/不命中返回错误码 90)
【免费下载链接】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),仅供参考