پذیرش کلاینت هلو رمزگذاری‌شده (ECH)

سلام رمزگذاری‌شده‌ی کلاینت (ECH) یک افزونه‌ی TLS است که فیلد نشانگر نام سرور (SNI) را در پیام handshake کلاینت رمزگذاری می‌کند. در اندروید ۱۷ (سطح API ۳۷) و بالاتر، ECH به طور پیش‌فرض پشتیبانی می‌شود. ECH با جلوگیری از مشاهده‌ی نام‌های میزبان توسط واسطه‌های شبکه، به خصوصی نگه داشتن ترافیک وب کاربران کمک می‌کند.

برای توسعه‌دهندگان اپلیکیشن

برای پذیرش ECH در درخواست خود:

  1. کتابخانه شبکه خود را برای پشتیبانی ECH بررسی کنید : مطمئن شوید که از نسخه کتابخانه‌ای استفاده می‌کنید که از ECH در اندروید پشتیبانی می‌کند. این پشتیبانی به زودی در OkHttp و HttpEngine ارائه خواهد شد.
  2. پیکربندی امنیت شبکه : به طور پیش‌فرض، اگر کتابخانه شما از ECH پشتیبانی کند، برای همه دامنه‌ها فعال است. اگر نیاز به غیرفعال کردن یا اجرای ECH دارید، عنصر domainEncryption را در پیکربندی امنیت شبکه خود پیکربندی کنید.
  3. به‌روزرسانی سطح SDK هدف : ECH فقط در اندروید ۱۷ (سطح API ۳۷) و بالاتر در دسترس است.

برای توسعه‌دهندگان کتابخانه

اگر در حال توسعه یک کتابخانه شبکه HTTP سفارشی یا گسترش یک کتابخانه موجود هستید، باید با تعامل با APIهای پلتفرم، پشتیبانی 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 پشتیبانی نمی‌کند، ECH GREASE را فعال کنید.

بازیابی تنظیمات ECH

برای اتصال به ECH، باید رکورد HTTPS DNS سرور که حاوی تنظیمات ECH است را resolve کنید. وقتی برنامه‌ها از DNS سیستم استفاده می‌کنند، این داده‌ها را می‌توان با استفاده از یکی از دو روش زیر بازیابی کرد:

روش ۱: استفاده از API سطح بالای DnsResolver.query

اگر کتابخانه شما به مکانیزم‌های سفارشی DNS Resolution نیاز ندارد، می‌توانید از API سطح بالای DnsResolver.query پلتفرم استفاده کنید. این API کوئری‌های موازی برای رکوردهای A/AAAA/HTTPS ایجاد می‌کند و نتایج را در یک HttpsEndpoint ترکیب می‌کند.

کاتلین

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 */ }
    })

جاوا

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 */ }
    });

روش دوم: استفاده از getAllByName و DnsResolver.rawQuery

برای کتابخانه‌هایی که اتصالات سوکت و خطوط لوله‌ی تحلیل DNS خود را مدیریت می‌کنند، ممکن است ترجیح دهید آدرس‌های IP را با استفاده از APIهای استاندارد حل کنید و رکورد HTTPS را جداگانه دریافت کنید:

  1. رکوردهای A/AAAA را با استفاده از InetAddress.getAllByName برای شبکه پیش‌فرض یا Network.getAllByName بازیابی کنید.
  2. رکورد خام HTTPS را به صورت موازی با استفاده از DnsResolver.rawQuery بازیابی کنید. نوع پرس و جو را DnsResolver.TYPE_HTTPS تعیین کنید.
مسئولیت توسعه‌دهنده و موارد حاشیه‌ای

اگر روش ۲ را انتخاب کنید، کتابخانه شما مسئولیت‌ها و موارد خاص دیگری دارد که باید در نظر بگیرد.

  • تجزیه رکورد DNS : برای استخراج EchConfigList باید بایت خام پاسخ DNS را از rawQuery تجزیه کنید.
  • مدیریت عدم تطابق رکوردها : شما باید ناسازگاری‌های بین کوئری‌های A/AAAA و HTTPS را مدیریت کنید.
  • شرایط رقابتی : شما باید نتایج جستجوهای موازی DNS را همگام‌سازی کنید. اگر یک پرس‌وجو قبل از دیگری حل شود یا اگر زمان پرس‌وجوی HTTPS تمام شود، باید به روش مناسب به عقب برگردید (برای مثال، با تلاش برای اتصال استاندارد TLS بدون ECH در صورت عدم موفقیت پرس‌وجوی HTTPS، یا استفاده از ECH GREASE در صورت فعال بودن توسط سیاست).

پیکربندی TLS

پس از اینکه کتابخانه لیست پیکربندی ECH ( EchConfigList ) را از HttpsRecord بازیابی کرد، قبل از شروع عملیات TLS handshake، این لیست را با استفاده از APIهای ابزار SSLSockets یا SSLEngines ارسال کنید.

کاتلین

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

جاوا

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 جدید دوباره امتحان کنید.

کاتلین

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
    }
}

جاوا

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 ببینید، به ویژه اینکه چرا احراز هویت برای نام عمومی ضروری است .