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:
- 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.
- Konfigurasi Konfigurasi Keamanan Jaringan: Secara default, ECH diaktifkan untuk semua
domain jika library Anda mendukungnya. Jika Anda perlu menonaktifkan atau menerapkan ECH,
konfigurasi elemen
domainEncryptiondi Konfigurasi Keamanan Jaringan Anda. - 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_DISABLEDdanDOMAIN_ENCRYPTION_MODE_UNKNOWN: Jangan ambil konfigurasi ECH atau coba ECH.DOMAIN_ENCRYPTION_MODE_ENABLEDdanDOMAIN_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:
- Resolve data A/AAAA menggunakan
InetAddress.getAllByNameuntuk jaringan default atauNetwork.getAllByName. - Ambil data HTTPS mentah secara paralel menggunakan
DnsResolver.rawQuery. TentukanDnsResolver.TYPE_HTTPSsebagai 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
rawQueryuntuk mengekstrakEchConfigList. - 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:
- Panggil
EchConfigMismatchException.getPublicHostnamepada pengecualian. - Verifikasi nama host publik yang ditampilkan menggunakan
HostnameVerifier. Jikanull, batalkan koneksi. - Jika verifikasi nama host berhasil, periksa konfigurasi yang diperbarui
menggunakan
EchConfigMismatchException.getRetryConfigList. - 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.