Adozione di Encrypted Client Hello (ECH)

Encrypted Client Hello (ECH) è un'estensione TLS che cripta il campo Server Name Indication (SNI) nel messaggio di handshake del client. In Android 17 (livello API 37) e versioni successive, ECH è supportato per impostazione predefinita. ECH contribuisce a mantenere privato il traffico web degli utenti impedendo agli intermediari di rete di visualizzare i nomi host a cui si connette un'app.

Per gli sviluppatori di app

Per adottare ECH nella tua applicazione:

  1. Controlla la tua libreria di rete per il supporto di ECH: assicurati di utilizzare una versione della libreria che supporti ECH su Android:
    • OkHttp: a partire da OkHttp 5.5.0, puoi attivare il supporto di ECH configurando AndroidDns o DnsOverHttps su OkHttpClient.Builder. Per ulteriori informazioni, consulta il log delle modifiche.
    • HttpEngine: il supporto sarà disponibile in Android 17 QPR2 (livello API 37.2). Non sarà necessaria alcuna configurazione speciale.
    • WebView: il supporto verrà aggiunto in una release futura.
  2. Configura Network Security Config: per impostazione predefinita, ECH è abilitato per tutti i domini se la tua libreria lo supporta. Se devi disattivare o applicare ECH, configura l'elemento domainEncryption nella configurazione della sicurezza di rete.
  3. Aggiorna il livello SDK di destinazione: ECH è disponibile solo su Android 17 (livello API 37) e versioni successive.

Per gli sviluppatori di librerie

Se stai sviluppando una libreria di rete HTTP personalizzata o estendendo una libreria esistente, devi implementare il supporto ECH interagendo con le API della piattaforma.

Controllare le norme di crittografia del dominio

Prima di eseguire query sulle configurazioni ECH o avviare connessioni, controlla le norme di crittografia del dominio dell'app chiamando NetworkSecurityPolicy.getDomainEncryptionMode.

A seconda della modalità restituita, gestisci ECH nel seguente modo:

  • DOMAIN_ENCRYPTION_MODE_DISABLED e DOMAIN_ENCRYPTION_MODE_UNKNOWN: non recuperare le configurazioni ECH o tentare di utilizzare ECH.
  • DOMAIN_ENCRYPTION_MODE_ENABLED e DOMAIN_ENCRYPTION_MODE_OPPORTUNISTIC: applica ECH. Recuperare le configurazioni ECH e utilizzare ECH se il server lo supporta. Se il server non supporta ECH, attiva ECH GREASE.

Recuperare le configurazioni ECH

Per connetterti a ECH, devi risolvere il record DNS HTTPS del server contenente le configurazioni ECH. Quando le app utilizzano il DNS di sistema, questi dati possono essere recuperati utilizzando uno dei due metodi seguenti:

Metodo 1: utilizza l'API DnsResolver.query di alto livello

Se la tua libreria non richiede meccanismi di risoluzione DNS personalizzati, puoi utilizzare l'API DnsResolver.query di alto livello della piattaforma. Questa API esegue query parallele per i record A/AAAA/HTTPS e combina i risultati in 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 */ }
    });

Metodo 2: utilizzare getAllByName e DnsResolver.rawQuery

Per le librerie che gestiscono le proprie connessioni socket e pipeline di risoluzione DNS, potresti preferire risolvere gli indirizzi IP utilizzando le API standard durante il recupero del record HTTPS separatamente:

  1. Risolvi i record A/AAAA utilizzando InetAddress.getAllByName per la rete predefinita o Network.getAllByName.
  2. Recupera il record HTTPS non elaborato in parallelo utilizzando DnsResolver.rawQuery. Specifica DnsResolver.TYPE_HTTPS come tipo di query.
Responsabilità dello sviluppatore e casi limite

Se scegli il metodo 2, la tua biblioteca ha ulteriori responsabilità e casi limite da considerare.

  • Analisi del record DNS: devi analizzare il payload di byte non elaborati della risposta DNS da rawQuery per estrarre EchConfigList.
  • Gestione delle mancate corrispondenze dei record: devi gestire le incoerenze tra le query A/AAAA e HTTPS.
  • Condizioni di competizione: devi sincronizzare i risultati delle ricerche DNS parallele. Se una query viene risolta prima dell'altra o se la query HTTPS scade, devi eseguire il fallback in modo appropriato (ad esempio, tentando una connessione TLS standard senza ECH se la query HTTPS non va a buon fine o utilizzando ECH GREASE se è abilitato tramite policy).

Configurare TLS

Una volta che la libreria ha recuperato l'elenco delle configurazioni ECH (EchConfigList) da HttpsRecord, passa questo elenco utilizzando le API di utilità SSLSockets o SSLEngines prima di iniziare l'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();
}

Gestire il flusso di nuovi tentativi

Se le configurazioni ECH del server non sono più sincronizzate, l'handshake non riesce con un errore EchConfigMismatchException (una sottoclasse di javax.net.ssl.SSLException). Il server potrebbe includere configurazioni ECH aggiornate nel rifiuto, che devono essere utilizzate per stabilire una nuova connessione. Se non viene eseguito un nuovo tentativo nonostante il server fornisca configurazioni di nuovi tentativi valide, la libreria deve segnalare un errore all'applicazione chiamante.

Per gestire i nuovi tentativi di ECH, intercetta l'eccezione ed esegui questi passaggi:

  1. Chiama EchConfigMismatchException.getPublicHostname in merito all'eccezione.
  2. Verifica il nome host pubblico restituito utilizzando il tuo HostnameVerifier. Se è null, interrompi la connessione.
  3. Se la verifica del nome host ha esito positivo, controlla le configurazioni aggiornate utilizzando EchConfigMismatchException.getRetryConfigList.
  4. Se sono disponibili configurazioni aggiornate, riprova la connessione con il nuovo 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
    }
}

Per ulteriori dettagli sul flusso di nuovi tentativi, consulta l'RFC 9849, in particolare perché è necessario autenticarsi per il nome pubblico.