عندما تفتح موقع البنك في المتصفح وترى القفل بجانب العنوان، فأنت تثق في أمرين معاً دون أن تفكر فيهما: أن البيانات بينك وبين السيرفر مشفرة فلا يقرؤها أحد في الطريق، وأن السيرفر الذي تتحدث معه هو سيرفر البنك فعلاً وليس جهازاً آخر ينتحل اسمه. والأمر الأول سهل نسبياً، أما الثاني فهو المشكلة الحقيقية، لأن التشفير مع الطرف الخطأ لا يحميك من شيء، والحل الذي يعتمد عليه الإنترنت كله لهذه المشكلة هو الشهادة الرقمية Digital Certificate. ومع الشهادة تأتي أسماء وملفات تحير أي مطور أو مدير سيرفرات في أول مرة: PEM وDER وCRT وCER وKEY وCSR وPFX وP12 وJKS، وكل برنامج يطلب صيغة مختلفة، وكل خطأ يظهر برسالة غامضة مثل unable to get local issuer certificate أو no start line.
وهذا المقال جزء من مرحلة الشبكات في خارطة الطريق، ويبدأ من المشكلة ثم يصل إلى الملفات والأوامر، وسوف نناقش في هذا المقال ما يلي:
- المشكلة: لماذا يفشل التشفير وحده، ولماذا يحتاج المفتاح العام إلى من يعرف به.
- المفاهيم: زوج المفاتيح والتوقيع والشهادة، وقراءة شهادة حقيقية حقلاً حقلاً بمعيار X.509.
- سلسلة الثقة من شهادة موقعك إلى الشهادة الرئيسية في نظام التشغيل، ولماذا يجب أن يرسل السيرفر الشهادة الوسيطة.
- أنواع الشهادات DV وOV وEV والشهادات الشاملة، ومدة الصلاحية التي تقصر إلى 47 يوماً، والإلغاء Revocation، وسجلات الشفافية Certificate Transparency.
- مصافحة TLS 1.3 باختصار، وأين يتم فحص الشهادة فيها.
- صيغ الملفات وامتداداتها: PEM وDER وPKCS#1 وPKCS#8 وPKCS#7 وPKCS#12 وJKS، وملفات Let's Encrypt الأربعة.
- تجربة عملية كاملة بمخرجات حقيقية: نبني جهة إصدار صغيرة، ونصدر شهادة، ونحولها بين الصيغ، ونشغل سيرفر TLS مع الشهادة الوسيطة ودونها.
- أين يوضع كل ملف في Nginx وHAProxy وApache وIIS وJava وKubernetes، ثم الأخطاء الشائعة وكيف تقرؤها، وجدول بالأوامر.
التشفير وحده لا يحميك: مع من تتحدث؟
لنفرض أن المتصفح يريد أن يرسل كلمة المرور إلى bank.example دون أن يقرأها أحد في الطريق، فالحل الأول الذي يخطر على البال هو التشفير بالمفتاح العام Public Key Encryption، أي أن يرسل السيرفر مفتاحه العام، ويشفر المتصفح البيانات به، فلا يستطيع فكها إلا من يملك المفتاح الخاص المقابل، وهو السيرفر وحده. وهذا صحيح رياضياً، ولكن هناك مشكلة رئيسية في الخطوة الأولى: كيف يعرف المتصفح أن المفتاح العام الذي وصله هو مفتاح البنك فعلاً؟ فلو كان هناك مهاجم في منتصف الطريق Man in the Middle، في شبكة WiFi عامة مثلاً أو عند مزود خبيث أو عبر DNS مزور، فإنه يستقبل طلب المتصفح ويرد عليه بمفتاحه العام هو، فيشفر المتصفح كلمة المرور بمفتاح المهاجم ويرسلها إليه وهو مطمئن أن الاتصال مشفر، ثم يفكها المهاجم ويقرؤها ويعيد تشفيرها بمفتاح البنك الحقيقي ويرسلها إليه، فلا يلاحظ البنك ولا المستخدم أي شيء. وبالتالي فالتشفير هنا يعمل بشكل ممتاز، ولكنه يعمل مع الطرف الخطأ، وهذا معنى القاعدة التي يجب أن تبقى في ذهنك طوال المقال: التشفير دون التحقق من الهوية لا قيمة له.
وقد يتساءل البعض: لماذا لا يحفظ المتصفح المفتاح العام لكل موقع مسبقاً؟ والإجابة أن هذا ممكن لعدد صغير من المواقع، ولكن الإنترنت فيه مئات الملايين من النطاقات، والمفاتيح تتغير، والمواقع الجديدة تظهر كل دقيقة، فلا يمكن أن يحمل كل متصفح قائمة بكل مفاتيح العالم. والحل الثاني أن يسأل المتصفح السيرفر نفسه «هل أنت البنك؟»، وهذا لا يفيد لأن المهاجم سوف يقول نعم بكل تأكيد. والحل الصحيح هو ما نفعله في الحياة اليومية مع شخص لا نعرفه: نطلب منه وثيقة صادرة من جهة نثق بها نحن، مثل الهوية الوطنية أو جواز السفر، فنحن لا نعرف الشخص ولكننا نعرف الجهة التي أصدرت الوثيقة ونعرف كيف نتحقق من ختمها. وفي الإنترنت تسمى هذه الجهة جهة إصدار الشهادات Certificate Authority واختصاراً CA، والوثيقة هي الشهادة، وفيها اسم النطاق ومفتاحه العام، مختومة بتوقيع الجهة، والمتصفح يحمل مسبقاً قائمة صغيرة بالجهات التي يثق بها بدلاً من مفاتيح كل المواقع.
الصورة التالية تبين الفرق بين الحالتين:

المفتاح الخاص والمفتاح العام والتوقيع في دقيقة
الشهادة كلها مبنية على فكرة واحدة شرحناها بالتفصيل في قسم التوقيع الرقمي بلغة بسيطة في مقال SPF وDKIM وDMARC، ونلخصها هنا فقط بالقدر الذي نحتاجه:
- زوج المفاتيح Key Pair: مفتاحان مرتبطان رياضياً بخوارزمية مثل RSA أو ECDSA، المفتاح الخاص Private Key يبقى سراً على السيرفر ولا يغادره، والمفتاح العام Public Key ينشر لكل العالم، ولا يمكن استنتاج المفتاح الخاص من المفتاح العام.
- التوقيع الرقمي Digital Signature: يحسب صاحب المفتاح الخاص هاش Hash البيانات ثم يوقعه بمفتاحه الخاص، وأي شخص يملك المفتاح العام يستطيع أن يتحقق أن التوقيع صحيح وأن البيانات لم تتغير ولو بحرف واحد، ولكنه لا يستطيع أن ينتج توقيعاً جديداً.
وهذه الفكرة نفسها تراها في Passkeys حيث يوقع جهازك قيمة عشوائية Challenge يرسلها الموقع، وفي مفاتيح JWKS التي يتحقق بها التطبيق من توقيع ID Token. والفرق المهم أن مفتاح DKIM ومفاتيح JWKS مفاتيح عامة «عارية» تنشر في DNS أو في رابط على موقع المزود، ويثق بها الطرف الآخر لأنه يجلبها من مصدر يعرفه، أما الشهادة فهي مفتاح عام ومعه توقيع جهة ثالثة تشهد أن هذا المفتاح يخص هذا الاسم.
ما هي الشهادة الرقمية؟
الشهادة الرقمية Digital Certificate هي ملف صغير يربط بين هوية، مثل اسم النطاق github.com، وبين مفتاح عام، مع مدة صلاحية، وكل ذلك موقع بالمفتاح الخاص لجهة إصدار Certificate Authority. والتشبيه الأقرب هو بطاقة الهوية: فيها اسمك وصورتك وتاريخ الإصدار والانتهاء، وعليها ختم الجهة التي أصدرتها، والموظف الذي يفحصها لا يعرفك شخصياً، وإنما يعرف شكل الختم ويثق في الجهة. والصورة في البطاقة تقابل المفتاح العام في الشهادة، فالبطاقة وحدها لا تثبت أنك صاحبها، وإنما مطابقة الصورة لوجهك، وكذلك الشهادة وحدها ليست سراً، فهي ترسل لكل زائر، والذي يثبت أن السيرفر صاحبها هو أنه يملك المفتاح الخاص المقابل للمفتاح العام فيها، كما سنرى في مصافحة TLS.
وصيغة الشهادات المستخدمة في TLS والبريد وتوقيع الكود هي X.509 النسخة الثالثة، وقد عرفها الاتحاد الدولي للاتصالات ITU، والملف المرجعي لها في الإنترنت هو RFC 5280. الصورة التالية تبين أجزاء الشهادة:

