Áp dụng tính năng Encrypted Client Hello (ECH)

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:

  1. 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.
  2. Đị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ử domainEncryption trong Cấu hình bảo mật mạng.
  3. 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_DISABLEDDOMAIN_ENCRYPTION_MODE_UNKNOWN: Không tìm nạp cấu hình ECH hoặc thử ECH.
  • DOMAIN_ENCRYPTION_MODE_ENABLEDDOMAIN_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 getAllByNameDnsResolver.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:

  1. Phân giải bản ghi A/AAAA bằng InetAddress.getAllByName cho mạng mặc định hoặc Network.getAllByName.
  2. Truy xuất song song bản ghi HTTPS thô bằng cách sử dụng DnsResolver.rawQuery. Chỉ định DnsResolver.TYPE_HTTPS là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ất EchConfigList.
  • 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:

  1. Gọi EchConfigMismatchException.getPublicHostname trên ngoại lệ.
  2. 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.
  3. 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.
  4. Nếu có cấu hình mới, hãy thử kết nối lại bằng EchConfigList mớ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.