Wdrażanie funkcji Encrypted Client Hello (ECH)

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

Dla deweloperów aplikacji

Aby wdrożyć ECH w aplikacji:

  1. Sprawdź, czy biblioteka sieciowa obsługuje ECH: upewnij się, że używasz wersji biblioteki, która obsługuje ECH w Androidzie. Wkrótce będzie ona obsługiwana w OkHttp i HttpEngine.
  2. Skonfiguruj ustawienia bezpieczeństwa sieci: jeśli Twoja biblioteka obsługuje ECH, jest ono domyślnie włączone we wszystkich domenach. Jeśli chcesz wyłączyć lub wymusić ECH, skonfiguruj element domainEncryption w ustawieniach bezpieczeństwa sieci.
  3. Zaktualizuj docelowy poziom pakietu SDK: ECH jest dostępne tylko w Androidzie 17 (poziom interfejsu API 37) i nowszych wersjach.

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

Zanim zapytasz o konfiguracje ECH lub zainicjujesz połączenia, sprawdź zasady szyfrowania domeny aplikacji, wywołując NetworkSecurityPolicy.getDomainEncryptionMode.

W zależności od zwróconego trybu postępuj z 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: wymuś ECH. Pobierz konfiguracje ECH i użyj ECH, jeśli serwer je obsługuje. Jeśli serwer nie obsługuje ECH, włącz ECH GREASE.

Pobieranie konfiguracji ECH

Aby połączyć się z ECH, musisz rozpoznać rekord DNS HTTPS serwera zawierający konfiguracje ECH. Gdy aplikacje używają DNS systemu, te dane można pobrać za pomocą jednej z 2 metod:

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

Jeśli Twoja biblioteka nie wymaga niestandardowych mechanizmów rozpoznawania nazw DNS, możesz użyć interfejsu API wysokiego poziomu DnsResolver.query 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. Korzystanie z 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. Rozpoznaj rekordy A/AAAA za pomocą InetAddress.getAllByName w przypadku domyślnej sieci lub Network.getAllByName.
  2. Pobierz równolegle surowy rekord HTTPS za pomocą DnsResolver.rawQuery. Jako typ zapytania określ DnsResolver.TYPE_HTTPS.
Obowiązki dewelopera i przypadki brzegowe

Jeśli wybierzesz metodę 2, Twoja biblioteka będzie miała dodatkowe obowiązki i przypadki brzegowe, które musisz wziąć pod uwagę.

  • Parsowanie rekordów 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 się wycofać (np. próbując standardowego połączenia TLS bez ECH, jeśli zapytanie HTTPS się nie powiedzie, lub używając ECH GREASE, jeśli jest włączone przez zasady).

Konfigurowanie TLS

Gdy biblioteka pobierze listę konfiguracji ECH (EchConfigList) z HttpsRecord, przekaż tę listę za pomocą interfejsów API narzędziowych 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 procesu ponawiania

Jeśli konfiguracje ECH serwera są niespójne, uzgadnianie nie powiedzie się z powodu EchConfigMismatchException (podklasy javax.net.ssl.SSLException). Serwer może dołączyć zaktualizowane konfiguracje ECH do odrzucenia, które należy wykorzystać do nawiązania nowego połączenia. Jeśli ponowienie nie zostanie podjęte pomimo tego, że serwer udostępnia prawidłowe konfiguracje ponawiania, biblioteka musi zgłosić błąd do aplikacji wywołującej.

Aby obsługiwać ponawianie ECH, przechwyć wyjątek i wykonaj te czynności:

  1. Wywołaj EchConfigMismatchException.getPublicHostname w przypadku wyjątku.
  2. Sprawdź zwróconą publiczną nazwę hosta za pomocą HostnameVerifier. Jeśli jest to null, przerwij połączenie.
  3. Jeśli weryfikacja nazwy hosta się powiedzie, sprawdź, czy są dostępne zaktualizowane konfiguracje za pomocą EchConfigMismatchException.getRetryConfigList.
  4. Jeśli dostępne są zaktualizowane konfiguracje, ponów połączenie z nową 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, w szczególności o tym, dlaczego konieczne jest uwierzytelnianie na podstawie nazwy publicznej.