Skip to content
العودة إلى المدوّنة
الأمن

خطأ تنسيق المفتاح الخاص RS256: رسالة واحدة وسبعة أسباب

يُرجع Node نفس خطأ DECODER عند اختلاف الحاوية أو إزاحة سطر الترويسة أو استخدام المفتاح العام. جداول حلول مختبرة لـ PKCS#1 وPKCS#8 ومولّد مفاتيح عبر الإنترنت.

13 دقيقة للقراءة

خطأ تنسيق المفتاح الخاص RS256: رسالة واحدة وسبعة أسباب

خطأ تنسيق المفتاح الخاص في RS256 نادراً ما يفصح عن سببه. فعلى Node v25.8.2، كل واحد من هذه الأخطاء يُنتِج السطر نفسه حرفياً:

code:    ERR_OSSL_UNSUPPORTED
message: error:1E08010C:DECODER routines::unsupported

خمسة أمور لا رابط بينها تُطلِق هذا السطر: حاوية OpenSSH في موضع كان يُنتظَر فيه مفتاح PEM، وسطر -----BEGIN مُزاح عن أول العمود، ومفتاح عام سُلِّم إلى مُوقِّع، وتسلسلات \n حرفية لم يفكّ أحد تهريبها، وملفّ جُرِّدت فواصل أسطره أثناء النقل. والقائمة المختبَرة في القسم 2 تبلغ سبعة. الكلمات نفسها في كل مرة، ولهذا فالبحث بنصّ الخطأ يُلقي بك في نقاش شخص آخر عن سبب آخر.

قبل كل شيء، اقسم المشكلة نصفين:

وفرزٌ من ثلاثين ثانية للحالة الأولى:

openssl rsa -in key.pem -noout -text | head -1

فإن أخفق هذا الأمر، فالملفّ هو المشكلة وستعثر عليها الأقسام من 3 إلى 6. وإن نجح، فقد فهم OpenSSL الحاوية، ومشكلتك في المكتبة أو فيما سلّمتها إياه، وذلك هو القسمان 4 و7.

كل ما يلي مقيس بتاريخ 2026-08-11 على OpenSSL 3.6.2 7 Apr 2026 وNode v25.8.2 وGo go1.26.1 darwin/arm64 وJava 1.8.0_162. وحيثما جاءت المعلومة من قراءة الشيفرة المصدرية لا من تشغيلها، يقول النصّ ذلك صراحةً.

1. أولاً، حدِّد أيّ إخفاق تواجه

الفارق كله في سؤال واحد: هل وُجِد كائن مفتاح أم لا؟ وإخفاقات وقت التحليل تقع قبل أن تعمل أي عملية تعمية. تقرأ المكتبة ملفّ PEM لديك، وتعجز عن تحويله إلى مفتاح، فتُلقي الخطأ. لم يُوقَّع شيء، ولم يُتحقَّق من شيء، والرمز الذي تصحّح خطأه لم يُنتَج قطّ. أمّا إخفاقات وقت التحقّق فعكس ذلك تماماً: حُمِّل المفتاح بنجاح، وحُسِب توقيع، ولم يتطابق. وهذه تنشأ عن خلافات على مستوى البايتات بين المُوقِّع والمُتحقِّق، ويغطّيها دليل invalid signature.

والتمييز بينهما يحتاج نظرة واحدة إلى أثر المكدس. فإخفاق وقت التحليل يسمّي مُفكِّك ترميز أو مواصفة مفتاح أو بنية ASN.1. وإخفاق وقت التحقّق يسمّي توقيعاً.

وهكذا يبدو المفتاح الخاص المرفوض في ثلاث بيئات:

بيئة التشغيلالإصدار المختبَرالرسالة حين يرفض المفتاح أن يُحمَّل
Node cryptov25.8.2error:1E08010C:DECODER routines::unsupported
Go crypto/x509go1.26.1 darwin/arm64x509: failed to parse private key (use ParsePKCS1PrivateKey instead for this key format)
Java PKCS8EncodedKeySpec1.8.0_162InvalidKeySpecException: java.security.InvalidKeyException: IOException : algid parse error, not a sequence

لاحظ كم تتفاوت في مقدار العون الذي تقدّمه. فـGo تخبرك بالضبط أيّ دالة تستدعي بدلاً عنها. وJava تذكر «algid» و«sequence» ثم تتركك تستنتج وحدك أن المقصود هو أن مفتاحك في الحاوية الخطأ. أمّا Node فلا تقول شيئاً صالحاً للاستعمال إطلاقاً.

