المصادقة والأمان

تثبيت شهادات SSL والتحقق من الشهادات

16 دقيقة الدرس 8 من 12

تثبيت شهادات SSL والتحقق من الشهادات

يُشفِّر بروتوكول TLS/SSL البيانات أثناء نقلها، غير أنه لا يحمي من المهاجم المتطور الذي يُقدِّم شهادة مزوَّرة صادرة عن جهة إصدار شهادات (CA) موثوقة — وهو هجوم الوسيط الكلاسيكي (MITM). يُحصِّن تثبيت SSL تطبيق Flutter الخاص بك بربطه بشهادة محددة أو بصمة مفتاح عام معروفة، بحيث يُرفض الاتصال بأي خادم آخر — حتى لو كان يحمل شهادة موقَّعة من CA صالح.

ملاحظة: يكتسب تثبيت SSL أهمية خاصة في تطبيقات الجوال، إذ يستطيع المهاجم على الشبكة ذاتها (Wi-Fi في مقهى، أو بروكسي مؤسسي) تثبيت شهادة CA مارقة على الجهاز واعتراض حركة HTTPS بأدوات شائعة كـ Charles Proxy أو mitmproxy. يجعل التثبيت هذه الفئة من الهجمات عديمة الأثر على تطبيقك.

لماذا يهمّ تثبيت الشهادات؟

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

  • تثبيت الشهادة: تُخزِّن الشهادة الكاملة بصيغة DER/PEM وتقارنها بايت بايت عند كل اتصال.
  • تثبيت المفتاح العام (بأسلوب HPKP): تُخزِّن بصمة SHA-256 فقط للـ SubjectPublicKeyInfo الخاصة بالخادم وتقارنها. تظل صالحة عند تجديد الشهادة طالما بقيت نفس زوج المفاتيح.
  • تثبيت الورقة مقابل الوسيط مقابل الجذر: تثبيت شهادة الورقة هو الأكثر تقييداً؛ أما تثبيت جهة إصدار وسيطة فهو موازنة شائعة بين الأمان والمرونة التشغيلية.
نصيحة: فضِّل تثبيت المفتاح العام على تثبيت الشهادة في بيئة الإنتاج. عند تجديد شهادتك بنفس المفتاح، يبقى التثبيت صالحاً ويستمر تطبيقك في العمل دون الحاجة إلى تحديث إجباري.

استخراج بصمة المفتاح العام

شغِّل أوامر openssl التالية على شهادة خادمك لاستخراج بصمة SHA-256 للـ SPKI:

استخراج بصمة SHA-256 للـ SPKI بـ OpenSSL

# تنزيل سلسلة الشهادات
openssl s_client -connect api.example.com:443 -showcerts </dev/null 2>&1 | \
  openssl x509 -outform DER > server.der

# حساب بصمة SHA-256 للـ SubjectPublicKeyInfo
openssl x509 -in server.der -inform DER -pubkey -noout | \
  openssl pkey -pubin -outform DER | \
  openssl dgst -sha256 -binary | \
  openssl base64

ينتج هذا سلسلة Base64 مثل abc123+xyz…==. خزِّن هذه السلسلة في تطبيقك — ستصبح التثبيت الخاص بك.

تطبيق تثبيت SSL مع Dio

Dio هو أشهر عميل HTTP في Flutter. تُتيح طبقة HttpClientAdapter الخاصة به دالة BadCertificateCallback التي تسمح لك باعتراض مصافحة TLS والتحقق من شهادة الخادم مقابل التثبيت المُخزَّن.

Dio — تثبيت المفتاح العام عبر BadCertificateCallback

import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';

class PinnedDioClient {
  /// بصمات SPKI المُشفَّرة بـ SHA-256 وBase64 لـ api.example.com
  static const List<String> _pins = [
    'abc123+primaryKeyHash==',   // مفتاح الشهادة الحالية
    'def456+backupKeyHash==',    // مفتاح احتياطي للتدوير
  ];

  static Dio create() {
    final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));

    (dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () {
      final client = HttpClient();

      client.badCertificateCallback =
          (X509Certificate cert, String host, int port) {
        // استخراج بايتات المفتاح العام الخام (DER-encoded SubjectPublicKeyInfo)
        final pubKeyBytes = cert.der; // بايتات شهادة DER الكاملة

        // حساب SHA-256 للشهادة الكاملة كبديل لبصمة SPKI
        final digest = sha256.convert(pubKeyBytes);
        final hash = base64.encode(digest.bytes);

        // السماح بالاتصال فقط عند تطابق البصمة مع تثبيت معروف
        return _pins.contains(hash);
      };

      return client;
    };

    return dio;
  }
}

