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:
- 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.
- 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
domainEncryptionw ustawieniach bezpieczeństwa sieci. - 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_DISABLEDiDOMAIN_ENCRYPTION_MODE_UNKNOWN: nie pobieraj konfiguracji ECH ani nie próbuj używać ECH.DOMAIN_ENCRYPTION_MODE_ENABLEDiDOMAIN_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:
- Rozpoznaj rekordy A/AAAA za pomocą
InetAddress.getAllByNamew przypadku domyślnej sieci lubNetwork.getAllByName. - Pobierz równolegle surowy rekord HTTPS za pomocą
DnsResolver.rawQuery. Jako typ zapytania określDnsResolver.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:
- Wywołaj
EchConfigMismatchException.getPublicHostnamew przypadku wyjątku. - Sprawdź zwróconą publiczną nazwę hosta za pomocą
HostnameVerifier. Jeśli jest tonull, przerwij połączenie. - Jeśli weryfikacja nazwy hosta się powiedzie, sprawdź, czy są dostępne zaktualizowane konfiguracje
za pomocą
EchConfigMismatchException.getRetryConfigList. - 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.