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 :
- 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.
- 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
domainEncryptiondans votre configuration de la sécurité réseau. - 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_DISABLEDetDOMAIN_ENCRYPTION_MODE_UNKNOWN: ne récupérez pas les configurations ECH et ne tentez pas d'utiliser l'ECH.DOMAIN_ENCRYPTION_MODE_ENABLEDetDOMAIN_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 :
- Résolvez les enregistrements A/AAAA à l'aide de
InetAddress.getAllByNamepour le réseau par défaut ouNetwork.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é 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
rawQuerypour extraireEchConfigList. - 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 :
- Appelez
EchConfigMismatchException.getPublicHostnamesur l' exception. - Vérifiez le nom d'hôte public renvoyé à l'aide de votre
HostnameVerifier. S'il 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 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.