الأدلة التقنية

الشهادات الرقمية TLS للمبتدئين وصيغ ملفاتها

كيف يعرف المتصفح أنه يتحدث مع الموقع الحقيقي، وماذا يوجد داخل الشهادة، ولماذا تأتي ملفاتها بأكثر من صيغة.

الشهادات الرقمية TLS للمبتدئين وصيغ ملفاتها

عندما تفتح موقع البنك في المتصفح وترى القفل بجانب العنوان، فأنت تثق في أمرين معاً دون أن تفكر فيهما: أن البيانات بينك وبين السيرفر مشفرة فلا يقرؤها أحد في الطريق، وأن السيرفر الذي تتحدث معه هو سيرفر البنك فعلاً وليس جهازاً آخر ينتحل اسمه. والأمر الأول سهل نسبياً، أما الثاني فهو المشكلة الحقيقية، لأن التشفير مع الطرف الخطأ لا يحميك من شيء، والحل الذي يعتمد عليه الإنترنت كله لهذه المشكلة هو الشهادة الرقمية 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. الصورة التالية تبين أجزاء الشهادة:

بطاقة شهادة مرسومة فيها الحقول Subject وSAN وIssuer وValidity وSerial وPublic Key وKey Usage، وفي أسفلها ختم أحمر يمثل توقيع جهة الإصدار، وسهم من جهة الإصدار ومفتاحها الخاص إلى الختم، وملاحظة أن التوقيع يغطي كل الحقول فإذا تغير حرف واحد بطلت الشهادة
الشهادة بيانات عامة يغطيها توقيع جهة الإصدار، فلا أحد يستطيع أن يغير الاسم أو المفتاح دون أن يبطل التوقيع

نقرأ شهادة حقيقية: 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:

  1. شهادة النطاق Leaf Certificate: شهادة موقعك، موقعة من الشهادة الوسيطة، وCA:FALSE.
  2. الشهادة الوسيطة Intermediate Certificate: موقعة من الشهادة الرئيسية، وCA:TRUE، وقد تكون هناك أكثر من شهادة وسيطة.
  3. الشهادة الرئيسية 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.com موقعة من الشهادة الوسيطة Sectigo DV E36، والوسيطة موقعة من الشهادة الرئيسية، والشهادة الرئيسية موجودة داخل مخزن الثقة في نظام التشغيل والمتصفح، والسيرفر يرسل الشهادتين الأوليين معاً، والكلاينت يتحقق من التوقيعات صعوداً بخطوات مرقمة
السيرفر يرسل شهادته والشهادة الوسيطة، والشهادة الرئيسية موجودة مسبقاً في جهاز الزائر

ويمكنك أن ترى السلسلة التي يرسلها 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 مارس 2026398 يوماً398 يوماً
من 15 مارس 2026200 يوم200 يوم
من 15 مارس 2027100 يوم100 يوم
من 15 مارس 202947 يوماً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 منشورة للجميع بمجرد أن تصدر لها شهادة عامة، فلا تعتمد على إخفاء الاسم كوسيلة حماية.

📌
في 29 أغسطس 2011 ظهرت شهادة مزورة لـ *.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 كما يلي:

  1. ClientHello: يرسل الكلاينت الخوارزميات التي يدعمها، وجزءه من تبادل المفاتيح Key Share، واسم النطاق الذي يريده في الامتداد SNI، والسبب أن سيرفراً واحداً قد يحمل شهادات لعشرات النطاقات.
  2. ServerHello: يرد السيرفر بالخوارزمية التي اختارها وجزئه من تبادل المفاتيح، ومن هذه اللحظة يملك الطرفان مفاتيح مشتركة، فيصبح كل ما بعدها مشفراً، بما في ذلك الشهادة نفسها، وهذا من الفروق عن TLS 1.2 الذي كان يرسل الشهادة دون تشفير.
  3. Certificate: يرسل السيرفر شهادته والشهادات الوسيطة.
  4. CertificateVerify: يوقع السيرفر بمفتاحه الخاص على كل رسائل المصافحة حتى هذه اللحظة، وهذا هو الإثبات أنه يملك المفتاح الخاص المقابل للشهادة، فالمهاجم يستطيع أن ينسخ شهادة GitHub لأنها عامة، ولكنه لا يستطيع أن ينتج هذا التوقيع.
  5. الفحص عند الكلاينت: يبني السلسلة حتى شهادة رئيسية في مخزنه، ويتحقق من كل توقيع فيها، ومن أن الوقت الحالي داخل مدة كل شهادة، ومن أن اسم النطاق الذي طلبه موجود في SAN، ومن أن Extended Key Usage يسمح بـ TLS Server، ثم يتحقق من توقيع CertificateVerify بالمفتاح العام في الشهادة. فإذا فشل أي فحص أوقف الاتصال وعرض المتصفح صفحة التحذير.
  6. Finished: يؤكد الطرفان أن أحداً لم يعدل رسائل المصافحة، ثم تبدأ البيانات المشفرة بمفاتيح الجلسة.