وإن لم تكن واثقاً أن الرمز الذي تطارده هو RS256 أصلاً، فالصقه في محلّل JWT واقرأ alg من الترويسة قبل أن تمضي أبعد. فترويسة HS256 تعني أنك تحتاج سرّاً مشتركاً لا زوج مفاتيح، وعندها سيدلّك كل عَرَض في هذا المقال إلى الاتجاه الخطأ.

2. من نصّ الخطأ إلى السبب الجذري: جدول البحث العكسي لخطأ تنسيق المفتاح الخاص RS256

ابحث عن سلسلتك بالضبط. والعمود الأخير يدلّك على وجهتك التالية.

نصّ الخطأمن أين يأتيما يعنيه فعلياً
error:1E08010C:DECODER routines::unsupportedNode v25.8.2سبعة أسباب محتملة، مسرودة أدناه
error:07880109:common libcrypto routines::interrupted or cancelledNode v25.8.2المفتاح مشفَّر ولم تُقدِّم عبارة مرور
x509: failed to parse private key (use ParsePKCS8PrivateKey instead for this key format)Go 1.26.1استدعيت ParsePKCS1PrivateKey على ملفّ PKCS#8
x509: failed to parse private key (use ParsePKCS1PrivateKey instead for this key format)Go 1.26.1استدعيت ParsePKCS8PrivateKey على ملفّ PKCS#1
asn1: structure error: tags don't match (16 vs {class:0 tag:6 ...})Go 1.26.1أول كتلة PEM هي EC PARAMETERS لا المفتاح
algid parse error, not a sequenceJava 1.8.0_162مفتاح PKCS#1 مُرِّر إلى PKCS8EncodedKeySpec
secretOrPrivateKey must have a valuejsonwebtoken، في الشيفرة المصدريةوسيط المفتاح قيمته زائفة وalg ليست none
secretOrPrivateKey is not valid key materialjsonwebtoken، في الشيفرة المصدريةتعذّر بناء مفتاح خاص أو مفتاح سرّي على السواء
secretOrPrivateKey must be a symmetric key when using ${header.alg}jsonwebtoken، في الشيفرة المصدريةalg تبدأ بـHS لكن المفتاح ليس سرّاً
secretOrPrivateKey must be an asymmetric key when using ${header.alg}jsonwebtoken، في الشيفرة المصدريةalg تطابق RS أو PS أو ES لكن المفتاح ليس خاصاً
secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg}jsonwebtoken، في الشيفرة المصدريةRS أو PS بمفتاح دون 2048 بت وallowInsecureKeySizes مُعطَّل

سلاسل secretOrPrivateKey الخمس مقروءة من ملفّ sign.js على فرع master في مستودع jsonwebtoken، ولم تُشغَّل محلياً، فتعامَل مع شروط إطلاقها بوصفها ما تقوله الشيفرة المصدرية لا بوصفها شيئاً أُعيد إنتاجه على هذا الجهاز. والجزء ${header.alg} عنصر نائب في قالب داخل تلك الشيفرة؛ وفي وقت التشغيل سترى مكانه اسم خوارزميتك أنت، ولهذا لا يعثر البحث عن السلسلة الحرفية بأقواسها المعقوفة على شيء.

السبل السبعة لإنتاج DECODER routines::unsupported

أُعيد إنتاج السبعة كلّها مقابل crypto.createPrivateKey() على Node v25.8.2، وأعطت السبعة الرمز والرسالة نفسيهما:

  1. حاوية OpenSSH. يبدأ الملفّ بـ-----BEGIN OPENSSH PRIVATE KEY----- وليس بنية مفتاح PEM أصلاً.
  2. سطر -----BEGIN مُزاح، أو سطر -----END مُزاح. أسطر المتن مستثناة؛ والقسم 5 يضبط الحدّ بدقة.
  3. مسافة بيضاء قبل ملفّ PEM كلّه. السطر الفارغ في البداية لا بأس به، أمّا المسافة في البداية فلا.
  4. حذف فواصل الأسطر كلّها، فتلتصق الترويسة والـbase64 والتذييل في سطر واحد.
  5. مفتاح عام في موضع كان يُنتظَر فيه مفتاح خاص.
  6. تسلسلات شرطة مائلة عكسية مع حرف n تُركت من دون فكّ تهريب، وهي ما يتحوّل إليه متغيّر بيئة من سطر واحد.
  7. فاصل بعدد خاطئ من الشرطات، أو كتابة begin وend بحروف صغيرة.

اثنان من هذه مشكلات حاوية، وأربعة مشكلات عبث بالنصّ، وواحد خلط بسيط. والرسالة عاجزة عن إخبارك أيّها هو، ولذا فأسرع طريق أمامك هو إقصاؤها واحداً بعد آخر.

ما الذي يقبله Node

