Adopter Encrypted Client Hello (ECH)

Encrypted Client Hello (ECH) est une extension TLS qui chiffre le champ d'indication du nom du serveur (SNI) dans le message de handshake du client. Dans Android 17 (niveau d'API 37) et versions ultérieures, l'ECH est compatible par défaut. L'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 l'ECH dans votre application :

  1. Vérifiez que votre bibliothèque réseau est compatible avec l'ECH : assurez-vous d'utiliser une version de bibliothèque compatible avec l'ECH sur Android. La compatibilité sera bientôt disponible dans OkHttp et HttpEngine.
  2. Configurez la configuration de la sécurité réseau : par défaut, l'ECH est activé pour tous les domaines si votre bibliothèque le prend en charge. Si vous devez désactiver ou appliquer l'ECH, configurez l'élément domainEncryption dans votre configuration de la sécurité réseau.
  3. Mettez à jour le niveau de SDK cible : l'ECH n'est disponible que sur Android 17 (niveau d'API 37) et versions ultérieures.

Pour les développeurs de bibliothèques

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

Vérifier la stratégie de chiffrement de domaine

Avant d'interroger les configurations ECH ou de lancer des connexions, vérifiez la stratégie de chiffrement de domaine de l'application en appelant NetworkSecurityPolicy.getDomainEncryptionMode.

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

  • DOMAIN_ENCRYPTION_MODE_DISABLED et DOMAIN_ENCRYPTION_MODE_UNKNOWN : ne récupérez pas les configurations ECH et ne tentez pas d'utiliser l'ECH.
  • DOMAIN_ENCRYPTION_MODE_ENABLED et DOMAIN_ENCRYPTION_MODE_OPPORTUNISTIC : appliquez l'ECH. Récupérez les configurations ECH et utilisez l'ECH si le serveur le prend en charge. Si le serveur ne prend pas en charge l'ECH, activez ECH GREASE.

Récupérer les configurations ECH

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

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 connexions de socket et pipelines 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 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é du développeur et cas extrêmes

Si vous choisissez la méthode 2, votre bibliothèque a des responsabilités supplémentaires et des cas extrêmes à prendre en compte.

  • Analyse des enregistrements DNS : vous devez analyser la charge utile d'octets bruts de la réponse DNS de rawQuery pour extraire EchConfigList.
  • Gestion des incompatibilités 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 revenir en arrière de manière appropriée (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 le handshake 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, le handshake échoue avec une 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. Vérifiez le nom d'hôte public renvoyé à l'aide de votre HostnameVerifier. S'il 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 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 nouvelles tentatives, consultez la RFC 9849, en particulier pourquoi il est nécessaire de s'authentifier pour le nom public.