การแก้ไขปัญหา OpenSSL: ปลดล็อกข้อผิดพลาด unable to get local issuer certificate
เมื่อสร้างการเชื่อมต่ออย่างปลอดภัยด้วย OpenSSL (เช่น การร้องขอผ่าน PHP, การเรียกใช้ openssl หรือ curl, หรือการสร้างเชื่อมต่อ SSL/TLS ใน Node.js/Python) นักพัฒนาหลายคนอาจพบข้อความผิดพลาด 20:unable to get local issuer certificate ซึ่งเป็นปัญหาที่พบได้บ่อยและเกี่ยวข้องกับขั้นตอนการตรวจสอบใบรับรองของ OpenSSL
ตามหลักความปลอดภัย OpenSSL จะต้องรับรองว่าใบรับรองที่เครื่องได้รับนั้นเกิดจาก root certificate authority (CA) ที่เชื่อถือได้ โดยค่าตั้งต้นถ้าไม่มีข้อมูล CA ที่เชื่อถือได้ หรือมีการตั้งค่าผิดพลาด OpenSSL จะไม่สามารถตรวจสอบใบรับรองเซิร์ฟเวอร์และแสดงข้อผิดพลาดนี้
บทความนี้จะอธิบายสาเหตุของข้อผิดพลาด พร้อมเสนอวิธีแก้ไขสำหรับการใช้งานใน ServBay รวมทั้งตัวอย่างการตั้งค่า CA สำหรับ PHP, Python, Node.js และการใช้งานผ่านคำสั่ง curl
ข้อผิดพลาด 20:unable to get local issuer certificate
รายละเอียดปัญหา
ทุกครั้งที่ OpenSSL พยายามตรวจสอบใบรับรอง SSL/TLS จากเซิร์ฟเวอร์ มันจะสร้าง chain ตั้งแต่ใบรับรองของเซิร์ฟเวอร์ไปจนถึง root CA ที่เชื่อถือ ถ้าเครื่องไม่มีใบรับรองกลางหรือใบรับรอง root CA ที่ต้องใช้ หรือไม่พบไฟล์/โฟลเดอร์ CA (CAFile หรือ CAPath), การตรวจสอบจะล้มเหลวและแสดงข้อผิดพลาด 20:unable to get local issuer certificate
พูดง่ายๆ คือ OpenSSL ไม่ทราบว่าจะเชื่อถือ CA ใด จึงไม่สามารถยืนยันตัวตนเซิร์ฟเวอร์ที่กำลังเชื่อมต่อได้
เวอร์ชั่น OpenSSL และเส้นทางใบรับรอง CA ใน ServBay
ServBay ทำหน้าที่เป็น local web development environment ที่ติดตั้ง OpenSSL และรวมไฟล์ CA root certificate สาธารณะมากมายไว้ให้ พร้อมใช้งาน ด้วยความแตกต่างด้านสถาปัตยกรรม อุปกรณ์แต่ละแบบจะติดตั้งเวอร์ชั่น OpenSSL แตกต่างกัน:
- Apple Silicon (ชิป M-Series): ใช้ OpenSSL เวอร์ชั่น 3.2.1
- Intel: ใช้ OpenSSL เวอร์ชั่น 1.1.1u
สำหรับ Windows จะใช้ OpenSSL เวอร์ชั่น 3.3.0
แต่ละเวอร์ชั่นจะมีไฟล์ CA certificate (cacert.pem) และโฟลเดอร์ certificate (certs) ใน path ที่เหมาะสมตามชนิดอุปกรณ์:
ini
# ไฟล์รวม root CA ที่เชื่อถือทั้งหมด
cafile=/Applications/ServBay/package/common/openssl/3.2/cacert.pem
# โฟลเดอร์ใบรับรองแยกรายไฟล์ (โดยทั่วไปใช้เพียง cacert.pem เพียงพอ)
capath=/Applications/ServBay/package/common/openssl/3.2/certs1
2
3
4
2
3
4
ini
# ไฟล์รวม root CA ที่เชื่อถือทั้งหมด
cafile=/Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem
# โฟลเดอร์ใบรับรองแยกรายไฟล์
capath=/Applications/ServBay/package/common/openssl/1.1.1u/certs1
2
3
4
2
3
4
ini
# ไฟล์รวม root CA ที่เชื่อถือทั้งหมด
cafile=C:\ServBay\package\common\openssl\3.3\cacert.pem
# โฟลเดอร์ใบรับรองแยกรายไฟล์
capath=C:\ServBay\package\common\openssl\3.3\certs1
2
3
4
2
3
4
วิธีแก้ปัญหาข้อผิดพลาดนี้คือ ระบุ cafile หรือ capath ให้ OpenSSL สามารถหาใบรับรอง CA ที่เชื่อถือได้
ตัวอย่างการแก้ไข
ตัวอย่างต่อไปนี้คือวิธีกำหนด CA root certificate ใน OpenSSL สำหรับเครื่องมือและภาษาแต่ละแบบ
ตัวอย่างที่ 1: ทดสอบด้วยคำสั่ง openssl
หากใช้คำสั่ง openssl s_client เพื่อทดสอบเชื่อมต่อและเกิดข้อผิดพลาด
bash
openssl s_client -quiet -connect gmail.com:4431
กรณีนี้คุณอาจเห็นบรรทัดที่มี 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
วิธีแก้ไข:
ใช้ option -CAfile ระบุเส้นทางไฟล์ CA certificate ที่ ServBay เตรียมให้
bash
openssl s_client -quiet -connect gmail.com:443 -CAfile /Applications/ServBay/package/common/openssl/3.2/cacert.pem1
bash
openssl s_client -quiet -connect gmail.com:443 -CAfile /Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem1
หากเชื่อมต่อสำเร็จและตรวจสอบใบรับรองผ่าน คุณจะไม่เห็นบรรทัด 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: การตั้งค่า OpenSSL ใน PHP
PHP ฟังก์ชันสำหรับการร้องขอเครือข่าย (เช่น file_get_contents ผ่าน HTTPS, stream_socket_client เชื่อมต่อ SSL, cURL) ล้วนพึ่งพา OpenSSL คุณสามารถตั้งค่า CA trust store ผ่านไฟล์ php.ini หรือในโค้ดโดยตรง
วิธีที่ A: แก้ไข php.ini (แนะนำที่สุด)
สำหรับการแก้ไขทุกโปรเจกต์บน PHP ให้เปิดไฟล์ php.ini ของเวอร์ชั่น PHP ที่กำลังใช้งาน (ปรับได้จาก ServBay Control Panel) ค้นหา section [openssl] แล้วระบุหรือปรับแต่งค่าดังนี้
ini
[openssl]
; กำหนดไฟล์ bundle ของ CA root certificates ที่เชื่อถือ
openssl.cafile=/Applications/ServBay/package/common/openssl/3.2/cacert.pem
; กำหนดโฟลเดอร์ CA certificates (ไม่ระบุก็ได้ แต่แนะนำให้ระบุ)
openssl.capath=/Applications/ServBay/package/common/openssl/3.2/certs1
2
3
4
5
2
3
4
5
ini
[openssl]
; กำหนดไฟล์ bundle ของ CA root certificates ที่เชื่อถือ
openssl.cafile=/Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem
; กำหนดโฟลเดอร์ CA certificates (ไม่ระบุก็ได้ แต่แนะนำให้ระบุ)
openssl.capath=/Applications/ServBay/package/common/openssl/1.1.1u/certs1
2
3
4
5
2
3
4
5
หลังจากบันทึกการแก้ไข php.ini ให้ restart PHP (หรือทั้ง ServBay) เพื่อใช้ค่าใหม่
วิธีที่ B: กำหนดในโค้ด (เฉพาะการเชื่อมต่อปัจจุบัน)
หากไม่ต้องการแก้ไข php.ini สามารถกำหนด CA ใน SSL context ผ่านฟังก์ชัน stream_context_create ตามตัวอย่างนี้
php
<?php
// ตัวอย่าง: เชื่อมต่อ SMTP ผ่าน SSL/TLS
$server = 'ssl0.ovh.net';
$port = 465;
// เลือกไฟล์ CA certificate ตามเวอร์ชั่น ServBay ของคุณ
// สำหรับ 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, // ตรวจสอบว่าเซิร์ฟเวอร์ตรงกับ host name
'allow_self_signed' => false, // ไม่อนุญาต self-signed certificate ยกเว้นเชื่อถือจริง
'cafile' => $caCertFile, // กำหนดไฟล์ CA bundle
// 'capath' => '/Applications/ServBay/package/common/openssl/3.2/certs', // ตัวเลือกเสริมสำหรับ CA directory
],
];
$context = stream_context_create($contextOptions);
// สร้างการเชื่อมต่อ SSL/TLS ด้วย context ที่ตั้งค่า
$connection = @stream_socket_client(
"ssl://$server:$port",
$errno,
$errstr,
30, // timeout
STREAM_CLIENT_CONNECT,
$context // ส่ง context option
);
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: ตั้งค่า OpenSSL ใน Python (ssl module)
โมดูล ssl ของ Python สามารถสร้าง SSL context และกำหนด CA ที่เชื่อถือได้โดยตรง
python
import ssl
import socket
server = 'ssl0.ovh.net'
port = 465
# เลือกเส้นทางไฟล์ CA certificate ตาม ServBay ของคุณ
# สำหรับ 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'
# สร้าง default SSL context พร้อมกำหนด CA
context = ssl.create_default_context(cafile=ca_cert_file)
# หรือใช้ capath directory: context = ssl.create_default_context(capath='/Applications/ServBay/package/common/openssl/3.2/certs')
try:
# สร้าง socket ปกติ
with socket.create_connection((server, port)) as sock:
# สร้าง SSL socket จาก socket ปกติ
with context.wrap_socket(sock, server_hostname=server) as ssock:
print(f"SSL connection สำเร็จ 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"เชื่อมต่อไม่สำเร็จหรือเกิด SSL error: {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: การใช้งาน OpenSSL ใน Node.js (tls module)
สำหรับ Node.js สามารถใช้ module tls แล้วกำหนด CA ผ่าน option ca โดยการอ่านเนื้อหาไฟล์ cacert.pem ที่ ServBay เตรียมไว้
javascript
const tls = require('tls');
const fs = require('fs');
const server = 'www.google.com'; // ตัวอย่างเซิร์ฟเวอร์สาธารณะที่เชื่อถือได้
const port = 443;
// เลือกไฟล์ CA certificate ตาม ServBay ของคุณ
// สำหรับ 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 certificate
ca: fs.readFileSync(caCertFile),
// โหมดตรวจสอบชื่อ host มีค่าเริ่มต้นปลอดภัย ไม่ควรปิด checkServerIdentity ยกเว้นกรณีที่ทราบผลกระทบ
// checkServerIdentity: () => { return null; } // <-- ไม่ควรใช้งานบรรทัดนี้ ปิดการตรวจสอบ host name แบบปลอดภัย!
};
const socket = tls.connect(options, () => {
console.log('เชื่อมต่อ SSL สำเร็จ');
// สำหรับ HTTPS สามารถสั่ง GET request ได้ ถ้าต้องการดูผล
// 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
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
หมายเหตุ: ตัวอย่าง Node.js ไม่มีบรรทัด checkServerIdentity: () => { return null; } เนื่องจากการปิดฟังก์ชันนี้จะลดทอนความปลอดภัย ข้อผิดพลาด unable to get local issuer certificate จะเกี่ยวข้องกับความน่าเชื่อถือของ CA โดยการระบุค่า ca ที่ถูกต้องเท่านั้น หากมีปัญหา host name ตรวจไม่ผ่าน ต้องแก้ไขที่ใบรับรอง ไม่ใช่ที่ CA
ตัวอย่างที่ 5: ใช้งาน curl ร่วมกับ OpenSSL
curl สามารถระบุไฟล์ CA ผ่าน option --cacert เพื่อปรับการตรวจสอบใบรับรอง
bash
# ใช้ไฟล์ CA จาก ServBay เพื่อเข้าถึงเว็บไซต์ HTTPS
# เลือก path ให้ถูกต้องตามที่คุณใช้งาน
# สำหรับ 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.com1
2
3
4
5
6
7
2
3
4
5
6
7
ถ้าระบุ CA ได้ถูกต้องและเซิร์ฟเวอร์มีใบรับรองที่ใช้งานได้จริง curl จะรับข้อมูลสำเร็จโดยไม่แสดงข้อผิดพลาด
สรุป
ข้อผิดพลาด 20:unable to get local issuer certificate เป็นเรื่องปกติสำหรับการพัฒนาโดยใช้ OpenSSL ในทุกภาษา/เครื่องมือ ต้นตอคือเครื่องจักรไม่ทราบข้อมูลของ root CA ที่เชื่อถือได้
ServBay รวมไฟล์ CA root certificate (cacert.pem) ที่รองรับ CA สาธารณะยอดนิยมไว้ ดูรายละเอียดใน path ตามเวอร์ชั่นและชนิดเครื่องของคุณ
วิธีแก้ง่ายๆ คือระบุไฟล์ CA certificate ที่ ServBay เตรียมไว้ (php.ini, ตัวเลือกในโค้ด, หรือระบุผ่าน command line เช่น -CAfile หรือ --cacert) โดยเลือกตาม chip (Apple Silicon หรือ Intel) และเวอร์ชั่น OpenSSL ที่ถูกต้อง เพียงเท่านี้ คุณก็สามารถสร้างการเชื่อมต่อที่ปลอดภัยระหว่างสภาพแวดล้อมพัฒนาและบริการ SSL/TLS ภายนอกได้ ไม่มีข้อผิดพลาด!
