ClientHello ที่เข้ารหัส (ECH) คือส่วนขยาย TLS ที่เข้ารหัสช่องการระบุชื่อเซิร์ฟเวอร์ (SNI) ในข้อความ Handshake ของไคลเอ็นต์ ใน Android 17 (API ระดับ 37) ขึ้นไป ระบบจะรองรับ ECH โดยค่าเริ่มต้น ECH ช่วยรักษาความเป็นส่วนตัวของการเข้าชมเว็บของผู้ใช้ด้วยการป้องกันไม่ให้ตัวกลางในเครือข่ายเห็นชื่อโฮสต์ที่แอปเชื่อมต่อ
สำหรับนักพัฒนาแอป
วิธีใช้ ECH ในแอปพลิเคชัน
- ตรวจสอบว่าไลบรารีเครือข่ายรองรับ ECH หรือไม่: ตรวจสอบว่าคุณใช้ ไลบรารีเวอร์ชันที่รองรับ ECH ใน Android โดยจะรองรับใน OkHttp และ HttpEngine เร็วๆ นี้
- กำหนดค่าการกำหนดค่าความปลอดภัยของเครือข่าย: โดยค่าเริ่มต้น ระบบจะเปิดใช้ ECH สำหรับทุก
โดเมนหากไลบรารีของคุณรองรับ หากต้องการปิดใช้หรือบังคับใช้ ECH,
ให้กำหนดค่าองค์ประกอบ
domainEncryptionใน การกำหนดค่าความปลอดภัยของเครือข่าย - อัปเดตระดับ 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: ใช้ API DnsResolver.query ระดับสูง
หากไลบรารีไม่จำเป็นต้องมีกลไกการแปลง 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 แล้ว ให้ส่งรายการนี้โดยใช้ API ยูทิลิตี SSLSockets หรือ SSLEngines ก่อนที่จะเริ่ม 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();
}
จัดการโฟลว์การลองอีกครั้ง
หากการกำหนดค่า ECH ของเซิร์ฟเวอร์ไม่ซิงค์กัน Handshake จะล้มเหลวโดยมี 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 โดยเฉพาะ เหตุผลที่ ต้องมีการตรวจสอบสิทธิ์สำหรับชื่อสาธารณะ