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 :
- 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
AndroidDnsouDnsOverHttpssurOkHttpClient.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.
- OkHttp : à partir d'OkHttp 5.5.0, vous pouvez activer la compatibilité avec ECH en configurant
- 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
domainEncryptiondans votre configuration de sécurité réseau. - 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_DISABLEDetDOMAIN_ENCRYPTION_MODE_UNKNOWN: ne pas récupérer les configurations ECH ni tenter d'utiliser ECH.DOMAIN_ENCRYPTION_MODE_ENABLEDetDOMAIN_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 :
- Résolvez les enregistrements A/AAAA à l'aide de
InetAddress.getAllByNamepour le réseau par défaut ou deNetwork.getAllByName. - Récupérez l'enregistrement HTTPS brut en parallèle à l'aide de
DnsResolver.rawQuery. SpécifiezDnsResolver.TYPE_HTTPScomme 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
rawQuerypour extraire leEchConfigList. - 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 :
- Appelez
EchConfigMismatchException.getPublicHostnamesur l'exception. - Validez le nom d'hôte public renvoyé à l'aide de votre
HostnameVerifier. Si la valeur estnull, abandonnez la connexion. - Si la vérification du nom d'hôte réussit, recherchez les configurations mises à jour à l'aide de
EchConfigMismatchException.getRetryConfigList. - 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 ?.