HTTPS¶
OkHttp试图平衡两个相互矛盾的问题:
- 连接 尽可能多的主机。 这包括运行最新版本的boringssl的高级主机和运行较旧版本的openssl的主机
- 连接的**安全性**。 这包括使用证书验证远程web服务器以及使用强密码交换的数据的私密性。
当协商到HTTPS服务器的连接时,OkHttp需要知道提供哪个tls 版本和cipher suites]。 想要最大化连接性的客户端会选择包括过时的TLS版本和弱设计密码套件。 想要最大限度提高安全性的严格客户端将仅限于最新的TLS版本和最强的密码套件。
特定的安全性与连接性决策由 ConnectionSpec 实现。OkHttp包括四个内置连接规格:
RESTRICTED_TLS
是一种安全配置,旨在满足更严格的合规要求。MODERN_TLS
是连接到现代HTTPS服务器的安全配置。COMPATIBLE_TLS
是一种安全配置,它连接到安全的(但不是现行的)HTTPS服务器。CLEARTEXT
是用于http://
url的不安全配置。
这些服务大致遵循Google Cloud Policies.]中设置的模型 我们在这里跟踪此策略的更改。
默认情况下,OkHttp会尝试Modern_TLS
连接。 但是,如果Modern配置失败,通过配置客户端连接规格,你可以允许回退到 COMPATIBLE_TLS
连接。
OkHttpClient client = new OkHttpClient.Builder()
.connectionSpecs(Arrays.asList(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS))
.build();
每个规范中的TLS版本和cipher suites(密码套件)可能会随着每个版本的不同而变化。 例如,在OkHttp 2.2中,我们为响应 POODLE 攻击而放弃了对SSL 3.0的支持。 在OkHttp2.3中,我们不再支持RC4. 与桌面web浏览器一样,保持OkHttp更新是保持安全的最佳方法。
你可以使用一组定制的TLS版本和密码套件构建你自己的连接规范。 例如,此配置仅限于三个备受推崇的密码套件。 它的缺点是,它需要Android 5.0+和类似的最新网络服务器。
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服务的ProviderInstaller。 这将提高用户的安全性,并增加与Web服务器的连接性。
Certificate Pinning (.kt, .java)¶
默认情况下,OkHttp信任主机平台的证书颁发机构。 此策略最大限度地提高了连接性,但会受到证书颁发机构的攻击,如2011年的DigiNotar攻击. 它还假定你的HTTPS服务器证书由证书颁发机构签名。
使用CertificatePinner限制信任哪些证书和证书颁发机构。 Certificate pinning可以提高安全性,但会限制服务器团队更新其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.
}