HTTPS¶
OkHttp 试图平衡两个相互竞争的关注点
- 连接到尽可能多的主机。这包括运行最新版本 boringssl 的高级主机以及运行旧版本 OpenSSL 但不过时的主机。
- 连接的安全性。这包括使用证书验证远程 Web 服务器,以及使用强大的密码套件保护数据交换的隐私。
与 HTTPS 服务器协商连接时,OkHttp 需要知道提供哪些 TLS 版本 和 密码套件。想要最大化连接性的客户端会包含过时的 TLS 版本和设计上较弱的密码套件。严格的客户端则会限制只使用最新的 TLS 版本和最强的密码套件。
ConnectionSpec 实现了特定的安全性与连接性权衡决策。OkHttp 包含四种内置连接规范:
RESTRICTED_TLS
是一种安全的配置,旨在满足更严格的合规性要求。MODERN_TLS
是一种安全的配置,用于连接现代 HTTPS 服务器。COMPATIBLE_TLS
是一种安全的配置,用于连接安全但非最新的 HTTPS 服务器。CLEARTEXT
是一种不安全的配置,用于http://
URL。
这些配置大致遵循 Google Cloud Policies 中设定的模型。我们会跟踪对此策略的更改。
默认情况下,OkHttp 会尝试进行 MODERN_TLS
连接。但通过配置客户端的 connectionSpecs,如果现代配置失败,你可以允许回退到 COMPATIBLE_TLS
连接。
OkHttpClient client = new OkHttpClient.Builder()
.connectionSpecs(Arrays.asList(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS))
.build();
每个规范中的 TLS 版本和密码套件会随每个版本发布而改变。例如,在 OkHttp 2.2 中,我们为了应对 POODLE 攻击而放弃了对 SSL 3.0 的支持。在 OkHttp 2.3 中,我们放弃了对 RC4 的支持。就像您的桌面 Web 浏览器一样,保持 OkHttp 最新是确保安全性的最佳方式。
你可以构建自己的连接规范,包含一组自定义的 TLS 版本和密码套件。例如,此配置仅限于三种备受推崇的密码套件。其缺点在于它要求 Android 5.0+ 和类似的最新 Web 服务器。
ConnectionSpec spec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.tlsVersions(TlsVersion.TLS_1_2)
.cipherSuites(
CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
CipherSuite.TLS_DHE_RSA_WITH_AES_128_GCM_SHA256)
.build();
OkHttpClient client = new OkHttpClient.Builder()
.connectionSpecs(Collections.singletonList(spec))
.build();
调试 TLS 握手失败¶
TLS 握手要求客户端和服务器共享一个通用的 TLS 版本和密码套件。这取决于 JVM 或 Android 版本、OkHttp 版本以及 Web 服务器配置。如果没有通用的密码套件和 TLS 版本,你的调用将失败,例如:
Caused by: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0x7f2719a89e80:
Failure in SSL library, usually a protocol error
error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake
failure (external/openssl/ssl/s23_clnt.c:770 0x7f2728a53ea0:0x00000000)
at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)
你可以使用 Qualys SSL Labs 检查 Web 服务器的配置。OkHttp 的 TLS 配置历史记录在此处跟踪。
预计安装在较旧 Android 设备上的应用应考虑采用 Google Play Services 的 ProviderInstaller。这将提高用户的安全性,并增强与 Web 服务器的连接性。
证书锁定 (.kt, .java)¶
默认情况下,OkHttp 信任宿主平台的证书颁发机构。此策略可最大化连接性,但它容易受到证书颁发机构攻击,例如 2011 年的 DigiNotar 攻击。它也假设您的 HTTPS 服务器证书由证书颁发机构签名。
使用 CertificatePinner 来限制信任哪些证书和证书颁发机构。证书锁定提高了安全性,但限制了您的服务器团队更新 TLS 证书的能力。在未经您的服务器 TLS 管理员许可的情况下,请勿使用证书锁定!
private val client = OkHttpClient.Builder()
.certificatePinner(
CertificatePinner.Builder()
.add("publicobject.com", "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=")
.build())
.build()
fun run() {
val request = Request.Builder()
.url("https://publicobject.com/robots.txt")
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Unexpected code $response")
for (certificate in response.handshake!!.peerCertificates) {
println(CertificatePinner.pin(certificate))
}
}
}
private final OkHttpClient client = new OkHttpClient.Builder()
.certificatePinner(
new CertificatePinner.Builder()
.add("publicobject.com", "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=")
.build())
.build();
public void run() throws Exception {
Request request = new Request.Builder()
.url("https://publicobject.com/robots.txt")
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
for (Certificate certificate : response.handshake().peerCertificates()) {
System.out.println(CertificatePinner.pin(certificate));
}
}
}
自定义受信任证书 (.kt, .java)¶
完整的代码示例展示了如何用您自己的证书集替换宿主平台的证书颁发机构。如前所述,在未经您的服务器 TLS 管理员许可的情况下,请勿使用自定义证书!
private val client: OkHttpClient
init {
val trustManager = trustManagerForCertificates(trustedCertificatesInputStream())
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, arrayOf<TrustManager>(trustManager), null)
val sslSocketFactory = sslContext.socketFactory
client = OkHttpClient.Builder()
.sslSocketFactory(sslSocketFactory, trustManager)
.build()
}
fun run() {
val request = Request.Builder()
.url("https://publicobject.com/helloworld.txt")
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Unexpected code $response")
for ((name, value) in response.headers) {
println("$name: $value")
}
println(response.body!!.string())
}
}
/**
* Returns an input stream containing one or more certificate PEM files. This implementation just
* embeds the PEM files in Java strings; most applications will instead read this from a resource
* file that gets bundled with the application.
*/
private fun trustedCertificatesInputStream(): InputStream {
... // Full source omitted. See sample.
}
private fun trustManagerForCertificates(inputStream: InputStream): X509TrustManager {
... // Full source omitted. See sample.
}
private final OkHttpClient client;
public CustomTrust() {
X509TrustManager trustManager;
SSLSocketFactory sslSocketFactory;
try {
trustManager = trustManagerForCertificates(trustedCertificatesInputStream());
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] { trustManager }, null);
sslSocketFactory = sslContext.getSocketFactory();
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
client = new OkHttpClient.Builder()
.sslSocketFactory(sslSocketFactory, trustManager)
.build();
}
public void run() throws Exception {
Request request = new Request.Builder()
.url("https://publicobject.com/helloworld.txt")
.build();
Response response = client.newCall(request).execute();
System.out.println(response.body().string());
}
private InputStream trustedCertificatesInputStream() {
... // Full source omitted. See sample.
}
public SSLContext sslContextForTrustedCertificates(InputStream in) {
... // Full source omitted. See sample.
}