سلام رمزگذاریشدهی کلاینت (ECH) یک افزونهی TLS است که فیلد نشانگر نام سرور (SNI) را در پیام handshake کلاینت رمزگذاری میکند. در اندروید ۱۷ (سطح API ۳۷) و بالاتر، ECH به طور پیشفرض پشتیبانی میشود. ECH با جلوگیری از مشاهدهی نامهای میزبان توسط واسطههای شبکه، به خصوصی نگه داشتن ترافیک وب کاربران کمک میکند.
برای توسعهدهندگان اپلیکیشن
برای پذیرش ECH در درخواست خود:
- کتابخانه شبکه خود را برای پشتیبانی ECH بررسی کنید : مطمئن شوید که از نسخه کتابخانهای استفاده میکنید که از ECH در اندروید پشتیبانی میکند. این پشتیبانی به زودی در OkHttp و HttpEngine ارائه خواهد شد.
- پیکربندی امنیت شبکه : به طور پیشفرض، اگر کتابخانه شما از ECH پشتیبانی کند، برای همه دامنهها فعال است. اگر نیاز به غیرفعال کردن یا اجرای ECH دارید، عنصر
domainEncryptionرا در پیکربندی امنیت شبکه خود پیکربندی کنید. - بهروزرسانی سطح SDK هدف : ECH فقط در اندروید ۱۷ (سطح API ۳۷) و بالاتر در دسترس است.
برای توسعهدهندگان کتابخانه
اگر در حال توسعه یک کتابخانه شبکه 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، باید رکورد HTTPS DNS سرور که حاوی تنظیمات ECH است را resolve کنید. وقتی برنامهها از DNS سیستم استفاده میکنند، این دادهها را میتوان با استفاده از یکی از دو روش زیر بازیابی کرد:
روش ۱: استفاده از API سطح بالای DnsResolver.query
اگر کتابخانه شما به مکانیزمهای سفارشی DNS Resolution نیاز ندارد، میتوانید از API سطح بالای DnsResolver.query پلتفرم استفاده کنید. این API کوئریهای موازی برای رکوردهای A/AAAA/HTTPS ایجاد میکند و نتایج را در یک HttpsEndpoint ترکیب میکند.
کاتلین
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 */ }
});
روش دوم: استفاده از getAllByName و DnsResolver.rawQuery
برای کتابخانههایی که اتصالات سوکت و خطوط لولهی تحلیل DNS خود را مدیریت میکنند، ممکن است ترجیح دهید آدرسهای IP را با استفاده از APIهای استاندارد حل کنید و رکورد HTTPS را جداگانه دریافت کنید:
- رکوردهای A/AAAA را با استفاده از
InetAddress.getAllByNameبرای شبکه پیشفرض یاNetwork.getAllByNameبازیابی کنید. - رکورد خام HTTPS را به صورت موازی با استفاده از
DnsResolver.rawQueryبازیابی کنید. نوع پرس و جو راDnsResolver.TYPE_HTTPSتعیین کنید.
مسئولیت توسعهدهنده و موارد حاشیهای
اگر روش ۲ را انتخاب کنید، کتابخانه شما مسئولیتها و موارد خاص دیگری دارد که باید در نظر بگیرد.
- تجزیه رکورد DNS : برای استخراج
EchConfigListباید بایت خام پاسخ DNS را ازrawQueryتجزیه کنید. - مدیریت عدم تطابق رکوردها : شما باید ناسازگاریهای بین کوئریهای A/AAAA و HTTPS را مدیریت کنید.
- شرایط رقابتی : شما باید نتایج جستجوهای موازی DNS را همگامسازی کنید. اگر یک پرسوجو قبل از دیگری حل شود یا اگر زمان پرسوجوی HTTPS تمام شود، باید به روش مناسب به عقب برگردید (برای مثال، با تلاش برای اتصال استاندارد TLS بدون ECH در صورت عدم موفقیت پرسوجوی HTTPS، یا استفاده از ECH GREASE در صورت فعال بودن توسط سیاست).
پیکربندی TLS
پس از اینکه کتابخانه لیست پیکربندی ECH ( EchConfigList ) را از HttpsRecord بازیابی کرد، قبل از شروع عملیات TLS handshake، این لیست را با استفاده از APIهای ابزار SSLSockets یا SSLEngines ارسال کنید.
کاتلین
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جدید دوباره امتحان کنید.
کاتلین
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 ببینید، به ویژه اینکه چرا احراز هویت برای نام عمومی ضروری است .