Einführung von Encrypted Client Hello (ECH)

Encrypted Client Hello (ECH) ist eine TLS-Erweiterung, mit der das Feld „Server Name Indication“ (SNI) 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 privat zu halten, da Netzwerkvermittler die Hostnamen nicht sehen können, mit denen sich eine App verbindet.

Für App-Entwickler

So übernehmen Sie ECH in Ihre Anwendung:

  1. Prüfen Sie, ob Ihre Netzwerkbibliothek ECH unterstützt: Achten Sie darauf, dass Sie eine Bibliotheksversion verwenden, die ECH unter Android unterstützt. Die Unterstützung wird bald in OkHttp und HttpEngine eingeführt.
  2. Netzwerksicherheitskonfiguration konfigurieren: Standardmäßig ist ECH für alle Domains aktiviert, wenn Ihre Bibliothek es unterstützt. Wenn Sie ECH deaktivieren oder erzwingen müssen, konfigurieren Sie das Element domainEncryption in Ihrer Netzwerksicherheitskonfiguration.
  3. Ziel-SDK-Level aktualisieren: ECH ist nur unter 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 die ECH-Unterstützung implementieren, indem Sie mit den Plattform-APIs interagieren.

Domainverschlüsselungsrichtlinie prüfen

Bevor Sie ECH-Konfigurationen abfragen oder Verbindungen initiieren, prüfen Sie die Domainverschlüsselungsrichtlinie der App mit NetworkSecurityPolicy.getDomainEncryptionMode.

Je nach zurückgegebenem Modus gehen Sie mit ECH so vor:

  • DOMAIN_ENCRYPTION_MODE_DISABLED und DOMAIN_ENCRYPTION_MODE_UNKNOWN: Rufen Sie keine ECH-Konfigurationen ab oder versuchen Sie es nicht mit ECH.
  • DOMAIN_ENCRYPTION_MODE_ENABLED und DOMAIN_ENCRYPTION_MODE_OPPORTUNISTIC: Erzwingen Sie ECH. Rufen Sie ECH-Konfigurationen ab und verwenden Sie ECH, 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 der beiden folgenden Methoden abgerufen werden:

Methode 1: Mit der API DnsResolver.query auf hoher Ebene

Wenn Ihre Bibliothek keine benutzerdefinierten DNS-Auflösungsmechanismen erfordert, können Sie die API DnsResolver.query auf hoher Ebene der Plattform verwenden. Diese API führt parallele Abfragen für die A/AAAA/HTTPS-Einträge aus und kombiniert die Ergebnisse in einem 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 */ }
    });

Methode 2: Mit getAllByName und DnsResolver.rawQuery

Bei Bibliotheken, die ihre eigenen Socketverbindungen 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 HTTPS-Rohdatensatz parallel mit DnsResolver.rawQuery ab. Geben Sie DnsResolver.TYPE_HTTPS als Abfragetyp an.
Verantwortung des Entwicklers und Grenzfälle

Wenn Sie Methode 2 auswählen, hat Ihre Bibliothek zusätzliche Verantwortlichkeiten und Grenzfälle zu berücksichtigen.

  • DNS-Einträge parsen: Sie müssen die Rohdaten-Nutzlast der DNS Antwort von rawQuery parsen, um die EchConfigList zu extrahieren.
  • Abweichungen bei Einträgen behandeln: Sie müssen Inkonsistenzen zwischen den A/AAAA- und HTTPS-Abfragen behandeln.
  • Race Conditions: Sie müssen die Ergebnisse der parallelen DNS-Lookups synchronisieren. Wenn eine Abfrage vor der anderen aufgelöst wird oder die HTTPS-Abfrage eine Zeitüberschreitung verursacht, müssen Sie entsprechend zurückgreifen (z. B. indem Sie eine Standard-TLS-Verbindung ohne ECH versuchen, wenn die HTTPS-Abfrage fehlschlägt, oder ECH GREASE verwenden, wenn es durch die Richtlinie aktiviert ist).

TLS konfigurieren

Sobald die Bibliothek die ECH-Konfigurationsliste (EchConfigList) aus dem HttpsRecord abgerufen hat, übergeben Sie diese Liste mit den Dienstprogramm-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 einer EchConfigMismatchException fehl (einer Unterklasse von javax.net.ssl.SSLException). Der Server kann aktualisierte ECH-Konfigurationen in seine Ablehnung aufnehmen, die zum Herstellen einer neuen Verbindung verwendet werden sollten. Wenn trotz gültiger Wiederholungskonfigurationen des Servers kein Wiederholungsversuch unternommen wird, muss die Bibliothek der aufrufenden Anwendung einen Fehler melden.

So behandeln Sie ECH-Wiederholungen: Fangen Sie die Ausnahme ab und führen Sie die folgenden Schritte aus:

  1. Rufen Sie EchConfigMismatchException.getPublicHostname für die Ausnahme auf.
  2. Bestätigen Sie den zurückgegebenen öffentlichen Hostnamen mit Ihrem HostnameVerifier. Wenn er null ist, brechen Sie die Verbindung ab.
  3. Wenn die Hostname-Bestätigung erfolgreich ist, prüfen Sie mit EchConfigMismatchException.getRetryConfigList auf aktualisierte Konfigurationen.
  4. Wenn aktualisierte Konfigurationen verfügbar sind, versuchen Sie es mit der neuen EchConfigList noch einmal.

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 warum eine Authentifizierung für den öffentlichen Namen erforderlich ist.