← All posts

الحيوات الثلاث لمفتاح توقيع JWT الخاص بي

Authagonal·July 17, 2026
authjwtkey-managementkmsvaultecdsaoidc

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

الحياة الأولى: المفتاح الذي احتفظ به الخادم في جيبه

في البداية عاش المفتاح حيث كان يُستخدم. أنشأ الخادم مفتاح RSA-2048، واحتفظ به في الذاكرة، ووقّع رموزه بخوارزمية RS256. هذا هو الوضع الافتراضي في كل مكان تقريباً، وهو جيّد تماماً إلى أن تنطق بكلمة الحيازة بصوت عالٍ. كان المفتاح الخاص في متناول أي شيء يستطيع قراءة ذاكرة العملية، أو إعداداتها، أو نسخة احتياطية من أيٍّ منهما. أردنا أن نتمكن من إخبار العملاء بأن تسرّب قاعدة البيانات لا يكشف أي شيء ذي قيمة، لكن وجود مفتاح توقيع على بُعد عملية واحدة من تلك القاعدة كان يجعل هذا الادّعاء كذباً. كان لا بد للمفتاح أن يغادر المبنى.

الحياة الثانية: المفتاح الذي انتقل إلى خزنة لا يستطيع مغادرتها

أدخلنا وصلةً — وهي ISignatureProvider يستدعيها الخادم كلما احتاج إلى توقيع — ووضعنا KMS خلفها. أصبح المفتاح الآن يُنشأ داخل محرك النقل (transit) في Vault ولا يُصدَّر أبداً. التطبيق لا يحتفظ به؛ بل يرسل البايتات المراد توقيعها إلى الخزنة ويستعيد توقيعاً. يمكنه أن يستخدم المفتاح ولا يمكنه أن يهرّبه.

هذا التمييز هو بيت القصيد كله. تفريغٌ للذاكرة، أو ملف إعدادات مُتسرِّب، أو نسخة احتياطية مُسرَّبة — لم يعد أيٌّ منها يحتوي على المفتاح، لأن المفتاح لم يكن قط في أيٍّ من تلك الأماكن. اختراق التطبيق الآن يمنح المهاجم القدرة على أن يطلب من الخزنة أن توقّع، وهو ما يمكننا تقييد معدله وتدقيقه وإبطاله. لم يعد يمنحه المفتاح نفسه، وهو ما لم نكن نستطيع فعل شيء حياله بمجرد أن يختفي.

وإليك الجزء الذي لم نضعه في ملاحظات الإصدار. التغييرة (commit) نفسها التي نقلت المفتاح إلى الخزنة خفّضت أيضاً في صمت عددَ تكرارات PBKDF2 لدينا إلى النصف، من 100,000 إلى 50,000. تغييرٌ واحد (diff)، وتعديلان أمنيان يشيران إلى اتجاهين متعاكسين: كان العنوان الرئيسي «التوقيع في KMS»، وكان يركب تحته خفضٌ لعامل جهد تجزئة كلمات المرور إلى النصف، في سطر لم يكن أحد ينظر إليه لأن القصة كانت عن شيء آخر. كلاهما شيفرة أمنية، فراجعناهما معاً بوصفهما تغييراً واحداً «أكثر أماناً» ومرّرناهما استناداً إلى قوة النصف الجيّد.

كان الإصلاح سطراً واحداً. أما الدرس فلم يكن كذلك. التغيير (diff) الموسوم بأنه أكثر أماناً يبقى تغييراً، والثوابت الحساسة أمنياً بداخله — أعداد التكرارات، وأحجام المفاتيح، والمُهَل الزمنية، وأسماء الخوارزميات — لا ترث هالةً من رسالة التغييرة (commit). صرنا الآن نتعامل مع رقم عامل الجهد بالطريقة نفسها التي نتعامل بها مع اختيار خوارزمية: شيء يُؤكَّد ويُفحَص، لا شيء تثق به لأن التغيير المحيط به كان فكرة جيّدة.

الحياة الثالثة: المفتاح الذي صغُر لكي يصير أسرع

