Encrypted Client Hello (ECH)는 클라이언트의 핸드셰이크 메시지에서 서버 이름 표시 (SNI) 필드를 암호화하는 TLS 확장 프로그램입니다. Android 17 (API 수준 37) 이상에서는 ECH가 기본적으로 지원됩니다. ECH는 네트워크 중개자가 앱이 연결하는 호스트 이름을 볼 수 없도록 하여 사용자의 웹 트래픽을 비공개로 유지하는 데 도움이 됩니다.
앱 개발자
애플리케이션에서 ECH를 채택하려면 다음 단계를 따르세요.
- 네트워킹 라이브러리에서 ECH 지원 확인: Android에서 ECH를 지원하는 라이브러리 버전을 사용하고 있는지 확인합니다. OkHttp 및 HttpEngine에서 곧 지원될 예정입니다.
- 네트워크 보안 구성 구성: 라이브러리에서 지원하는 경우 기본적으로 모든
도메인에 ECH가 사용 설정됩니다. ECH를 사용 중지하거나 적용해야 하는 경우
네트워크 보안 구성에서
domainEncryption요소를 구성합니다. Network Security Config. - 타겟 SDK 수준 업데이트: ECH는 Android 17 (API 수준 37) 이상에서만 사용할 수 있습니다.
라이브러리 개발자
맞춤 HTTP 네트워킹 라이브러리를 개발하거나 기존 라이브러리를 확장하는 경우 플랫폼 API와 상호작용하여 ECH 지원을 구현해야 합니다.
도메인 암호화 정책 확인
ECH 구성을 쿼리하거나 연결을 시작하기 전에 앱의
도메인 암호화 정책을 호출하여
NetworkSecurityPolicy.getDomainEncryptionMode 확인합니다.
반환된 모드에 따라 다음과 같이 ECH를 처리합니다.
DOMAIN_ENCRYPTION_MODE_DISABLED및DOMAIN_ENCRYPTION_MODE_UNKNOWN: ECH 구성을 가져오거나 ECH를 시도하지 않습니다.DOMAIN_ENCRYPTION_MODE_ENABLED및DOMAIN_ENCRYPTION_MODE_OPPORTUNISTIC: ECH를 적용합니다. 서버에서 지원하는 경우 ECH 구성을 가져오고 ECH를 사용합니다. 서버에서 ECH를 지원하지 않으면 ECH GREASE를 사용 설정합니다.
ECH 구성 가져오기
ECH와 연결하려면 ECH 구성이 포함된 서버의 HTTPS DNS 레코드를 확인해야 합니다. 앱에서 시스템 DNS를 사용하는 경우 다음 두 가지 방법 중 하나를 사용하여 이 데이터를 가져올 수 있습니다.
방법 1: 상위 수준 DnsResolver.query API 사용
라이브러리에 맞춤 DNS 변환 메커니즘이 필요하지 않은 경우 플랫폼의 상위 수준 DnsResolver.query API를 사용할 수 있습니다. 이 API는 A/AAAA/HTTPS 레코드에 대한 병렬
쿼리를 실행하고 결과를
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 */ }
})
자바
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 */ }
});
방법 2: getAllByName 및 DnsResolver.rawQuery 사용
자체 소켓 연결 및 DNS 변환 파이프라인을 관리하는 라이브러리의 경우 HTTPS 레코드를 별도로 가져오는 동안 표준 API를 사용하여 IP 주소를 확인하는 것이 좋습니다.
- 기본 네트워크의 경우
InetAddress.getAllByName을 사용하거나Network.getAllByName을 사용하여 A/AAAA 레코드를 확인합니다. -
DnsResolver.rawQuery를 사용하여 원시 HTTPS 레코드를 병렬로 가져옵니다.DnsResolver.TYPE_HTTPS를 쿼리 유형으로 지정합니다.
개발자 책임 및 엣지 케이스
방법 2를 선택하면 라이브러리에 고려해야 할 추가 책임과 엣지 케이스가 있습니다.
- DNS 레코드 파싱: DNS
응답에서 원시 바이트 페이로드를 파싱하여
rawQueryEchConfigList을(를) 추출해야 합니다. - 레코드 불일치 처리: A/AAAA 및 HTTPS 쿼리 간의 불일치를 처리해야 합니다.
- 경합 상태: 병렬 DNS 조회 결과를 동기화해야 합니다. 한 쿼리가 다른 쿼리보다 먼저 확인되거나 HTTPS 쿼리가 타임아웃되는 경우 적절하게 대체해야 합니다 (예: HTTPS 쿼리가 실패하면 ECH 없이 표준 TLS 연결을 시도하거나 정책에 따라 사용 설정된 경우 ECH GREASE 사용).
TLS 구성
라이브러리가 HttpsRecord에서 ECH 구성 목록
(EchConfigList)을 가져오면 TLS
핸드셰이크를 시작하기 전에 SSLSockets 또는 SSLEngines 유틸리티 API 중 하나를 사용하여 이 목록을 전달합니다.
Kotlin
fun establishEchConnection(echConfigList: EchConfigList) {
val socket = sslSocketFactory.createSocket(ipAddress, port) as SSLSocket
SSLSockets.setEchConfigList(socket, echConfigList)
socket.startHandshake()
}
자바
public void establishEchConnection(EchConfigList echConfigList)
throws IOException {
SSLSocket socket =
(SSLSocket) sslSocketFactory.createSocket(ipAddress, port);
SSLSockets.setEchConfigList(socket, echConfigList);
socket.startHandshake();
}
재시도 흐름 처리
서버의 ECH 구성이 동기화되지 않은 경우 핸드셰이크가 EchConfigMismatchException (javax.net.ssl.SSLException의 하위 클래스)으로 실패합니다. 서버는 거부에 업데이트된 ECH 구성을 포함할 수 있으며, 이는 새 연결을 설정하는 데 사용해야 합니다. 서버에서 유효한 재시도 구성을 제공함에도 불구하고 재시도를 시도하지 않으면 라이브러리에서 호출 애플리케이션에 오류를 보고해야 합니다.
ECH 재시도를 처리하려면 예외를 포착하고 다음 단계를 실행합니다.
- 예외에서
EchConfigMismatchException.getPublicHostname을 호출합니다. HostnameVerifier를 사용하여 반환된 공개 호스트 이름을 확인합니다.null이면 연결을 중단합니다.- 호스트 이름 확인에 성공하면
EchConfigMismatchException.getRetryConfigList를 사용하여 업데이트된 구성을 확인합니다. - 업데이트된 구성을 사용할 수 있는 경우
새
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
}
}
자바
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
}
}
재시도 흐름에 관한 자세한 내용은 RFC 9849, 특히 공개 이름에 인증이 필요한 이유를 참고하세요.