القائمة العكسية أنفع، لأن كل بند فيها فرضية تستطيع إسقاطها فوراً. فعلى Node v25.8.2، قبِلت crypto.createPrivateKey() كل ما يلي من دون اعتراض:

  • مفاتيح PKCS#1 وPKCS#8 الخاصة
  • مفاتيح EC SEC1 الخاصة
  • نهايات أسطر CRLF
  • غياب سطر جديد في النهاية
  • متن base64 في سطر واحد غير مطويّ
  • أسطر متن مُزاحة
  • سطر فارغ قبل ملفّ PEM
  • علامة ترتيب البايتات UTF-8 BOM، في الصيغتين معاً: '' + pem وكذلك Buffer يبدأ بـ0xEF 0xBB 0xBF
  • ترويسة PKCS#1 ملفوفة حول متن PKCS#8

وفي البند الأخير، يقرأ مُفكِّك الترميز بنية DER داخل الـbase64 ويتجاهل الملصَق الذي في الخارج، فالملفّ الذي يقول BEGIN RSA PRIVATE KEY فوق محتوى PKCS#8 يُحمَّل على أي حال. وهذا يفيدك في التشخيص، ويحذّرك أيضاً مما يأتي في القسم 3: سطر الترويسة قرينة وليس ضمانة.

3. سطر ترويسة PEM: أيّ حاوية بين يديك فعلاً

كل ملفّ PEM يعلن عن نفسه في سطره الأول. وهذه هي قيم الترويسة التي تكتبها OpenSSL 3.6.2:

المحتوىالسطر الأول
مفتاح PKCS#8 خاص-----BEGIN PRIVATE KEY-----
مفتاح PKCS#1 خاص-----BEGIN RSA PRIVATE KEY-----
مفتاح خاص مشفَّر-----BEGIN ENCRYPTED PRIVATE KEY-----
مفتاح OpenSSH خاص-----BEGIN OPENSSH PRIVATE KEY-----
مفتاح EC SEC1 خاص-----BEGIN EC PARAMETERS-----، ثم كتلة ثانية -----BEGIN EC PRIVATE KEY-----
مفتاح SPKI عام-----BEGIN PUBLIC KEY-----
مفتاح PKCS#1 عام-----BEGIN RSA PUBLIC KEY-----
مفتاح Ed25519 خاص-----BEGIN PRIVATE KEY-----، والملفّ كلّه ثلاثة أسطر

فأمر head -1 key.pem إذاً يجيب عن أول سؤال في أي تحقيق. وفي الجدول ثلاثة صفوف تخدع أكثر من غيرها.

ENCRYPTED PRIVATE KEY ليس خطأ تنسيق. بل هو عبارة مرور نسيت تمريرها. وNode تُبلِّغ عنه بصورة مغايرة لكل ما عداه، بالرمز ERR_OSSL_CRYPTO_INTERRUPTED_OR_CANCELLED والرسالة error:07880109:common libcrypto routines::interrupted or cancelled، لأن المكتبة طلبت عبارة مرور فلم يعُد إليها شيء. لا تخلط هذه الرسالة برسالة DECODER؛ فلا صلة بينهما.

OPENSSH PRIVATE KEY عالم آخر بالكامل. يكتب OpenSSH حاويته الخاصة، وهي ليست PKCS#1 ولا PKCS#8 رغم جلوسها بين فواصل تشبه فواصل PEM. وNode ترفضها، وكذلك تفعل محلّلات crypto/x509 في Go وصنف PKCS8EncodedKeySpec في JDK. فإن كان مفتاح توقيع JWT لديك صادراً عن ssh-keygen، فهذه هي المشكلة.

ملفّات EC SEC1 تحمل كتلتين. يكتب openssl ecparam -genkey كتلة EC PARAMETERS أولاً والمفتاح الخاص ثانياً. وكل ما يقرأ كتلة PEM الأولى وحدها يحصل على المعاملات ويفشل بطريقة لا تذكر أياً من الاثنين. والقسم 4 يعرض نسخة Go من ذلك الإخفاق.

ولأن الترويسة مجرّد ملصَق، فالفحص المعاكس مهمّ أيضاً: الملفّ الذي تقول ترويسته شيئاً وتقول بنية DER فيه شيئاً آخر يُحلَّل بحسب الـDER. فقراءة head -1 موثوقة للملفّات الخارجة مباشرةً من OpenSSL، وغير موثوقة للملفّات التي مرّت بيد إنسان أو بصفحة ويكي أو بسكربت يستبدل السلاسل النصية.

4. أيّ مكتبة تقبل ماذا: PKCS#1 مقابل PKCS#8 عبر ثلاث بيئات

هذه هي المصفوفة التي تفسّر معظم الخلافات على التنسيق بين الفرق. وكل صفّ فيها مقيس على الإصدارات المذكورة في صدر هذا المقال.

