การแก้ไขปัญหา 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 ของหน่วยงานออกใบรับรอง (CA) ที่เชื่อถือได้อย่างชัดเจน เพื่อวัตถุประสงค์ด้านความปลอดภัย หากไม่มี หรือตั้งค่าไม่ถูกต้อง OpenSSL จะไม่สามารถตรวจสอบความถูกต้องของใบรับรองที่ได้รับจากเซิร์ฟเวอร์ และจะแจ้งข้อผิดพลาดนี้
บทความนี้จะอธิบายสาเหตุ และแนวทางแก้ไขในสภาพแวดล้อม ServBay โดยครอบคลุมวิธีแก้ไขสำหรับ PHP, Python, Node.js และคำสั่ง curl
เพื่อกำหนดแหล่งเก็บ CA ที่ OpenSSL ใช้งาน
ข้อผิดพลาด 20:unable to get local issuer certificate
ลักษณะปัญหา
เมื่อ OpenSSL ตรวจสอบใบรับรอง SSL/TLS ของเซิร์ฟเวอร์ จะต้องสร้างสายโซ่ใบรับรอง (certificate chain) จากใบรับรองเซิร์ฟเวอร์ไปจนถึง root CA ที่ระบบเชื่อถือ หากหาส่วนใดในสายโซ่นั้น (เช่น Intermediate CA หรือ Root CA) ไม่เจอ หรือหาค่า CAFile หรือ CAPath ที่เหมาะสมไม่พบ ระบบจะตรวจสอบไม่สำเร็จและแจ้งข้อผิดพลาด 20:unable to get local issuer certificate
กล่าวอย่างง่าย คือ OpenSSL ไม่ทราบว่าควรเชื่อถือใคร ทำให้ไม่สามารถยืนยันตัวตนเซิร์ฟเวอร์ได้
เวอร์ชัน OpenSSL และเส้นทาง CA Certificates ใน ServBay
ServBay เป็นชุดเครื่องมือพัฒนาเว็บท้องถิ่นแบบครบวงจรที่มี OpenSSL และไฟล์ root CA สำคัญพร้อมใช้งาน โดยเวอร์ชัน OpenSSL ที่แถมมาขึ้นกับชนิดชิปของอุปกรณ์ดังนี้
- Apple Silicon (ชิปตระกูล M): ใช้ OpenSSL เวอร์ชัน 3.2.1
- Intel: ใช้ OpenSSL เวอร์ชัน 1.1.1u
ไฟล์ CA certificate (cacert.pem
) และโฟลเดอร์ใบรับรอง (certs
) จะอยู่ในเส้นทางติดตั้งของแพ็กเกจ ServBay คุณต้องเลือกใช้เส้นทางให้ตรงตามเวอร์ชันของ ServBay ที่ใช้งาน
ini
# ไฟล์รวมใบรับรอง root ที่เชื่อถือได้ทั้งหมด
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
# ไฟล์รวมใบรับรอง root ที่เชื่อถือได้ทั้งหมด
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
คือการระบุเส้นทาง cafile
หรือ capath
ที่ถูกต้องให้กับ OpenSSL ในทุกจุดที่จำเป็นต้องใช้งาน
ตัวอย่างการแก้ไขปัญหา
ด้านล่างนี้คือตัวอย่างวิธีระบุแหล่งเก็บใบรับรอง CA ให้ OpenSSL ในเครื่องมือและภาษาต่างๆ
ตัวอย่างที่ 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
ระบุเส้นทางไฟล์ CA ที่ ServBay เตรียมไว้ให้
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 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 extension) จะใช้งาน OpenSSL เป็นแกนหลัก คุณสามารถกำหนดแหล่ง CA ได้ทั้งในไฟล์ php.ini
หรือผ่าน options เฉพาะในโค้ด
วิธีที่ A: แก้ไข php.ini
(แนะนำ)
เป็นวิธีระดับโลกสำหรับ PHP ทุกเวอร์ชัน ให้แก้ไขไฟล์ php.ini
(หาได้จากแผงควบคุม ServBay) ที่ส่วน [openssl]
เพิ่มหรือแก้ไขบรรทัดดังนี้ เลือกเส้นทางให้ตรงกับชิป
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
กรุณารีสตาร์ท PHP ใน ServBay หรือรีสตาร์ท ServBay ทั้งหมด
วิธีที่ B: ระบุในโค้ด (สำหรับเชื่อมต่อครั้งนั้นเท่านั้น)
หากไม่ต้องการแก้ระดับ globle ให้ระบุ path ของ CA ใน context ขณะสร้าง SSL/TLS connection
php
<?php
// ตัวอย่าง: เชื่อมต่อ SMTP server ผ่าน SSL/TLS
$server = 'ssl0.ovh.net';
$port = 465;
// เลือกเส้นทาง CA ให้ตรงกับ 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, // เปิดตรวจสอบ certificate peer
'verify_peer_name' => true, // ตรวจสอบ host name กับใบรับรอง
'allow_self_signed' => false, // ห้ามใช้ self-signed certificate
'cafile' => $caCertFile, // ระบุไฟล์ CA
// 'capath' => '/Applications/ServBay/package/common/openssl/3.2/certs', // ไม่จำเป็นเสมอแต่สามารถระบุได้
],
];
$context = stream_context_create($contextOptions);
// สร้าง connection ด้วย context ที่เตรียมไว้
$connection = @stream_socket_client(
"ssl://$server:$port",
$errno,
$errstr,
30, // timeout
STREAM_CLIENT_CONNECT,
$context // ส่ง 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";
}
?>
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 สามารถกำหนดเส้นทาง CA ได้ขณะสร้าง SSL context
python
import ssl
import socket
server = 'ssl0.ovh.net'
port = 465
# เลือก path CA certificate ให้ถูกต้อง
# 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 file
context = ssl.create_default_context(cafile=ca_cert_file)
# หรือใช้แบบ 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:
# หุ้ม 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}")
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)
โมดูล tls
ใน Node.js สามารถระบุ CA ได้ผ่านออปชั่นชื่อ ca
อาจระบุเนื้อหาของใบรับรองหลายชุด (string หรือ Buffer) ทางเลือกที่สะดวกคือต้องอ่านไฟล์ cacert.pem
โดยตรง
javascript
const tls = require('tls');
const fs = require('fs');
const server = 'www.google.com'; // ตัวอย่างเว็บไซต์ที่เชื่อถือได้
const port = 443;
// เลือกเส้นทางตามเวอร์ชัน 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),
// โมดูล tls ของ Node.js จะตรวจสอบ host name โดยอัตโนมัติ
// หากระบุไฟล์ CA ที่ถูกต้องและใบรับรองเซิร์ฟเวอร์ถูกต้อง ฟีเจอร์นี้จะผ่านได้
// ไม่ควรปิด checkServerIdentity เด็ดขาด (เพราะจะลดความปลอดภัย)
// checkServerIdentity: () => { return null; } // <--- หลีกเลี่ยง! ไม่ปลอดภัย
};
const socket = tls.connect(options, () => {
console.log('SSL connection established');
// สำหรับ HTTPS อาจส่ง HTTP 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('Connection closed');
});
socket.on('error', (error) => {
console.error('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; }
ออกแล้ว เพราะฟังก์ชันดังกล่าวจะปิดการตรวจสอบชื่อโฮสต์ ซึ่งไม่ปลอดภัย ข้อผิดพลาด unable to get local issuer certificate
เป็นเรื่องของ trust-store ไม่เกี่ยวกับการตรวจสอบ host name (verify_peer_name) ดังนั้นเพียงแค่ระบุไฟล์ CA ให้ถูกต้อง ระบบ Node.js จะทำงานตามปกติ
ตัวอย่างที่ 5: ใช้งาน CA กับคำสั่ง curl
คำสั่ง curl
สำหรับ HTTPS ใช้ OpenSSL อยู่เบื้องหลัง สามารถระบุไฟล์ CA ได้ด้วย --cacert
bash
# ใช้งาน CA certificate ที่ 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.com
1
2
3
4
5
6
7
2
3
4
5
6
7
ถ้าไฟล์ CA ถูกต้อง และใบรับรองปลายทางถูกต้อง curl
จะไม่แจ้งข้อผิดพลาดด้านใบรับรอง
สรุป
ข้อผิดพลาด 20:unable to get local issuer certificate
มักพบบ่อยเมื่อเชื่อมต่อ SSL/TLS ผ่าน OpenSSL และเกิดจากการไม่ได้กำหนด trust store (เช่น root CA) ไว้อย่างถูกต้อง สำหรับ ServBay ได้เตรียมไฟล์ cacert.pem
ที่รวม CA สาธารณะยอดนิยมไว้ให้แล้ว
การแก้ไขคือการระบุ path ของไฟล์ cacert.pem
ที่ ServBay ให้มา ผ่านไฟล์ตั้งค่า (เช่น php.ini
), ออปชั่นใน code, หรือผ่าน argument ของคำสั่ง (เช่น -CAfile
สำหรับ openssl
, --cacert
สำหรับ curl
) โดยต้องเลือก path ให้เหมาะกับชนิดชิป (Apple Silicon หรือ Intel) และเวอร์ชัน OpenSSL ที่ใช้งาน การตั้งค่านี้จะช่วยให้การพัฒนาท้องถิ่นกับบริการ SSL/TLS ภายนอกปลอดภัยและราบรื่น