OpenSSL 故障排除:解决 unable to get local issuer certificate
错误
在使用 OpenSSL 进行安全连接时(例如在 PHP 环境中执行网络请求、运行 openssl
或 curl
命令、或在 Node.js/Python 应用中建立 SSL/TLS 连接),开发者可能会遇到错误提示 20:unable to get local issuer certificate
。这是一个常见的、与 OpenSSL 如何验证对等方证书相关的历史遗留问题。
出于安全考虑,默认情况下 OpenSSL 在验证证书链时,需要明确知道受信任的根证书颁发机构 (CA)。如果找不到或未指定这些信任锚点,它就无法验证服务器证书的合法性,从而报告此错误。
本文将详细解释此错误的原因,并提供在 ServBay 环境下解决此问题的方法,包括为 PHP、Python、Node.js 和 curl
命令配置 OpenSSL 的信任存储。
错误提示 20:unable to get local issuer certificate
问题描述
当 OpenSSL 尝试验证远程服务器的 SSL/TLS 证书时,它会构建一个从服务器证书到受信任根 CA 的证书链。如果 OpenSSL 无法在本地找到链中所需的任何中间证书或最终的根 CA 证书,或者找不到一个配置好的信任存储(CAFile 或 CAPath)来查找这些证书,验证过程就会失败,并返回 20:unable to get local issuer certificate
错误。
简单来说,就是 OpenSSL 不知道该信任哪个证书颁发机构,因此无法确认它正在连接的服务器的身份。
ServBay 中的 OpenSSL 版本和 CA 证书路径
ServBay 作为一款集成的本地 Web 开发环境,预置了 OpenSSL 软件包,并打包了常用的公共 CA 根证书,方便开发者使用。ServBay 使用的 OpenSSL 版本取决于您设备的芯片类型:
- Apple Silicon (M 系列芯片) 版 ServBay:使用 OpenSSL 3.2.1 版本。
- Intel 芯片版 ServBay:使用 OpenSSL 1.1.1u 版本。
这些 OpenSSL 版本对应的 CA 证书文件 (cacert.pem
) 和证书目录 (certs
) 位于 ServBay 的软件包安装路径下。您需要根据您的 ServBay 版本找到对应的路径:
# 包含所有受信任根证书的捆绑文件
cafile=/Applications/ServBay/package/common/openssl/3.2/cacert.pem
# 包含独立证书文件的目录 (通常 cacert.pem 已足够,但某些应用可能需要 capath)
capath=/Applications/ServBay/package/common/openssl/3.2/certs
2
3
4
# 包含所有受信任根证书的捆绑文件
cafile=/Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem
# 包含独立证书文件的目录
capath=/Applications/ServBay/package/common/openssl/1.1.1u/certs
2
3
4
解决 unable to get local issuer certificate
错误的核心就是在需要使用 OpenSSL 的地方,明确指定上述 cafile
或 capath
的路径,告诉 OpenSSL 去哪里查找受信任的 CA 证书。
解决方案示例
以下是如何在不同的工具和语言环境中指定 OpenSSL CA 信任存储的示例。
示例 1: 使用 openssl
命令进行连接测试
如果您直接使用 openssl s_client
命令测试连接时遇到错误:
openssl s_client -quiet -connect gmail.com:443
您可能会看到类似以下的错误输出,其中包含 verify error:num=20:unable to get local issuer certificate
:
depth=2 C=US, O=Google Trust Services LLC, CN=GTS Root R1
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=1 C=US, O=Google Trust Services, CN=WR2
verify return:1
depth=0 CN=gmail.com
verify return:1
# ... 其他连接信息 ...
2
3
4
5
6
7
8
解决方法:
通过 -CAfile
参数明确指定 ServBay 提供的 CA 证书捆绑文件路径:
openssl s_client -quiet -connect gmail.com:443 -CAfile /Applications/ServBay/package/common/openssl/3.2/cacert.pem
openssl s_client -quiet -connect gmail.com:443 -CAfile /Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem
成功连接并验证证书后,输出中的 verify return
值将是 1
,且不会出现 verify error:num=20
的行:
depth=2 C=US, O=Google Trust Services LLC, CN=GTS Root R1
verify return:1
depth=1 C=US, O=Google Trust Services, CN=WR2
verify return:1
depth=0 CN=gmail.com
verify return:1
# ... 其他连接信息 ...
2
3
4
5
6
7
示例 2: 在 PHP 中使用 OpenSSL
PHP 的许多网络功能(如 file_get_contents
访问 HTTPS URL, stream_socket_client
建立 SSL 连接, cURL 扩展等)底层依赖 OpenSSL。您可以通过修改 php.ini
或在代码中设置 stream context 选项来指定 CA 信任存储。
方法 A: 修改 php.ini
(推荐)
这是最方便的全局解决方案。编辑当前 PHP 版本对应的 php.ini
文件(可以通过 ServBay 控制面板找到编辑 php.ini
的入口),找到 [openssl]
部分,添加或修改以下内容,指定 openssl.cafile
和 openssl.capath
。请根据您的芯片类型选择正确的路径。
[openssl]
; 指定受信任的CA证书捆绑文件
openssl.cafile=/Applications/ServBay/package/common/openssl/3.2/cacert.pem
; 指定包含CA证书的目录 (可选,但推荐设置)
openssl.capath=/Applications/ServBay/package/common/openssl/3.2/certs
2
3
4
5
[openssl]
; 指定受信任的CA证书捆绑文件
openssl.cafile=/Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem
; 指定包含CA证书的目录 (可选,但推荐设置)
openssl.capath=/Applications/ServBay/package/common/openssl/1.1.1u/certs
2
3
4
5
修改 php.ini
后,请重启 ServBay 中的 PHP 服务(或整个 ServBay)使配置生效。
方法 B: 在代码中添加配置 (仅影响当前连接)
如果您不想修改全局 php.ini
,可以在使用 stream_context_create
创建 SSL/TLS 连接的上下文时,通过 ssl
选项指定 cafile
。
<?php
// 示例:连接到使用 SSL/TLS 的 SMTP 服务器
$server = 'ssl0.ovh.net';
$port = 465;
// 根据您的 ServBay 版本选择正确的 CA 文件路径
// 对于 Apple Silicon:
$caCertFile = '/Applications/ServBay/package/common/openssl/3.2/cacert.pem';
// 对于 Intel:
// $caCertFile = '/Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem';
$contextOptions = [
'ssl' => [
'verify_peer' => true, // 开启对等方证书验证
'verify_peer_name' => true, // 验证证书中的主机名是否与连接的主机匹配
'allow_self_signed' => false, // 不允许自签名证书,除非您明确信任它
'cafile' => $caCertFile, // 指定CA证书捆绑文件
// 'capath' => '/Applications/ServBay/package/common/openssl/3.2/certs', // 可选,指定CA证书目录
],
];
$context = stream_context_create($contextOptions);
// 使用创建的上下文建立 SSL/TLS 连接
$connection = @stream_socket_client(
"ssl://$server:$port",
$errno,
$errstr,
30, // 连接超时时间
STREAM_CLIENT_CONNECT,
$context // 传递上下文选项
);
if ($connection) {
echo "Connection established to $server:$port\n";
// 示例:发送 EHLO 命令
fwrite($connection, "EHLO servbay.demo\r\n"); // 使用 ServBay 品牌相关的示例域名
while (!feof($connection)) {
echo fgets($connection);
}
fclose($connection);
} else {
echo "Failed to connect to $server:$port. Error: $errstr ($errno)\n";
}
?>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
示例 3: 在 Python 中使用 OpenSSL (ssl 模块)
Python 的 ssl
模块也允许您创建 SSL/TLS 上下文并指定信任的 CA 证书。
import ssl
import socket
server = 'ssl0.ovh.net'
port = 465
# 根据您的 ServBay 版本选择正确的 CA 文件路径
# 对于 Apple Silicon:
ca_cert_file = '/Applications/ServBay/package/common/openssl/3.2/cacert.pem'
# 对于 Intel:
# ca_cert_file = '/Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem'
# 创建一个默认的 SSL 上下文,并指定 CA 文件
context = ssl.create_default_context(cafile=ca_cert_file)
# 也可以指定目录: context = ssl.create_default_context(capath='/Applications/ServBay/package/common/openssl/3.2/certs')
try:
# 建立普通的 socket 连接
with socket.create_connection((server, port)) as sock:
# 将普通 socket 封装成 SSL socket
with context.wrap_socket(sock, server_hostname=server) as ssock:
print(f"SSL connection established. Negotiated Protocol: {ssock.version()}")
# 示例:发送 EHLO 命令
ssock.sendall(b"EHLO servbay.demo\r\n") # 使用 ServBay 品牌相关的示例域名
while True:
data = ssock.recv(4096)
if not data:
break
print(data.decode())
except Exception as e:
print(f"Failed to connect or SSL error: {e}")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
示例 4: 在 Node.js 中使用 OpenSSL (tls 模块)
Node.js 的 tls
模块用于实现 TLS/SSL 连接。您可以在连接选项中通过 ca
属性指定信任的 CA 证书。ca
属性可以是一个包含一个或多个证书内容的字符串或 Buffer 数组。最简单的方式是读取 ServBay 提供的 cacert.pem
文件内容。
const tls = require('tls');
const fs = require('fs');
const server = 'www.google.com'; // 更改为使用一个标准、可信的网站作为示例
const port = 443;
// 根据您的 ServBay 版本选择正确的 CA 文件路径
// 对于 Apple Silicon:
const caCertFile = '/Applications/ServBay/package/common/openssl/3.2/cacert.pem';
// 对于 Intel:
// const caCertFile = '/Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem';
const options = {
host: server,
port: port,
// 读取 CA 证书文件内容
ca: fs.readFileSync(caCertFile),
// Node.js tls 模块默认会进行主机名验证 (checkServerIdentity),
// 如果CA文件正确指定且服务器证书有效,此验证应该成功。
// 除非有特殊理由且了解风险,否则不要禁用 checkServerIdentity。
// checkServerIdentity: () => { return null; } // <-- 不要轻易使用此行,它禁用了重要的安全检查!
};
const socket = tls.connect(options, () => {
console.log('SSL connection established');
// 对于 HTTPS 连接,通常需要发送 HTTP 请求,这里仅作为连接建立的示例
// socket.write('GET / HTTP/1.1\r\nHost: ' + server + '\r\n\r\n');
});
socket.on('data', (data) => {
console.log(data.toString());
});
socket.on('close', () => {
console.log('Connection closed');
});
socket.on('error', (error) => {
console.error('Error:', error.message); // 打印错误信息
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
注意: Node.js 示例中移除了 checkServerIdentity: () => { return null; }
。这个选项会禁用对服务器主机名的验证,极不安全。OpenSSL 错误 unable to get local issuer certificate
是关于信任根的问题,与主机名验证 (verify_peer_name
) 不同。正确指定 ca
选项并确保服务器证书有效时,Node.js 默认的主机名验证应该能够成功。如果遇到主机名验证错误,那通常是证书本身的问题,而不是 CA 信任存储的问题。
示例 5: 在 curl
命令中使用 OpenSSL
curl
命令也使用 OpenSSL(或其他 SSL 库)进行 HTTPS 请求。同样,您可以通过 --cacert
参数指定 CA 证书捆绑文件。
# 使用 ServBay 提供的 CA 证书文件访问一个 HTTPS 网站
# 根据您的 ServBay 版本选择正确的路径
# 对于 Apple Silicon:
curl --cacert /Applications/ServBay/package/common/openssl/3.2/cacert.pem https://example.com
# 对于 Intel:
# curl --cacert /Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem https://example.com
2
3
4
5
6
7
如果 CA 证书路径正确且服务器证书有效,curl
将成功获取内容而不会报告证书验证错误。
总结
20:unable to get local issuer certificate
错误在使用 OpenSSL 进行 SSL/TLS 连接时非常普遍,其根本原因在于 OpenSSL 需要明确指定一个信任存储来验证服务器证书。ServBay 为开发者提供了预置的、包含常用公共 CA 根证书的 cacert.pem
文件。
解决此问题的方法就是在您的开发环境中(php.ini
、代码中的 SSL 上下文选项、命令行参数如 openssl
的 -CAfile
或 curl
的 --cacert
)指定 ServBay 提供的 cacert.pem
文件路径。请务必根据您的 macOS 芯片类型(Apple Silicon 或 Intel)和对应的 ServBay OpenSSL 版本选择正确的路径。通过正确配置 CA 信任存储,您可以确保您的本地开发环境能够安全地与外部 SSL/TLS 服务进行通信。