مع وجود التوقيع خلف KMS، صار كل توقيع رحلةَ ذهاب وإياب عبر الشبكة، وتوقيع RSA هو النوع المكلف. لذلك غيّر المفتاح فصيلته: صار RS256 على RSA-2048 هو ES256 على منحنى P-256. توقيع المنحنى الإهليلجي أرخص من RSA بنحو رتبة قدرٍ كاملة، وتوقيع P-256 يبلغ 64 بايت بينما توقيع RSA-2048 يبلغ 256، ومجموعة المفاتيح العامة التي يُنزّلها المُتحقِّقون تتقلص تبعاً لذلك. مفتاح أصغر، وتوقيع أصغر، وJWKS أصغر، ورموز أسرع — كل ذلك من اختيار منحنى أفضل.

المعضلة أنك لا تستطيع أن تبدّل خوارزمية التوقيع دفعةً واحدة على جهة إصدار حيّة. كل رمز صدر بالفعل وُقّع بخوارزمية RS256 وعليه أن يبقى صالحاً حتى ينتهي. كل مُتحقِّق يخزّن مفتاحك العام القديم في الذاكرة المؤقتة. غيّرِ الخوارزمية في خطوة واحدة فتُبطلَ كل رمز في طريقه وكل مجموعة مفاتيح مخزّنة في اللحظة نفسها — انقطاعٌ تُلحِقه بنفسك متنكّراً في هيئة ترقية.

ما نجح كان أن نكفّ عن التظاهر بأنه لم يكن هناك سوى مفتاح واحد قط. صار مخزن المفاتيح لا يبالي بالخوارزمية: يحتفظ بمفتاح RSA ومفتاح EC في الوقت نفسه، وينشرهما معاً في JWKS، كلٌّ تحت معرّف مفتاحه وخوارزميته الخاصة. ثمة مُنتقٍ منفصل للمفتاح النشط يقرر أي مفتاح يوقّع الرموز الجديدة، وهو ينصرف عن مفتاح RSA القديم بنفسه لحظةَ وجود مفتاح EC — دون راية إعدادات، ودون نشر مُوقَّت ليتزامن مع تدوير. خلال فترة التداخل، يعلن الاكتشاف (discovery) عن كلا المفتاحين العامّين، فتبقى الرموز الموقّعة بأيٍّ منهما صالحة. وبمجرد انتهاء آخر رمز موقّع بـ RS256، يعلن الاكتشاف عن ES256 فقط ويثبّت التحقق ValidAlgorithms = ES256، وهو ما يوصد الباب أيضاً في وجه هجمات الخلط بين الخوارزميات التي تحاول إقناع مُتحقِّق بقبول النظام الخاطئ. الترحيل تلاشٍ متبادل (crossfade) تجريه جهة الإصدار على نفسها، لا مفتاح تبديل يقلبه أحد.

الدرس، مُستخلَصاً

يبدو مفتاح التوقيع ثابتاً: سرٌّ واحد، يُضبط مرة واحدة، ويُشار إليه إلى الأبد. لكن مفتاحنا لم يكن كذلك. في حياته غيّر حيازته، من العملية إلى KMS، وغيّر فصيلته، من RSA إلى المنحنى الإهليلجي، وكان لا بد من إجراء كلا الانتقالين والرموز في طريقها والمُتحقِّقون يخزّنون في الذاكرة المؤقتة أشياء لا نملكها. الخاصية التي جعلت كل انتقال قابلاً للنجاة كانت واحدة في المرتين: لم يفترض النظام قط أن ثمة مفتاحاً واحداً بالضبط، أو خوارزمية واحدة، أو موطناً واحداً. ابنِ مخزن المفاتيح ليحمل عدة مفاتيح ويعلن بأمانة عن أيها يحمل، عندئذٍ يكفّ التدوير — سواء لمكان سكن المفتاح أو لنوعه — عن أن يكون انقطاعاً تجدوِله، ويصير خاصيةً تديرها جهة الإصدار بنفسها.

إن كنت تُشغّل نظام مصادقة، فإن الشيء الذي يوقّع رموزك ينبغي أن يكون أكثر أغراضك مللاً، وأكثرها قابلية للفحص، وأقلّها ذكاءً. أما مفتاحنا فقد استغرق ثلاث حيوات ليبلغ ذلك. وقد كان يستحق كل واحدة منها.