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 dengan aplikasi.

Untuk Developer Aplikasi

Untuk mengadopsi ECH di aplikasi Anda:

  1. Periksa library jaringan Anda untuk dukungan ECH: Pastikan Anda menggunakan versi library yang mendukung ECH di Android. Dukungan akan segera hadir di OkHttp dan HttpEngine.
  2. Konfigurasi 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 (API level 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 ambil konfigurasi ECH atau coba ECH.
  • DOMAIN_ENCRYPTION_MODE_ENABLED dan DOMAIN_ENCRYPTION_MODE_OPPORTUNISTIC: Terapkan ECH. Ambil konfigurasi ECH dan gunakan 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 tingkat tinggi platform DnsResolver.query. API ini membuat kueri paralel untuk data A/AAAA/HTTPS dan menggabungkan hasilnya ke dalam 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 sendiri, Anda mungkin lebih suka me-resolve alamat IP menggunakan API standar sambil mengambil data HTTPS secara terpisah:

  1. Resolve data A/AAAA menggunakan InetAddress.getAllByName untuk jaringan default atau Network.getAllByName.
  2. Ambil data 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 dan kasus ekstrem tambahan yang perlu dipertimbangkan.

  • Penguraian Data DNS: Anda harus mengurai payload byte mentah dari respons DNS dari rawQuery untuk mengekstrak EchConfigList.
  • Menangani Ketidakcocokan Data: Anda harus menangani ketidakkonsistenan antara kueri A/AAAA dan HTTPS.
  • Kondisi Balap: Anda harus menyinkronkan hasil pencarian DNS paralel. Jika satu kueri di-resolve sebelum kueri lainnya atau jika kueri HTTPS habis waktunya, 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 salah satu 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 lagi sinkron, handshake akan gagal dengan EchConfigMismatchException (subclass javax.net.ssl.SSLException). Server dapat menyertakan konfigurasi ECH yang diperbarui dalam penolakannya, yang harus digunakan untuk membuat koneksi baru. Jika percobaan ulang tidak dicoba meskipun server memberikan konfigurasi percobaan ulang yang valid, library harus melaporkan error ke aplikasi panggilan.

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. Jika null, batalkan koneksi.
  3. Jika verifikasi nama host berhasil, periksa konfigurasi yang diperbarui menggunakan EchConfigMismatchException.getRetryConfigList.
  4. Jika konfigurasi yang diperbarui tersedia, coba lagi koneksi dengan yang baru 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
    }
}

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