Mengadopsi Encrypted Client Hello (ECH)

Encrypted Client Hello (ECH) adalah ekstensi TLS yang mengenkripsi kolom Server Name Indication (SNI) dalam pesan handshake klien. Di Android 17 (API level 37) dan yang lebih tinggi, ECH didukung secara default. ECH membantu menjaga privasi traffic web pengguna dengan mencegah perantara jaringan melihat nama host yang terhubung ke aplikasi.

Untuk Developer Aplikasi

Untuk menerapkan ECH di aplikasi Anda:

  1. Periksa dukungan ECH di library jaringan Anda: Pastikan Anda menggunakan versi library yang mendukung ECH di Android:
    • OkHttp: Mulai OkHttp 5.5.0, Anda dapat mengaktifkan dukungan ECH dengan mengonfigurasi AndroidDns atau DnsOverHttps di OkHttpClient.Builder. Untuk mengetahui informasi selengkapnya, lihat log perubahan.
    • HttpEngine: Dukungan akan hadir di Android 17 QPR2 (level API 37.2). Tidak diperlukan konfigurasi khusus.
    • WebView: Dukungan akan ditambahkan dalam rilis mendatang.
  2. Mengonfigurasi Konfigurasi Keamanan Jaringan: Secara default, ECH diaktifkan untuk semua domain jika library Anda mendukungnya. Jika Anda perlu menonaktifkan atau menerapkan ECH, konfigurasi elemen domainEncryption di Konfigurasi Keamanan Jaringan Anda.
  3. Perbarui level SDK target: ECH hanya tersedia di Android 17 (level API 37) dan yang lebih tinggi.

Untuk Developer Library

Jika Anda mengembangkan library jaringan HTTP kustom atau memperluas library yang sudah ada, Anda harus menerapkan dukungan ECH dengan berinteraksi dengan API platform.

Memeriksa kebijakan enkripsi domain

Sebelum membuat kueri konfigurasi ECH atau memulai koneksi, periksa kebijakan enkripsi domain aplikasi dengan memanggil NetworkSecurityPolicy.getDomainEncryptionMode.

Bergantung pada mode yang ditampilkan, tangani ECH sebagai berikut:

  • DOMAIN_ENCRYPTION_MODE_DISABLED dan DOMAIN_ENCRYPTION_MODE_UNKNOWN: Jangan mengambil konfigurasi ECH atau mencoba ECH.
  • DOMAIN_ENCRYPTION_MODE_ENABLED dan DOMAIN_ENCRYPTION_MODE_OPPORTUNISTIC: Menerapkan ECH. Mengambil konfigurasi ECH dan menggunakan ECH jika server mendukungnya. Jika server tidak mendukung ECH, aktifkan ECH GREASE.

Mengambil konfigurasi ECH

Untuk terhubung dengan ECH, Anda harus me-resolve data DNS HTTPS server yang berisi konfigurasi ECH. Saat aplikasi menggunakan DNS sistem, data ini dapat diambil menggunakan salah satu dari dua metode:

Metode 1: Menggunakan DnsResolver.query API tingkat tinggi

Jika library Anda tidak memerlukan mekanisme resolusi DNS kustom, Anda dapat menggunakan API DnsResolver.query tingkat tinggi platform. API ini membuat kueri paralel untuk data A/AAAA/HTTPS dan menggabungkan hasilnya menjadi 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 */ }
    });

Metode 2: Menggunakan getAllByName dan DnsResolver.rawQuery

Untuk library yang mengelola koneksi soket dan pipeline resolusi DNS-nya sendiri, Anda mungkin lebih memilih untuk menyelesaikan alamat IP menggunakan API standar sambil mengambil rekaman HTTPS secara terpisah:

  1. Selesaikan data A/AAAA menggunakan InetAddress.getAllByName untuk jaringan default atau Network.getAllByName.
  2. Ambil rekaman HTTPS mentah secara paralel menggunakan DnsResolver.rawQuery. Tentukan DnsResolver.TYPE_HTTPS sebagai jenis kueri.
Tanggung jawab developer dan kasus ekstrem

Jika Anda memilih Metode 2, library Anda memiliki tanggung jawab tambahan dan kasus ekstrem yang perlu dipertimbangkan.

  • Penguraian Data DNS: Anda harus mengurai payload byte mentah respons DNS dari rawQuery untuk mengekstrak EchConfigList.
  • Menangani Ketidakcocokan Data: Anda harus menangani inkonsistensi antara kueri A/AAAA dan HTTPS.
  • Kondisi Persaingan (Race Conditions): Anda harus menyinkronkan hasil pencarian DNS paralel. Jika salah satu kueri diselesaikan sebelum kueri lainnya atau jika kueri HTTPS mengalami waktu tunggu habis, Anda harus melakukan penggantian yang sesuai (misalnya, dengan mencoba koneksi TLS standar tanpa ECH jika kueri HTTPS gagal, atau menggunakan ECH GREASE jika diaktifkan oleh kebijakan).

Mengonfigurasi TLS

Setelah library mengambil daftar konfigurasi ECH (EchConfigList) dari HttpsRecord, teruskan daftar ini menggunakan API utilitas SSLSockets atau SSLEngines sebelum memulai handshake 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();
}

Menangani alur percobaan ulang

Jika konfigurasi ECH server tidak sinkron, handshake akan gagal dengan EchConfigMismatchException (subclass dari javax.net.ssl.SSLException). Server dapat menyertakan konfigurasi ECH yang diperbarui dalam penolakannya, yang harus digunakan untuk membuat koneksi baru. Jika percobaan ulang tidak dilakukan meskipun server memberikan konfigurasi percobaan ulang yang valid, library harus melaporkan error ke aplikasi yang memanggil.

Untuk menangani percobaan ulang ECH, tangkap pengecualian dan lakukan langkah-langkah berikut:

  1. Panggil EchConfigMismatchException.getPublicHostname pada pengecualian.
  2. Verifikasi nama host publik yang ditampilkan menggunakan HostnameVerifier Anda. Jika null, batalkan koneksi.
  3. Jika verifikasi nama host berhasil, periksa konfigurasi yang diperbarui menggunakan EchConfigMismatchException.getRetryConfigList.
  4. Jika konfigurasi yang diperbarui tersedia, coba hubungkan lagi dengan EchConfigList baru.

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

Lihat detail selengkapnya tentang alur percobaan ulang di RFC 9849, khususnya alasan autentikasi diperlukan untuk nama publik.