المكتبةPKCS#1PKCS#8OpenSSHهل يشرح الخطأ نفسه؟
Node cryptoنعمنعملالا. أسباب كثيرة ورسالة DECODER routines::unsupported واحدة
Go crypto/x509نعم، بدالة مخصّصةنعم، بدالة مخصّصةلانعم. تسمّي الدالة التي ينبغي الانتقال إليها
المكتبة القياسية في Javaلانعملالا. الرسالة algid parse error, not a sequence مضلّلة بامتياز

يكفي أن تقرأ الأعمدة رأسياً لتنحلّ أكثر هذه الخلافات. فخدمة على Node وخدمة على Java تتشاركان ملفّ مفتاح واحد تعملان بلا مشكلة إلى أن يصير المفتاح PKCS#1، فتواصل Node التوقيع وتُلقي Java رسالة عن متتاليات ASN.1. ولا أحد يشكّ في المفتاح، لأنه يعمل في الإنتاج على الخدمة الأخرى أمام أعينهم.

Node. لا شيء تضبطه هنا. فإن كانت الحاوية PKCS#1 أو PKCS#8، قبِلتها createPrivateKey(). وحين تُلقي خطأً، ابحث في الأسباب السبعة في القسم 2 قبل أن تشكّ في التنسيق.

const fs = require('node:fs');
const { createPrivateKey } = require('node:crypto');

try {
  const key = createPrivateKey(fs.readFileSync('key.pem'));
  console.log('parsed:', key.asymmetricKeyType);
} catch (err) {
  console.log(err.code, '/', err.message);
}

شغّل ذلك على الملفّ الذي يحمّله تطبيقك فعلاً، لا على نسخة صنعتها بيدك، ليطبع فرع الالتقاط زوج الرمز والرسالة الذي تبحث عنه في القسم 2.

Go. حاويتان ودالّتان، واستدعاء الخطأ منهما هو أشيع إخفاق في Go. والرسالة تخبرك أيّهما تستخدم، فالإصلاح ميكانيكي. وتجريب الاثنتين بالترتيب يعفيك من الاختيار:

priv, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
	rsaKey, err2 := x509.ParsePKCS1PrivateKey(block.Bytes)
	if err2 != nil {
		log.Fatalf("neither container parsed: %v / %v", err, err2)
	}
	priv = rsaKey
}

غير أن فخّ EC يحتاج معالجة قبل ذلك. فمع ملفّ خارج من openssl ecparam -genkey، تُرجِع pem.Decode كتلةً نوعها EC PARAMETERS، وتفشل عليها دوالّ التحليل الثلاث كلّها بالرسالة:

asn1: structure error: tags don't match (16 vs {class:0 tag:6 ...})

وتلك الرسالة لا تذكر كتل PEM إطلاقاً، فيكون ردّ الفعل المعتاد هو الشكّ في المفتاح. تخطَّ كتلة المعاملات بدلاً من ذلك:

block, rest := pem.Decode(pemBytes)
if block == nil {
	log.Fatal("no PEM block found")
}
if block.Type == "EC PARAMETERS" {
	block, _ = pem.Decode(rest)
}

أو تجنّب إنتاج الكتلة الزائدة من الأساس بإضافة -noout إلى أمر ecparam الذي يكتب الملفّ.

Java. المكتبة القياسية تقرأ PKCS#8 ولا شيء غيره. وحين تمرّر إلى PKCS8EncodedKeySpec مفتاح PKCS#1 على Java 1.8.0_162 تحصل على:

InvalidKeySpecException: java.security.InvalidKeyException: IOException : algid parse error, not a sequence

و«algid» هو مُعرِّف الخوارزمية، أي الحقل الذي يضيفه PKCS#8 ولا يملكه PKCS#1. بحث المحلّل عنه، فوجد بداية معامل RSA، فاستسلم. والرسالة صحيحة وعديمة النفع بالقدر نفسه. وتحويل الملفّ يُنهي الخطأ:

openssl pkcs8 -topk8 -nocrypt -in pkcs1.pem -out pkcs8.pem

ومسار التحميل العامل في Java 8، بعد أن يصير الملفّ PKCS#8، قصير بما يكفي لتضمينه داخل اختبار بينما تتأكّد من الإصلاح:

String pem = new String(Files.readAllBytes(Paths.get("key.pem")), StandardCharsets.UTF_8)
        .replace("-----BEGIN PRIVATE KEY-----", "")
        .replace("-----END PRIVATE KEY-----", "")
        .replaceAll("\\s+", "");
byte[] der = Base64.getDecoder().decode(pem);
PrivateKey key = KeyFactory.getInstance("RSA")
        .generatePrivate(new PKCS8EncodedKeySpec(der));