الصورة التالية تبين المصافحة ومكان الفحص فيها:

مخطط تسلسلي بين متصفح وسيرفر: ClientHello مع SNI وKey Share، ثم ServerHello مع Key Share، ثم رسائل مشفرة Certificate وCertificateVerify وFinished، ثم مربع أخضر عند المتصفح فيه خطوات الفحص الخمس: السلسلة حتى مخزن الثقة، والتوقيعات، والتاريخ، والاسم في SAN، وتوقيع CertificateVerify، ثم Finished من المتصفح والبيانات المشفرة
في TLS 1.3 تصل الشهادة مشفرة، ويفحصها الكلاينت قبل أن يرسل أي بيانات للتطبيق

ولاحظ أن كل هذا يحمي الاتصال الذي يبدأ بـ 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 RequestPEM غالباً
.p7b و.p7cسلسلة شهادات دون مفتاح، PKCS#7PEM أو 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 -subject
subject=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، وهي صيغة عامة لأي نوع من المفاتيح، وفيها حقل يذكر الخوارزمية، ولذلك جاء مفتاح RSA server.key ومفتاح EC root.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 LabPass123
Certificate 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 LabPass123
Importing 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".

الصورة التالية تجمع الصيغ كلها وما يحمله كل منها:

خريطة للصيغ: في الأعلى ملف DER ثنائي يبدأ بالبايتين 30 82 يتحول عبر Base64 إلى ملف PEM نصي بين سطري BEGIN وEND، وفي الأسفل جدول مرسوم فيه PKCS#1 وPKCS#8 وPKCS#7 وPKCS#12 وJKS، ولكل منها علامات لما يحمله: مفتاح خاص، شهادة، سلسلة، كلمة مرور، مع الامتدادات الشائعة لكل صيغة
PEM وDER طريقتان لكتابة البايتات، وPKCS تحدد ما بداخل الملف، والامتداد مجرد عادة

ملفات 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، لذلك يطلب التوثيق أن تشير إعدادات السيرفر إليها مباشرة ولا تنسخها، فالنسخة المنسوخة لن تتجدد. الصورة التالية تبين كل ملف ومكانه:

مجلد live/example.com فيه أربعة ملفات: privkey.pem بمفتاح ذهبي، وcert.pem بشهادة واحدة، وchain.pem بالشهادة الوسيطة، وfullchain.pem بالشهادتين معاً، وأسهم من fullchain.pem وprivkey.pem إلى Nginx وApache، ومن دمج الملفين إلى HAProxy، ومن التحويل إلى PKCS#12 إلى IIS وJava، وعلامة خطأ على استخدام cert.pem وحده
أغلب البرامج تحتاج fullchain.pem وprivkey.pem، وHAProxy يريدهما في ملف واحد، وWindows وJava يريدان PKCS#12

تجربة عملية: نبني جهة إصدار صغيرة ونصدر شهادة

الآن سوف نطبق كل ما سبق بأيدينا، فنبني جهة إصدار داخلية بشهادة رئيسية وشهادة وسيطة، ثم نصدر شهادة لسيرفر، ونتحقق منها، ونشغل سيرفر 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 sha256
SHA2-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 -enddate
notAfter=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 للشهادات الشاملة.

💡
شهادات SSH التي شرحناها في دليل IAM وPAM تقوم على الفكرة نفسها، جهة توقع مفتاح المستخدم لمدة محددة، ولكنها صيغة خاصة بـ OpenSSH وليست X.509، فلا تفتحها أوامر openssl ولا تصلح لـ TLS، ولا تصلح شهادة TLS لـ SSH.

الأخطاء الشائعة وكيف تقرؤها

