Encrypted Client Hello'nun (ECH) benimsenmesi

Encrypted Client Hello (ECH), istemcinin el sıkışma mesajındaki Sunucu Adı Göstergesi (SNI) alanını şifreleyen bir TLS uzantısıdır. Android 17 (API düzeyi 37) ve sonraki sürümlerde ECH varsayılan olarak desteklenir. ECH, bir uygulamanın bağlandığı ana makine adlarının ağ aracıları tarafından görülmesini engelleyerek kullanıcıların web trafiğinin gizli kalmasına yardımcı olur.

Uygulama Geliştiriciler İçin

Uygulamanızda ECH'yi kullanmak için:

  1. Ağ kitaplığınızda ECH desteği olup olmadığını kontrol edin: Android'de ECH'yi destekleyen bir kitaplık sürümü kullandığınızdan emin olun. OkHttp ve HttpEngine'de destek yakında kullanıma sunulacak.
  2. Ağ Güvenlik Yapılandırması'nı yapılandırın: Kitaplığınız destekliyorsa ECH varsayılan olarak tüm alan adları için etkindir. ECH'yi devre dışı bırakmanız veya zorunlu kılmanız gerekiyorsa domainEncryption öğesini Network Security Config dosyanızda yapılandırın.
  3. Hedef SDK düzeyini güncelleyin: ECH yalnızca Android 17 (API düzeyi 37) ve sonraki sürümlerde kullanılabilir.

Kitaplık Geliştiricileri İçin

Özel bir HTTP ağ kitaplığı geliştiriyorsanız veya mevcut bir kitaplığı genişletiyorsanız platform API'leriyle etkileşimde bulunarak ECH desteğini uygulamanız gerekir.

Alan şifreleme politikasını kontrol etme

ECH yapılandırmalarını sorgulamadan veya bağlantı başlatmadan önce NetworkSecurityPolicy.getDomainEncryptionMode işlevini çağırarak uygulamanın alan şifreleme politikasını kontrol edin.

Döndürülen moda bağlı olarak ECH'yi aşağıdaki gibi işleyin:

  • DOMAIN_ENCRYPTION_MODE_DISABLED ve DOMAIN_ENCRYPTION_MODE_UNKNOWN: ECH yapılandırmalarını getirmeyin veya ECH'yi denemeyin.
  • DOMAIN_ENCRYPTION_MODE_ENABLED ve DOMAIN_ENCRYPTION_MODE_OPPORTUNISTIC: ECH'yi zorunlu kılın. ECH yapılandırmalarını alıp sunucu destekliyorsa ECH'yi kullanır. Sunucu ECH'yi desteklemiyorsa ECH GREASE'i etkinleştirin.

ECH yapılandırmalarını alma

ECH ile bağlantı kurmak için sunucunun ECH yapılandırmalarını içeren HTTPS DNS kaydını çözümlemeniz gerekir. Uygulamalar sistem DNS'sini kullanırken bu veriler iki yöntemden biriyle alınabilir:

1. yöntem: Üst düzey DnsResolver.query API'yi kullanma

Kitaplığınız özel DNS çözümleme mekanizmaları gerektirmiyorsa platformun üst düzey DnsResolver.query API'sini kullanabilirsiniz. Bu API, A/AAAA/HTTPS kayıtları için paralel sorgular yapar ve sonuçları bir HttpsEndpoint içinde birleştirir.

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. yöntem: getAllByName ve DnsResolver.rawQuery özelliğini kullanma

Kendi soket bağlantılarını ve DNS çözümleme işlem hatlarını yöneten kitaplıklar için, HTTPS kaydını ayrı olarak getirirken IP adreslerini standart API'ler kullanarak çözmeyi tercih edebilirsiniz:

  1. Varsayılan ağ veya InetAddress.getAllByName için Network.getAllByName kullanarak A/AAAA kayıtlarını çözün.
  2. DnsResolver.rawQuery kullanarak ham HTTPS kaydını paralel olarak alın. Sorgu türü olarak DnsResolver.TYPE_HTTPS simgesini belirtin.
Geliştiricinin sorumluluğu ve uç durumlar

2. yöntemi seçerseniz kitaplığınızın ek sorumlulukları ve dikkate alması gereken uç durumlar olur.

  • DNS Kaydı Ayrıştırma: rawQuery'den gelen DNS yanıtının ham bayt yükünü ayrıştırarak EchConfigList'yi çıkarmanız gerekir.
  • Kayıt Uyuşmazlıklarını İşleme: A/AAAA ve HTTPS sorguları arasındaki tutarsızlıkları işlemeniz gerekir.
  • Yarışma Koşulları: Paralel DNS aramalarının sonuçlarını senkronize etmeniz gerekir. Bir sorgu diğerinden önce çözümlenirse veya HTTPS sorgusunun zaman aşımı olursa uygun şekilde geri dönmeniz gerekir (ör. HTTPS sorgusu başarısız olursa ECH olmadan standart bir TLS bağlantısı denemek veya politika tarafından etkinleştirilmişse ECH GREASE kullanmak).

TLS'yi yapılandırma

Kitaplık, ECH yapılandırma listesini (EchConfigList) HttpsRecord'den aldıktan sonra TLS el sıkışması başlatılmadan önce SSLSockets veya SSLEngines yardımcı programı API'lerini kullanarak bu listeyi iletin.

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();
}

Yeniden deneme akışını işleme

Sunucunun ECH yapılandırmaları senkronize olmamışsa el sıkışma işlemi EchConfigMismatchException (javax.net.ssl.SSLException alt sınıfı) ile başarısız olur. Sunucu, reddetme işlemine güncellenmiş ECH yapılandırmalarını ekleyebilir. Bu yapılandırmalar yeni bir bağlantı oluşturmak için kullanılmalıdır. Sunucu geçerli yeniden deneme yapılandırmaları sağladığı halde yeniden deneme yapılmazsa kitaplık, çağıran uygulamaya bir hata bildirmelidir.

ECH yeniden denemelerini işlemek için istisnayı yakalayın ve şu adımları uygulayın:

  1. İstisna için EchConfigMismatchException.getPublicHostname numaralı telefonu arayın.
  2. HostnameVerifier kullanarak döndürülen genel ana makine adını doğrulayın. null ise bağlantıyı iptal edin.
  3. Ana makine adı doğrulaması başarılı olursa EchConfigMismatchException.getRetryConfigList kullanarak güncellenmiş yapılandırmaları kontrol edin.
  4. Güncellenmiş yapılandırmalar varsa yeni EchConfigList ile bağlantıyı yeniden deneyin.

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

Yeniden deneme akışıyla ilgili daha fazla bilgiyi RFC 9849'da, özellikle de ortak ad için neden kimlik doğrulamanın gerekli olduğu hakkında bulabilirsiniz.