Client Hello được mã hoá (ECH) là một tiện ích TLS mã hoá trường Chỉ báo tên máy chủ (SNI) trong thông báo bắt tay của máy khách. Trong Android 17 (API cấp 37) trở lên, ECH được hỗ trợ theo mặc định. ECH giúp bảo mật lưu lượng truy cập web của người dùng bằng cách ngăn các trung gian mạng xem tên máy chủ mà một ứng dụng kết nối.
Dành cho nhà phát triển ứng dụng
Cách áp dụng ECH trong ứng dụng:
- Kiểm tra thư viện mạng để biết thông tin hỗ trợ ECH: Đảm bảo bạn đang sử dụng phiên bản thư viện hỗ trợ ECH trên Android. Chúng tôi sẽ sớm hỗ trợ trong OkHttp và HttpEngine.
- Định cấu hình Network Security Config: Theo mặc định, ECH được bật cho tất cả các miền nếu thư viện của bạn hỗ trợ ECH. Nếu bạn cần tắt hoặc thực thi ECH, hãy định cấu hình phần tử
domainEncryptiontrong Cấu hình bảo mật mạng. - Cập nhật cấp độ SDK mục tiêu: ECH chỉ có trên Android 17 (API cấp 37) trở lên.
Dành cho nhà phát triển thư viện
Nếu đang phát triển một thư viện mạng HTTP tuỳ chỉnh hoặc mở rộng một thư viện hiện có, bạn nên triển khai tính năng hỗ trợ ECH bằng cách tương tác với các API nền tảng.
Kiểm tra chính sách mã hoá miền
Trước khi truy vấn cấu hình ECH hoặc bắt đầu kết nối, hãy kiểm tra chính sách mã hoá miền của ứng dụng bằng cách gọi NetworkSecurityPolicy.getDomainEncryptionMode.
Tuỳ thuộc vào chế độ được trả về, hãy xử lý ECH như sau:
DOMAIN_ENCRYPTION_MODE_DISABLEDvàDOMAIN_ENCRYPTION_MODE_UNKNOWN: Không tìm nạp cấu hình ECH hoặc thử ECH.DOMAIN_ENCRYPTION_MODE_ENABLEDvàDOMAIN_ENCRYPTION_MODE_OPPORTUNISTIC: Thực thi ECH. Truy xuất cấu hình ECH và sử dụng ECH nếu máy chủ hỗ trợ. Nếu máy chủ không hỗ trợ ECH, hãy bật ECH GREASE.
Truy xuất cấu hình ECH
Để kết nối với ECH, bạn phải phân giải bản ghi DNS HTTPS của máy chủ có chứa cấu hình ECH. Khi các ứng dụng đang sử dụng DNS hệ thống, dữ liệu này có thể được truy xuất bằng một trong hai phương thức sau:
Phương thức 1: Sử dụng API DnsResolver.query cấp cao
Nếu thư viện của bạn không yêu cầu cơ chế phân giải DNS tuỳ chỉnh, bạn có thể sử dụng API DnsResolver.query cấp cao của nền tảng. API này tạo các truy vấn song song cho bản ghi A/AAAA/HTTPS và kết hợp các kết quả thành một 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 */ }
});
Cách 2: Sử dụng getAllByName và DnsResolver.rawQuery
Đối với các thư viện quản lý các kết nối ổ cắm và quy trình phân giải DNS của riêng chúng, bạn có thể muốn phân giải địa chỉ IP bằng các API tiêu chuẩn trong khi tìm nạp riêng bản ghi HTTPS:
- Phân giải bản ghi A/AAAA bằng
InetAddress.getAllByNamecho mạng mặc định hoặcNetwork.getAllByName. - Truy xuất song song bản ghi HTTPS thô bằng cách sử dụng
DnsResolver.rawQuery. Chỉ địnhDnsResolver.TYPE_HTTPSlàm loại truy vấn.
Trách nhiệm của nhà phát triển và các trường hợp đặc biệt
Nếu bạn chọn Phương thức 2, thư viện của bạn sẽ có thêm trách nhiệm và các trường hợp đặc biệt cần cân nhắc.
- Phân tích cú pháp bản ghi DNS: Bạn phải phân tích cú pháp tải trọng byte thô của phản hồi DNS từ
rawQueryđể trích xuấtEchConfigList. - Xử lý các bản ghi không khớp: Bạn phải xử lý sự không nhất quán giữa các truy vấn A/AAAA và HTTPS.
- Điều kiện xung đột: Bạn phải đồng bộ hoá kết quả của các lệnh tìm kiếm DNS song song. Nếu một truy vấn phân giải trước truy vấn kia hoặc nếu truy vấn HTTPS hết thời gian chờ, bạn phải quay lại một cách thích hợp (ví dụ: bằng cách thử kết nối TLS tiêu chuẩn mà không có ECH nếu truy vấn HTTPS không thành công hoặc sử dụng ECH GREASE nếu được bật theo chính sách).
Định cấu hình TLS
Sau khi thư viện truy xuất danh sách cấu hình ECH (EchConfigList) từ HttpsRecord, hãy truyền danh sách này vào bằng cách sử dụng API tiện ích SSLSockets hoặc SSLEngines trước khi bắt đầu bắt tay 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();
}
Xử lý quy trình thử lại
Nếu cấu hình ECH của máy chủ không đồng bộ, thì quá trình bắt tay sẽ thất bại với EchConfigMismatchException (một lớp con của javax.net.ssl.SSLException). Máy chủ có thể đưa cấu hình ECH đã cập nhật vào yêu cầu từ chối của mình. Bạn nên dùng cấu hình này để thiết lập một kết nối mới. Nếu không thử lại mặc dù máy chủ cung cấp cấu hình thử lại hợp lệ, thì thư viện phải báo cáo lỗi cho ứng dụng gọi.
Để xử lý các lần thử lại ECH, hãy bắt ngoại lệ và thực hiện các bước sau:
- Gọi
EchConfigMismatchException.getPublicHostnametrên ngoại lệ. - Xác minh tên máy chủ công khai được trả về bằng
HostnameVerifier. Nếu lànull, hãy huỷ kết nối. - Nếu quá trình xác minh tên máy chủ thành công, hãy kiểm tra các cấu hình đã cập nhật bằng cách sử dụng
EchConfigMismatchException.getRetryConfigList. - Nếu có cấu hình mới, hãy thử kết nối lại bằng
EchConfigListmới.
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
}
}
Xem thêm thông tin chi tiết về quy trình thử lại trong RFC 9849, đặc biệt là lý do cần xác thực cho tên công khai.