نقرأ شهادة حقيقية: github.com
بدلاً من الشرح النظري سوف نجلب شهادة موقع معروف ونقرؤها حقلاً حقلاً. الأمر openssl s_client يفتح اتصال TLS ويطبع الشهادات التي يرسلها السيرفر، ثم نمرر الشهادة الأولى إلى openssl x509 فيحفظها في ملف ويعرضها بشكل مقروء:
echo | openssl s_client -connect github.com:443 -servername github.com 2>/dev/null | openssl x509 -out github.pem
openssl x509 -in github.pem -text -nooutوالمخرج سوف يكون كما يلي:
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
a5:9e:bd:b5:96:75:1d:b7:f5:c0:95:07:96:13:95:3c
Signature Algorithm: ecdsa-with-SHA256
Issuer: C=GB, O=Sectigo Limited, CN=Sectigo Public Server Authentication CA DV E36
Validity
Not Before: Sep 1 00:00:00 2026 GMT
Not After : Nov 29 23:59:59 2026 GMT
Subject: CN=github.com
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:85:36:1b:34:bc:b3:51:f7:20:e9:aa:9a:cb:e8:
27:2d:60:d1:31:7b:1d:38:d1:d8:c7:d7:a0:fa:5a:
b1:f1:2f:28:e7:99:51:46:61:22:38:b2:3e:b0:2b:
75:75:ec:00:e2:69:a6:cf:13:4e:2f:42:4e:fa:76:
35:b4:0d:3f:2a
ASN1 OID: prime256v1
NIST CURVE: P-256
X509v3 extensions:
X509v3 Authority Key Identifier:
17:99:A8:04:C1:6F:E4:2D:70:A8:0A:10:3D:03:D3:E9:1A:B8:26:63
X509v3 Subject Key Identifier:
66:98:EC:4C:11:35:F7:4B:50:84:8B:A8:1C:36:65:D0:17:56:D4:E0
X509v3 Key Usage: critical
Digital Signature
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Extended Key Usage:
TLS Web Server Authentication
X509v3 Certificate Policies:
Policy: 1.3.6.1.4.1.6449.1.2.2.7
CPS: https://sectigo.com/CPS
Policy: 2.23.140.1.2.1
Authority Information Access:
CA Issuers - URI:http://crt.sectigo.com/SectigoPublicServerAuthenticationCADVE36.crt
OCSP - URI:http://ocsp.sectigo.com
CT Precertificate SCTs:
Signed Certificate Timestamp:
Version : v1 (0x0)
Log ID : D7:6D:7D:10:D1:A7:F5:77:C2:C7:E9:5F:D7:00:BF:F9:
82:C9:33:5A:65:E1:D0:B3:01:73:17:C0:C8:C5:69:77
Timestamp : Sep 1 00:10:32.017 2026 GMT
Extensions: none
Signature : ecdsa-with-SHA256
30:45:02:20:7A:BB:A0:82:C9:0B:1A:A7:EA:66:4E:EC:
FB:01:BF:53:1C:F2:D6:56:D4:11:3D:3A:92:36:E0:BC:
65:63:BA:79:02:21:00:B3:37:AC:9A:5D:BB:45:D5:72:
F4:96:DB:DF:39:46:33:83:87:EF:41:05:1E:67:F1:F0:
79:94:BE:CB:39:79:E6
Signed Certificate Timestamp:
Version : v1 (0x0)
Log ID : C8:A3:C4:7F:C7:B3:AD:B9:35:6B:01:3F:6A:7A:12:6D:
E3:3A:4E:43:A5:C6:46:F9:97:AD:39:75:99:1D:CF:9A
Timestamp : Sep 1 00:10:31.981 2026 GMT
Extensions: none
Signature : ecdsa-with-SHA256
30:45:02:20:70:B1:0B:F0:7D:54:AE:11:C1:AA:2A:9E:
25:37:F8:D6:3D:45:2E:B9:D5:17:F9:2B:78:7C:89:95:
0F:10:8A:99:02:21:00:D1:C9:BD:67:73:B1:71:FF:03:
DB:82:AD:2C:F5:B1:0E:24:DF:2A:30:4D:8C:6D:56:D7:
D3:25:CF:75:A3:37:B6
X509v3 Subject Alternative Name:
DNS:github.com, DNS:www.github.com
Signature Algorithm: ecdsa-with-SHA256
Signature Value:
30:44:02:20:17:41:57:b6:3f:e6:dd:a8:f7:cf:2f:30:10:c6:
2c:91:25:2d:60:0b:cc:f2:64:b7:77:69:86:6b:19:93:d0:84:
02:20:09:ab:16:bf:d5:cf:09:61:85:87:b0:6c:b5:ad:03:54:
6f:77:df:b8:22:54:ee:99:e3:8e:a2:5f:76:28:a6:85في المخرج أعلاه لاحظ التالي:
- الرقم التسلسلي Serial Number: رقم فريد تعطيه جهة الإصدار لكل شهادة، وهو الذي يذكر في قوائم الإلغاء عندما تلغى الشهادة.
- خوارزمية التوقيع Signature Algorithm: الجهة وقعت هذه الشهادة بـ ECDSA مع SHA-256، والتوقيع نفسه في آخر المخرج تحت
Signature Value. - الجهة المصدرة Issuer: من وقع الشهادة، وهنا شهادة وسيطة لشركة Sectigo اسمها
Sectigo Public Server Authentication CA DV E36، وليست الشهادة الرئيسية، كما سنرى في سلسلة الثقة. - الصلاحية Validity: من 1 سبتمبر إلى 29 نوفمبر 2026، أي 90 يوماً تقريباً، وبعدها يرفضها كل متصفح حتى لو كان كل شيء آخر صحيحاً، ولاحظ أن الوقت بتوقيت UTC.
- صاحب الشهادة Subject:
CN=github.com، والحقل CN أو Common Name بقي لأسباب تاريخية، والمتصفحات اليوم لا تعتمد عليه وإنما على الحقل SAN. - المفتاح العام Subject Public Key Info: مفتاح ECDSA على المنحنى P-256، وهو المفتاح الذي سوف يتحقق به المتصفح أن السيرفر يملك المفتاح الخاص.
- استخدام المفتاح Key Usage وExtended Key Usage: هذا المفتاح للتوقيع فقط، وهذه الشهادة لسيرفر TLS فقط
TLS Web Server Authentication، فلا تصلح لتوقيع الكود ولا لتوقيع البريد. - القيود الأساسية Basic Constraints:
CA:FALSEيعني أن صاحب هذه الشهادة لا يحق له أن يصدر شهادات لغيره، وهذا ما يمنع أي موقع من أن يستخدم شهادته لتوقيع شهادة باسم موقع آخر. - السياسات Certificate Policies: الرقم
2.23.140.1.2.1معناه أن الشهادة من نوع DV كما في المتطلبات الأساسية Baseline Requirements لمنتدى CA/Browser Forum، أي أن الجهة تحققت من التحكم في النطاق فقط. - معلومات الجهة Authority Information Access: رابط لتنزيل الشهادة الوسيطة
CA Issuers، ورابط خدمة OCSP لفحص الإلغاء. - CT Precertificate SCTs: إيصالان من سجلين عامين للشفافية يثبتان أن الشهادة نشرت علناً قبل إصدارها، وسوف نشرح السبب في قسم سجلات الشفافية.
- الأسماء البديلة Subject Alternative Name: الأسماء التي تصلح لها الشهادة فعلاً،
github.comوwww.github.com، فإذا فتحتapi.github.comفلا بد أن يرسل السيرفر شهادة أخرى فيها هذا الاسم.
سلسلة الثقة: من شهادة موقعك إلى الشهادة الرئيسية
لاحظ أن شهادة GitHub لم توقعها الجهة الرئيسية لشركة Sectigo مباشرة، وإنما شهادة وسيطة Intermediate Certificate، والسبب أن المفتاح الخاص للشهادة الرئيسية Root Certificate هو أثمن ما تملكه الجهة، فلو سرق لأصبحت كل الشهادات التي تحته بلا قيمة، ولذلك تحفظه الجهات في أجهزة HSM غير متصلة بالشبكة، ولا تستخدمه إلا نادراً لتوقيع شهادات وسيطة، والشهادات الوسيطة هي التي توقع شهادات المواقع كل يوم. وإذا اخترقت شهادة وسيطة أمكن إلغاؤها وإصدار أخرى دون أن تتغير الشهادة الرئيسية الموجودة في ملايين الأجهزة. وبهذا تتكون سلسلة الثقة Chain of Trust:
- شهادة النطاق Leaf Certificate: شهادة موقعك، موقعة من الشهادة الوسيطة، و
CA:FALSE. - الشهادة الوسيطة Intermediate Certificate: موقعة من الشهادة الرئيسية، و
CA:TRUE، وقد تكون هناك أكثر من شهادة وسيطة. - الشهادة الرئيسية Root Certificate: موقعة من نفسها Self-Signed، أي أن Issuer هو نفسه Subject، ولا يمكن التحقق منها بتوقيع، ولذلك لا تأتي ثقتها من السلسلة وإنما من وجودها مسبقاً في مخزن الثقة Trust Store على جهازك.
ومخزن الثقة هو قائمة الشهادات الرئيسية التي يثق بها نظام التشغيل أو المتصفح، وتديرها برامج رسمية لها شروط تدقيق صارمة، مثل برنامج Mozilla الذي تعتمد عليه Firefox وأغلب توزيعات Linux عبر الحزمة ca-certificates، وبرنامج Chrome Root Program، إضافة إلى مخزن Windows ومخزن Apple. وفي Debian 13 الذي استخدمناه في التجربة يحتوي الملف /etc/ssl/certs/ca-certificates.crt على 150 شهادة رئيسية فقط، وهذه هي القائمة الصغيرة التي تغني المتصفح عن معرفة مفاتيح كل مواقع العالم. وعند الاتصال يبني الكلاينت السلسلة من شهادة النطاق صعوداً، ويتحقق من كل توقيع بالمفتاح العام للشهادة التي فوقه، حتى يصل إلى شهادة رئيسية موجودة في مخزنه، فإذا لم يصل رفض الاتصال.
الصورة التالية تبين السلسلة ومكان كل جزء منها:

ويمكنك أن ترى السلسلة التي يرسلها GitHub فعلاً بالخيار -showcerts، أو من أول مخرج s_client:
echo | openssl s_client -connect github.com:443 -servername github.com 2>&1 | head -18والمخرج سوف يكون كما يلي:
Connecting to 20.233.83.145
depth=2 C=GB, O=Sectigo Limited, CN=Sectigo Public Server Authentication Root E46
verify return:1
depth=1 C=GB, O=Sectigo Limited, CN=Sectigo Public Server Authentication CA DV E36
verify return:1
depth=0 CN=github.com
verify return:1
CONNECTED(00000003)
---
Certificate chain
0 s:CN=github.com
i:C=GB, O=Sectigo Limited, CN=Sectigo Public Server Authentication CA DV E36
a:PKEY: EC, (prime256v1); sigalg: ecdsa-with-SHA256
v:NotBefore: Sep 1 00:00:00 2026 GMT; NotAfter: Nov 29 23:59:59 2026 GMT
1 s:C=GB, O=Sectigo Limited, CN=Sectigo Public Server Authentication CA DV E36
i:C=GB, O=Sectigo Limited, CN=Sectigo Public Server Authentication Root E46
a:PKEY: EC, (prime256v1); sigalg: ecdsa-with-SHA384
v:NotBefore: Mar 22 00:00:00 2021 GMT; NotAfter: Mar 21 23:59:59 2036 GMTفي المخرج أعلاه لاحظ التالي:
- السطور
depthهي ترتيب التحقق، فالعمق 0 شهادة النطاق، والعمق 1 الوسيطة، والعمق 2 الشهادة الرئيسية التي وجدها OpenSSL في مخزن النظام، وverify return:1يعني أن كل مستوى نجح. - في
Certificate chainالسطرs:هو Subject والسطرi:هو Issuer، وكل Issuer يجب أن يساوي Subject الشهادة التي تليه، وهكذا تترابط السلسلة. - الشهادة الوسيطة صالحة حتى 2036، بينما شهادة النطاق 90 يوماً فقط، وهذا الفرق مقصود: الشهادات العليا قليلة ومحمية، وشهادات المواقع كثيرة وتتغير باستمرار.
ولو استعرضت المخرج كاملاً لوجدت أن GitHub يرسل شهادة ثالثة أيضاً، فيها Sectigo Public Server Authentication Root E46 موقعة من جهة أقدم اسمها USERTrust ECC Certification Authority، وهذا ما يسمى التوقيع المتقاطع Cross-Signing، والفكرة أن الشهادة الرئيسية الجديدة قد لا تكون موجودة بعد في مخازن الأجهزة القديمة، فتوقعها جهة قديمة موجودة فيها، فيستطيع الجهاز القديم أن يكمل السلسلة إلى الجهة القديمة والجهاز الحديث يتوقف عند الجديدة.
لماذا يجب أن يرسل السيرفر الشهادة الوسيطة؟
الشهادات الرئيسية موجودة في جهاز الزائر، أما الشهادات الوسيطة فهي كثيرة وتتغير، ولذلك تطلب مواصفة TLS 1.3 من السيرفر أن يرسل شهادته ومعها الشهادات التي تربطها بالشهادة الرئيسية. فإذا أعددت السيرفر بملف شهادة النطاق وحده فسوف يعمل الموقع في بعض المتصفحات ويفشل في غيرها، والسبب أن Firefox مثلاً ينزل كل الشهادات الوسيطة المعروفة مسبقاً لأن هذا الخطأ من أكثر أخطاء إعداد السيرفرات شيوعاً، وبعض المتصفحات تجلب الشهادة الناقصة من الرابط CA Issuers الذي رأيناه في الشهادة، بينما curl وPython وJava وتطبيقات الجوال وأغلب المكتبات لا تفعل ذلك، فتظهر الرسالة unable to get local issuer certificate. وهذا الفرق هو سبب الجملة المشهورة: «الموقع يعمل في المتصفح ولكن التطبيق يرفضه»، وسوف نرى الخطأ بأنفسنا في التجربة العملية، والحل دائماً أن يقرأ السيرفر ملف السلسلة الكامل fullchain.pem وليس cert.pem.
أنواع الشهادات: DV وOV وEV والشهادات الشاملة
تختلف الشهادات في مقدار ما تحققت منه الجهة قبل إصدارها، وليس في قوة التشفير، فالتشفير واحد في الأنواع الثلاثة:
- التحقق من النطاق Domain Validation واختصاراً DV: تتحقق الجهة فقط أنك تتحكم في النطاق، بملف على الموقع أو بسجل في DNS عبر بروتوكول ACME، وهذا ما تصدره Let's Encrypt مجاناً وتلقائياً، وقد شرحنا طرق التحقق في دليل Nginx Proxy Manager.
- التحقق من المنظمة Organization Validation واختصاراً OV: تتحقق الجهة إضافة إلى ذلك من وجود المنظمة قانونياً، ويظهر اسمها في الحقل
O=داخل Subject. - التحقق الموسع Extended Validation واختصاراً EV: تحقق أشد بخطوات موثقة، وكانت المتصفحات تعرض اسم الشركة باللون الأخضر بجانب العنوان، ولكن Chrome نقل هذا العرض منذ الإصدار 77 إلى نافذة معلومات الصفحة بعد أن تبين أن المستخدمين لا يلاحظونه، ولذلك لم يعد لـ EV أثر يراه الزائر في أغلب الحالات.
أما من حيث الأسماء التي تغطيها الشهادة فهناك نوعان قد يجتمعان في شهادة واحدة:
- شهادة متعددة الأسماء SAN Certificate: فيها عدة أسماء في الحقل SAN، مثل
github.comوwww.github.comكما رأينا. - الشهادة الشاملة Wildcard Certificate: فيها اسم مثل
*.example.comيغطي كل نطاق فرعي من مستوى واحد، ولا يغطيexample.comنفسه ولاa.b.example.com، ولا تصدرها Let's Encrypt إلا بالتحقق عبر DNS، وتفاصيلها في قسم الشهادة الشاملة.
ومتى تختار شهادة لكل اسم أو شهادة SAN واحدة أو شهادة شاملة، وكيف يختار السيرفر الشهادة بإشارة SNI، شرحناه بتجربة عملية في مقال سيرفر واحد ونطاقات كثيرة.
لماذا تقصر مدة الشهادات كل عام؟
كلما طالت مدة الشهادة طالت الفترة التي يستطيع فيها من سرق المفتاح الخاص أن ينتحل الموقع، وطالت الفترة التي تبقى فيها معلومات قديمة مثل نطاق بيع لشخص آخر صحيحة في شهادة صادرة، والإلغاء كما سنرى في القسم التالي لا يعمل بشكل موثوق، ولذلك فالحل العملي الوحيد هو أن تنتهي الشهادة بسرعة. وقد وافق منتدى CA/Browser Forum، الذي يجمع جهات الإصدار والمتصفحات، في 11 أبريل 2025 على القرار SC-081v3 بأغلبية 25 جهة إصدار دون أي اعتراض، وبموافقة Apple وGoogle وMicrosoft وMozilla، وأصبح الجدول التالي جزءاً من المتطلبات الأساسية:
| الشهادات الصادرة | أقصى مدة للشهادة | أقصى مدة لإعادة استخدام التحقق من النطاق |
|---|---|---|
| قبل 15 مارس 2026 | 398 يوماً | 398 يوماً |
| من 15 مارس 2026 | 200 يوم | 200 يوم |
| من 15 مارس 2027 | 100 يوم | 100 يوم |
| من 15 مارس 2029 | 47 يوماً | 10 أيام |
وLet's Encrypt تسبق هذا الجدول، فشهاداتها 90 يوماً منذ البداية، وقد أعلنت في ديسمبر 2025 أنها سوف تنتقل في الملف الافتراضي إلى 64 يوماً في 10 فبراير 2027 ثم إلى 45 يوماً في 16 فبراير 2028، وتنصح بالتجديد عند ثلثي مدة الشهادة تقريباً، أو باستخدام خدمة ARI التي تخبر برنامج ACME بموعد التجديد المناسب. والنتيجة العملية أن التجديد اليدوي للشهادات أصبح غير ممكن، فشهادة كل 47 يوماً لعشرين نطاقاً تعني أنك سوف تجدد شهادة كل يومين ونصف تقريباً، ولذلك يجب أن يكون التجديد آلياً بالكامل عبر ACME، كما تفعل Nginx Proxy Manager وTraefik وCaddy، ومع مراقبة تنبهك إذا فشل التجديد.
إلغاء الشهادة قبل موعدها: CRL وOCSP
إذا سرق المفتاح الخاص لموقعك فيجب أن تلغي Revoke الشهادة فوراً، ولكن كيف يعرف كل متصفح في العالم أن شهادة ما زالت صالحة التاريخ أصبحت ملغاة؟ هناك طريقتان تاريخياً:
- قائمة الشهادات الملغاة Certificate Revocation List واختصاراً CRL: ملف تنشره الجهة دورياً فيه الأرقام التسلسلية للشهادات الملغاة، والكلاينت ينزله ويبحث فيه.
- بروتوكول OCSP: يسأل الكلاينت الجهة عن شهادة واحدة برقمها التسلسلي ويحصل على جواب موقع: صالحة أو ملغاة، كما في RFC 6960.
ويبدو OCSP الحل الأنسب لأنه سريع وخاص بشهادة واحدة، ولكن له مشكلتين، الأولى أن الجهة تعرف من سؤال الكلاينت أي موقع يزوره هذا العنوان IP ومتى، والثانية أن المتصفحات إذا لم تصل إلى خدمة OCSP لا ترفض الاتصال عادة، وإلا لتعطل نصف الإنترنت مع كل عطل عند الجهة، وهذا يعني أن المهاجم الذي يحجب OCSP يعطل الفحص كله. ولذلك أعلنت Let's Encrypt في ديسمبر 2024 إنهاء OCSP والاعتماد على CRL فقط، وذكرت السبب صراحة: الخطر على الخصوصية، فحذفت روابط OCSP من شهاداتها في 7 مايو 2025، وأوقفت خدماتها في 6 أغسطس 2025. فإذا قرأت شهادة صادرة من Let's Encrypt اليوم فسوف تجد فيها CRL Distribution Points بدلاً من رابط OCSP، وإذا كان في إعداد Nginx القديم ssl_stapling on لهذه الشهادات فلم يعد له عمل. والمتصفحات نفسها تتجه إلى قوائم مجمعة تصلها مع التحديثات بدلاً من سؤال الجهة مع كل زيارة، مثل CRLite في Firefox، وهذا سبب آخر لتقصير مدة الشهادات، فالشهادة القصيرة تنتهي قبل أن تحتاج إلى إلغاء في أغلب الحالات.
سجلات الشفافية Certificate Transparency وسجل CAA
كل ما سبق يفترض أن جهات الإصدار نفسها لا تخطئ، ولكن ماذا لو أصدرت جهة شهادة باسم نطاقك لشخص آخر، بخطأ أو لأنها اخترقت؟ فالمتصفح سوف يقبلها لأن توقيعها صحيح، وأنت لن تعرف بوجودها أصلاً. والحل هو سجلات الشفافية Certificate Transparency واختصاراً CT، المعرفة في RFC 6962، وهي سجلات عامة لا يمكن الحذف منها ولا التعديل فيها، تشغلها جهات مستقلة، وكل شهادة عامة يجب أن تسجل فيها قبل أن تصدر، والسجل يعطي الجهة إيصالاً موقعاً اسمه SCT، وهو ما رأيناه مرتين في شهادة GitHub. ومتصفح Chrome يرفض أي شهادة عامة لا تحمل العدد المطلوب من هذه الإيصالات. والنتيجة أن أي شهادة تصدر باسم نطاقك تظهر علناً، وتستطيع أن تبحث عنها في موقع crt.sh بكتابة اسم نطاقك، أو بالرابط https://crt.sh/?q=example.com، فإذا وجدت شهادة لم تطلبها فهذا إنذار مبكر بأن هناك من يحاول انتحال موقعك. ولاحظ أن هذا يعني أيضاً أن أسماء نطاقاتك الفرعية مثل staging-old.example.com منشورة للجميع بمجرد أن تصدر لها شهادة عامة، فلا تعتمد على إخفاء الاسم كوسيلة حماية.
*.google.com صادرة من جهة الإصدار الهولندية DigiNotar، واستخدمت في هجمات اعتراض على مستخدمي Gmail في إيران، وقد كشفها Chrome لأنه كان يحمل قائمة بالجهات المسموح لها بإصدار شهادات Google، ثم أزالت Mozilla ثقتها في الشهادة الرئيسية للجهة بالكامل، وتبين أن DigiNotar اكتشفت الاختراق في 19 يوليو ولم تحدد كل الشهادات التي أصدرها المخترق، فأعلنت إفلاسها في 20 سبتمبر 2011. والدرس أن الثقة في أي جهة إصدار يجب أن تكون قابلة للمراقبة، ومن حوادث مثل هذه جاءت سجلات الشفافية.والأداة الثانية في يدك هي سجل CAA في DNS، فهو يحدد الجهات المسموح لها بإصدار شهادات لنطاقك، ويجب على كل جهة أن تفحصه قبل الإصدار، فإذا كان سجلك يسمح لـ letsencrypt.org وحدها فسوف ترفض أي جهة أخرى أن تصدر شهادة باسمك، وقد شرحنا السجل بمثال حقيقي في قسم سجل CAA. وبهذا يكون لديك منع مسبق بسجل CAA، ومراقبة لاحقة بسجلات الشفافية.
أين تفحص الشهادة في مصافحة TLS 1.3؟
الشهادة لا تشفر البيانات بنفسها، وإنما تستخدم مرة واحدة في بداية الاتصال، في المصافحة Handshake، لإثبات هوية السيرفر، ثم يتفق الطرفان على مفاتيح جلسة Session Keys متماثلة سريعة يشفران بها كل ما بعد ذلك. والمصافحة في TLS 1.3، المعرفة في RFC 8446، تتم في دورة ذهاب وعودة واحدة Round Trip كما يلي:
- ClientHello: يرسل الكلاينت الخوارزميات التي يدعمها، وجزءه من تبادل المفاتيح Key Share، واسم النطاق الذي يريده في الامتداد SNI، والسبب أن سيرفراً واحداً قد يحمل شهادات لعشرات النطاقات.
- ServerHello: يرد السيرفر بالخوارزمية التي اختارها وجزئه من تبادل المفاتيح، ومن هذه اللحظة يملك الطرفان مفاتيح مشتركة، فيصبح كل ما بعدها مشفراً، بما في ذلك الشهادة نفسها، وهذا من الفروق عن TLS 1.2 الذي كان يرسل الشهادة دون تشفير.
- Certificate: يرسل السيرفر شهادته والشهادات الوسيطة.
- CertificateVerify: يوقع السيرفر بمفتاحه الخاص على كل رسائل المصافحة حتى هذه اللحظة، وهذا هو الإثبات أنه يملك المفتاح الخاص المقابل للشهادة، فالمهاجم يستطيع أن ينسخ شهادة GitHub لأنها عامة، ولكنه لا يستطيع أن ينتج هذا التوقيع.
- الفحص عند الكلاينت: يبني السلسلة حتى شهادة رئيسية في مخزنه، ويتحقق من كل توقيع فيها، ومن أن الوقت الحالي داخل مدة كل شهادة، ومن أن اسم النطاق الذي طلبه موجود في SAN، ومن أن Extended Key Usage يسمح بـ TLS Server، ثم يتحقق من توقيع CertificateVerify بالمفتاح العام في الشهادة. فإذا فشل أي فحص أوقف الاتصال وعرض المتصفح صفحة التحذير.
- Finished: يؤكد الطرفان أن أحداً لم يعدل رسائل المصافحة، ثم تبدأ البيانات المشفرة بمفاتيح الجلسة.
الصورة التالية تبين المصافحة ومكان الفحص فيها:

