استكشاف أخطاء 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 بشكل افتراضي وجود معرفة واضحة بسلطة إصدار الشهادات الجذرية (CA) الموثوقة عند التحقق من سلسلة الشهادات. في حال عدم توفر أو تحديد هذه النقاط المرجعية، يتعذر على OpenSSL التحقق من شرعية شهادة الخادم، مما يؤدي لظهور هذا الخطأ.
تقدم هذه المقالة شرحًا تفصيليًا لأسباب الخطأ، بالإضافة إلى كيفية حله ضمن بيئة ServBay، وتوضيح كيفية تهيئة تخزين الشهادات الموثوقة في OpenSSL لكل من PHP، Python، Node.js، وأمر curl
.
رسالة الخطأ 20:unable to get local issuer certificate
وصف المشكلة
عندما يحاول OpenSSL التحقق من شهادة SSL/TLS الخاصة بخادم بعيد، يقوم ببناء سلسلة شهادات تبدأ بشهادة الخادم وتنتهي بسلطة إصدار الشهادات الجذرية الموثوقة. إذا لم يتمكن OpenSSL من العثور محليًا على أي شهادة وسيطة أو شهادة CA الجذرية المطلوبة في السلسلة، أو إذا لم يجد مسار تخزين موثوق معد مسبقًا (CAFile أو CAPath) للبحث فيها، سيفشل التحقق ويظهر الخطأ 20:unable to get local issuer certificate
.
بعبارة بسيطة: OpenSSL لا يعرف أي سلطة إصدار شهادات يمكن الوثوق بها وبالتالي لا يستطيع التأكد من هوية الخادم المتصل به.
إصدار OpenSSL ومسارات شهادات CA في ServBay
تعد ServBay بيئة تطوير ويب محلية متكاملة، حيث تأتي مع مجموعة أدوات OpenSSL مضمّنة بالإضافة إلى شهادات CA الجذرية الشائعة لتسهيل العمل على المطورين. تعتمد نسخة OpenSSL المستخدمة في ServBay على نوع معالج جهازك:
- ServBay على Apple Silicon (رقائق سلسلة M): يستخدم إصدار OpenSSL 3.2.1.
- ServBay على أجهزة Intel: يستخدم إصدار OpenSSL 1.1.1u.
توجد ملفات شهادات 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
هي تحديد مسار cafile
أو capath
المناسب في أي مكان يتطلب استخدام OpenSSL، لإبلاغه بمكان العثور على شهادات CA الموثوقة.
أمثلة الحلول
فيما يلي أمثلة توضح كيفية تحديد تخزين شهادات 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
طريقة الحل:
حدد مسار ملف شهادات CA الخاص بـ ServBay عبر وسيطة -CAfile
:
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
في الإخراج، وسيبقى قيمة verify return
هي 1
:
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 مثل الوصول إلى عناوين HTTPS بواسطة file_get_contents
، أو إنشاء اتصال SSL بـ stream_socket_client
، أو استخدام امتداد cURL، تعتمد على OpenSSL بشكل أساسي. يمكنك تحديد تخزين شهادات CA الموثوقة عبر تعديل ملف php.ini
أو إعداد إعدادات سياق الاتصال في الكود.
الطريقة الأولى: تعديل ملف php.ini
(مستحسن)
هذه الطريقة هي الأسهل لضمان الحل بشكل عام لجميع الاتصالات. عدّل ملف php.ini
الخاص بالإصدار المستخدم من PHP (يمكنك الوصول له بسهولة عبر لوحة تحكم 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 بالكامل) لتفعيل التعديل.
الطريقة الثانية: ضبط الإعدادات ضمن الكود (يؤثر فقط على الاتصال الحالي)
إذا كنت لا ترغب في تعديل ملف php.ini
بشكل عام، يمكنك تمرير إعدادات شهادة CA عند إنشاء سياق الاتصال بـ stream_context_create
:
php
<?php
// مثال على الاتصال بسيرفر SMTP عبر 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, // تفعيل تحقق شهادة الطرف الآخر
'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 "تم إنشاء الاتصال بـ $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)
تسمح وحدة ssl في Python بإنشاء سياق SSL/TLS وتحديد ملف شهادات CA الموثوقة:
python
import ssl
import socket
server = 'ssl0.ovh.net'
port = 465
# تحديد مسار ملف شهادات 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:
# تغليف الاتصال بـ SSL
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: استخدام OpenSSL في Node.js (وحدة tls)
تستخدم وحدة tls في Node.js لإنشاء اتصال TLS/SSL، ويمكن تحديد CA عبر الخاصية ca
في خيارات الاتصال. الخاصية ca
تستقبل محتوى الشهادة بصيغة نصية أو مصفوفة Buffer. أبسط طريقة هي قراءة محتوى ملف cacert.pem
الخاص بـ ServBay:
javascript
const tls = require('tls');
const fs = require('fs');
const server = 'www.google.com'; // يُستخدم موقع موثوق قياسي كمثال
const port = 443;
// مسار ملف شهادة 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),
// وحدة tls في Node.js تتحقق افتراضيًا من اسم المضيف (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
ملاحظة: تمت إزالة خيار checkServerIdentity: () => { return null; }
من مثال Node.js، لأن هذا الخيار إذا استخدمته سيُلغي التحقق من اسم المضيف، وهو إجراء غير آمن. خطأ OpenSSL unable to get local issuer certificate
متعلق بمشكلة الجذر الموثوق وليس بعملية تحقق اسم المضيف (verify_peer_name
). عند تحديد خيار ca
بشكل سليم وكان خادم الموقع يستخدم شهادة صالحة، تستمر عملية التحقق افتراضيًا في Node.js. أي خطأ في تحقق اسم المضيف غالبًا ما يكون سببه مشكلة في شهادة الخادم وليس بمسار CA.
المثال 5: استخدام أمر curl
مع OpenSSL
يعتمد أمر curl
على OpenSSL (أو مكتبات SSL أخرى) لتنفيذ طلبات HTTPS. يمكنك تحديد ملف شهادة CA عبر وسيطة --cacert
:
bash
# استخدام ملف شهادة CA الخاص بـ ServBay للوصول إلى موقع HTTPS
# اختر المسار المناسب حسب النوع
# بالنسبة لـ 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 ويرجع أساسًا إلى الحاجة لتحديد مسار تخزين شهادات CA الموثوقة. توفر ServBay مسبقًا ملف cacert.pem
الذي يضم أحدث شهادات CA العامة الشائعة.
طريقة الحل تكمن في تعيين مسار ملف شهادات CA الخاص بـ ServBay في إعدادات بيئة العمل (داخل ملف php.ini
، إعدادات سياق الاتصال، أو عبر متغيرات الأمر مثل -CAfile
في openSSL أو --cacert
في curl). انتبه لاختيار المسار الصحيح تبعًا لنوع المعالج لديك (Apple Silicon أو Intel) وإصدار OpenSSL المستخدم في ServBay. من خلال هذا التهيئة الصحيحة لتخزين الشهادات الموثوقة، يمكنك ضمان اتصال آمن وسليم بين بيئة التطوير المحلية والخدمات الخارجية عبر SSL/TLS.