ClientHello ที่เข้ารหัส (ECH) คือส่วนขยาย TLS ที่เข้ารหัสช่องการระบุชื่อเซิร์ฟเวอร์ (SNI) ในข้อความแฮนด์เชคของไคลเอ็นต์ ใน Android 17 (API ระดับ 37) ขึ้นไป ระบบจะรองรับ ECH โดยค่าเริ่มต้น ECH ช่วยให้การรับส่งข้อมูลทางเว็บของผู้ใช้เป็นส่วนตัวโดยป้องกันไม่ให้ตัวกลางในเครือข่ายเห็นชื่อโฮสต์ที่แอปเชื่อมต่อ
สำหรับนักพัฒนาแอป
หากต้องการใช้ ECH ในแอปพลิเคชัน ให้ทำดังนี้
- ตรวจสอบไลบรารีเครือข่ายว่ารองรับ ECH หรือไม่: ตรวจสอบว่าคุณใช้ไลบรารีเวอร์ชันที่รองรับ ECH ใน Android ดังนี้
- OkHttp: ตั้งแต่ OkHttp 5.5.0 เป็นต้นไป คุณจะเปิดใช้การรองรับ ECH ได้โดย
กำหนดค่า
AndroidDnsหรือDnsOverHttpsในOkHttpClient.Builderดูข้อมูลเพิ่มเติมได้ที่บันทึกการเปลี่ยนแปลง - HttpEngine: จะรองรับใน Android 17 QPR2 (ระดับ API 37.2) คุณไม่จำเป็นต้องกำหนดค่าพิเศษใดๆ
- WebView: เราจะเพิ่มการรองรับในรุ่นต่อๆ ไป
- OkHttp: ตั้งแต่ OkHttp 5.5.0 เป็นต้นไป คุณจะเปิดใช้การรองรับ ECH ได้โดย
กำหนดค่า
- กำหนดค่ากำหนดความปลอดภัยของเครือข่าย: โดยค่าเริ่มต้น ECH จะเปิดใช้สำหรับโดเมนทั้งหมด หากไลบรารีรองรับ หากต้องการปิดใช้หรือบังคับใช้ ECH ให้
กำหนดค่าองค์ประกอบ
domainEncryptionใน Network Security Config - อัปเดตระดับ SDK เป้าหมาย: ECH พร้อมใช้งานใน Android 17 (API ระดับ 37) ขึ้นไปเท่านั้น
สำหรับนักพัฒนาไลบรารี
หากคุณกำลังพัฒนาไลบรารีเครือข่าย HTTP ที่กำหนดเองหรือขยายไลบรารีที่มีอยู่ คุณควรใช้การรองรับ ECH โดยการโต้ตอบกับ API ของแพลตฟอร์ม
ตรวจสอบนโยบายการเข้ารหัสโดเมน
ก่อนที่จะค้นหาการกำหนดค่า ECH หรือเริ่มการเชื่อมต่อ ให้ตรวจสอบนโยบายการเข้ารหัสโดเมนของแอปโดยเรียกใช้ NetworkSecurityPolicy.getDomainEncryptionMode
จัดการ ECH ดังนี้โดยขึ้นอยู่กับโหมดที่ส่งคืน
DOMAIN_ENCRYPTION_MODE_DISABLEDและDOMAIN_ENCRYPTION_MODE_UNKNOWN: ไม่ต้องดึงข้อมูลการกำหนดค่า ECH หรือลองใช้ ECHDOMAIN_ENCRYPTION_MODE_ENABLEDและDOMAIN_ENCRYPTION_MODE_OPPORTUNISTIC: บังคับใช้ ECH ดึงข้อมูลการกำหนดค่า ECH และใช้ ECH หากเซิร์ฟเวอร์รองรับ หากเซิร์ฟเวอร์ไม่รองรับ ECH ให้เปิดใช้ ECH GREASE
เรียกข้อมูลการกำหนดค่า ECH
หากต้องการเชื่อมต่อกับ ECH คุณต้องแก้ไขระเบียน DNS ของ HTTPS ของเซิร์ฟเวอร์ที่มี การกำหนดค่า ECH เมื่อแอปใช้ DNS ของระบบ ข้อมูลนี้จะ ดึงข้อมูลได้โดยใช้วิธีใดวิธีหนึ่งต่อไปนี้
วิธีที่ 1: ใช้ DnsResolver.query API ระดับสูง
หากไลบรารีไม่จำเป็นต้องมีกลไกการแปลง DNS ที่กำหนดเอง คุณสามารถใช้ API DnsResolver.query ระดับสูงของแพลตฟอร์มได้ 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 */ }
})
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 */ }
});
วิธีที่ 2: ใช้ getAllByName และ DnsResolver.rawQuery
สำหรับไลบรารีที่จัดการการเชื่อมต่อซ็อกเก็ตและไปป์ไลน์การแปลง DNS ของตัวเอง คุณอาจต้องการแปลงที่อยู่ IP โดยใช้ API มาตรฐานขณะดึงข้อมูลระเบียน HTTPS แยกกัน
- แก้ไขระเบียน A/AAAA โดยใช้
InetAddress.getAllByNameสำหรับเครือข่ายเริ่มต้นหรือNetwork.getAllByName - เรียกข้อมูลระเบียน HTTPS ดิบแบบขนานโดยใช้
DnsResolver.rawQueryระบุDnsResolver.TYPE_HTTPSเป็น ประเภทการค้นหา
ความรับผิดชอบของนักพัฒนาซอฟต์แวร์และกรณีที่พบได้ยาก
หากเลือกวิธีที่ 2 ไลบรารีของคุณจะมีหน้าที่รับผิดชอบเพิ่มเติมและมีกรณีขอบ ที่ต้องพิจารณา
- การแยกวิเคราะห์ระเบียน DNS: คุณต้องแยกวิเคราะห์เพย์โหลดไบต์ดิบของคำตอบ DNS จาก
rawQueryเพื่อดึงEchConfigList - การจัดการการไม่ตรงกันของระเบียน: คุณต้องจัดการความไม่สอดคล้องกันระหว่าง การค้นหา A/AAAA และ HTTPS
- เงื่อนไขการแข่งขัน: คุณต้องซิงโครไนซ์ผลลัพธ์ของการค้นหา DNS แบบขนาน หากคำค้นหาหนึ่งๆ แก้ไขได้ก่อนอีกคำค้นหาหนึ่ง หรือหากคำค้นหา HTTPS หมดเวลา คุณต้องกลับไปใช้คำค้นหาที่เหมาะสม (เช่น ลองใช้การเชื่อมต่อ TLS มาตรฐานโดยไม่มี ECH หากคำค้นหา HTTPS ล้มเหลว หรือใช้ ECH GREASE หากนโยบายเปิดใช้)
กำหนดค่า TLS
เมื่อไลบรารีเรียกข้อมูลรายการการกำหนดค่า ECH
(EchConfigList) จาก HttpsRecord แล้ว ให้ส่งรายการนี้โดยใช้ SSLSockets หรือ SSLEngines API ยูทิลิตีก่อนเริ่มแฮนด์เชค 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();
}
จัดการโฟลว์การลองอีกครั้ง
หากการกำหนดค่า 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
}
}
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
}
}
ดูรายละเอียดเพิ่มเติมเกี่ยวกับขั้นตอนการลองใหม่ได้ใน RFC 9849 โดยเฉพาะเหตุผลที่ต้องมีการตรวจสอบสิทธิ์สำหรับชื่อสาธารณะ