成功的 HTTPS 请求涉及 HTTP 客户端根据已知且受信任的根证书列表来验证服务器提供的 TLS 证书。PHP Curl 扩展也不例外;Curl 扩展使用 libcurl 发起 HTTPS 请求,而 libcurl 进而使用 OpenSSL 等 TLS 库来验证请求。
Curl 扩展需要一个包含所有受信任根证书的有效文件来完成 HTTPS 验证,PHP 将此作为 php.ini 文件中的一个指令暴露出来。
在 Linux、BSD 和 macOS 上,libcurl 可以默认使用系统根证书,但在 Windows 上这是不可能的,因为 Windows 没有提供一个包含所有系统根证书的单一文件。
本文讨论了在使用 Curl 扩展成功发起 HTTPS 请求时的两种可能方法,以及哪些不应该做,以免导致 HTTPS 请求不安全。
为什么失败
$ch = curl_init('https://www.ggdoc.cn');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_exec($ch); // false
curl_error($ch);
// SSL certificate problem: unable to get local issuer certificate如果 curl_exec 调用返回 false,并且 curl_error 指示出现 SSL certificate problem: unable to get local issuer certificate 错误,这意味着未向 Curl 提供包含根证书的文件,或者它无法发现该文件。
此错误在 Linux、BSD 和 macOS 系统中并不常见,但在 Windows 上非常常见,因为没有指定的文件来获取根证书,而且 PHP 本身也不附带根证书列表。
解决方案是提供一个包含最新根证书的文件,或者理想情况下,让 Curl 解析底层操作系统提供的原生根存储。
使用原生证书颁发机构
在 Curl 7.71 及更高版本中,可以设置一个 Curl 选项以请求 Curl 使用原生(系统)根证书。这甚至在 Windows 上也有效,其中 Curl 会解析系统根证书并使用它们。
当 CURLOPT_SSL_OPTIONS 选项设置为 CURLSSLOPT_NATIVE_CA 或包含这些位的掩码时,Curl 尝试使用原生根证书存储,具体取决于底层 TLS 库的功能和版本。
这是推荐的修复方案,如果 Curl 扩展是使用 Curl 7.71 或更高版本以及 PHP 8.2 或更高版本构建的。
$ch = curl_init('https://www.ggdoc.cn');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
curl_exec($ch);请注意,上面的代码片段没有检查 Curl 版本和 PHP 版本,并假设满足 PHP 和 Curl 的版本要求。以下是一个示例,展示了如何有条件地添加 Curl 选项:
$ch = curl_init('https://www.ggdoc.cn');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
if (defined('CURLSSLOPT_NATIVE_CA')
&& version_compare(curl_version()['version'], '7.71', '>=')) {
curl_setopt($ch, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
}
curl_exec($ch);下载并维护 cacert.pem 文件
对于运行在低于 8.2 版本的 PHP 上的应用程序(其中 CURLSSLOPT_NATIVE_CA 常量不可用),或者当 Curl 版本低于 7.71 时,推荐的替代解决方案是下载兼容 Curl 的根证书文件,并配置 PHP 或 Curl 请求以使用它。
Curl 项目维护着最新的证书列表。参见从 Mozilla 提取的 CA 证书。
- 下载
cacert.pem文件 - 将文件移动到 PHP 和 Web 服务器可访问的目录。例如,移动到
C:/php/cacert.pem。 - 编辑
php.ini文件并修改curl.cainfo条目,使其指向cacert.pem文件的绝对路径。[curl] ; A default value for the CURLOPT_CAINFO option. This is required to be an ; absolute path. -;curl.cainfo = +curl.cainfo = "C:/php/cacert.pem" - 可选地,重启 Web 服务器(如 Apache)以重新加载 INI 文件。
这种方法的缺点是必须定期更新 cacert.pem 文件。例如,由 Curl 项目提供的 cacert.pem 文件是从 Mozilla 维护的根存储中提取的。平均而言,此列表和文件每年更新 4-5 次。为了确保与最新的根证书列表兼容,请务必定期更新此文件的本地副本。
如果无法修改 INI 文件,也可以在 Curl 请求中指定 cacert.pem 文件的绝对路径:
$ch = curl_init('https://www.ggdoc.cn');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_CAINFO, 'C:/php/cacert.pem');
curl_exec($ch);在带有 Curl 7.77 的 PHP 8.2+ 上,可以使用 CURLOPT_CAINFO_BLOB 选项传入包含 cacert.pem 内容的字符串。
切勿禁用证书验证
在互联网论坛和文章中发现的一个常见的错误建议是禁用证书验证。这是一个安全问题,因为如果没有证书验证,Curl 将乐意接受任何 TLS 证书,包括可能被拦截或修改的内容。
以下示例展示了一个经常被提出的解决方案,它是不安全且不推荐的。
$ch = curl_init('https://www.ggdoc.cn');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Disables peer certificate validation.
// DO NOT DO THIS!!!
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_exec($ch);