编程语言

cURL error 60:PHP 证书校验失败的原因与修法

作者: · 最后核实

报错信息cURL error 60: SSL certificate problem: unable to get local issuer certificate

cURL error 60: SSL certificate problem 的意思是:PHP 的 curl 扩展连上了服务器,但无法验证对方的证书链。 在 Windows 和自己编译的 PHP 上,通常是 PHP 没配置 CA 证书包;在 Linux 服务器上,更多是对方服务器缺中间证书,或者对方用的是 PHP 不认识的私有根证书。

快速修复

多数情况给 php.ini 配上 CA 证书包就能解决。先在报错的同一个环境(网页还是命令行)里,查看 PHP 用的是哪个 CA 文件:

php --ini
php -i | grep -iE 'curl.cainfo|openssl.cafile|openssl.capath'
php -r 'print_r(openssl_get_cert_locations());'

如果 curl.cainfo 和 openssl.cafile 都是空的,默认文件也不存在,就在实际加载的 php.ini 里设置:

curl.cainfo = "/etc/ssl/certs/ca-certificates.crt"
openssl.cafile = "/etc/ssl/certs/ca-certificates.crt"

Windows 上填从 curl.se 下载的 cacert.pem 的路径。改完重启 PHP-FPM、Apache 或 IIS。如果证书包本来就没问题,用 SSL 检测工具检查对方服务器的证书链。

原因

Windows 和自编译 PHP 上最常见的是没配置 CA 证书包;Linux 服务器上更多是对方证书链不完整,或者对方用了私有根证书。

1. PHP 没有配置 CA 证书包

Windows 版 PHP、XAMPP、WampServer、phpStudy(小皮面板)以及很多自行编译的版本,自带的 libcurl 都没有可用的 CA 路径。

  • 确认: openssl_get_cert_locations() 指向的文件不存在,curl.cainfo 为空。
  • 修复: 设置 curl.cainfo(curl 扩展使用)和 openssl.cafile(file_get_contents('https://...') 等流函数使用),都指向有效的证书包。

2. 对方服务器证书链不完整

如果只有某一个网站或接口报错、其他都正常,多半是对方没装中间证书。对接一些小型第三方接口时并不少见。

  • 确认: openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts 只列出一张证书。
  • 修复: 需要对方服务器装上完整证书链,见缺少中间证书和证书链补全工具。可以把检测结果发给对方运维。

3. CA 证书包过旧

很久没更新的 cacert.pem 或者没打补丁的系统(如 CentOS 6/7)缺少较新的根证书,访问用新根证书的网站就会失败。

  • 修复: 更新系统的 ca-certificates 包,或重新下载 cacert.pem。

4. 公司代理或内网 CA

做 HTTPS 解密的公司代理、内部接口用的根证书都不在 Mozilla 列表里。

  • 修复: 把 PEM 格式的根证书追加到证书包副本里,让 curl.cainfo 指向副本,或者按请求单独指定:
$ch = curl_init('https://intranet.example.com/api');
curl_setopt($ch, CURLOPT_CAINFO, '/etc/ssl/certs/company-bundle.pem');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$body = curl_exec($ch);

5. 改错了 php.ini

CLI 和 FPM 常用不同的文件,例如 Debian、Ubuntu 上分别是 /etc/php/8.3/cli/php.ini 和 /etc/php/8.3/fpm/php.ini。

  • 修复: 以网页端 phpinfo() 里的“Loaded Configuration File”为准修改。

各环境修法

每个环境都要先找到实际生效的 php.ini 或客户端选项,再让它指向可信的 CA 证书包。

宝塔面板: PHP 的配置文件在 /www/server/php/<版本号>/etc/php.ini,例如 PHP 8.1 是 /www/server/php/81/etc/php.ini。可以在面板“软件商店 → PHP → 设置 → 配置修改”里搜索 curl.cainfo,填入 /etc/pki/tls/certs/ca-bundle.crt(CentOS 系)或 /etc/ssl/certs/ca-certificates.crt(Debian/Ubuntu 系),保存后重启 PHP。

Debian / Ubuntu:

sudo apt-get install --reinstall ca-certificates
sudo systemctl restart php8.3-fpm   # 换成你的 PHP 版本

CentOS / RHEL / Alibaba Cloud Linux: 证书包是 /etc/pki/tls/certs/ca-bundle.crt,私有根证书放进 /etc/pki/ca-trust/source/anchors/ 后执行 update-ca-trust。

Windows、XAMPP、WampServer、phpStudy: 下载 cacert.pem,在 Apache 实际使用的 php.ini 里设置两个参数,然后在控制面板里重启 Apache。

Docker: 确认镜像里装了 ca-certificates,私有根证书复制进镜像后再执行 update-ca-certificates。

框架: Guzzle、Laravel 用 'verify' => '/path/to/bundle.pem',Symfony HttpClient 用 cafile 选项。Composer 有自己的配置,见 Composer SSL operation failed。

WordPress: 核心的 HTTP 请求用的是 wp-includes/certificates/ 里自带的证书包,保持 WordPress 更新即可保持证书包最新;直接调用 curl 的插件则使用 PHP 的设置。

不要这样做

网上流传的几种绕法会关闭证书校验,或者引入来路不明的信任。

  • curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false)。 这是网上转载最多的“解决方法”,它关闭了证书链校验,任何网络上的攻击者都能冒充接口服务器,支付回调、用户数据都可能被篡改。
  • 把 CURLOPT_SSL_VERIFYHOST 设为 0。 关闭域名校验后,任意其他域名的有效证书都会被接受。
  • Guzzle 或 Laravel 里写 'verify' => false。 效果相同,而且经常是“临时测一下”后被带进了生产环境。
  • 使用来路不明的 cacert.pem。 只用 curl 项目官方发布的文件或系统包管理器提供的证书包。

常见问题

php.ini 改了还是报 60,为什么?

多半改错了文件。php --ini 显示的是命令行用的 php.ini,PHP-FPM 和 Apache 的 mod_php 通常加载另一份。在网页里输出 phpinfo(),看“Loaded Configuration File”是哪个文件,改它并重启 PHP-FPM 或 Apache。宝塔用户直接在“软件商店 → PHP → 设置 → 配置修改”里改,保存后重启该版本 PHP。

Windows 上的 cacert.pem 从哪里下载?

从 curl 官方的 CA extract 页面(curl.se/docs/caextract.html)下载,这是转换成 PEM 格式的 Mozilla 根证书列表。放到固定路径(比如 PHP 安装目录下),把 curl.cainfo 和 openssl.cafile 指向它,并定期更新。

同一台服务器上 curl 命令正常,PHP 却报错,怎么回事?

curl 命令和 PHP 的 curl 扩展可能链接的是不同的 TLS 库、默认 CA 路径不同,PHP 也可能跑在 chroot 或容器里。对比 curl -v 输出里的 CAfile: 和 php -r "print_r(openssl_get_cert_locations());" 的结果。

Guzzle 或 Laravel 里怎么指定 CA 文件?

给 verify 选项传路径:new GuzzleHttp\Client(['verify' => '/etc/ssl/certs/ca-certificates.crt']),Laravel 里用 Http::withOptions(['verify' => '/path/to/cacert.pem'])。这个值只应该是路径或 true,写成 false 就是关闭校验。

资料来源

  1. PHP manual: curl_setopt (CURLOPT_CAINFO, CURLOPT_SSL_VERIFYPEER)
  2. PHP manual: cURL runtime configuration (curl.cainfo)
  3. PHP manual: OpenSSL runtime configuration (openssl.cafile)
  4. curl: CA certificates extracted from Mozilla
  5. libcurl error codes