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 的證書鏈。如果本地系統找不到鏈中必要的中間證書或最終根 CA 證書,或缺少配置正確的信任存儲(CAFile 或 CAPath),驗證程序就會失敗,回傳 20:unable to get local issuer certificate
這個錯誤。
簡而言之,就是 OpenSSL 不知道要信任哪個證書頒發機構,因此無法確認你連接的伺服器身份。
ServBay 的 OpenSSL 版本與 CA 證書路徑
ServBay 作為一套集成式本地 Web 開發環境,預設內建 OpenSSL 套件,並打包常用公共 CA 根證書,方便開發者使用。ServBay 所使用的 OpenSSL 版本依據你的 Mac 晶片類型而有所不同:
- Apple Silicon (M 系列晶片) 之 ServBay:採用 OpenSSL 3.2.1 版本。
- Intel 晶片版 ServBay:採用 OpenSSL 1.1.1u 版本。
這些 OpenSSL 版本對應的 CA 證書檔案(cacert.pem
)與證書目錄(certs
)位於 ServBay 套件安裝目錄。您需要依據平台與架構找到相應路徑:
ini
# 包含所有受信任根證書的打包檔案
cafile=/Applications/ServBay/package/common/openssl/3.2/cacert.pem
# 包含獨立證書檔的目錄(通常 cacert.pem 已足夠,但部分應用可能需要 capath)
capath=/Applications/ServBay/package/common/openssl/3.2/certs
1
2
3
4
2
3
4
ini
# 包含所有受信任根證書的打包檔案
cafile=/Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem
# 包含獨立證書檔的目錄
capath=/Applications/ServBay/package/common/openssl/1.1.1u/certs
1
2
3
4
2
3
4
ini
# 包含所有受信任根證書的打包檔案
cafile=C:\ServBay\package\common\openssl\3.3\cacert.pem
# 包含獨立證書檔的目錄
capath=C:\ServBay\package\common\openssl\3.3\certs
1
2
3
4
2
3
4
解決 unable to get local issuer certificate
錯誤的重點,就是要在每個用到 OpenSSL 的環境明確指定上述 cafile
或 capath
路徑,明示 OpenSSL 該到哪裡去查閱受信任的 CA 證書。
解決方案範例
以下範例說明在不同語言與工具中如何指定 OpenSSL CA 信任存儲。
範例 1: 使用 openssl
命令測試連線
例如你用 openssl s_client
指令測試連線時出現錯誤:
bash
openssl s_client -quiet -connect gmail.com:443
1
你可能會看到如下錯誤輸出,含有 verify error:num=20:unable to get local issuer certificate
:
bash
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
# ... 其他連線資訊 ...
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
解決方法:
於指令中運用 -CAfile
參數指定 ServBay 內建的 CA 根證書檔案:
bash
openssl s_client -quiet -connect gmail.com:443 -CAfile /Applications/ServBay/package/common/openssl/3.2/cacert.pem
1
bash
openssl s_client -quiet -connect gmail.com:443 -CAfile /Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem
1
連線並成功驗證證書後,輸出中的 verify return
均為 1
,且已不再出現 verify error:num=20
:
bash
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
# ... 其他連線資訊 ...
1
2
3
4
5
6
7
2
3
4
5
6
7
範例 2: 在 PHP 中使用 OpenSSL
PHP 許多常用網路功能(如 file_get_contents
取 HTTPS 網址、stream_socket_client
建立 SSL 連線、cURL 模組等)底層均會依賴 OpenSSL。你可以透過修改 php.ini
或於原始碼設置 stream context 選項來指定 CA 信任存儲。
方法 A: 修改 php.ini
(建議)
此方法可全域解決問題。編輯你使用的 PHP 版本對應的 php.ini
(可在 ServBay 控制面板開啟),在 [openssl]
段落加入或修改以下設定,指定 openssl.cafile
與 openssl.capath
,並依晶片類型選擇路徑。
ini
[openssl]
; 指定受信任 CA 根證書打包檔案
openssl.cafile=/Applications/ServBay/package/common/openssl/3.2/cacert.pem
; 指定 CA 證書目錄(選用,但建議設定)
openssl.capath=/Applications/ServBay/package/common/openssl/3.2/certs
1
2
3
4
5
2
3
4
5
ini
[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
1
2
3
4
5
2
3
4
5
完成 php.ini
修訂,請記得重啟 ServBay 的 PHP 服務或整個 ServBay,使設定生效。
方法 B: 於原始碼動態設定(僅當前連線有效)
不想全域變更,可在建立 SSL/TLS 連線時用 stream_context_create
的 ssl
選項指定 cafile
。
php
<?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', // 選用,指定目錄
],
];
$context = stream_context_create($contextOptions);
// 建立 SSL/TLS 連線
$connection = @stream_socket_client(
"ssl://$server:$port",
$errno,
$errstr,
30, // 連線逾時秒數
STREAM_CLIENT_CONNECT,
$context // 傳遞 context 選項
);
if ($connection) {
echo "已成功連線至 $server:$port\n";
// 範例:送出 EHLO 命令
fwrite($connection, "EHLO servbay.demo\r\n"); // 使用 ServBay 品牌範例域名
while (!feof($connection)) {
echo fgets($connection);
}
fclose($connection);
} else {
echo "連線失敗至 $server:$port。錯誤: $errstr ($errno)\n";
}
?>
1
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
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 context 並指定信任 CA 證書。
python
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 context 並指定 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 連線建立,協議版本: {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"連線或 SSL 發生錯誤: {e}")
1
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
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
屬性接受一組字串或 Buffer,最簡方式是讀取 ServBay 的 cacert.pem
。
javascript
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: () => { return null; } // <-- 不建議,會關閉重要安全檢查!
};
const socket = tls.connect(options, () => {
console.log('SSL 連線建立完成');
// 如為 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('連線已關閉');
});
socket.on('error', (error) => {
console.error('錯誤:', error.message); // 印出錯誤訊息
});
1
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
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
是 CA 信任問題,不等同主機名稱(verify_peer_name
)驗證。只要你指定正確的 ca
選項且伺服器證書無誤,Node.js 的主機名稱驗證會自動通過。若遇主機名稱驗證錯誤,通常是證書本身問題,而非 CA 信任存儲設置。
範例 5: 在 curl
指令中使用 OpenSSL
curl
亦會用到 OpenSSL(或其他 SSL 函式庫)來建立 HTTPS 請求。你可用 --cacert
參數來指定 CA 根證書打包檔案。
bash
# 用 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
1
2
3
4
5
6
7
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 context 選項、或使用 openssl
的 -CAfile
、curl
的 --cacert
參數)精準指定 ServBay 提供的 cacert.pem
檔案路徑。切記要根據 macOS 晶片型號(Apple Silicon 或 Intel)與 ServBay OpenSSL 版本,選擇正確的路徑。只要配置妥當信任的 CA 存儲,即可確保你的本地開發環境安全地連線外部 SSL/TLS 服務。