Einführung von Encrypted Client Hello (ECH)

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:

  1. 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 AndroidDns oder DnsOverHttps für OkHttpClient.Builder konfigurieren. 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.
  2. 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.
  3. 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_DISABLED und DOMAIN_ENCRYPTION_MODE_UNKNOWN: ECH-Konfigurationen nicht abrufen oder ECH nicht versuchen.
  • DOMAIN_ENCRYPTION_MODE_ENABLED und DOMAIN_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:

  1. Lösen Sie A‑/AAAA‑Einträge mit InetAddress.getAllByName für das Standardnetzwerk oder Network.getAllByName auf.
  2. Rufen Sie den rohen HTTPS-Eintrag parallel mit DnsResolver.rawQuery ab. Geben Sie DnsResolver.TYPE_HTTPS als 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 rawQuery parsen, um die EchConfigList zu 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:

  1. Rufen Sie EchConfigMismatchException.getPublicHostname für die Ausnahme auf.
  2. Prüfen Sie den zurückgegebenen öffentlichen Hostnamen mit Ihrem HostnameVerifier. Wenn es null ist, brechen Sie die Verbindung ab.
  3. Wenn die Hostname-Überprüfung erfolgreich ist, prüfen Sie mit EchConfigMismatchException.getRetryConfigList auf aktualisierte Konfigurationen.
  4. Wenn aktualisierte Konfigurationen verfügbar sind, versuchen Sie noch einmal, eine Verbindung mit dem neuen EchConfigList herzustellen.

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?.