Encrypted Client Hello (ECH) ist eine TLS-Erweiterung, mit der das SNI-Feld (Server Name Indication) in der Handshake-Nachricht des Clients verschlüsselt wird. In Android 17 (API-Level 37) und höher wird ECH standardmäßig unterstützt. ECH trägt dazu bei, den Web-Traffic von Nutzern zu schützen, indem verhindert wird, dass Netzwerkzwischenstellen die Hostnamen sehen, mit denen eine App eine Verbindung herstellt.
Für App-Entwickler
So implementieren Sie ECH in Ihrer Anwendung:
- Netzwerkbibliothek auf ECH-Unterstützung prüfen: Achten Sie darauf, dass Sie eine Bibliotheksversion verwenden, die ECH unter Android unterstützt:
- OkHttp: Ab OkHttp 5.5.0 können Sie die ECH-Unterstützung aktivieren, indem Sie
AndroidDnsoderDnsOverHttpsfürOkHttpClient.Builderkonfigurieren. Weitere Informationen finden Sie im Änderungslog. - HttpEngine: Die Unterstützung ist in Android 17 QPR2 (API-Level 37.2) verfügbar. Es ist keine spezielle Konfiguration erforderlich.
- WebView: Die Unterstützung wird in einer künftigen Version hinzugefügt.
- OkHttp: Ab OkHttp 5.5.0 können Sie die ECH-Unterstützung aktivieren, indem Sie
- Network Security Config konfigurieren: Standardmäßig ist ECH für alle Domains aktiviert, wenn Ihre Bibliothek dies unterstützt. Wenn Sie ECH deaktivieren oder erzwingen möchten, konfigurieren Sie das
domainEncryption-Element in Ihrer Netzwerksicherheitskonfiguration. - Ziel-SDK-Level aktualisieren: ECH ist nur auf Android 17 (API-Level 37) und höher verfügbar.
Für Bibliotheksentwickler
Wenn Sie eine benutzerdefinierte HTTP-Netzwerkbibliothek entwickeln oder eine vorhandene erweitern, sollten Sie ECH-Unterstützung implementieren, indem Sie mit den Plattform-APIs interagieren.
Domainverschlüsselungsrichtlinie prüfen
Bevor Sie ECH-Konfigurationen abfragen oder Verbindungen herstellen, rufen Sie die Richtlinie zur Domainverschlüsselung der App mit NetworkSecurityPolicy.getDomainEncryptionMode auf.
Je nach zurückgegebenem Modus gehen Sie so vor:
DOMAIN_ENCRYPTION_MODE_DISABLEDundDOMAIN_ENCRYPTION_MODE_UNKNOWN: ECH-Konfigurationen nicht abrufen oder ECH nicht versuchen.DOMAIN_ENCRYPTION_MODE_ENABLEDundDOMAIN_ENCRYPTION_MODE_OPPORTUNISTIC: ECH erzwingen. ECH-Konfigurationen abrufen und ECH verwenden, wenn der Server es unterstützt. Wenn der Server ECH nicht unterstützt, aktivieren Sie ECH GREASE.
ECH-Konfigurationen abrufen
Um eine Verbindung mit ECH herzustellen, müssen Sie den HTTPS-DNS-Eintrag des Servers auflösen, der die ECH-Konfigurationen enthält. Wenn Apps das System-DNS verwenden, können diese Daten mit einer von zwei Methoden abgerufen werden:
Methode 1: DnsResolver.query API auf hoher Ebene verwenden
Wenn für Ihre Bibliothek keine benutzerdefinierten DNS-Auflösungsmechanismen erforderlich sind, können Sie die DnsResolver.query-API auf hoher Ebene der Plattform verwenden. Mit dieser API werden parallele Anfragen für die A-/AAAA-/HTTPS-Einträge gestellt und die Ergebnisse in einem HttpsEndpoint kombiniert.
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 */ }
});
Methode 2: getAllByName und DnsResolver.rawQuery verwenden
Bei Bibliotheken, die ihre eigenen Socket-Verbindungen und DNS-Auflösungspipelines verwalten, ist es möglicherweise besser, IP-Adressen mit Standard-APIs aufzulösen und den HTTPS-Eintrag separat abzurufen:
- Lösen Sie A‑/AAAA‑Einträge mit
InetAddress.getAllByNamefür das Standardnetzwerk oderNetwork.getAllByNameauf. - Rufen Sie den rohen HTTPS-Eintrag parallel mit
DnsResolver.rawQueryab. Geben SieDnsResolver.TYPE_HTTPSals Abfragetyp an.
Verantwortung der Entwickler und Grenzfälle
Wenn Sie Methode 2 auswählen, müssen Sie in Ihrer Bibliothek zusätzliche Verantwortlichkeiten und Grenzfälle berücksichtigen.
- DNS-Einträge parsen: Sie müssen die Rohbyte-Nutzlast der DNS-Antwort von
rawQueryparsen, um dieEchConfigListzu extrahieren. - Umgang mit nicht übereinstimmenden Einträgen: Sie müssen Inkonsistenzen zwischen den A-/AAAA- und HTTPS-Anfragen berücksichtigen.
- Race Conditions: Sie müssen die Ergebnisse der parallelen DNS-Lookups synchronisieren. Wenn eine Anfrage vor der anderen aufgelöst wird oder die HTTPS-Anfrage ein Zeitlimit überschreitet, müssen Sie entsprechend reagieren, z. B. indem Sie eine Standard-TLS-Verbindung ohne ECH versuchen, wenn die HTTPS-Anfrage fehlschlägt, oder ECH GREASE verwenden, wenn es durch die Richtlinie aktiviert ist.
TLS konfigurieren
Sobald die Bibliothek die ECH-Konfigurationsliste (EchConfigList) vom HttpsRecord abgerufen hat, übergeben Sie diese Liste entweder über die Utility-APIs SSLSockets oder SSLEngines, bevor Sie den TLS-Handshake starten.
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();
}
Wiederholungsablauf behandeln
Wenn die ECH-Konfigurationen des Servers nicht mehr synchron sind, schlägt der Handshake mit einem EchConfigMismatchException (einer Unterklasse von javax.net.ssl.SSLException) fehl. Der Server kann aktualisierte ECH-Konfigurationen in seine Ablehnung aufnehmen, die zum Herstellen einer neuen Verbindung verwendet werden sollten. Wenn kein Wiederholungsversuch unternommen wird, obwohl der Server gültige Wiederholungskonfigurationen bereitstellt, muss die Bibliothek der aufrufenden Anwendung einen Fehler melden.
So gehen Sie mit ECH-Wiederholungen um: Fangen Sie die Ausnahme ab und führen Sie die folgenden Schritte aus:
- Rufen Sie
EchConfigMismatchException.getPublicHostnamefür die Ausnahme auf. - Prüfen Sie den zurückgegebenen öffentlichen Hostnamen mit Ihrem
HostnameVerifier. Wenn esnullist, brechen Sie die Verbindung ab. - Wenn die Hostname-Überprüfung erfolgreich ist, prüfen Sie mit
EchConfigMismatchException.getRetryConfigListauf aktualisierte Konfigurationen. - Wenn aktualisierte Konfigurationen verfügbar sind, versuchen Sie noch einmal, eine Verbindung mit dem neuen
EchConfigListherzustellen.
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
}
}
Weitere Informationen zum Wiederholungsablauf finden Sie in RFC 9849, insbesondere unter Warum ist eine Authentifizierung für den öffentlichen Namen erforderlich?.