نموذج الأمان
على العمل الأمني لجهاز يمكن لأي هاوٍ بناؤه أن يجيب عن "ضد من؟" قبل "كيف؟". يعرض هذا الفصل نموذج التهديدات بصراحة، ثم يمشي بالطبقات من الشبكة إلى السيليكون.
نموذج التهديدات
| الخصم | داخل النطاق | الدفاعات الأساسية |
|---|---|---|
| مراقب سلبي على الشبكة المحلية | نعم | TLS في كل مكان |
| مهاجم نشط على الشبكة المحلية (ضيف عدائي، حاسوب محمول مخترق) | نعم | رمز وصول على كل عمليات التغيير، تحقق من المدخلات، تحديد المعدل |
| مصدر تحديث خبيث أو مختطف | نعم | HTTPS مع تحقق CA، تثبيت الروابط، صور موقعة تحت الإقلاع الآمن |
| شخص بحيازة فيزيائية، عادي | نعم (ملف الإنتاج) | تشفير الفلاش، تشفير NVS |
| شخص بحيازة فيزيائية، بمستوى مختبري | خارج النطاق صراحة | حقن الأعطال ونزع الغلاف يهزمان أي جهاز من فئة الهواة؛ والتظاهر بغير ذلك سيكون مسرحاً |
| المطور (نحن) يتجسس | خارج بنيوياً | لا جمع بيانات عن بعد، لا سحابة، لا حسابات؛ لا يوجد شيء يمكن تسريبه أصلاً |
طبقات وقت التشغيل
فُصلت كل طبقة في فصلها الخاص؛ مجمعة هنا كوضعية واحدة:
- النقل: كل شيء عبر TLS، بما في ذلك لوحة التحكم وWebSocket (فصل HTTPS). تتحقق البثوث وOTA من شهادات CA حقيقية عبر حزمة الشهادات.
- المصادقة: رمز وصول واحد من 256 بتاً، يُسك من مولّد الأرقام العشوائية في العتاد عند أول إقلاع، ويُطبع فقط على المنفذ التسلسلي الفيزيائي، ويُقارن في زمن ثابت. حيازة العتاد هي الاعتماد الجذري، بما يطابق كيف يملك البشر فعلياً أجهزتهم (تصميم REST).
- سطح التفويض: القراءات التي تحتاجها لوحة تحكم قبل الرمز عامة ومخفية (نقطة نهاية الإعدادات تعيد SSID، ولا تعيد كلمة المرور أبداً)؛ كل تغيير يتطلب الرمز.
- تحقق المدخلات عند الحدود: كل قيمة تأتي من الخارج تُفحص في طبقة المعالجات قبل وصولها إلى مكوّن: قوائم سماح لمخططات الروابط، رفض تجاوز المسار، قصّ النطاقات (تصميم REST).
- الدفاع عن الموارد: حدود معدل لكل فئة مقاسة لحماية RAM، لا لإبطاء تخمين كلمات المرور (رموز 256 بتاً لا تُخمَّن).
طبقات السيليكون (ملف الإنتاج)
تعمل بنيات التطوير من دونها بغرض التصحيح؛ sdkconfig.production يشغلها، ولا تكون منطقية إلا معاً:
الإقلاع الآمن
الإقلاع الآمن V2 في ESP32-S3: يتحقق ROM من توقيع محمل الإقلاع RSA-3072 مقابل ملخص مفتاح عام محروق في eFuses؛ ويتحقق محمل الإقلاع من صورة التطبيق بالطريقة نفسها. السلسلة تعني أن الشريحة لن تشغل كوداً غير موقّع بمفتاحك، وهذا ما يجعل OTA جديراً بالثقة حتى مع مضيف إصدار مخترق: TLS تصادق على الخادم، والتوقيع يصادق على الصورة.
يُولَّد المفتاح الخاص مرة واحدة، ويعيش خارج المستودع إلى الأبد، ويطلبه build.sh --production عبر البيئة (SAFI_SECURE_BOOT_SIGNING_KEY). حرق eFuse غير قابل للتراجع لكل جهاز؛ يرتب فصل التزويد الأمر بحيث يكلف الخطأ لوحة تجريبية واحدة، لا دفعة.
تشفير الفلاش
XTS-AES فوق محتويات الفلاش الخارجي بمفتاح لكل جهاز في eFuse: شريحة الفلاش المنزوعة تُقرأ كضجيج. هذا ما يحمي فعلياً كلمة مرور WiFi ورمز API ومفتاح TLS الخاص ضد خصم "الحيازة الفيزيائية العادية"؛ ويضيف تشفير NVS (بمفتاحه في قسم nvs_key) الضمانة نفسها على مخزن الإعدادات تحديداً. التكلفة: عبء وقت تشغيل مهمل (التشفير في مسار الذاكرة الخابية)، إضافة إلى انضباط تشغيلي حقيقي: لا يمكن إعادة كتابة فلاش جهاز مشفر عبر USB عرضاً، وهذه هي الميزة.
سياسة تفريغ الذاكرة
تفريغ الذاكرة معطل في sdkconfig.production (CONFIG_ESP_COREDUMP_ENABLE_TO_NONE يختار خيار بلا وجهة): يحمل التفريغ لقطة RAM تحتوي بيانات اعتماد WiFi ورمز API، ومع أن تشفير الفلاش سيحمي تلك الكتلة وهي ساكنة، فليس للإنتاج حلقة تصحيح تسلسلية تستهلكها يوماً، فتخزينها مخاطرة صرفة بلا قارئ.
بوابات الإنتاج
CONFIG_SAFI_ENFORCE_PRODUCTION_GATES يجعل الملف المحصّن يدافع عن نفسه في زمن الترجمة: بناء الإنتاج يفشل في البناء إذا كان سحب OTA غير الآمن مفعلاً، أو الإقلاع الآمن أو تشفير الفلاش مغلقين، أو بقيت تسهيلات تصحيح. فرضية التصميم أن قوائم التحقق تتآكل بينما المترجمات لا تتآكل؛ الشخص الوحيد الذي يبني إصداراً في الثانية فجراً لا يستطيع نسيان ما يرفض البناء فعله. تشغل tools/production_preflight.sh الفحوص نفسها إضافة إلى وجود مواد المفاتيح قبل الكتابة على الفلاش.
الانتقال إلى ESP-IDF v6
انتقل البرنامج الثابت من ESP-IDF 5.5.1 إلى v6.0.2. لم يتغير شيء في نموذج التهديدات أعلاه؛ ما تغير هو البنية التحتية للتشفير ومسار التزويد، لذا يُوثق الانتقال هنا، والأمان أولاً.
تشفير PSA وMbedTLS 4
ينفذ ESP-IDF v6 تشفيره الداخلي عبر واجهة PSA Crypto API، ويرث التطبيق تحصين MbedTLS 4.x لأن IDF يملك مكدس TLS. كود تطبيق صافي خالٍ من PSA فيما عدا ذلك: تشفير NVS وجلسات TLS ومصافحة التزويد كلها مستهلكات PSA داخلية في IDF. اللمستان التطبيقيتان المتعمدتان هما اشتقاق PoP (main/pop_derive.c يستخدم psa_mac_compute مباشرة للـHMAC) وسلك إنذار دفاعي psa_crypto_init() في نهاية system_init.c: يعمل بعد أن يكون كل ما يحتاج PSA قد بدأ بالفعل، ويسجل تحذيراً مبكراً فقط، ولا يصلح أبداً. وظيفته كلها أن يفشل بصوت عالٍ في السجل عند تغيير مستقبلي في IDF يتوقف عن تهيئة PSA أثناء بدء التشغيل، حتى لا يتدهور جهاز معطوب بصمت.
ما يرثه MbedTLS 4 من الانتقال إلى IDF v6: أُزيلت أجنحة DHE وتبادل مفاتيح RSA وECDH الثابت، والمنحنيات دون 250 بتاً غير مدعومة. عملاء TLS الحديثون لا يتأثرون. لم يكن أي من نظراء صافي بحاجة إلى شيء أُزيل، فلم يكلف الانتقال شيئاً وكسب سطح هجوم أصغر.
التزويد: protocomm بالمستوى الأمني 2، وملصق PoP
أزال ESP-IDF v6 تقنية SmartConfig (ESPTouch) كلياً، لذا أُعيد بناء مسار إدخال WiFi على مكوّن espressif/network_provisioning: يعلن الجهاز عن SoftAP، ويتصل به تطبيق الهاتف "ESP SoftAP Provisioning" من Espressif، وتنتقل بيانات الاعتماد عبر protocomm بالمستوى الأمني 2 (مصادقة SRP6a إضافة إلى تشفير جلسة AES-GCM)، لا نصاً صريحاً.
يرسو المستوى الأمني 2 على إثبات حيازة (PoP): قيمة من 16 محرفاً، فريدة لكل جهاز، يجب أن يقدمها التطبيق قبل أن يقبل الجهاز أي شيء. يطلب التطبيق أيضاً اسم مستخدم تزويد، وهو الثابت على مستوى البرنامج الثابت safi (PROV_USERNAME في main/wifi_manager.c)، لذا يطبع ملصق العلبة اسم المستخدم وPoP معاً ولا يبقى شيء للتخمين.
في بنيات الإنتاج يكون PoP مشتقاً لا مخزناً: يحسب main/pop_derive.c قيمة HMAC-SHA256(السر المصنعي، عنوان MAC الأساسي لـWiFi) عبر PSA (psa_mac_compute، SHA-256) ويرسم أول 16 بايتاً من الملخص بمقياس 62 على [A-Za-z0-9]. السر المصنعي سلسلة سداسية عشرية (طول زوجي، >= 32 محرفاً) يُحقن في زمن البناء عبر متغير البيئة SAFI_POP_FACTORY_SECRET (يمرره main/CMakeLists.txt كتعريف ترجمة)، لذا لا يُكتب PoP في NVS أبداً، وتفريغ NVS لجهاز مسرب لا يسرب PoP. يوجد الاشتقاق نفسه في tools/nvs_mass_provisioning.py (عمود mac لكل جهاز في البيان) وtools/provision_device.sh (يقرأ MAC عبر esptool.py read_mac)، لذا يطابق الملصق المطبوع على الخط دائماً ما يشتقه البرنامج الثابت عند الإقلاع. مصدر MAC هو MAC الأساسي في eFuse المصنعي (esp_read_mac(ESP_MAC_WIFI_STA))، وتبقي ملفات sdkconfig الخاصة بصافي عنونة MAC الشاملة الافتراضية (عنوان MAC للمحطة يساوي MAC الأساسي)، لذا تبقى القيمة مستقرة. ترفض tools/production_preflight.sh بناء إنتاج من دون SAFI_POP_FACTORY_SECRET سليم البنية، والبناء الحامل للسر الذي يفشل اشتقاقه يرفض التزويد بدلاً من الارتداد إلى PoP عشوائي.
ما هذا المخطط وما ليس هو، بصراحة: السر المصنعي سلسلة في زمن الترجمة موجودة في كل ملف إنتاج ثنائي، وMAC قابل للملاحظة على العلبة وفي الهواء. أي شخص يستخرج البرنامج الثابت (وهو ما يمنعه تشفير الفلاش عن المالك العادي لكن ليس عن خصم مختبري المستوى) يستطيع إعادة إنتاج PoP لأي جهاز. يدافع PoP المشتق إذن ضد الاقتران الانتهازي (شخص قريب من الجهاز أثناء التزويد لا يستطيع تخمين PoP أو التقاطه من الشبكة)، لا ضد استخراج البرنامج الثابت. اشتقاق مفتاح محروق لكل جهاز (سر لكل شريحة في eFuse) مذكور كتحصين مستقبلي لذلك التهديد الأقوى؛ التصميم الكامل محدد في قسم مفاتيح PoP لكل جهاز.
بنيات التطوير بلا سر مصنعي: يرتد البرنامج الثابت إلى NVS (نطاق الأسماء provisioning، المفتاح pop)، والجهاز الذي بلا PoP مصنعي يولّد واحداً عند أول إقلاع ويطبعه على المنفذ التسلسلي مرة واحدة، إلى جانب اسم المستخدم. الخاصية الأمنية المهمة: مهاجم عشوائي على الشبكة المحلية يرى SoftAP لكنه لا يستطيع الانضمام إلى جلسة التزويد من دون PoP المطبوع على العلبة، وهو نموذج الثقة القائم على الحيازة نفسه الذي يحكم رمز API.
مفاتيح PoP لكل جهاز: تصميم المفتاح المحروق
للسر المصنعي المشترك ضعف بنيوي واحد: إنه سلسلة واحدة مترجمة في كل ملف إنتاج ثنائي، لذا صورة برنامج ثابت واحدة مستخرجة تعيد إنتاج PoP لكل جهاز شُحن يوماً. يحدد هذا القسم التصميم الذي يزيل السر المشترك كلياً. إنه مستند تصميم: لا شيء في هذا القسم منفذ في البرنامج الثابت أو الأدوات الحالية. لا مسار كود يقرأ مفتاحاً لكل جهاز، ولا أداة تحرق واحداً، ويبقى SAFI_POP_FACTORY_SECRET آلية السجل. يوجد القسم حتى يكون الانتقال، عند حدوثه، قرار بناء مقابل تصميم مراجع بدلاً من تمرين هندسي جديد.
الهدف. استبدال السر المشترك المترجم بسلسلة بمفتاح لكل جهاز من 256 بتاً حتى لا يعود استخراج البرنامج الثابت لجهاز واحد يسفر عن PoP لأي جهاز آخر. الاشتقاق نفسه لم يُمَس: PoP = HMAC-SHA256(مفتاح الجهاز، عنوان MAC الأساسي لـWiFi)، بايتاً ببايت خط أنابيب pop_derive_compute نفسه في main/pop_derive.c (PSA psa_mac_compute، SHA-256، أول 16 بايتاً من الملخص بمقياس 62 على [A-Za-z0-9])، وderive_pop نفسه في tools/nvs_mass_provisioning.py. مصدر المفتاح وحده يتغير: تأتي البايتات الـ32 من كتلة مفتاح eFuse بدلاً من سلسلة في زمن الترجمة.
الآلية. تحصل كل وحدة على مفتاح جديد من 256 بتاً (32 بايتاً خاماً، openssl rand 32)، يُحرق في المصنع في كتلة مفتاح eFuse شاغرة بغرض المفتاح الذي يطابق كيفية استخدامه. يثبت تخطيط الإنتاج الحالي BLOCK_KEY0 (ملخص الإقلاع الآمن)، BLOCK_KEY1 (تشفير الفلاش XTS) وBLOCK_KEY2 (HMAC الخاص بـNVS، انظر التخطيط)؛ BLOCK_KEY3 وKEY4 وKEY5 شاغرة، وBLOCK_KEY3 هو الموطن الطبيعي. لأن pop_derive.c ينفذ HMAC برمجياً عبر PSA، يجب أن يتمكن البرنامج الثابت من قراءة المفتاح، وهذا بالضبط ما يوجد له غرض USER في واجهة eFuse ("استخدام برمجي فقط"): الكتلة محمية الكتابة بعد الحرق، وتبقى مقروءة للبرنامج الثابت، وغير مرئية لمن لا يملك سوى محتويات الفلاش. أمر الحرق، لكل وحدة:
espefuse.py --port "$PORT" burn_key --no-read-protect BLOCK_KEY3 key.bin USER
يجب أن يكون ملف المفتاح 32 بايتاً بالضبط من بيانات مفتاح خام؛ يطبق espefuse ترميز RS ويحمي كتابة الكتلة وحقل الغرض كجزء من الحرق. علم --no-read-protect إلزامي لهذا التصميم: يحمي espefuse قراءةَ المفاتيح المحروقة افتراضياً، ولا يستطيع البرنامج الثابت قراءة كتلة مفتاح محمية القراءة، وهو ما يكسر اشتقاق HMAC البرمجي رأساً. يعكس تدفق المصنع التسلسل الموجود في فصل التزويد: ولّد المفتاح على المنضدة، احرقه في BLOCK_KEY3، اشتق PoP الملصق من ذلك المفتاح ومن MAC الأساسي، وعندها فقط دع الوحدة تغادر الخط. تتغير أدوات الملصق في مكان واحد: حيث يكون SAFI_POP_FACTORY_SECRET متغير بيئة مشتركاً، يتدفق مفتاح الجهاز من المولّد مباشرة إلى الحرق وإلى اشتقاق الملصق، وهو مادة مفتاح طوال وجوده خارج الجهاز، لذا فإن مصنعاً يسجله (عمود pop_key في البيان مثلاً) يجب أن يعامل ذلك البيان كما يعامل السر المصنعي اليوم: في خزنة، لا يُودع في git، ولا يُشارك.
تحذير الحرق غير القابل للتراجع، بصياغة بارزة. حرق eFuse غير قابل للتراجع. مفتاح خاطئ، أو غرض خاطئ، أو كتلة خاطئة لا يمكن التراجع عنه ولا إعادة كتابته، والكتلة المحروقة تُستهلك إلى الأبد. يجب التحقق من هذا التصميم على لوحات تجريبية قبل أي استخدام تصنيعي، تماماً كما هي حالات حرق الإقلاع الآمن وتشفير الفلاش اليوم (يرتب فصل التزويد كل خطوة غير قابلة للتراجع على لوحة واحدة أولاً حتى يكلف الخطأ لوحة واحدة لا دفعة)، ولا يجوز أن يشحن أي جهاز إنتاج بهذا المفتاح حتى تثبت لوحة تجريبية أن البرنامج الثابت وأدوات الملصق والمفتاح المحروق كلها تشتق PoP نفسه.
ما يدافع عنه هذا التصميم وما لا يدافع عنه، بصراحة. يدافع ضد الاختراق الجماعي للسر المشترك: تفريغ برنامج ثابت (أو نواتج بناء المستودع نفسها) لم يعد يحوي مفتاحاً يعمل لأي جهاز آخر، ومصنع يوزع مفاتيح فريدة لكل وحدة يحصر أي تسريب واحد في تلك الوحدة. لا يدافع ضد الاستخراج لكل جهاز: مفاتيح USER مقروءة للبرنامج الثابت، لذا فإن خصماً يهزم الإقلاع الآمن ويشغل كوداً على جهاز واحد يستطيع قراءة مفتاح ذلك الجهاز واشتقاق PoP لذلك الجهاز. تبقى لعبة الوصول الفيزيائي حيث وضعها نموذج التهديدات بالضبط: حيازة مخبرية المستوى لجهاز بعينه تبقى خارج النطاق، وهذا التصميم يضيق مدى الانفجار بدلاً من التظاهر بإغلاقه. يوجد بديل أقوى ويُؤجل عمداً: حرق المفتاح بغرض HMAC_UP واشتقاق PoP بملحق HMAC العتادي (esp_hmac_calculate) سيجعل المفتاح غير مقروء حتى للبرنامج الثابت، لكنه سيغير pop_derive.c، وسيكسر اشتقاق الملصق دون اتصال، لأن مفتاحاً محمي القراءة لا يمكن استعادته في المصنع لطباعة الملصق. تلك المفاضلة (تغييرات في البرنامج الثابت والأدوات مقابل خاصية أقوى لكل جهاز) قرار منفصل؛ يحدد هذا القسم تصميم غرض USER أعلاه فقط.
قرار SoftAP المفتوح
نقطة وصول التزويد مفتوحة (بلا عبارة مرور)، وهو عكس الغريزة العابرة، وهو قرار لا إغفال:
- توجد نقطة الوصول داخل نافذة التزويد فقط، وحيازة PoP هي ما يصادق على الجلسة؛ عبارة مرور لنقطة الوصول لن تحمي شيئاً لا يحميه PoP بالفعل وستضيف سراً ثانياً للغرض نفسه.
- يمنح المستوى الأمني 2 الهاتفَ والجهازَ جلسة مشفرة ذات مصادقة متبادلة فوق نقطة وصول مفتوحة؛ القناة نفسها لا تحمل نصاً صريحاً بصرف النظر عن طبقة الوصلة.
wpa3_compatible_mode(SAE) مؤجل لأن SAE يتطلب نقطة وصول محمية بعبارة مرور، وهو ما يعيد مشكلة السر الثاني. إذا أتاح IDF مستقبلاً SAE على نقطة وصول مفتوحة تعتمد PoP فقط، فأعد النظر عندها.
المشي للمستخدم في دليل المستخدم؛ والآليات في وقت التشغيل في فصل WiFi.
تخطيط كتل مفاتيح eFuse
أغراض المفاتيح الإنتاجية الثلاثة مثبتة على كتل مفاتيح eFuse منفصلة في ESP32-S3:
| الكتلة | الغرض | حُرقت بواسطة |
|---|---|---|
| BLOCK_KEY0 | ملخص الإقلاع الآمن V2 (SECURE_BOOT_DIGEST0) | tools/provision_device.sh |
| BLOCK_KEY1 | مفتاح تشفير الفلاش XTS (XTS_AES_128_KEY) | tools/provision_device.sh |
| BLOCK_KEY2 | مفتاح HMAC الخاص بـNVS (HMAC_UP) | tools/provision_device.sh |
لذا فإن CONFIG_NVS_SEC_HMAC_EFUSE_KEY_ID هو 2 وليس 1: معرف المفتاح 1 يشير إلى BLOCK_KEY1 التي تحمل مفتاح XTS. سيتصادم الاثنان، وستشفر طبقة NVS بمفتاح تشفير الفلاش، فيكسر NVS في أفضل الأحوال ويعيد استخدام غرض مفتاح بصمت في أسوئها. مفتاح HMAC الخاص بـNVS عشوائي لكل جهاز (32 بايتاً من openssl rand)، يُحرق بغرض HMAC_UP قبل أول إقلاع؛ مفتاح XTS هو مفتاح تشفير الفلاش من 32 بايتاً على BLOCK_KEY1 وفق التدفق الراسخ.
تغييرات زمن البناء والمنصة
- Picolibc أصبح مكتبة C الآن: صورة أصغر بنسبة 2.34 في المئة (من 2,549,403 إلى 2,489,727 بايتاً). نجح اختبار التزامن على العتاد: 6000 من أصل 6000 سطر تحقق (sentinel) لم تتمزق تحت ثلاث مهام تكتب stdio في وقت واحد مع تفعيل مؤقت مراقبة الإنتاج. يبقى ارتداد newlib على بعد خيار Kconfig واحد (
CONFIG_LIBC_NEWLIB=y). - GCC 15.1 يعامل التحذيرات كأخطاء افتراضياً؛ وقاعدة الكود نظيفة من التحذيرات. حد أدنى لسلسلة الأدوات: CMake 3.22.1 أو أحدث، وPython 3.10 أو أحدث.
- مكوّن
jsonالمدمج لم يعد في IDF؛ يعلن صافي الآنespressif/cjson ^1.7.19، الواجهة نفسها. - مجموعة المكونات على خط v6:
esp_lvgl_port ^2.9.0، LVGL 9.5.0،joltwallet/littlefs ~1.22.3، mdns 1.11.3 (لم تتغير)،esp_audio_codec 2.3.0،pn532 0.2.1. - بوابات الإنتاج (الإقلاع الآمن، تشفير الفلاش، تشفير NVS، منع التراجع، تثبيت OTA) لم تتغير: كانت معبراً عنها بالفعل كفحوص في زمن الترجمة ونجت من انتقال سلسلة الأدوات سليمة.
التعامل مع الأسرار التي لا مفر منها
| السر | يعيش | أبداً لا |
|---|---|---|
| بيانات اعتماد WiFi | NVS (مشفرة في الإنتاج)؛ sdkconfig.local على أجهزة التطوير، وهو مستثنى من git | في git، في استجابات API |
| رمز API | NVS؛ المنفذ التسلسلي عند أول إقلاع | عبر الشبكة |
| مفتاح TLS الخاص | قسم التخزين (تطوير) أو مزود لكل جهاز | في المستودع (شهادات الإنتاج تولد خارج الشجرة) |
| مفتاح التوقيع | خزنتك | قرب المستودع في أي مكان |
الإبلاغ
تقارير الأمان مرحب بها عبر الإبلاغ الخاص عن الثغرات في المستودع؛ ومسألة عادية تكفي لأي شيء أصبح عاماً بالفعل. التزام المشرفين: فرز صادق وإصلاحات منسوبة.