والبديل عن التحويل هو إضافة BouncyCastle، وهي تقرأ PKCS#1 فعلاً. لكن التحويل أمر واحد بلا اعتمادية جديدة، فحوِّل ما لم يكن شيء آخر في مكدّسك يحتاج المكتبة.

5. المحارف التي لا تراها

النصيحة الشائعة في هذا الباب صحيحة في موضع واحد، ومقلوبة أو زائدة في الباقي.

الإزاحة: عكس ما قيل لك تماماً

ثمة إرشاد يتكرّر في كل مكان يقول إن كل سطر في ملفّ PEM عدا الفواصل يجب أن يبدأ من العمود صفر. وبالاختبار على Node v25.8.2، الأمر مقلوب:

التغيير على الملفّالنتيجة
إزاحة كل الأسطريفشل
إزاحة سطر -----BEGIN وحدهيفشل
إزاحة سطر -----END وحدهيفشل
إزاحة أسطر متن الـbase64 وحدهامقبول
مسافة قبل ملفّ PEM كلّهيفشل
سطر فارغ قبل ملفّ PEM كلّهمقبول

فالقاعدة إذاً هي: سطرا -----BEGIN و-----END يجب أن يبدآ من العمود صفر، وإزاحة أسطر المتن لا تهمّ. أي أن السطرين اللذين يُقال للناس إن بوسعهم إزاحتهما هما بالضبط السطران اللذان ينكسران، والأسطر التي يُقال لهم أن يحاذوها هي التي تملك الفسحة.

ولهذا أثر عملي، لأن الإزاحة لا تنشأ عشوائياً. فلا أحد يُزيح ملفّ PEM بيده. إنما يحدث ذلك حين يُلصَق مفتاح داخل كتلة YAML، أو ملفّ قيم Helm، أو مستند heredoc في Terraform، أو سلسلة نصية ثلاثية الاقتباس في Python داخل جسم صنف. وكل واحد من هذه يُزيح الكلّ إزاحة موحّدة بما فيه الفواصل، وهو الصفّ الأول من ذلك الجدول.

تسلسلات الشرطة المائلة العكسية مع n من متغيّر بيئة في سطر واحد

ملفّ PEM يحوي فواصل أسطر، ومتغيّر البيئة عملياً لا يحويها. فتنتهي المفاتيح في ملفّات .env سطراً واحداً مكتوباً فيه \n محرفين اثنين. وأياً كان ما يقرأ ذلك الملفّ فإنه يسلّم شيفرتك سلسلة نصية فيها شرطات مائلة عكسية، فيرى المحلّل فاصلاً يتبعه هراء. وعلى Node هذا هو السبب رقم 6 من القسم 2، برسالة DECODER routines::unsupported نفسها التي لكل شيء آخر.

تراجَع عنه عند نقطة الاستعمال:

const pem = process.env.PRIVATE_KEY.replace(/\\n/g, '\n');

واحتَط لأمرين حول هذا الاستبدال. أولاً، طبِّقه فقط حين تحتوي السلسلة على التسلسل المكوَّن من محرفين، وإلا فقيمة متعدّدة الأسطر حقاً مرّت عبر مُحمِّل آخر ستبقى كما هي على أي حال. وثانياً، فضِّل الـbase64 لملفّ PEM كلّه إن سمحت منصّتك: خزّن سطراً واحداً من base64، وفُكّ ترميزه عند الإقلاع، فلا يبقى سؤال تهريب من الأساس.

BOM: غير ضارّة على Node، وغير مختبَرة في غيره

علامة ترتيب البايتات ثلاثة بايتات، EF BB BF، يكتبها بعض محرّرات Windows في مستهلّ ملفّ UTF-8. والنصيحة بإزالتها قبل تحميل مفتاح شائعة. لكنها على Node v25.8.2 لم تُحدِث فرقاً: فملفّ PEM مسبوق بـBOM حُلِّل بنجاح، سلسلةً نصية وكذلك Buffer يبدأ بتلك البايتات الثلاثة.

وحدِّد نطاق هذه النتيجة بعناية. قِيست على Node v25.8.2 وحدها. أمّا Java وPython وسائر المحلّلات فلم تُختبَر هنا، ولا شيء في هذا المقال يقول كيف تتصرّف. فإن كنت تصحّح خدمة على Java، فالـBOM تبقى سؤالاً مفتوحاً لا احتمالاً مستبعَداً.

والـBOM تكسر أشياء أخرى فعلاً، وربما من هناك انتقلت النصيحة إلى موضوع المفاتيح. فاستدعاء JSON.parse على سلسلة مسبوقة بـBOM إخفاق حقيقي وموثَّق جيداً، يغطّيه دليل خطأ تحليل JSON بسبب علامة UTF-8 BOM. وعليه فملفّ مفتاح مخزَّن داخل إعدادات JSON قد يفشل قبل أن ينظر أحد إلى المفتاح بوقت طويل.

