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 版本找到對應路徑:
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
解決 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:於程式中指定設定(僅適用於當前連線)
如果不想動用全域 php.ini
,可在用 stream_context_create
建立 SSL/TLS 連線上下文時,透過 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', // 可選,CA 憑證目錄
],
];
$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 憑證。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。
// 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
關乎信任根憑證問題,和主機名稱驗證(verify_peer_name
)並不相同。只要正確指定 ca
並確保伺服器憑證無誤,Node.js 預設的主機名稱驗證即可順利通過。如果出現主機名稱驗證錯誤,多半是憑證自身問題,而非 CA 信任設定問題。
範例 5:於 curl
指令使用 OpenSSL
curl
指令執行 HTTPS 時亦使用 OpenSSL(或其他 SSL 函式庫)。你可用 --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 服務連線。