编程

如何修复 Windows 上 PHP Curl HTTPS 证书授权问题

74 2024-05-09 23:16:00

成功的 HTTPS 请求涉及 HTTP 客户端根据已知和受信任的根证书列表验证服务器提供的 TLS证书。PHP Curl 扩展也一样;Curl 扩展使用 libcurl 来发出 HTTPS 请求,而 libcurl 又使用 TLS 库(如 OpenSSL)来验证请求。
Curl 扩展需要一个包含所有受信任根证书的有效文件来完成 HTTPS 验证,PHP在 php.ini 文件中将其作为指令暴露。
在 Linux、BSD 和 macOS 上,libcurl 可以默认为系统根证书,但这在 Windows 上是不可能的,因为 Windows 没有包含所有系统根证书的单个文件。
本文讨论了使用 Curl 扩展成功发出 HTTPS 请求的两种可能方法,不这样做会使 HTTPS 请求不安全。

为何失败

$ch = curl_init('https://php.watch');  
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);  
curl_exec($ch); // false  

curl_error($ch);
// SSL certificate problem: unable to get local issuer certificate

如果 curl_exec 调用失败并返回错误响应,并且 curl_error 发出 SSL certificate problem: unable to get local issuer certificate 错误,则意味着没有向 curl 提供包含根证书的文件,或者无法发现该文件。
这个错误在 Linux、BSD 和 macOS 系统上并不常见,但在 Windows 上很常见,因为没有指定的文件来获取根证书,而且 PHP 本身也没有根证书列表。
解决方案是提供一个包含最新根证书的文件,或者理想情况下,让 Curl 解析底层操作系统提供的本机根存储。

使用本机证书颁发机构

Curl 7.71 及更高版本上,可以设置一个 Curl 选项来请求 Curl 使用本机(系统)根证书。这甚至适用于 Windows,在 Windows 中,Curl 解析系统根证书并使用它们。
CURLOPT_SSL_OPTIONS 选项设置为 CURLSSLOPT_NATIVE_CA 或包含这些位的位掩码时,Curl 将尝试使用本机根证书存储,但受底层 TLS 库的功能和版本的限制。
如果 Curl 扩展是用 Curl 7.71 或更高版本和 PHP 8.2 及更高版本构建的,则建议使用此修复程序。

  $ch = curl_init('https://php.watch');
  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://php.watch');  
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 证书

  1. 下载 cacert.pem 文件
  2. 将文件移动到 PHP 和 web 服务器可访问的目录中。例如,移动到 C:/php/cacert.pem
  3. 编辑 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"
  1. 重启 Web 服务器(比如 Apache)以重载 INI 文件。

这种方法的缺点是必须定期更新 cacert.pem 文件。例如,Curl 项目提供的 cacert.pem 文件是从 Mozilla 维护的根存储中提取的。该列表和文件平均每年更新 4-5 次。为确保与最新的根证书列表兼容,请确保定期更新此文件的本地副本。
如果无法修改 INI文件,请在 Curl 请求中指定 cacert.pem 文件的绝对路径:

  $ch = curl_init('https://php.watch');
  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://php.watch');
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);

 

PHP