نهايات الأسطر والسطر الأخير وعرض الطيّ

ثلاثة مشتبَه بهم آخرون برّأتهم Node v25.8.2:

  • نهايات أسطر CRLF. مقبولة. فالمفتاح الذي سافر عبر Windows ليس معطوباً تلقائياً.
  • غياب السطر الجديد الأخير. مقبول. ولاحظ أن هذه النقطة خاصة بالمحلّل: يُقال إن بعض المحلّلات ترفض ملفّ PEM بلا سطره الأخير، وNode ليست منها. Node تقبله؛ أمّا المحلّلات الأخرى فلم تُختبَر هنا.
  • متن غير مطويّ. مقبول. فالـbase64 لا يحتاج أن يُطوى عند 64 محرفاً.

أمّا ما يكسر متن الـbase64 فعلاً فهو محرف ضائع أو مُقحَم أو مستبدَل، وذلك إخفاق مختلف عن الطيّ. فبرنامج محادثة يحوّل فاصل السطر إلى مسافة، أو حقل نصّي يبتلع المحرف الأخير، يُنتِج متناً لم يعد يقبل فكّ الترميز. انسخ بزرّ نسخ لا بسحب الفأرة.

6. OpenSSL 3.x غيّرت القيمة الافتراضية من دون أن تخبرك

مقيس على OpenSSL 3.6.2 7 Apr 2026:

الأمرالحاوية التي يكتبها
openssl genrsa -out k.pem 2048PKCS#8، بترويسة BEGIN PRIVATE KEY
openssl genrsa -traditional -out k.pem 2048PKCS#1
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048PKCS#8
openssl genpkey -algorithm ED25519PKCS#8
openssl pkcs8 -topk8 -nocrypt -in a.pem -out b.pemيحوّل PKCS#1 إلى PKCS#8
openssl rsa -in b.pem -traditional -out a.pemيحوّل PKCS#8 إلى PKCS#1

اقرأ الصفّين الأولين مرة أخرى. فـgenrsa يعطيك PKCS#8 افتراضياً على هذه النسخة، و-traditional هو ما يُنتِج ملفّ BEGIN RSA PRIVATE KEY. وما زالت أدلّة كثيرة تصف genrsa بأنه أمر PKCS#1 وgenpkey بأنه أمر PKCS#8، واتّباعها سيتركك موقناً أنك ولّدت تنسيقاً لم تولّده.

والنتيجة العملية تظهر في عمليات الترحيل. فريق يعمل على Java يستلم مفتاحاً عاملاً من زميل يشغّل نسخة OpenSSL أقدم، وكل شيء على ما يرام، ثم بعد ستة أشهر يعيد أحدهم توليد المفتاح على جهاز جديد. الأمر نفسه والتوثيق نفسه وحاوية مختلفة، فترمي JDK الآن algid parse error, not a sequence في وجه مفتاح «وُلِّد بالطريقة نفسها تماماً». لم يكن كذلك.

فلا تفترض إذاً، بل تحقّق:

head -1 key.pem

سطر مخرَجات واحد، وجدول القسم 3 يخبرك بما بين يديك. افعل ذلك قبل تشغيل أي أمر تحويل، لأن تحويل ملفّ PKCS#8 إلى PKCS#8 عملية لاغية تبدو إصلاحاً ولا تُصلِح شيئاً.

وإن كنت تفضّل ألّا تفكّر في هذه الرايات أصلاً، فـمولّد مفاتيح RSA عبر الإنترنت يُخرِج الحاويتين من زوج المفاتيح نفسه بمبدّل واحد، فتستطيع إنتاج نسخة PKCS#1 ونسخة PKCS#8 من مفتاح واحد وتجريب كلٍّ منهما على المكتبة التي ترفضك.

7. حدّ الـ2048 بت الذي يرفض مفتاحاً سليماً تماماً

ثمة إخفاق يبدو مشكلة تنسيق وليس كذلك. ففي الشيفرة المصدرية لـjsonwebtoken، يُلقي ملفّ sign.js:

secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg}

وترفعه الشيفرة حين تكون alg خوارزمية من عائلة RS أو PS، ويكون المفتاح دون 2048 بت، ولا يكون allowInsecureKeySizes قد ضُبِط. وذلك فحص خاص بالمكتبة نفسها لا ببيئة التشغيل. فـNode v25.8.2 تحلّل مفتاح RSA بطول 1024 بت من دون اعتراض؛ وmodulusLength: 1024 تُنتِج كائن مفتاح كأي كائن آخر. فالمفتاح سليم بنيوياً، والحاوية صحيحة، وOpenSSL تقرأه، ومع ذلك يفشل نداء التوقيع.

