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 se la tua libreria di rete supporta ECH: assicurati di utilizzare una versione della libreria che supporti ECH su Android. Il supporto sarà disponibile a breve in OkHttp e HttpEngine.
  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 in Network Security Config.
  3. Aggiorna il livello SDK target: 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 estendendone una esistente, devi implementare il supporto ECH interagendo con le API della piattaforma.

Controlla la policy di criptaggio del dominio

Prima di eseguire query sulle configurazioni ECH o avviare connessioni, controlla la policy di criptaggio 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 né tentare di utilizzare ECH.
  • DOMAIN_ENCRYPTION_MODE_ENABLED e DOMAIN_ENCRYPTION_MODE_OPPORTUNISTIC: applica ECH. Recupera le configurazioni ECH e utilizza ECH se il server lo supporta. Se il server non supporta ECH, attiva ECH GREASE.

Recupera le configurazioni ECH

Per connetterti con 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: utilizzo dell'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: utilizzo di 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 separato del record HTTPS:

  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 libreria ha ulteriori responsabilità e casi limite da considerare.

  • Analisi dei record DNS: devi analizzare il payload di byte non elaborato 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 gara: devi sincronizzare i risultati delle ricerche DNS parallele. Se una query viene risolta prima dell'altra o se la query HTTPS va in timeout, 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 dalla policy).

Configura TLS

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

Gestisci il flusso di nuovi tentativi

Se le configurazioni ECH del server non sono più sincronizzate, l'handshake non va a buon fine con un'eccezione EchConfigMismatchException (una sottoclasse di javax.net.ssl.SSLException). Il server può includere configurazioni ECH aggiornate nel rifiuto, che devono essere utilizzate per stabilire una nuova connessione. Se non viene tentato 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 ECH, intercetta l'eccezione ed esegui questi passaggi:

  1. Chiama EchConfigMismatchException.getPublicHostname sull' eccezione.
  2. Verifica il nome host pubblico restituito utilizzando HostnameVerifier. Se è null, interrompi la connessione.
  3. Se la verifica del nome host va a buon fine, controlla se sono presenti configurazioni aggiornate utilizzando EchConfigMismatchException.getRetryConfigList.
  4. Se sono disponibili configurazioni aggiornate, riprova a stabilire 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 RFC 9849, in particolare il motivo per cui è necessario eseguire l'autenticazione per il nome pubblico.