اعتماد بروتوكول Encrypted Client Hello (ECH)

‫Encrypted Client Hello ‏(ECH) هي إضافة إلى بروتوكول أمان طبقة النقل (TLS) تعمل على تشفير حقل "إشارة اسم الخادم" (SNI) في رسالة المصافحة التي يرسلها العميل. في الإصدار 17 من نظام التشغيل Android (المستوى 37 من واجهة برمجة التطبيقات) والإصدارات الأحدث، تكون ميزة ECH متاحة تلقائيًا. تساعد ميزة ECH في الحفاظ على خصوصية حركة بيانات الويب الخاصة بالمستخدمين من خلال منع الوسطاء على الشبكة من رؤية أسماء المضيفين التي يتصل بها أحد التطبيقات.

لمطوّري التطبيقات

لاستخدام ECH في تطبيقك، اتّبِع الخطوات التالية:

  1. التحقّق من توفّر ميزة ECH في مكتبة الشبكات: تأكَّد من أنّك تستخدم إصدار مكتبة متوافقًا مع ميزة ECH على Android:
    • OkHttp: بدءًا من الإصدار 5.5.0 من OkHttp، يمكنك تفعيل ميزة ECH من خلال ضبط AndroidDns أو DnsOverHttps على OkHttpClient.Builder. لمزيد من المعلومات، يُرجى الاطّلاع على سجلّ التغيير.
    • HttpEngine: ستتوفّر هذه الميزة في الإصدار 17 من Android QPR2 (المستوى 37.2 من واجهة برمجة التطبيقات). لن يكون عليك إجراء أي إعدادات خاصة.
    • WebView: ستتم إضافة ميزة التوافق في إصدار مستقبلي.
  2. ضبط إعدادات أمان الشبكة: يتم تلقائيًا تفعيل ميزة ECH لجميع النطاقات إذا كانت المكتبة تتيحها. إذا كنت بحاجة إلى إيقاف ECH أو فرضه، اضبط العنصر domainEncryption في إعداد أمان الشبكة.
  3. تعديل مستوى حزمة تطوير البرامج (SDK) المستهدَفة: لا تتوفّر ميزة "التشفير الشامل" إلا على الإصدار Android 17 (المستوى 37 من واجهة برمجة التطبيقات) والإصدارات الأحدث.

لمطوّري المكتبات

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

التحقّق من سياسة تشفير النطاق

قبل طلب البحث عن إعدادات ECH أو بدء عمليات الربط، تحقَّق من سياسة تشفير النطاق الخاصة بالتطبيق من خلال استدعاء NetworkSecurityPolicy.getDomainEncryptionMode.

استنادًا إلى الوضع الذي تم إرجاعه، تعامَل مع ECH على النحو التالي:

  • ‫DOMAIN_ENCRYPTION_MODE_DISABLED وDOMAIN_ENCRYPTION_MODE_UNKNOWN: عدم استرجاع إعدادات ECH أو محاولة استخدام ECH
  • DOMAIN_ENCRYPTION_MODE_ENABLED وDOMAIN_ENCRYPTION_MODE_OPPORTUNISTIC: لفرض استخدام ECH. استرداد إعدادات ECH واستخدامها إذا كان الخادم يتيحها إذا كان الخادم لا يتيح استخدام ECH، فعِّل ECH GREASE.

استرداد إعدادات ECH

للاتصال باستخدام ECH، يجب تحويل سجلّ نظام أسماء النطاقات الخاص بالخادم عبر بروتوكول HTTPS والذي يتضمّن إعدادات ECH. عندما تستخدم التطبيقات نظام أسماء النطاقات (DNS) التابع للنظام، يمكن استرداد هذه البيانات باستخدام إحدى الطريقتَين التاليتَين:

الطريقة 1: استخدام واجهة برمجة التطبيقات DnsResolver.query ذات المستوى العالي

إذا كانت مكتبتك لا تتطلّب آليات مخصّصة للتحويل باستخدام نظام أسماء النطاقات (DNS)، يمكنك استخدام واجهة برمجة التطبيقات DnsResolver.query العالية المستوى الخاصة بالنظام الأساسي. تُجري واجهة برمجة التطبيقات هذه طلبات بحث متوازية عن سجلات A/AAAA/HTTPS وتجمع النتائج في HttpsEndpoint.

Kotlin

val resolver = DnsResolver(context, looper)
resolver.query(network, hostname, DnsResolver.TYPE_HTTPS, executor,
    DnsResolver.HTTPS_QUERY_WAIT_AUTO, cancellationSignal,
    object : DnsResolver.Callback<HttpsEndpoint> {
        override fun onAnswer(answer: HttpsEndpoint, rcode: Int) {
            val record = answer.httpsRecords.firstOrNull() ?: return
            val echConfigList = record.echConfigList ?: return
            establishEchConnection(echConfigList)
        }
        override fun onError(error: DnsResolver.DnsException) { /* Handle error */ }
    })

Java

DnsResolver resolver = new DnsResolver(context, looper);
resolver.query(network, hostname, DnsResolver.TYPE_HTTPS, executor,
    DnsResolver.HTTPS_QUERY_WAIT_AUTO, cancellationSignal,
    new DnsResolver.Callback<HttpsEndpoint>() {
        @Override
        public void onAnswer(HttpsEndpoint answer, int rcode) {
            HttpsRecord record = answer.getHttpsRecords().stream().findFirst().orElse(null);
            if (record == null) return;
            EchConfigList echConfigList = record.getEchConfigList();
            if (echConfigList == null) return;
            establishEchConnection(echConfigList);
        }

        @Override
        public void onError(DnsResolver.DnsException error) { /* Handle error */ }
    });

