Wdrażanie funkcji Encrypted Client Hello (ECH)

Encrypted Client Hello (ECH) to rozszerzenie TLS, które szyfruje pole Server Name Indication (SNI) w wiadomości uzgadniania klienta. W Androidzie 17 (API na poziomie 37) i nowszych wersjach ECH jest obsługiwane domyślnie. ECH pomaga zachować prywatność ruchu w internecie, uniemożliwiając pośrednikom sieciowym wyświetlanie nazw hostów, z którymi łączy się aplikacja.

Dla deweloperów aplikacji

Aby wdrożyć ECH w aplikacji:

  1. Sprawdź, czy Twoja biblioteka sieciowa obsługuje ECH: upewnij się, że używasz wersji biblioteki, która obsługuje ECH na Androidzie:
    • OkHttp: od wersji OkHttp 5.5.0 możesz włączyć obsługę ECH, konfigurując AndroidDns lub DnsOverHttps w OkHttpClient.Builder. Więcej informacji znajdziesz w historii zmian.
    • HttpEngine: obsługa zostanie wprowadzona w Androidzie 17 QPR2 (poziom API 37.2). Nie będzie wymagana żadna specjalna konfiguracja.
    • WebView: obsługa zostanie dodana w przyszłej wersji.
  2. Skonfiguruj ustawienia bezpieczeństwa sieci: domyślnie rozszerzenie ECH jest włączone dla wszystkich domen, jeśli Twoja biblioteka je obsługuje. Jeśli chcesz wyłączyć lub wymusić ECH, skonfiguruj element domainEncryption w konfiguracji zabezpieczeń sieciowych.
  3. Zaktualizuj docelowy poziom SDK: ECH jest dostępny tylko na Androidzie 17 (poziom interfejsu API 37) i nowszym.

Dla deweloperów bibliotek

Jeśli tworzysz niestandardową bibliotekę sieciową HTTP lub rozszerzasz istniejącą, musisz zaimplementować obsługę ECH, korzystając z interfejsów API platformy.

Sprawdzanie zasad szyfrowania domeny

Przed wysłaniem zapytania o konfiguracje ECH lub zainicjowaniem połączeń sprawdź zasady szyfrowania domeny aplikacji, wywołując funkcję NetworkSecurityPolicy.getDomainEncryptionMode.

W zależności od zwróconego trybu postępuj z szyfrowaniem ECH w ten sposób:

  • DOMAIN_ENCRYPTION_MODE_DISABLED i DOMAIN_ENCRYPTION_MODE_UNKNOWN: nie pobieraj konfiguracji ECH ani nie próbuj używać ECH.
  • DOMAIN_ENCRYPTION_MODE_ENABLED i DOMAIN_ENCRYPTION_MODE_OPPORTUNISTIC: wymuszaj ECH. Pobierać konfiguracje ECH i używać ECH, jeśli serwer go obsługuje. Jeśli serwer nie obsługuje rozszerzenia ECH, włącz ECH GREASE.

Pobieranie konfiguracji ECH

Aby połączyć się z ECH, musisz rozwiązać rekord DNS HTTPS serwera zawierający konfiguracje ECH. Gdy aplikacje korzystają z systemowego DNS, dane te można pobrać jedną z 2 metod:

Metoda 1. Korzystanie z interfejsu API DnsResolver.query wysokiego poziomu

Jeśli biblioteka nie wymaga niestandardowych mechanizmów rozpoznawania nazw DNS, możesz użyć interfejsu API DnsResolver.query wysokiego poziomu platformy. Ten interfejs API wykonuje równoległe zapytania o rekordy A/AAAA/HTTPS i łączy wyniki w 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 */ }
    });

Metoda 2. Używanie właściwości getAllByName i DnsResolver.rawQuery

W przypadku bibliotek, które zarządzają własnymi połączeniami gniazd i potokami rozpoznawania nazw DNS, możesz woleć rozpoznawać adresy IP za pomocą standardowych interfejsów API, a rekord HTTPS pobierać osobno:

  1. Rozwiąż rekordy A/AAAA za pomocą InetAddress.getAllByName w przypadku sieci domyślnej lub Network.getAllByName.
  2. Pobierz równolegle nieprzetworzony rekord HTTPS za pomocą polecenia DnsResolver.rawQuery. Określ DnsResolver.TYPE_HTTPS jako typ zapytania.
Odpowiedzialność dewelopera i przypadki graniczne

Jeśli wybierzesz metodę 2, Twoja biblioteka będzie mieć dodatkowe obowiązki i sytuacje brzegowe, które należy wziąć pod uwagę.

  • Parsowanie rekordu DNS: musisz przeanalizować surowy ładunek bajtowy odpowiedzi DNS z rawQuery, aby wyodrębnić EchConfigList.
  • Obsługa niezgodności rekordów: musisz obsługiwać niezgodności między zapytaniami A/AAAA i HTTPS.
  • Warunki wyścigu: musisz zsynchronizować wyniki równoległych wyszukiwań DNS. Jeśli jedno zapytanie zostanie rozwiązane przed drugim lub jeśli zapytanie HTTPS przekroczy limit czasu, musisz odpowiednio zareagować (np. spróbować nawiązać standardowe połączenie TLS bez ECH, jeśli zapytanie HTTPS się nie powiedzie, lub użyć ECH GREASE, jeśli jest włączone przez zasady).

Konfigurowanie TLS

Gdy biblioteka pobierze listę konfiguracji ECH (EchConfigList) z HttpsRecord, przekaż ją za pomocą interfejsów API narzędzi SSLSockets lub SSLEngines przed rozpoczęciem uzgadniania 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();
}

Obsługa przepływu ponawiania

Jeśli konfiguracje ECH serwera zostaną rozsynchronizowane, uzgadnianie zakończy się niepowodzeniem z błędem EchConfigMismatchException (podklasa błędu javax.net.ssl.SSLException). Serwer może dołączyć do odrzucenia zaktualizowane konfiguracje ECH, które należy wykorzystać do nawiązania nowego połączenia. Jeśli ponowna próba nie zostanie podjęta pomimo tego, że serwer podaje prawidłowe konfiguracje ponownych prób, biblioteka musi zgłosić błąd do aplikacji wywołującej.

Aby obsłużyć ponowne próby ECH, przechwyć wyjątek i wykonaj te czynności:

  1. Zadzwoń pod numer EchConfigMismatchException.getPublicHostname, aby zgłosić wyjątek.
  2. Sprawdź zwróconą publiczną nazwę hosta za pomocą HostnameVerifier. Jeśli jest to null, przerwij połączenie.
  3. Jeśli weryfikacja nazwy hosta zakończy się powodzeniem, sprawdź zaktualizowane konfiguracje za pomocą polecenia EchConfigMismatchException.getRetryConfigList.
  4. Jeśli dostępne są zaktualizowane konfiguracje, spróbuj ponownie nawiązać połączenie z nowym 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
    }
}

Więcej informacji o procesie ponawiania znajdziesz w dokumencie RFC 9849, a w szczególności w wyjaśnieniu, dlaczego konieczne jest uwierzytelnianie w przypadku nazwy publicznej.