والعلامة الفارقة أن هذه الرسالة تذكر عدداً. فأخطاء التنسيق تتحدّث عن مُفكِّكات ترميز ومتتاليات ومادة مفاتيح؛ وهذه تتحدّث عن بتّات. فإن رأيت حجماً في الرسالة، فكفّ عن النظر في ملفّ PEM.

ومصدر مفاتيح الـ1024 بت هو التاريخ عادةً: مفتاح وُلِّد قبل سنوات على قيمة افتراضية تغيّرت منذئذ، أو تجهيزة اختبار لم يراجعها أحد لأن المفاتيح الصغيرة أسرع توليداً. والإصلاح هو توليد زوج جديد بطول 2048 بت فأكثر؛ أمّا مخرج الطوارئ فيطفئ فحصاً موجوداً لسبب وجيه.

ولتتأكّد أن الحجم هو المشكلة الوحيدة الباقية، وقّع الحمولة نفسها بمفتاح جديد بالحجم الصحيح في أداة ترميز وتوليد JWT أونلاين. فإن أنتجت رمزاً ولم تنتجه شيفرتك أنت، فالفرق في مفتاحك لا في مطالباتك ولا في إعداداتك.

8. مسار عمل قابل للتكرار لإخفاقات مفاتيح RS256

نفِّذ هذه الخطوات بالترتيب. كل خطوة إمّا تعثر على السبب وإمّا تشطب فرعاً.

  1. اقرأ سطر الترويسة. head -1 key.pem، ثم طابقه مع جدول القسم 3. هذا يخبرك بالحاوية، وهل الملفّ مشفَّر، وهل هو مفتاح OpenSSH لن ينجح أبداً.
  2. اطلب من OpenSSL أن يحلّله. استخدم openssl rsa -in key.pem -noout -text | head -1 مع RSA، أو openssl pkey -in key.pem -noout مع أي خوارزمية. والنجاح يعني أن البايتات مفتاح صالح وأن المشكلة في جانب المكتبة. والفشل يعني أن الملفّ تالف فتنتقل إلى الخطوة 4.
  3. راجع صفّ مكتبتك في المصفوفة. القسم 4. فإن كنت على Java بملفّ PKCS#1، أو على Go تستدعي دالة التحليل الخطأ، فقد انتهيت هنا.
  4. انظر في المحارف غير المرئية. الأمر head -c 32 key.pem | xxd يعرض أول البايتات، وهو ما يلتقط علامة BOM ومسافة في المستهلّ وفاصلاً مُزاحاً في نظرة واحدة. ثم تأكّد أن سطري -----BEGIN و-----END يبدآن من العمود صفر، وفق القسم 5.
  5. جزِّئ المشكلة بمفتاح معروف السلامة. ولّد زوجاً جديداً في مولّد مفاتيح RSA عبر الإنترنت، ووجّه شيفرتك إليه، وانظر هل ينجو الخطأ. فإن نجا، فالعلّة في شيفرة التحميل لديك لا في ملفّ المفتاح، ولن ينفع أي قدر من إعادة تنسيق الملفّ الأصلي. وإن اختفى، فالملفّ الأصلي هو المذنب وصار بيدك الآن مفتاح عامل تقارنه به.
  6. افحص الخوارزمية والحجم في الآخِر. تأكّد أن الترويسة تقول RS256، وأن المفتاح 2048 بت على الأقلّ، وفق القسم 7.

الخطوة 5 هي التي يتخطّاها الناس وهي التي توفّر أكبر قدر من الوقت. فمفتاح مرجعي نظيف يحوّل شكوى غامضة من نوع «المفتاح لا يعمل» إلى جواب ثنائي عن أيّ الجانبين هو المكسور.

الأسئلة الشائعة

ما الفرق بين BEGIN RSA PRIVATE KEY وBEGIN PRIVATE KEY؟

هما حاويتان حول مفتاح RSA نفسه. فـBEGIN RSA PRIVATE KEY هو PKCS#1 ويحمل أعداد RSA مباشرةً؛ وBEGIN PRIVATE KEY هو PKCS#8 ويضيف مُعرِّف خوارزمية، ولهذا يستطيع حمل مفاتيح ECDSA وEd25519 أيضاً. وأيّهما تحتاج يتوقّف كلياً على المكتبة، ومولّد مفاتيح RSA عبر الإنترنت يكتب أياً منهما.

لماذا يُنتِج openssl genrsa تنسيقاً مغايراً لما يعرضه الشرح الذي أتبعه؟