الطريقة 2: استخدام getAllByName وDnsResolver.rawQuery

بالنسبة إلى المكتبات التي تدير قنواتها الخاصة لعمليات ربط المقابس والتحويل باستخدام نظام أسماء النطاقات (DNS)، قد تفضّل تحويل عناوين IP باستخدام واجهات برمجة التطبيقات العادية مع استرداد سجلّ HTTPS بشكل منفصل:

  1. يمكنك ربط سجلّات A/AAAA باستخدام InetAddress.getAllByName للشبكة التلقائية أو Network.getAllByName.
  2. استرداد سجلّ HTTPS الأولي بالتوازي باستخدام DnsResolver.rawQuery حدِّد DnsResolver.TYPE_HTTPS كنوع طلب البحث.
مسؤولية المطوّرين والحالات الهامشية

في حال اختيار الطريقة 2، تتحمّل المكتبة مسؤوليات إضافية ويجب أن تأخذ في الاعتبار حالات أخرى.

  • تحليل سجلّ نظام أسماء النطاقات: يجب تحليل حمولة البايت الأولية لردّ نظام أسماء النطاقات من rawQuery لاستخراج EchConfigList.
  • التعامل مع حالات عدم تطابق السجلات: يجب التعامل مع حالات عدم التطابق بين طلبات البحث عن سجلات A/AAAA وHTTPS.
  • حالات التزامن: يجب مزامنة نتائج عمليات البحث المتوازية عن نظام أسماء النطاقات. إذا تم حلّ أحد طلبَي البحث قبل الآخر أو إذا انتهت المهلة المحدّدة لطلب البحث عبر HTTPS، عليك الرجوع إلى الخيار المناسب (على سبيل المثال، من خلال محاولة إجراء اتصال TLS عادي بدون ECH إذا تعذّر طلب البحث عبر HTTPS، أو استخدام ECH GREASE إذا كانت مفعّلة بموجب السياسة).

ضبط إعدادات بروتوكول أمان طبقة النقل (TLS)

بعد أن تسترد المكتبة قائمة إعدادات ECH (EchConfigList) من HttpsRecord، مرِّر هذه القائمة باستخدام إحدى واجهتَي برمجة التطبيقات المساعدتَين SSLSockets أو SSLEngines قبل بدء عملية المصافحة عبر TLS.

Kotlin

fun establishEchConnection(echConfigList: EchConfigList) {
    val socket = sslSocketFactory.createSocket(ipAddress, port) as SSLSocket
    SSLSockets.setEchConfigList(socket, echConfigList)
    socket.startHandshake()
}

Java

public void establishEchConnection(EchConfigList echConfigList)
    throws IOException {
    SSLSocket socket =
        (SSLSocket) sslSocketFactory.createSocket(ipAddress, port);
    SSLSockets.setEchConfigList(socket, echConfigList);
    socket.startHandshake();
}

التعامل مع مسار إعادة المحاولة

إذا لم تتزامن إعدادات ECH الخاصة بالخادم، ستفشل عملية المصافحة مع ظهور الخطأ EchConfigMismatchException (فئة فرعية من javax.net.ssl.SSLException). وقد يتضمّن الخادم إعدادات ECH معدَّلة في الرفض، ويجب استخدامها لإنشاء اتصال جديد. إذا لم تتم محاولة إعادة المحاولة على الرغم من أنّ الخادم يوفّر إعدادات صالحة لإعادة المحاولة، يجب أن تعرض المكتبة رسالة خطأ للتطبيق الذي تم استدعاؤه.

للتعامل مع عمليات إعادة المحاولة في ECH، عليك رصد الاستثناء واتّباع الخطوات التالية:

  1. يُرجى الاتصال بالرقم EchConfigMismatchException.getPublicHostname بشأن الاستثناء.
  2. تحقَّق من اسم الجهاز المضيف العام الذي تم عرضه باستخدام HostnameVerifier. إذا كانت القيمة null، عليك إلغاء عملية الربط.
  3. إذا نجح التحقّق من اسم المضيف، ابحث عن الإعدادات المعدَّلة باستخدام EchConfigMismatchException.getRetryConfigList.
  4. في حال توفُّر إعدادات معدَّلة، أعِد محاولة الاتصال باستخدام EchConfigList الجديد.

Kotlin

try {
    socket.startHandshake()
} catch (e: EchConfigMismatchException) {
    val publicName = e.publicHostname ?: throw e
    if (hostnameVerifier.verify(publicName, socket.session)) {
        val retryConfigList = e.retryConfigList
        if (retryConfigList != null) {
            retryConnection(retryConfigList)
        }
    } else {
        throw e // Hostname mismatch
    }
}

Java

try {
    socket.startHandshake();
} catch (EchConfigMismatchException e) {
    String publicName = e.getPublicHostname();
    if (publicName == null) {
        throw e;
    }
    if (hostnameVerifier.verify(publicName, socket.getSession())) {
        EchConfigList retryConfigList = e.getRetryConfigList();
        if (retryConfigList != null) {
            retryConnection(retryConfigList);
        }
    } else {
        throw e; // Hostname mismatch
    }
}

يمكنك الاطّلاع على مزيد من التفاصيل حول مسار إعادة المحاولة في RFC 9849، وتحديدًا سبب ضرورة إثبات الهوية للحصول على الاسم العام.