// الاستخدام
final dio = PinnedDioClient.create();
final response = await dio.get('/users/me');
تحذير: إعادة true من badCertificateCallback تسمح بشهادة كانت ستُرفض في الأصل. إعادة false يُبقي الرفض الافتراضي. لا تُعِد true بشكل غير مشروط — فذلك يُعطِّل كل التحقق من الشهادات وهو أسوأ من عدم التثبيت أصلاً.

تطبيق تثبيت SSL مع حزمة http

تستخدم حزمة http القياسية الـ HttpClient المدمج في Dart. يمكنك تغليفه في IOClient مخصص وتطبيق نفس نمط badCertificateCallback.

حزمة http — التثبيت عبر SecurityContext و IOClient

import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';

Future<http.Client> buildPinnedHttpClient(
    List<int> pinnedCertDerBytes) async {
  // تحميل الشهادة التي يجب الوثوق بها
  final context = SecurityContext(withTrustedRoots: false);
  context.setTrustedCertificatesBytes(pinnedCertDerBytes);

  final httpClient = HttpClient(context: context);
  // رفض الاتصالات بأي مضيف لا يطابق الشهادة المُثبَّتة
  httpClient.badCertificateCallback =
      (X509Certificate cert, String host, int port) => false;

  return IOClient(httpClient);
}

// في طبقة الخدمات
Future<void> fetchSecureData(List<int> pinnedCert) async {
  final client = await buildPinnedHttpClient(pinnedCert);
  try {
    final response = await client.get(
      Uri.parse('https://api.example.com/data'),
    );
    print('Status: \${response.statusCode}');
  } finally {
    client.close();
  }
}

تدوير الشهادات والتثبيتات الاحتياطية

يجب أن تتضمن كل عملية تثبيت على الأقل تثبيتَين: مفتاح الشهادة الحالية ومفتاح احتياطي مُولَّد مسبقاً. عند انتهاء صلاحية الشهادة الأساسية:

  • أنشئ زوج مفاتيح وCSR جديدَين على جانب الخادم قبل انتهاء الشهادة القديمة.
  • انشر نسخة تطبيق جديدة ترقِّي فيها المفتاح الاحتياطي إلى أساسي وتُضيف احتياطياً جديداً.
  • أكمل طرحاً تدريجياً قبل انتهاء صلاحية الشهادة القديمة.
  • لا تدع جميع نسخ التطبيق النشطة تعتمد على تثبيت واحد على وشك الإلغاء.
نصيحة: خزِّن التثبيتات في خدمة إعداد عن بُعد (كـ Firebase Remote Config) لتتمكن من تحديثها دون تحديث إجباري للتطبيق. احمِ التحديث بالتحقق من التوقيع حتى لا يتعرض الإعداد نفسه للتلاعب.

ملاحظات المنصة وقيود الويب

واجهات dart:io الخاصة بـ HttpClient وSecurityContext متاحة فقط على Android وiOS وسطح المكتب. يستخدم Flutter Web جلب المتصفح داخلياً، حيث لا يمكن الوصول إلى التثبيت من Dart. لمستهدفات الويب، اعتمد على رؤوس HSTS الخاصة بخادمك وسجلات DNS من نوع CAA بدلاً من ذلك. نظِّم كودك بحيث يُحقَن العميل المُثبَّت ويمكن استبداله بعميل عادي في بيئات الويب باستخدام الاستيرادات الشرطية (dart.library.io مقابل dart.library.html).

الخلاصة

يُعدّ تثبيت SSL طبقة دفاعية حيوية للتطبيقات التي تتواصل مع واجهات برمجية حساسة. أبرز النقاط التي يجب تذكّرها:

  • فضِّل تثبيت بصمة المفتاح العام (SPKI) على تثبيت الشهادة الكاملة لمرونة تشغيلية أفضل.
  • استخدم IOHttpClientAdapter في Dio أو IOClient من حزمة http مع badCertificateCallback مخصص لفرض التثبيت على مستوى HTTP.
  • أدرج دائماً تثبيتاً احتياطياً وضع إجراء تدوير الشهادات قبل الشحن.
  • لا تُعِد true بشكل غير مشروط من badCertificateCallback — فذلك يُعطِّل كل الأمان.