لأن القيمة الافتراضية تغيّرت. فعلى OpenSSL 3.6.2، يكتب openssl genrsa -out k.pem 2048 تنسيق PKCS#8 بترويسة BEGIN PRIVATE KEY. وللحصول على بنية PKCS#1 التقليدية التي تصفها الأدلّة الأقدم، أضِف -traditional. وشغّل head -1 على المخرَج بدل الوثوق بأي شرح عمّا تنتجه نسختك أنت.

كيف أُصلِح algid parse error, not a sequence في Java؟

تعني هذه الرسالة على Java 1.8.0_162 أنك أعطيت PKCS8EncodedKeySpec مفتاح PKCS#1. والمكتبة القياسية لا تقرأ PKCS#1 إطلاقاً. حوِّل مرة واحدة بالأمر openssl pkcs8 -topk8 -nocrypt -in pkcs1.pem -out pkcs8.pem، أو أضِف BouncyCastle إن كان شيء آخر في المشروع يحتاجها أصلاً.

هل يجب أن يبدأ كل سطر في المفتاح الخاص من العمود صفر؟

لا، والنصيحة الشائعة مقلوبة. فبالاختبار على Node v25.8.2، إزاحة أسطر متن الـbase64 وحدها تُحلَّل بلا مشكلة، بينما إزاحة سطر -----BEGIN وحده أو سطر -----END وحده تفشل. والسطر الفارغ قبل ملفّ PEM مقبول؛ أمّا المسافة في المستهلّ فلا.

كيف يُخزَّن المفتاح الخاص في ملفّ .env؟

إمّا سطراً واحداً بين علامتَي اقتباس بتهريب \n تتراجع عنه عند التحميل بـ.replace(/\\n/g, '\n')، وإمّا سطراً واحداً من base64 تفكّ ترميزه عند الإقلاع. والثاني أسلم لأنه لا يترك عُرفَ تهريب يمكن لمُحمِّل الإعدادات أن يخطئ فيه.

هل يمكنني استخدام مفتاح 1024 بت مع RS256؟

Node v25.8.2 تحلّل مفتاح RSA بطول 1024 بت بلا خطأ، لكن الشيفرة المصدرية لـjsonwebtoken ترفض التوقيع به: secretOrPrivateKey has a minimum key size of 2048 bits، ما لم يُضبَط allowInsecureKeySizes. ولّد مفتاح 2048 بت بدلاً منه. والرسالة تذكر عدد بتّات، وبهذا تميّزها عن مشكلة تنسيق.

لماذا يقول خطأ تنسيق المفتاح الخاص RS256 إنه يحتاج مفتاحاً غير متماثل مع أنني مرّرت ملفّ مفتاح خاص؟

في الشيفرة المصدرية لـjsonwebtoken، تنطلق secretOrPrivateKey must be an asymmetric key when using ${header.alg} حين تكون alg من RS أو PS أو ES ولا يكون المفتاح مفتاحاً خاصاً. وغالباً ما تكون القيمة سلسلة سرّ على طريقة HS256 بقيت من إعداد سابق. فالسلسلة العشوائية مكانها مع HS256 ومع مولّد مفتاح JWT السري؛ أمّا RS256 فيحتاج زوج مفاتيح لا سرّاً.

الخلاصة

تكلفة هذا الصنف من العلل كلها في التشخيص؛ أمّا الإصلاح فأمر واحد في الغالب. فسلسلة خطأ واحدة تغطّي سبعة أسباب على Node، ورسالة Java تشير إلى ASN.1 بينما الجواب هو «الحاوية خطأ»، وأكثر نصيحة تنسيق تتكرّر في هذا الموضوع مقلوبة رأساً على عقب. ولهذا لا تصل إلى الجواب بقراءة الرسالة، بل بالإقصاء بالترتيب: سطر الترويسة، ثم تحليل OpenSSL، ثم مصفوفة المكتبات، ثم المحارف غير المرئية، ثم مفتاح معروف السلامة.

وعادتان تمنعان التكرار. دوِّن أيّ حاوية تشترطها كل خدمة، إلى جوار المفتاح في مخزن الأسرار لديك، لأن القيد يسكن في المكتبة لا في المفتاح. واحتفظ بزوج مفاتيح معروف السلامة في بيئة التطوير بوصفه عيّنة ضابطة ليس إلا، حتى يصير أول سؤال في أي إخفاق مفتاح قابلاً للحسم بنعم أو لا خلال دقيقة.

وللسؤال الأوسع عن كيفية إصدار هذه المفاتيح وتدويرها وتحديد نطاقها بعد أن تُحمَّل بنجاح، راجع أفضل ممارسات أمان JWT: الهجمات وسبل الحماية لعام 2026.

الوسوم: jwt rsa pem openssl debugging security

مقالات ذات صلة

عرض جميع المقالات