كل الأخطاء التالية ظهرت لنا في التجربة نفسها، وأغلبها يظهر في curl برمز الخروج 60 مع سطر يحدد السبب، فاقرأ السطر الأول ولا تتوقف عند رمز الخطأ.

  1. الشهادة منتهية: curl: (60) SSL certificate problem: certificate has expired، أو في openssl verify الخطأ error 10 at 0 depth lookup: certificate has expired. والعمق يخبرك أي شهادة انتهت، فالعمق 0 شهادتك أنت، والسبب في الغالب أن التجديد الآلي فشل أو أن السيرفر لم يعد تحميل الشهادة الجديدة، فافحص التاريخ من السيرفر نفسه بأمر s_client في الخطوة 9.
  2. السلسلة ناقصة: unable to get local issuer certificate أو unable to verify the first certificate. السيرفر يرسل cert.pem بدلاً من fullchain.pem في أغلب الحالات، والدليل أن s_client يعرض شهادة واحدة في Certificate chain. وإذا كانت السلسلة كاملة وظهر الخطأ نفسه فالشهادة من جهة داخلية لم تضف إلى مخزن الثقة عند الكلاينت.
  3. الاسم لا يطابق: 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.
  4. شهادة موقعة ذاتياً: curl: (60) SSL certificate problem: self-signed certificate عندما يرسل السيرفر شهادة صنعها لنفسه كما في openssl req -x509. والحل شهادة من جهة عامة أو داخلية، ولا تحل المشكلة بالخيار curl -k أو verify=False في الكود، والسبب أنها تعطل التحقق من الهوية بالكامل فتعود إلى المشكلة التي بدأنا بها المقال.
  5. شهادة موقعة ذاتياً داخل السلسلة: verify error:num=19:self-signed certificate in certificate chain. السلسلة تنتهي بشهادة رئيسية لا يعرفها الكلاينت، وتراه كثيراً في الشركات التي تفحص حركة HTTPS بجهاز وسيط يعيد توقيع الاتصالات بجهته الخاصة، والحل إضافة الشهادة الرئيسية لتلك الجهة إلى مخزن الثقة.
  6. مفتاح مشفر: إذا كان المفتاح BEGIN ENCRYPTED PRIVATE KEY فسوف يطلب Nginx عبارة المرور ثم يفشل عند التشغيل في الخلفية برسالة طويلة تنتهي بـ bad password read، والحل فك تشفير المفتاح مرة واحدة بالأمر openssl pkey -in server-enc.key -out server.key وحمايته بالصلاحيات.
  7. الساعة غير مضبوطة: 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
طلب شهادة CSRopenssl req -new -key key.pem -subj "/CN=host" -addext "subjectAltName=DNS:host" -out req.csr
قراءة CSRopenssl req -in req.csr -noout -text -verify
التحقق من السلسلةopenssl verify -CAfile root.pem -untrusted chain.pem cert.pem
PEM إلى DERopenssl x509 -in cert.pem -outform DER -out cert.der
DER إلى PEMopenssl x509 -inform DER -in cert.der -out cert.pem
PKCS#8 إلى PKCS#1openssl pkey -in key.pem -traditional -out key-pkcs1.pem
فك تشفير مفتاحopenssl pkey -in enc.key -out key.pem
صنع PKCS#12openssl pkcs12 -export -inkey key.pem -in cert.pem -certfile chain.pem -out bundle.p12
شهادة من PKCS#12openssl pkcs12 -in bundle.p12 -clcerts -nokeys -out cert.pem
مفتاح من PKCS#12openssl pkcs12 -in bundle.p12 -nocerts -noenc -out key.pem
شهادات PKCS#7 إلى PEMopenssl 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
محتوى مخزن Javakeytool -list -v -keystore store.p12

والجدول التالي يقارن الصيغ:

الصيغةالترميزما تحملهالامتداد المعتادمن يستخدمها
PEMنص Base64 بين BEGIN وENDأي شيء، حسب سطر BEGIN.pem .crt .key .csrLinux، Nginx، Apache، HAProxy، Postfix، Kubernetes
DERثنائي ASN.1شهادة واحدة أو مفتاح.der .cerWindows، Java، الأجهزة المدمجة
PKCS#1PEM أو DERمفتاح RSA فقط.keyالبرامج والإصدارات القديمة
PKCS#8PEM أو DERمفتاح خاص من أي نوع، مشفر أو لا.key .pemOpenSSL 3 افتراضياً، وأغلب البرامج الحديثة
PKCS#7PEM أو DERشهادات وسلسلة، دون مفتاح.p7b .p7cWindows، بعض الجهات التجارية
PKCS#12ثنائي بكلمة مرورمفتاح وشهادة وسلسلة.pfx .p12IIS و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.
نشرة عرب رووت | ArabRoot

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.