Adopter Encrypted Client Hello (ECH)

Encrypted Client Hello (ECH) est une extension TLS qui chiffre le champ "Server Name Indication" (Indication du nom du serveur) dans le message de handshake du client. Dans Android 17 (niveau d'API 37) et versions ultérieures, ECH est compatible par défaut. ECH contribue à préserver la confidentialité du trafic Web des utilisateurs en empêchant les intermédiaires réseau de voir les noms d'hôte auxquels une application se connecte.

Pour les développeurs d'applications

Pour adopter ECH dans votre application :

  1. Vérifiez que votre bibliothèque réseau est compatible avec ECH : assurez-vous d'utiliser une version de bibliothèque compatible avec ECH sur Android :
    • OkHttp : à partir d'OkHttp 5.5.0, vous pouvez activer la compatibilité avec ECH en configurant AndroidDns ou DnsOverHttps sur OkHttpClient.Builder. Pour en savoir plus, consultez le journal des modifications.
    • HttpEngine : la compatibilité sera disponible dans Android 17 QPR2 (niveau d'API 37.2). Aucune configuration spéciale ne sera requise.
    • WebView : la compatibilité sera ajoutée dans une prochaine version.
  2. Configurer la configuration de la sécurité réseau : par défaut, ECH est activé pour tous les domaines si votre bibliothèque le prend en charge. Si vous devez désactiver ou appliquer ECH, configurez l'élément domainEncryption dans votre configuration de sécurité réseau.
  3. Mettez à jour le niveau du SDK cible : ECH n'est disponible que sur Android 17 (niveau d'API 37) ou version ultérieure.

Pour les développeurs de bibliothèques

Si vous développez une bibliothèque de mise en réseau HTTP personnalisée ou si vous en étendez une existante, vous devez implémenter la prise en charge d'ECH en interagissant avec les API de la plate-forme.

Vérifier la règle de chiffrement du domaine

Avant d'interroger les configurations ECH ou d'initier des connexions, vérifiez la règle de chiffrement de domaine de l'application en appelant NetworkSecurityPolicy.getDomainEncryptionMode.

En fonction du mode renvoyé, gérez ECH comme suit :

  • DOMAIN_ENCRYPTION_MODE_DISABLED et DOMAIN_ENCRYPTION_MODE_UNKNOWN : ne pas récupérer les configurations ECH ni tenter d'utiliser ECH.
  • DOMAIN_ENCRYPTION_MODE_ENABLED et DOMAIN_ENCRYPTION_MODE_OPPORTUNISTIC : appliquer ECH. Récupérez les configurations ECH et utilisez ECH si le serveur le prend en charge. Si le serveur n'accepte pas ECH, activez ECH GREASE.

Récupérer les configurations ECH

Pour vous connecter avec ECH, vous devez résoudre l'enregistrement DNS HTTPS du serveur contenant les configurations ECH. Lorsque les applications utilisent le DNS du système, ces données peuvent être récupérées de deux manières :

Méthode 1 : Utiliser l'API DnsResolver.query de haut niveau

Si votre bibliothèque ne nécessite pas de mécanismes de résolution DNS personnalisés, vous pouvez utiliser l'API DnsResolver.query de haut niveau de la plate-forme. Cette API effectue des requêtes parallèles pour les enregistrements A/AAAA/HTTPS et combine les résultats dans un 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 */ }
    });

Méthode 2 : Utiliser getAllByName et DnsResolver.rawQuery

Pour les bibliothèques qui gèrent leurs propres pipelines de connexions de socket et de résolution DNS, vous pouvez préférer résoudre les adresses IP à l'aide d'API standards tout en récupérant l'enregistrement HTTPS séparément :

  1. Résolvez les enregistrements A/AAAA à l'aide de InetAddress.getAllByName pour le réseau par défaut ou de Network.getAllByName.
  2. Récupérez l'enregistrement HTTPS brut en parallèle à l'aide de DnsResolver.rawQuery. Spécifiez DnsResolver.TYPE_HTTPS comme type de requête.
Responsabilités des développeurs et cas extrêmes

Si vous choisissez la méthode 2, votre bibliothèque doit tenir compte de responsabilités et de cas extrêmes supplémentaires.

  • Analyse des enregistrements DNS : vous devez analyser la charge utile d'octets bruts de la réponse DNS de rawQuery pour extraire le EchConfigList.
  • Gérer les incohérences d'enregistrements : vous devez gérer les incohérences entre les requêtes A/AAAA et HTTPS.
  • Conditions de concurrence : vous devez synchroniser les résultats des recherches DNS parallèles. Si une requête est résolue avant l'autre ou si la requête HTTPS expire, vous devez effectuer un retour approprié (par exemple, en tentant une connexion TLS standard sans ECH si la requête HTTPS échoue, ou en utilisant ECH GREASE si elle est activée par une règle).

Configurer TLS

Une fois que la bibliothèque a récupéré la liste de configuration ECH (EchConfigList) à partir de HttpsRecord, transmettez cette liste à l'aide des API utilitaires SSLSockets ou SSLEngines avant de démarrer l'établissement de liaison 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();
}

Gérer le flux de nouvelles tentatives

Si les configurations ECH du serveur ne sont plus synchronisées, l'établissement de la liaison échoue avec un EchConfigMismatchException (une sous-classe de javax.net.ssl.SSLException). Le serveur peut inclure des configurations ECH mises à jour dans son refus, qui doivent être utilisées pour établir une nouvelle connexion. Si aucune nouvelle tentative n'est effectuée alors que le serveur fournit des configurations de nouvelle tentative valides, la bibliothèque doit signaler une erreur à l'application appelante.

Pour gérer les nouvelles tentatives ECH, interceptez l'exception et procédez comme suit :

  1. Appelez EchConfigMismatchException.getPublicHostname sur l'exception.
  2. Validez le nom d'hôte public renvoyé à l'aide de votre HostnameVerifier. Si la valeur est null, abandonnez la connexion.
  3. Si la vérification du nom d'hôte réussit, recherchez les configurations mises à jour à l'aide de EchConfigMismatchException.getRetryConfigList.
  4. Si des configurations mises à jour sont disponibles, réessayez d'établir la connexion avec le nouveau 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
    }
}

Pour en savoir plus sur le flux de nouvelle tentative, consultez la RFC 9849, en particulier la section Pourquoi est-il nécessaire de s'authentifier pour le nom public ?.