ولاحظ أن كل هذا يحمي الاتصال الذي يبدأ بـ HTTPS، أما الطلب الأول الذي يكتب فيه المستخدم اسم الموقع دون https:// فيخرج عبر HTTP عادي، وهذا ما تحله الترويسة HSTS التي شرحناها في قسم HSTS.
صيغ الملفات: PEM وDER وPKCS وامتداداتها
الآن نصل إلى الجزء الذي يحير الجميع، وأول ما يجب أن تفهمه أن هناك سؤالين مختلفين عن أي ملف: كيف كتبت البايتات؟ وهذا هو الترميز، PEM أو DER، وماذا يحتوي الملف؟ شهادة أو مفتاح أو طلب شهادة أو سلسلة أو حزمة كاملة، وهذا ما تحدده معايير PKCS. أما الامتداد .crt أو .pem فهو مجرد عادة يتبعها كل برنامج بطريقته، ولا يحدد أياً من الأمرين.
DER وPEM: الملف نفسه بشكلين
الشهادة في الأصل بنية بيانات معرفة بلغة ASN.1، وتكتب بايتاتها بقواعد الترميز DER، فالملف بصيغة DER ملف ثنائي Binary. أما PEM فهو الملف نفسه بعد ترميزه بـ Base64، بين سطرين يحددان نوع المحتوى، مثل -----BEGIN CERTIFICATE----- و-----END CERTIFICATE-----، كما في RFC 7468، والسبب نفسه الذي شرحناه في مقال الترميز Base64: النص يمر في البريد ويلصق في ملفات الإعداد ولوحات التحكم، والبايتات الخام لا تمر. ولنتأكد بأنفسنا، نحول شهادة من التجربة إلى DER ونقارن:
openssl x509 -in server.crt -outform DER -out server.der
ls -l server.crt server.der
od -A x -t x1 -N 16 server.der
openssl base64 -in server.der | head -2; sed -n 2,3p server.crtوالمخرج سوف يكون كما يلي:
-rw-r--r-- 1 root root 1038 Oct 5 23:28 server.crt
-rw-r--r-- 1 root root 726 Oct 5 23:28 server.der
000000 30 82 02 d2 30 82 02 77 a0 03 02 01 02 02 14 05
000010
MIIC0jCCAnegAwIBAgIUBcTUw7oaNj03gSyNlejIrLkVeUYwCgYIKoZIzj0EAwIw
SzELMAkGA1UEBhMCU0ExFTATBgNVBAoMDEFyYWJSb290IExhYjElMCMGA1UEAwwc
MIIC0jCCAnegAwIBAgIUBcTUw7oaNj03gSyNlejIrLkVeUYwCgYIKoZIzj0EAwIw
SzELMAkGA1UEBhMCU0ExFTATBgNVBAoMDEFyYWJSb290IExhYjElMCMGA1UEAwwcفي المخرج أعلاه لاحظ التالي:
- ملف DER حجمه 726 بايت، وملف PEM 1038 بايت، أي أكبر بالثلث تقريباً مع السطرين والأسطر الجديدة، وهذه زيادة Base64 المعروفة.
- ملف DER يبدأ بالبايتين
30 82، و30في ASN.1 تعني SEQUENCE، فإذا فتحت ملفاً ورأيت هذين البايتين في بدايته فهو غالباً DER. ولذلك تبدأ كل شهادة PEM تقريباً بالحروفMII، لأنها ترميز Base64 لهذين البايتين وما بعدهما. - أسطر Base64 من DER هي نفسها حرفاً بحرف السطور داخل ملف PEM، فالفرق في الشكل فقط والمحتوى واحد.
وإذا أردت أن ترى البنية نفسها فالأمر openssl asn1parse -in server.crt | head -12 يعرضها، والمخرج سوف يكون كما يلي:
0:d=0 hl=4 l= 722 cons: SEQUENCE
4:d=1 hl=4 l= 631 cons: SEQUENCE
8:d=2 hl=2 l= 3 cons: cont [ 0 ]
10:d=3 hl=2 l= 1 prim: INTEGER :02
13:d=2 hl=2 l= 20 prim: INTEGER :05C4D4C3BA1A363D37812C8D95E8C8ACB9157946
35:d=2 hl=2 l= 10 cons: SEQUENCE
37:d=3 hl=2 l= 8 prim: OBJECT :ecdsa-with-SHA256
47:d=2 hl=2 l= 75 cons: SEQUENCE
49:d=3 hl=2 l= 11 cons: SET
51:d=4 hl=2 l= 9 cons: SEQUENCE
53:d=5 hl=2 l= 3 prim: OBJECT :countryName
58:d=5 hl=2 l= 2 prim: PRINTABLESTRING :SAوهنا ترى الحقول التي قرأناها في شهادة GitHub بشكلها الخام: الإصدار 02 الذي يعني النسخة الثالثة، ثم الرقم التسلسلي، ثم خوارزمية التوقيع، ثم بداية اسم الجهة المصدرة.
الامتدادات: الاسم لا يحدد الصيغة
الجدول التالي يبين الامتدادات الشائعة وما تحتويه عادة، ولاحظ كلمة «عادة»:
| الامتداد | ما يحتويه عادة | الترميز المعتاد |
|---|---|---|
.pem | أي شيء: شهادة أو مفتاح أو سلسلة أو كلها معاً | PEM |
.crt | شهادة، أو سلسلة شهادات | PEM في Linux، وقد يكون DER |
.cer | شهادة | DER في Windows غالباً، وقد يكون PEM |
.der | شهادة واحدة أو مفتاح | DER |
.key | مفتاح خاص | PEM غالباً |
.csr | طلب شهادة Certificate Signing Request | PEM غالباً |
.p7b و.p7c | سلسلة شهادات دون مفتاح، PKCS#7 | PEM أو DER |
.pfx و.p12 | شهادة ومفتاح خاص وسلسلة بكلمة مرور، PKCS#12 | ثنائي دائماً |
.jks | مخزن مفاتيح Java القديم | ثنائي |
ولذلك إذا وصلك ملف لا تعرف ما فيه فلا تحكم عليه من اسمه، وإنما افتحه: إذا كان نصاً يبدأ بـ -----BEGIN فهو PEM والسطر نفسه يخبرك بنوعه، وإذا كان ثنائياً يبدأ بـ 30 82 فهو DER أو PKCS#12. ولاحظ أن OpenSSL 3 يتعرف على الصيغة تلقائياً في أغلب الأوامر، فإذا نسخنا ملف DER باسم .crt فسوف يقرؤه دون مشكلة:
cp server.der mystery.crt; openssl x509 -in mystery.crt -noout -subjectsubject=CN=app.lab.testولكن Nginx وHAProxy وأغلب البرامج لا تفعل ذلك، فهي تنتظر PEM ولا شيء غيره، وسوف نرى الخطأ الذي يظهر حينها في قسم الأخطاء الشائعة. وهذا سبب الحيرة المعتادة: الملف يفتح في OpenSSL ويرفضه السيرفر.
المفتاح الخاص: PKCS#1 وPKCS#8 والمفتاح المشفر
المفتاح الخاص يكتب أيضاً بأكثر من صيغة، وتعرفها من السطر الأول:
openssl pkey -in server.key -traditional -out server-pkcs1.key
openssl pkey -in server.key -aes256 -passout pass:LabPass123 -out server-enc.key
head -1 server.key server-pkcs1.key server-enc.key root.keyوالمخرج سوف يكون كما يلي:
==> server.key <==
-----BEGIN PRIVATE KEY-----
==> server-pkcs1.key <==
-----BEGIN RSA PRIVATE KEY-----
==> server-enc.key <==
-----BEGIN ENCRYPTED PRIVATE KEY-----
==> root.key <==
-----BEGIN PRIVATE KEY-----في المخرج أعلاه لاحظ التالي:
BEGIN PRIVATE KEYهي صيغة PKCS#8 المعرفة في RFC 5958، وهي صيغة عامة لأي نوع من المفاتيح، وفيها حقل يذكر الخوارزمية، ولذلك جاء مفتاح RSAserver.keyومفتاح ECroot.keyبالسطر نفسه، وهي ما يكتبه OpenSSL 3 افتراضياً.BEGIN RSA PRIVATE KEYهي الصيغة القديمة PKCS#1 من RFC 8017، وهي لمفاتيح RSA فقط، وتراها في المفاتيح التي ولدت بإصدارات OpenSSL القديمة، ومثلها لمفاتيح EC الصيغةBEGIN EC PRIVATE KEY. والمفتاح نفسه لم يتغير، والأمرopenssl pkeyيحول بينهما في الاتجاهين.BEGIN ENCRYPTED PRIVATE KEYهو PKCS#8 مشفر بكلمة مرور، وأي برنامج يقرؤه يجب أن يسأل عن كلمة المرور، ولذلك لا تستخدمه مع Nginx أو Nginx Proxy Manager الذي يرفض المفتاح المحمي بعبارة مرور، والسبب أن السيرفر سوف يتوقف عند كل إعادة تشغيل منتظراً شخصاً يكتبها. احم المفتاح بصلاحيات الملفchmod 600بدلاً من ذلك.
PKCS#7: سلسلة شهادات دون مفتاح
الملف .p7b بصيغة PKCS#7 من RFC 2315 يجمع عدة شهادات في ملف واحد ولا يحمل مفتاحاً خاصاً أبداً، وتراه عندما تصدر الشهادة من بعض الجهات التجارية أو عندما تصدرها من Windows مع السلسلة. ونصنعه ونقرؤه كما يلي:
openssl crl2pkcs7 -nocrl -certfile fullchain.pem -out chain.p7b
head -1 chain.p7b
openssl pkcs7 -in chain.p7b -print_certs -nooutوالمخرج سوف يكون كما يلي:
-----BEGIN PKCS7-----
subject=CN=app.lab.test
issuer=C=SA, O=ArabRoot Lab, CN=ArabRoot Lab Intermediate CA
subject=C=SA, O=ArabRoot Lab, CN=ArabRoot Lab Intermediate CA
issuer=C=SA, O=ArabRoot Lab, CN=ArabRoot Lab Root CAولاحظ أن الأمر الأخير مفيد مع أي ملف سلسلة PEM أيضاً، فهو يعرض ترتيب الشهادات داخله، وقد استخدمناه في دليل HAProxy لفحص ترتيب الملف المجمع. فإذا أعطتك الجهة ملف .p7b وأردت أن تستخدمه مع Nginx فحوله إلى PEM بالأمر openssl pkcs7 -in chain.p7b -print_certs -out chain.pem.
PKCS#12: الشهادة والمفتاح والسلسلة في ملف واحد
الملف .pfx أو .p12 بصيغة PKCS#12 من RFC 7292 هو الحزمة الكاملة: المفتاح الخاص وشهادته والشهادات الوسيطة، كلها مشفرة بكلمة مرور في ملف ثنائي واحد، وهي الصيغة التي يطلبها Windows وIIS عند الاستيراد، وتستخدمها Java وكثير من الأجهزة. ونصنعها من ملفات PEM ثم نقرأ محتواها:
openssl pkcs12 -export -inkey server.key -in server.crt -certfile inter.crt -name app.lab.test -passout pass:LabPass123 -out server.p12
openssl pkcs12 -in server.p12 -passin pass:LabPass123 -info -nooutوالمخرج سوف يكون كما يلي:
MAC: sha256, Iteration 2048
MAC length: 32, salt length: 8
PKCS7 Encrypted data: PBES2, PBKDF2, AES-256-CBC, Iteration 2048, PRF hmacWithSHA256
Certificate bag
Certificate bag
PKCS7 Data
Shrouded Keybag: PBES2, PBKDF2, AES-256-CBC, Iteration 2048, PRF hmacWithSHA256في المخرج أعلاه لاحظ التالي:
- في الملف شهادتان
Certificate bag، شهادة النطاق والوسيطة، ومفتاح واحدShrouded Keybagأي مفتاح مشفر. - التشفير AES-256-CBC مع PBKDF2، وهو الافتراضي في OpenSSL 3، بينما الإصدارات القديمة كانت تستخدم RC2 و3DES. وبعض الأنظمة القديمة لا تفتح الملف الحديث، فإذا احتجت ملفاً لنظام قديم فأضف الخيار
-legacyعند التصدير.
والاستخراج في الاتجاه العكسي، من PFX وصلك من Windows إلى ملفات PEM لسيرفر Linux، يتم بثلاثة أوامر:
openssl pkcs12 -in server.p12 -passin pass:LabPass123 -clcerts -nokeys -out cert-from-p12.pem
openssl pkcs12 -in server.p12 -passin pass:LabPass123 -cacerts -nokeys -out chain-from-p12.pem
openssl pkcs12 -in server.p12 -passin pass:LabPass123 -nocerts -noenc -out key-from-p12.pem
grep -h BEGIN cert-from-p12.pem chain-from-p12.pem key-from-p12.pem
head -4 cert-from-p12.pemوالمخرج سوف يكون كما يلي:
-----BEGIN CERTIFICATE-----
-----BEGIN CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
Bag Attributes
localKeyID: 43 CD A4 69 90 5B 6D 4B E3 23 91 71 C5 02 80 C8 46 5E F8 71
friendlyName: app.lab.test
subject=CN=app.lab.testفي الأوامر أعلاه لاحظ التالي:
-clcertsيخرج شهادة صاحب المفتاح فقط، و-cacertsالشهادات الوسيطة فقط، و-nocerts -noencالمفتاح دون تشفير، والخيار-noencهو الاسم الجديد لـ-nodesالذي أصبح قديماً منذ OpenSSL 3.0.- يضيف OpenSSL أسطراً قبل كل كتلة مثل
Bag Attributesوsubject=، وأغلب البرامج تتجاهل ما قبل-----BEGIN، فإذا رفضها برنامج فنظف الملف بالأمرopenssl x509 -in cert-from-p12.pem -out cert.pem. - المفتاح المستخرج غير مشفر، فاحذفه أو احمه بالصلاحيات فوراً، ولا تتركه في مجلد التنزيلات.
مخازن Java: JKS وPKCS12
تطبيقات Java لا تقرأ ملفات PEM مباشرة في أغلب الحالات، وإنما تقرأ مخزن مفاتيح KeyStore، وكانت صيغته الافتراضية JKS، وهي صيغة خاصة بـ Java، ثم أصبحت PKCS12 هي الافتراضية منذ JDK 9، أي أن ملف .p12 الذي صنعناه بـ OpenSSL هو مخزن Java صالح كما هو. ونتأكد بالأداة keytool في Container من Eclipse Temurin 25:
keytool -list -keystore server.p12 -storepass LabPass123
keytool -list -v -keystore server.p12 -storepass LabPass123 | grep -E "Alias|Entry type|chain length|^Owner|^Issuer|Valid"والمخرج سوف يكون كما يلي:
Keystore type: PKCS12
Keystore provider: SUN
Your keystore contains 1 entry
app.lab.test, Oct 5, 2026, PrivateKeyEntry,
Certificate fingerprint (SHA-256): A1:92:B9:6A:5B:BE:7A:A1:AC:EC:54:34:BE:32:28:73:34:1C:C4:24:51:19:B4:6D:82:3A:08:81:27:DB:77:2F
Alias name: app.lab.test
Entry type: PrivateKeyEntry
Certificate chain length: 2
Owner: CN=app.lab.test
Issuer: CN=ArabRoot Lab Intermediate CA, O=ArabRoot Lab, C=SA
Valid from: Mon Oct 05 23:28:27 GMT 2026 until: Sat Nov 21 23:28:27 GMT 2026
Owner: CN=ArabRoot Lab Intermediate CA, O=ArabRoot Lab, C=SA
Issuer: CN=ArabRoot Lab Root CA, O=ArabRoot Lab, C=SA
Valid from: Mon Oct 05 23:28:09 GMT 2026 until: Sat Oct 04 23:28:09 GMT 2031ولاحظ أن الاسم -name app.lab.test الذي وضعناه عند التصدير أصبح الاسم المستعار Alias الذي يطلبه إعداد التطبيق عادة، وأن السلسلة طولها 2. وفي Java نوعان من المخازن يجب ألا تخلط بينهما: مخزن المفاتيح KeyStore وفيه مفتاح التطبيق وشهادته PrivateKeyEntry، ومخزن الثقة TrustStore وفيه الشهادات الرئيسية التي يثق بها التطبيق عندما يتصل بغيره trustedCertEntry، ونضيف إليه شهادة جهتنا الداخلية كما يلي:
keytool -importcert -noprompt -alias arabroot-lab-root -file root.crt -keystore truststore.p12 -storepass LabPass123
keytool -list -keystore truststore.p12 -storepass LabPass123Certificate was added to keystore
Keystore type: PKCS12
Keystore provider: SUN
Your keystore contains 1 entry
arabroot-lab-root, Oct 5, 2026, trustedCertEntry,
Certificate fingerprint (SHA-256): A0:48:1E:8E:B1:C1:78:20:D4:22:AC:77:EC:E7:63:2F:8F:EF:C8:03:A2:A6:3A:B9:8B:3A:D7:45:4C:46:99:75وإذا طلب تطبيق قديم ملف JKS فالتحويل بالأمر keytool -importkeystore، وسوف تلاحظ أن keytool نفسه ينصحك بالعكس:
keytool -importkeystore -srckeystore server.p12 -srcstorepass LabPass123 -destkeystore server.jks -deststoretype JKS -deststorepass LabPass123Importing keystore server.p12 to server.jks...
Entry for alias app.lab.test successfully imported.
Import command completed: 1 entries successfully imported, 0 entries failed or cancelled
Warning:
The JKS keystore uses a proprietary format. It is recommended to migrate to PKCS12 which is an industry standard format using "keytool -importkeystore -srckeystore server.jks -destkeystore server.jks -deststoretype pkcs12".الصورة التالية تجمع الصيغ كلها وما يحمله كل منها:

ملفات Let's Encrypt الأربعة: أيها تستخدم؟
إذا استخدمت Certbot مباشرة، كما فعلنا في دليل HAProxy، فسوف تجد في المجلد /etc/letsencrypt/live/example.com/ أربعة ملفات، كما في توثيق Certbot:
privkey.pem: المفتاح الخاص، وهو الملف السري الوحيد بينها، ويوضع فيssl_certificate_keyفي Nginx وSSLCertificateKeyFileفي Apache.fullchain.pem: شهادة النطاق ثم الشهادة الوسيطة، وهو الملف الذي تضعه فيssl_certificateفي Nginx وSSLCertificateFileفي Apache 2.4.8 وما بعده، وفي أغلب البرامج الأخرى.cert.pem: شهادة النطاق وحدها، ولا تستخدمه كشهادة للسيرفر، والسبب أنه يسبب خطأ السلسلة الناقصة الذي شرحناه.chain.pem: الشهادة الوسيطة وحدها، ويحتاجه Apache القديم معSSLCertificateChainFile، أو برنامج يطلب الشهادة والسلسلة في حقلين منفصلين.
ولاحظ أن هذه الملفات روابط رمزية Symlinks إلى أحدث نسخة في مجلد archive، لذلك يطلب التوثيق أن تشير إعدادات السيرفر إليها مباشرة ولا تنسخها، فالنسخة المنسوخة لن تتجدد. الصورة التالية تبين كل ملف ومكانه:

تجربة عملية: نبني جهة إصدار صغيرة ونصدر شهادة
الآن سوف نطبق كل ما سبق بأيدينا، فنبني جهة إصدار داخلية بشهادة رئيسية وشهادة وسيطة، ثم نصدر شهادة لسيرفر، ونتحقق منها، ونشغل سيرفر TLS بها. نفذنا التجربة في Container من Debian 13.7 فيه OpenSSL 3.5.7 وcurl 8.14.1، وكل الأوامر تعمل على أي توزيعة Linux فيها OpenSSL 3، والأسماء app.lab.test وكلمة المرور LabPass123 للتجربة فقط.
الخطوة 1: الشهادة الرئيسية
mkdir pki && cd pki
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out root.key
openssl req -x509 -new -key root.key -sha256 -days 3650 -subj "/C=SA/O=ArabRoot Lab/CN=ArabRoot Lab Root CA" -addext "basicConstraints=critical,CA:TRUE" -addext "keyUsage=critical,keyCertSign,cRLSign" -out root.crt
openssl x509 -in root.crt -noout -subject -issuer -dates -ext basicConstraints,keyUsageوالمخرج سوف يكون كما يلي:
subject=C=SA, O=ArabRoot Lab, CN=ArabRoot Lab Root CA
issuer=C=SA, O=ArabRoot Lab, CN=ArabRoot Lab Root CA
notBefore=Oct 5 23:27:46 2026 GMT
notAfter=Oct 2 23:27:46 2036 GMT
X509v3 Basic Constraints: critical
CA:TRUE
X509v3 Key Usage: critical
Certificate Sign, CRL Signفي الإعداد أعلاه لاحظ التالي:
req -x509يصنع شهادة موقعة من نفسها مباشرة، ولذلك Subject وIssuer متساويان، وهذه علامة كل شهادة رئيسية.CA:TRUEمعkeyCertSignيسمحان لهذه الشهادة بتوقيع شهادات أخرى، ومدتها عشر سنوات لأنها لا تستخدم إلا نادراً.- في جهة حقيقية يولد
root.keyعلى جهاز غير متصل بالشبكة ويحفظ في HSM أو على الأقل مشفراً في مكان آمن، ولا يبقى على سيرفر يعمل.
الخطوة 2: الشهادة الوسيطة
نكتب امتدادات الشهادة الوسيطة في ملف inter.ext:
basicConstraints = critical, CA:TRUE, pathlen:0
keyUsage = critical, keyCertSign, cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyidثم نولد مفتاحها وطلبها ونوقعه بالشهادة الرئيسية:
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out inter.key
openssl req -new -key inter.key -subj "/C=SA/O=ArabRoot Lab/CN=ArabRoot Lab Intermediate CA" -out inter.csr
openssl x509 -req -in inter.csr -CA root.crt -CAkey root.key -days 1825 -sha256 -extfile inter.ext -out inter.crt
openssl x509 -in inter.crt -noout -subject -issuer -serial -ext basicConstraintsوالمخرج سوف يكون كما يلي:
Certificate request self-signature ok
subject=C=SA, O=ArabRoot Lab, CN=ArabRoot Lab Intermediate CA
subject=C=SA, O=ArabRoot Lab, CN=ArabRoot Lab Intermediate CA
issuer=C=SA, O=ArabRoot Lab, CN=ArabRoot Lab Root CA
serial=5356DBAB29B46324544B208B3D27E615CB39AF7C
X509v3 Basic Constraints: critical
CA:TRUE, pathlen:0ولاحظ pathlen:0، ومعناه أن هذه الشهادة الوسيطة تستطيع أن توقع شهادات مواقع فقط، ولا تستطيع أن توقع شهادة وسيطة أخرى تحتها، وأن OpenSSL 3 ولد رقماً تسلسلياً عشوائياً بطول 20 بايت دون أن نطلب.
الخطوة 3: مفتاح السيرفر وطلب الشهادة CSR
طلب توقيع الشهادة Certificate Signing Request واختصاراً CSR هو ما ترسله إلى أي جهة إصدار، تجارية كانت أو داخلية: فيه المفتاح العام والأسماء المطلوبة، وموقع بالمفتاح الخاص ليثبت أنك تملكه، والمفتاح الخاص نفسه لا يغادر سيرفرك أبداً. سوف نستخدم RSA هنا حتى نرى الفرق في الصيغ لاحقاً:
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out server.key
openssl req -new -key server.key -subj "/CN=app.lab.test" -addext "subjectAltName=DNS:app.lab.test,DNS:www.app.lab.test" -out server.csr
openssl req -in server.csr -noout -text -verifyوالمخرج سوف يكون كما يلي، بعد اختصار أرقام المفتاح والتوقيع:
Certificate request self-signature verify OK
Certificate Request:
Data:
Version: 1 (0x0)
Subject: CN=app.lab.test
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:d4:70:ef:ee:9a:de:f9:94:80:b5:cb:40:20:92:
…
Exponent: 65537 (0x10001)
Attributes:
Requested Extensions:
X509v3 Subject Alternative Name:
DNS:app.lab.test, DNS:www.app.lab.test
Signature Algorithm: sha256WithRSAEncryption
Signature Value:
1b:14:ae:3a:96:5b:bc:b2:13:f2:01:ea:59:e4:eb:0f:82:69:
…ولاحظ self-signature verify OK، أي أن الطلب موقع بالمفتاح الخاص المقابل للمفتاح العام فيه، وأن الأسماء في Requested Extensions هي ما سوف يظهر في SAN إذا وافقت الجهة.
الخطوة 4: توقيع شهادة السيرفر
نكتب امتدادات شهادة السيرفر في server.ext:
basicConstraints = critical, CA:FALSE
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyidثم توقع الشهادة الوسيطة الطلب، ونجعل المدة 47 يوماً لأنها المدة القصوى التي سوف تصل إليها الشهادات العامة في 2029:
openssl x509 -req -in server.csr -CA inter.crt -CAkey inter.key -days 47 -sha256 -copy_extensions copy -extfile server.ext -out server.crt
openssl x509 -in server.crt -noout -subject -issuer -dates -ext subjectAltName,keyUsage,extendedKeyUsage,basicConstraintsوالمخرج سوف يكون كما يلي:
Certificate request self-signature ok
subject=CN=app.lab.test
subject=CN=app.lab.test
issuer=C=SA, O=ArabRoot Lab, CN=ArabRoot Lab Intermediate CA
notBefore=Oct 5 23:28:27 2026 GMT
notAfter=Nov 21 23:28:27 2026 GMT
X509v3 Subject Alternative Name:
DNS:app.lab.test, DNS:www.app.lab.test
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authenticationالخيار -copy_extensions copy ينقل SAN من الطلب إلى الشهادة، وهذا مناسب في التجربة لأننا نحن من كتب الطلب، أما الجهة الحقيقية فلا تنسخ ما في الطلب دون فحص، وإنما تتحقق من كل اسم فيه قبل أن تضعه في الشهادة، وإلا لطلب أي شخص شهادة لـ google.com.
الخطوة 5: التحقق من السلسلة
openssl verify -CAfile root.crt server.crt
openssl verify -CAfile root.crt -untrusted inter.crt server.crt
cat server.crt inter.crt > fullchain.pem
grep -c "BEGIN CERTIFICATE" fullchain.pemوالمخرج سوف يكون كما يلي:
CN=app.lab.test
error 20 at 0 depth lookup: unable to get local issuer certificate
error server.crt: verification failed
server.crt: OK
2في المخرج أعلاه لاحظ التالي:
- في المحاولة الأولى أعطينا OpenSSL الشهادة الرئيسية فقط، فلم يجد من وقع
server.crt، لأن الذي وقعها هو الشهادة الوسيطة، فظهر الخطأ 20، وهذا بالضبط ما يحدث لأي كلاينت عندما لا يرسل السيرفر الشهادة الوسيطة. - الخيار
-untrusted inter.crtيعطي الشهادة الوسيطة كحلقة في السلسلة دون أن يثق بها كجهة رئيسية، فاكتملت السلسلة ونجح التحقق. - الملف
fullchain.pemهو شهادة السيرفر ثم الوسيطة بهذا الترتيب، وهو ما سوف يقرؤه السيرفر.
الخطوة 6: سيرفر TLS دون الشهادة الوسيطة ثم معها
أضفنا الاسم app.lab.test إلى /etc/hosts ليشير إلى 127.0.0.1، ثم شغلنا سيرفر TLS تجريبياً بالأمر openssl s_server على المنفذ 8443، مع شهادة السيرفر وحدها كما يفعل من يضع cert.pem بدلاً من fullchain.pem:
openssl s_server -accept 127.0.0.1:8443 -cert server.crt -key server.key -www &
echo | openssl s_client -connect 127.0.0.1:8443 -servername app.lab.test -CAfile root.crt -verify_hostname app.lab.test 2>&1 | grep -E "depth|verify|Verif|Certificate chain| [0-9] s:| i:"
curl -sS --cacert root.crt https://app.lab.test:8443/ -o /dev/nullوالمخرج سوف يكون كما يلي:
depth=0 CN=app.lab.test
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 CN=app.lab.test
verify error:num=21:unable to verify the first certificate
verify return:1
depth=0 CN=app.lab.test
verify return:1
Certificate chain
0 s:CN=app.lab.test
i:C=SA, O=ArabRoot Lab, CN=ArabRoot Lab Intermediate CA
Verification error: unable to verify the first certificate
Verify return code: 21 (unable to verify the first certificate)
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the webpage mentioned above.ولاحظ أن الكلاينت يثق بالشهادة الرئيسية root.crt، ومع ذلك فشل، لأن السلسلة فيها شهادة واحدة فقط 0 s:، ولا يعرف الكلاينت الشهادة الوسيطة التي تربطها بالشهادة الرئيسية. الآن نوقف السيرفر ونشغله مرة أخرى مع الشهادة الوسيطة بالخيار -cert_chain:
pkill -x openssl
openssl s_server -accept 127.0.0.1:8443 -cert server.crt -key server.key -cert_chain inter.crt -www &
echo | openssl s_client -connect 127.0.0.1:8443 -servername app.lab.test -CAfile root.crt -verify_hostname app.lab.test 2>&1 | grep -E "depth|verify|Verif|Certificate chain| [0-9] s:| i:|Protocol|Cipher is"
curl -sS --cacert root.crt https://app.lab.test:8443/ -o /dev/null -w "%{http_code} %{ssl_verify_result}\n"والمخرج سوف يكون كما يلي:
depth=2 C=SA, O=ArabRoot Lab, CN=ArabRoot Lab Root CA
verify return:1
depth=1 C=SA, O=ArabRoot Lab, CN=ArabRoot Lab Intermediate CA
verify return:1
depth=0 CN=app.lab.test
verify return:1
Certificate chain
0 s:CN=app.lab.test
i:C=SA, O=ArabRoot Lab, CN=ArabRoot Lab Intermediate CA
1 s:C=SA, O=ArabRoot Lab, CN=ArabRoot Lab Intermediate CA
i:C=SA, O=ArabRoot Lab, CN=ArabRoot Lab Root CA
Verification: OK
Verified peername: app.lab.test
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Protocol: TLSv1.3
Verify return code: 0 (ok)
200 0والآن السلسلة شهادتان، والتحقق وصل إلى العمق 2، وVerified peername يؤكد أن الاسم في SAN، وcurl أعاد 200. والفرق بين الحالتين سطر واحد في الإعداد، وهذا السطر هو الفرق بين cert.pem وfullchain.pem.
الخطوة 7: إضافة جهتك الداخلية إلى مخزن الثقة
في المحاولات السابقة مررنا --cacert root.crt لكل أمر، أما إذا أردت أن يثق كل برنامج على السيرفر بجهتك الداخلية فأضف الشهادة الرئيسية إلى مخزن النظام، وفي Debian وUbuntu يتم ذلك كما يلي:
cp root.crt /usr/local/share/ca-certificates/arabroot-lab-root.crt && update-ca-certificates
curl -sS https://app.lab.test:8443/ -o /dev/null -w "%{http_code}\n"Updating certificates in /etc/ssl/certs...
rehash: warning: skipping ca-certificates.crt, it does not contain exactly one certificate or CRL
1 added, 0 removed; done.
Running hooks in /etc/ca-certificates/update.d...
done.
200ولاحظ أن الملف يجب أن ينتهي بالامتداد .crt وأن يكون PEM، وإلا تجاهله update-ca-certificates، والتحذير عن ca-certificates.crt طبيعي لأنه ملف الحزمة المجمع. وتذكر أن Container يحمل مخزنه الخاص، فإضافة الشهادة إلى السيرفر لا تضيفها إلى ال Containers التي عليه، ويجب أن تضيفها في ال Docker Image أو تربطها بها، ولكل لغة أحياناً مخزنها الخاص كما رأينا مع Java.
الخطوة 8: هل هذا المفتاح لهذه الشهادة؟
عندما تجد على السيرفر عدة ملفات .key و.crt ولا تعرف أي مفتاح لأي شهادة، فقارن المفتاح العام في الشهادة بالمفتاح العام المستخرج من المفتاح الخاص، فإذا تساوى الهاش فهما زوج واحد:
openssl x509 -in server.crt -noout -pubkey | openssl sha256
openssl pkey -in server.key -pubout | openssl sha256
openssl pkey -in inter.key -pubout | openssl sha256SHA2-256(stdin)= 565f2e9b344b5164ab29353ee23388e6dd6d8fbafc8cb77021b1ab35883e6d41
SHA2-256(stdin)= 565f2e9b344b5164ab29353ee23388e6dd6d8fbafc8cb77021b1ab35883e6d41
SHA2-256(stdin)= 7b25805af432e2622a014a640292aa74604f6701f46922db0287524d48753b7fالسطران الأولان متساويان، فالمفتاح server.key لهذه الشهادة، والثالث مختلف. وقد تجد في الشروحات القديمة مقارنة Modulus بالأمرين openssl x509 -noout -modulus وopenssl rsa -noout -modulus، وهذه تعمل مع RSA فقط وأعطت في تجربتنا الهاش نفسه للطرفين، أما مقارنة المفتاح العام فتعمل مع كل الأنواع، ولذلك هي الأنسب.
الخطوة 9: متى تنتهي الشهادة؟
الخيار -checkend يأخذ عدد ثوان ويخبرك هل تنتهي الشهادة خلالها، ويعيد رمز خروج Exit Code غير صفر إذا كانت ستنتهي، فيصلح لسكربت مراقبة يرسل تنبيهاً قبل 30 يوماً، أي 2592000 ثانية:
openssl x509 -in server.crt -noout -enddate -checkend 2592000
openssl x509 -in expired.crt -noout -enddate -checkend 0
echo | openssl s_client -connect github.com:443 -servername github.com 2>/dev/null | openssl x509 -noout -enddatenotAfter=Nov 21 23:28:27 2026 GMT
Certificate will not expire
notAfter=Sep 15 00:00:00 2026 GMT
Certificate will expire
notAfter=Nov 29 23:59:59 2026 GMTالأمر الأخير يقرأ تاريخ انتهاء شهادة أي موقع مباشرة من السيرفر، وهو أصدق من الملف على القرص، لأنه يكشف أيضاً السيرفر الذي جددت شهادته ولم يعد تحميلها. أما expired.crt فشهادة صنعناها منتهية عمداً بالخيارين -not_before و-not_after الموجودين في OpenSSL 3.4 وما بعده، حتى نرى الخطأ في القسم التالي.
إزالة التجربة
إذا نفذت التجربة على جهازك فاحذف المجلد pki، واحذف /usr/local/share/ca-certificates/arabroot-lab-root.crt ثم نفذ update-ca-certificates --fresh، والسبب أن أي شخص يحصل على root.key أو inter.key من هذا المجلد يستطيع أن يصدر شهادة لأي موقع وسوف يثق بها جهازك.
أين يوضع كل ملف؟
بعد أن عرفت الصيغ أصبح السؤال العملي: ماذا يطلب كل برنامج؟ والجواب في أغلب الحالات PEM، والفرق في عدد الملفات وترتيبها:
- Nginx وNginx Proxy Manager: ملفان PEM، السلسلة الكاملة في
ssl_certificateوالمفتاح فيssl_certificate_key، وفي واجهة NPM ثلاثة حقول للشهادة المخصصة: المفتاح والشهادة والشهادة الوسيطة، كما في قسم رفع شهادة مخصصة. - Traefik وCaddy: يصدران الشهادات ويجددانها بأنفسهما عبر ACME ويحفظانها في ملف
acme.jsonأو في مجلد البيانات، ولا تحتاج إلى أي ملف إلا إذا أردت شهادة من جهة أخرى، فتعطيهما حينها ملفي PEM، وتفاصيل التخزين في دليل Traefik ودليل Caddy. - HAProxy: ملف PEM واحد فيه شهادة النطاق ثم الوسيطة ثم المفتاح، أي
cat fullchain.pem privkey.pem، وقد شرحنا السكربت الذي يجهزه بعد كل تجديد في دليل HAProxy. - Apache: منذ 2.4.8 تكفي السلسلة الكاملة في
SSLCertificateFileوالمفتاح فيSSLCertificateKeyFile، وأصبحSSLCertificateChainFileقديماً Deprecated. - IIS وWindows: ملف PFX واحد يستورد إلى مخزن الشهادات في النظام، فإذا كانت لديك ملفات PEM فاجمعها بأمر
openssl pkcs12 -exportكما في قسم PKCS#12، وإذا رفض نظام Windows قديم الملف فأعد التصدير مع-legacy. - تطبيقات Java: مخزن PKCS12، وأحياناً JKS في التطبيقات القديمة، مع كلمة المرور والاسم المستعار Alias في إعداد التطبيق، ومخزن ثقة منفصل إذا كان التطبيق يتصل بخدمات داخلية بشهادات من جهتك.
- Kubernetes: Secret من النوع
kubernetes.io/tlsفيه المفتاحانtls.crtوtls.key، والقيم ملفات PEM نفسها بعد ترميز Base64 مرة ثانية، وهذا ترميز وليس تشفيراً كما شرحنا في مقال الترميز. والأمرkubectl create secret tls app-tls --cert=fullchain.pem --key=privkey.pemيقوم بالترميز عنك. - سيرفرات البريد: Postfix وDovecot يقرآن PEM أيضاً، السلسلة الكاملة والمفتاح، كما في قسم شهادة TLS في دليل Postfix وDovecot.
وقد يتساءل البعض: متى أحتاج إلى جهة إصدار داخلية مثل التي بنيناها؟ والإجابة أن الشهادات العامة لا تصدر إلا لنطاقات عامة، فإذا كانت لديك أسماء داخلية مثل db.lan، أو اتصالات بين خدماتك تريدها مشفرة بشهادات للطرفين mTLS، أو أجهزة لا تصل إلى الإنترنت، فأنت تحتاج إلى جهة داخلية. وبناؤها يدوياً بـ OpenSSL كما فعلنا مناسب للفهم وللمختبر، أما في الإنتاج Production فأداة مثل step-ca تعطيك جهة داخلية تدعم ACME، فيجدد Caddy وTraefik وCertbot الشهادات منها تلقائياً كما يفعلون مع Let's Encrypt. وأما للنطاقات العامة فلا يوجد سبب اليوم لشراء شهادة DV أو تجديدها يدوياً، فـ Let's Encrypt مع ACME هي الحل الأنسب، بالتحقق عبر HTTP أو عبر DNS للشهادات الشاملة.
openssl ولا تصلح لـ TLS، ولا تصلح شهادة TLS لـ SSH.الأخطاء الشائعة وكيف تقرؤها
كل الأخطاء التالية ظهرت لنا في التجربة نفسها، وأغلبها يظهر في curl برمز الخروج 60 مع سطر يحدد السبب، فاقرأ السطر الأول ولا تتوقف عند رمز الخطأ.
- الشهادة منتهية:
curl: (60) SSL certificate problem: certificate has expired، أو فيopenssl verifyالخطأerror 10 at 0 depth lookup: certificate has expired. والعمق يخبرك أي شهادة انتهت، فالعمق 0 شهادتك أنت، والسبب في الغالب أن التجديد الآلي فشل أو أن السيرفر لم يعد تحميل الشهادة الجديدة، فافحص التاريخ من السيرفر نفسه بأمرs_clientفي الخطوة 9. - السلسلة ناقصة:
unable to get local issuer certificateأوunable to verify the first certificate. السيرفر يرسلcert.pemبدلاً منfullchain.pemفي أغلب الحالات، والدليل أنs_clientيعرض شهادة واحدة فيCertificate chain. وإذا كانت السلسلة كاملة وظهر الخطأ نفسه فالشهادة من جهة داخلية لم تضف إلى مخزن الثقة عند الكلاينت. - الاسم لا يطابق:
curl: (60) SSL: no alternative certificate subject name matches target hostname 'wrong.lab.test'، أوVerify return code: 62 (hostname mismatch)في OpenSSL. الاسم الذي كتبته غير موجود في SAN، فإما أن الشهادة لم تشمله، أو أن السيرفر أرسل شهادة افتراضية لأنه لم يتعرف على SNI، كما في رسالة TRAEFIK DEFAULT CERT في دليل Traefik. - شهادة موقعة ذاتياً:
curl: (60) SSL certificate problem: self-signed certificateعندما يرسل السيرفر شهادة صنعها لنفسه كما فيopenssl req -x509. والحل شهادة من جهة عامة أو داخلية، ولا تحل المشكلة بالخيارcurl -kأوverify=Falseفي الكود، والسبب أنها تعطل التحقق من الهوية بالكامل فتعود إلى المشكلة التي بدأنا بها المقال. - شهادة موقعة ذاتياً داخل السلسلة:
verify error:num=19:self-signed certificate in certificate chain. السلسلة تنتهي بشهادة رئيسية لا يعرفها الكلاينت، وتراه كثيراً في الشركات التي تفحص حركة HTTPS بجهاز وسيط يعيد توقيع الاتصالات بجهته الخاصة، والحل إضافة الشهادة الرئيسية لتلك الجهة إلى مخزن الثقة. - مفتاح مشفر: إذا كان المفتاح
BEGIN ENCRYPTED PRIVATE KEYفسوف يطلب Nginx عبارة المرور ثم يفشل عند التشغيل في الخلفية برسالة طويلة تنتهي بـbad password read، والحل فك تشفير المفتاح مرة واحدة بالأمرopenssl pkey -in server-enc.key -out server.keyوحمايته بالصلاحيات. - الساعة غير مضبوطة:
error 9 at 0 depth lookup: certificate is not yet validعندما تكون ساعة الجهاز متأخرة عن تاريخ بداية الشهادة، أوcertificate has expiredلشهادة صالحة عندما تكون الساعة متقدمة، وتراه في الأجهزة التي فقدت بطارية الساعة وفي الأجهزة الافتراضية بعد استعادة Snapshot قديم. افحص الساعة قبل أن تشك في الشهادة، وقد شرحنا ضبطها في قسم مزامنة الساعة.
ملف PFX قديم في OpenSSL 3: عند فتح ملف صدر من نظام قديم بتشفير RC2 تظهر الرسالة التالية، والحل إضافة -legacy إلى أمر openssl pkcs12:
Error outputting keys and certificates
40A73C3D107F0000:error:0308010C:digital envelope routines:inner_evp_generic_fetch:unsupported:../crypto/evp/evp_fetch.c:376:Global default library context, Algorithm (RC2-40-CBC : 0), Properties ()صيغة خاطئة: عندما وضعنا ملف DER مكان PEM في Nginx 1.31.6 ظهرت الرسالة التالية، وno start line معناها أن البرنامج بحث عن سطر -----BEGIN فلم يجده، فإما أن الملف DER أو PFX، أو أن فيه شيئاً آخر مثل مفتاح في مكان الشهادة، والحل التحويل بأمر openssl x509 -inform DER -in file.cer -out file.pem:
nginx: [emerg] cannot load certificate "/etc/nginx/certs/fullchain.pem": PEM_read_bio_X509_AUX() failed (SSL: error:0480006C:PEM routines::no start line:Expecting: TRUSTED CERTIFICATE)المفتاح لا يطابق الشهادة: في Nginx تظهر الرسالة التالية عند nginx -t، وفي HAProxy الرسالة inconsistencies between private key and certificate، والحل مقارنة المفتاح العام كما في الخطوة 8:
nginx: [emerg] SSL_CTX_use_PrivateKey("/etc/nginx/certs/privkey.pem") failed (SSL: error:05800074:x509 certificate routines::key values mismatch)جدول الأوامر والصيغ
الجدول التالي يجمع الأوامر التي استخدمناها في المقال:
| المهمة | الأمر |
|---|---|
| قراءة شهادة | openssl x509 -in cert.pem -text -noout |
| الاسم والجهة والتواريخ فقط | openssl x509 -in cert.pem -noout -subject -issuer -dates -ext subjectAltName |
| شهادة موقع وسلسلته | echo | openssl s_client -connect host:443 -servername host -showcerts |
| توليد مفتاح | openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out key.pem |
| طلب شهادة CSR | openssl req -new -key key.pem -subj "/CN=host" -addext "subjectAltName=DNS:host" -out req.csr |
| قراءة CSR | openssl req -in req.csr -noout -text -verify |
| التحقق من السلسلة | openssl verify -CAfile root.pem -untrusted chain.pem cert.pem |
| PEM إلى DER | openssl x509 -in cert.pem -outform DER -out cert.der |
| DER إلى PEM | openssl x509 -inform DER -in cert.der -out cert.pem |
| PKCS#8 إلى PKCS#1 | openssl pkey -in key.pem -traditional -out key-pkcs1.pem |
| فك تشفير مفتاح | openssl pkey -in enc.key -out key.pem |
| صنع PKCS#12 | openssl pkcs12 -export -inkey key.pem -in cert.pem -certfile chain.pem -out bundle.p12 |
| شهادة من PKCS#12 | openssl pkcs12 -in bundle.p12 -clcerts -nokeys -out cert.pem |
| مفتاح من PKCS#12 | openssl pkcs12 -in bundle.p12 -nocerts -noenc -out key.pem |
| شهادات PKCS#7 إلى PEM | openssl pkcs7 -in chain.p7b -print_certs -out chain.pem |
| هل المفتاح للشهادة؟ | openssl x509 -in cert.pem -noout -pubkey | openssl sha256 ثم openssl pkey -in key.pem -pubout | openssl sha256 |
| هل تنتهي خلال 30 يوماً؟ | openssl x509 -in cert.pem -noout -checkend 2592000 |
| محتوى مخزن Java | keytool -list -v -keystore store.p12 |
والجدول التالي يقارن الصيغ:
| الصيغة | الترميز | ما تحمله | الامتداد المعتاد | من يستخدمها |
|---|---|---|---|---|
| PEM | نص Base64 بين BEGIN وEND | أي شيء، حسب سطر BEGIN | .pem .crt .key .csr | Linux، Nginx، Apache، HAProxy، Postfix، Kubernetes |
| DER | ثنائي ASN.1 | شهادة واحدة أو مفتاح | .der .cer | Windows، Java، الأجهزة المدمجة |
| PKCS#1 | PEM أو DER | مفتاح RSA فقط | .key | البرامج والإصدارات القديمة |
| PKCS#8 | PEM أو DER | مفتاح خاص من أي نوع، مشفر أو لا | .key .pem | OpenSSL 3 افتراضياً، وأغلب البرامج الحديثة |
| PKCS#7 | PEM أو DER | شهادات وسلسلة، دون مفتاح | .p7b .p7c | Windows، بعض الجهات التجارية |
| PKCS#12 | ثنائي بكلمة مرور | مفتاح وشهادة وسلسلة | .pfx .p12 | IIS وWindows، Java، الجوال والأجهزة |
| JKS | ثنائي خاص بـ Java | مفاتيح وشهادات | .jks | تطبيقات Java القديمة |
تدريب عملي
- اكتب اسم نطاقك في crt.sh، وراجع كل الشهادات الصادرة له، وتأكد أنك تعرف من طلب كل واحدة منها.
- نفذ على كل نطاق لديك الأمر
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | grep -A4 "Certificate chain"، وتأكد أن السلسلة فيها شهادتان على الأقل.
الخلاصة
وصلنا لنهاية الموضوع، وأهم ما فيه:
- التشفير دون التحقق من الهوية لا يحمي من المهاجم في منتصف الطريق، والشهادة تربط اسم النطاق بمفتاحه العام بتوقيع جهة إصدار يثق بها جهازك.
- الشهادة X.509 بيانات عامة: Subject وSAN وIssuer وValidity والمفتاح العام والاستخدامات، والذي يثبت أن السيرفر صاحبها هو توقيعه بالمفتاح الخاص في المصافحة.
- السلسلة تبدأ بشهادة النطاق ثم الوسيطة وتنتهي بشهادة رئيسية في مخزن الثقة، ويجب أن يرسل السيرفر الوسيطة دائماً، أي
fullchain.pemوليسcert.pem. - مدة الشهادات العامة تقصر إلى 47 يوماً في مارس 2029، وOCSP في طريقه إلى الزوال، فاجعل التجديد آلياً بالكامل وراقبه، وراقب نطاقك في سجلات الشفافية وقيده بسجل CAA.
- PEM وDER طريقتان لكتابة البايتات نفسها، وPKCS#1 وPKCS#8 للمفاتيح، وPKCS#7 للسلاسل، وPKCS#12 للحزمة الكاملة بكلمة مرور، والامتداد لا يحدد شيئاً، فافتح الملف واقرأ سطر BEGIN.
- أغلب الأخطاء سلسلة ناقصة، أو اسم لا يطابق، أو شهادة منتهية، أو مفتاح لا يطابق، أو صيغة خاطئة، وكل واحد منها له رسالة تقرؤها وأمر يكشفه في دقيقة.
دليلالترميز Encoding للمطورين: Base64 وأخواتهلماذا نحول البايتات إلى حروف حتى تمر في البريد والروابط، وكيف تفك أي سلسلة منها، ولماذا لا يحمي Base64 أي سر.
دليلHSTS وCSP: ترويسات الأمان التي يحتاجها كل تطبيقما الذي تطلبه كل ترويسة أمان من المتصفح، ولماذا يصعب التراجع عن HSTS، وكيف تبني سياسة CSP لا تكسر تطبيقك.
دليلNginx Proxy Manager: نطاقات وشهادات TLS لكل خدماتككيف تربط كل خدمة على سيرفرك بنطاقها وتمنحها شهادة HTTPS مجانية من واجهة بسيطة، دون أن تكتب إعدادات Nginx بنفسك.
دليلإدارة الهوية والوصول IAM والوصول المميز PAMمن يملك صلاحية root على سيرفراتك ومنذ متى، وكيف تضبط حسابات المديرين بصلاحيات مؤقتة وجلسات مسجلة.
دليلشرح DNS وسجلاته للمبتدئين بمثال حقيقيكيف يتحول اسم نطاقك إلى عنوان سيرفرك، وما وظيفة كل سجل تضيفه في لوحة النطاق، والأخطاء التي تضيع فيها الساعات عادة.سجل التحديثات
- أكتوبر 2026: كتابة المقال، وتنفيذ الأمثلة على Debian 13.7 مع OpenSSL 3.5.7 وcurl 8.14.1، وNginx 1.31.6، وkeytool من Eclipse Temurin 25.0.4.1، وقراءة شهادة github.com كما كانت في أكتوبر 2026.
- أكتوبر 2026: إضافة رابط في قسم أنواع الشهادات إلى مقال إشارة SNI والشهادة متعددة الأسماء SAN.