1
0
mirror of https://github.com/square/okhttp.git synced 2025-08-08 23:42:08 +03:00

OpenJSSE Platform support (#5369)

Adds support for OpenJSSE when registered as the first security provider.
This commit is contained in:
Yuri Schimke
2019-08-18 12:03:51 +03:00
committed by GitHub
parent a1994dab34
commit 7cf508a30b
21 changed files with 267 additions and 65 deletions

View File

@@ -140,6 +140,20 @@ jobs:
- runtests:
platform: jdk8alpn
testopenjsse:
docker:
- image: circleci/openjdk:8u171-jdk
environment:
JVM_OPTS: -Xmx1g
TERM: dumb
steps:
- checkout
- runtests:
platform: openjsse
testjdk11:
docker:
- image: circleci/openjdk:11.0.3-jdk-stretch
@@ -205,6 +219,11 @@ workflows:
filters:
branches:
only: master
- testopenjsse:
filters:
branches:
ignore:
- gh-pages
- testjdk11:
filters:
branches:
@@ -236,6 +255,9 @@ workflows:
- testjdk8alpn:
requires:
- compile
- testopenjsse:
requires:
- compile
- testjdk11:
requires:
- compile

View File

@@ -159,6 +159,10 @@ subprojects { project ->
dependencies {
testRuntime "org.conscrypt:conscrypt-openjdk-uber:${versions.conscrypt}"
}
} else if (platform == "openjsse") {
dependencies {
testRuntime deps.openjsse
}
}
dependencies {

View File

@@ -3,6 +3,7 @@ dependencies {
api deps.junit
api deps.assertj
api deps.conscrypt
api deps.openjsse
compileOnly deps.jsr305
}

View File

@@ -15,12 +15,16 @@
*/
package okhttp3;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import okhttp3.internal.http2.Header;
import static org.junit.Assume.assumeNoException;
public final class TestUtil {
public static final InetSocketAddress UNREACHABLE_ADDRESS
= new InetSocketAddress("198.51.100.1", 8080);
@@ -52,4 +56,12 @@ public final class TestUtil {
Thread.sleep(100);
System.runFinalization();
}
public static void assumeNetwork() {
try {
InetAddress.getByName("www.google.com");
} catch (UnknownHostException uhe) {
assumeNoException(uhe);
}
}
}

View File

@@ -18,6 +18,7 @@ package okhttp3.testing
import okhttp3.internal.platform.ConscryptPlatform
import okhttp3.internal.platform.Jdk8WithJettyBootPlatform
import okhttp3.internal.platform.Jdk9Platform
import okhttp3.internal.platform.OpenJSSEPlatform
import okhttp3.internal.platform.Platform
import org.conscrypt.Conscrypt
import org.hamcrest.BaseMatcher
@@ -34,6 +35,7 @@ import org.junit.Assume.assumeTrue
import org.junit.AssumptionViolatedException
import org.junit.rules.TestRule
import org.junit.runners.model.Statement
import org.openjsse.net.ssl.OpenJSSE
import java.security.Security
/**
@@ -57,6 +59,8 @@ open class PlatformRule @JvmOverloads constructor(
try {
setupPlatform()
System.err.println("Running with ${Platform.get().javaClass.simpleName}")
base.evaluate()
} catch (e: AssumptionViolatedException) {
throw e
@@ -95,6 +99,10 @@ open class PlatformRule @JvmOverloads constructor(
expectFailure(platformMatches(CONSCRYPT_PROPERTY))
}
fun expectFailureOnOpenJSSEPlatform() {
expectFailure(platformMatches(OPENJSSE_PROPERTY))
}
fun expectFailureFromJdkVersion(majorVersion: Int) {
expectFailure(fromMajor(majorVersion))
}
@@ -118,7 +126,7 @@ open class PlatformRule @JvmOverloads constructor(
fun fromMajor(version: Int): Matcher<PlatformVersion> {
return object : TypeSafeMatcher<PlatformVersion>() {
override fun describeTo(description: org.hamcrest.Description) {
override fun describeTo(description: Description) {
description.appendText("JDK with version from $version")
}
@@ -171,6 +179,11 @@ open class PlatformRule @JvmOverloads constructor(
JDK9_PROPERTY))
}
fun assumeOpenJSSE() {
assumeThat(getPlatformSystemProperty(), equalTo(
OPENJSSE_PROPERTY))
}
fun assumeJdk8() {
assumeThat(getPlatformSystemProperty(), equalTo(
JDK8_PROPERTY))
@@ -206,6 +219,11 @@ open class PlatformRule @JvmOverloads constructor(
JDK8_ALPN_PROPERTY))
}
fun assumeNotOpenJSSE() {
assumeThat(getPlatformSystemProperty(), not(
OPENJSSE_PROPERTY))
}
fun assumeNotHttp2Support() {
assumeThat(getPlatformSystemProperty(), equalTo(
JDK8_PROPERTY))
@@ -221,6 +239,7 @@ open class PlatformRule @JvmOverloads constructor(
const val JDK9_PROPERTY = "jdk9"
const val JDK8_ALPN_PROPERTY = "jdk8alpn"
const val JDK8_PROPERTY = "jdk8"
const val OPENJSSE_PROPERTY = "openjsse"
init {
if (getPlatformSystemProperty() == CONSCRYPT_PROPERTY && Security.getProviders()[0].name != "Conscrypt") {
@@ -238,7 +257,15 @@ open class PlatformRule @JvmOverloads constructor(
if (isAlpnBootEnabled()) {
System.err.println("Warning: ALPN Boot enabled unintentionally")
}
} else if (getPlatformSystemProperty() == OPENJSSE_PROPERTY && Security.getProviders()[0].name != "OpenJSSE") {
if (!OpenJSSEPlatform.isSupported) {
System.err.println("Warning: OpenJSSE not available")
}
Security.insertProviderAt(OpenJSSE(), 1)
}
Platform.resetForTests()
}
@JvmStatic
@@ -249,6 +276,7 @@ open class PlatformRule @JvmOverloads constructor(
if (property == null) {
property = when (Platform.get()) {
is ConscryptPlatform -> CONSCRYPT_PROPERTY
is OpenJSSEPlatform -> OPENJSSE_PROPERTY
is Jdk8WithJettyBootPlatform -> CONSCRYPT_PROPERTY
is Jdk9Platform -> JDK9_PROPERTY
else -> JDK8_PROPERTY
@@ -261,6 +289,9 @@ open class PlatformRule @JvmOverloads constructor(
@JvmStatic
fun conscrypt() = PlatformRule(CONSCRYPT_PROPERTY)
@JvmStatic
fun openjsse() = PlatformRule(OPENJSSE_PROPERTY)
@JvmStatic
fun jdk9() = PlatformRule(JDK9_PROPERTY)

View File

@@ -26,6 +26,7 @@ dependencies {
api deps.okio
api deps.kotlinStdlib
compileOnly deps.conscrypt
compileOnly deps.openjsse
compileOnly deps.android
compileOnly deps.jsr305
compileOnly deps.animalSniffer

View File

@@ -542,3 +542,27 @@ inline fun Any.notify() = (this as Object).notify()
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN", "NOTHING_TO_INLINE")
inline fun Any.notifyAll() = (this as Object).notifyAll()
fun <T> readFieldOrNull(instance: Any, fieldType: Class<T>, fieldName: String): T? {
var c: Class<*> = instance.javaClass
while (c != Any::class.java) {
try {
val field = c.getDeclaredField(fieldName)
field.isAccessible = true
val value = field.get(instance)
return if (!fieldType.isInstance(value)) null else fieldType.cast(value)
} catch (_: NoSuchFieldException) {
}
c = c.superclass
}
// Didn't find the field we wanted. As a last gasp attempt,
// try to find the value on a delegate.
if (fieldName != "delegate") {
val delegate = readFieldOrNull(instance, Any::class.java, "delegate")
if (delegate != null) return readFieldOrNull(delegate, fieldType, fieldName)
}
return null
}

View File

@@ -15,7 +15,6 @@
*/
package okhttp3.internal.platform
import android.annotation.SuppressLint
import android.os.Build
import okhttp3.Protocol
import okhttp3.internal.platform.android.CloseGuard
@@ -225,7 +224,6 @@ class AndroidPlatform : Platform() {
}
companion object {
@SuppressLint("PrivateApi")
val isSupported: Boolean = try {
// Trigger an early exception over a fatal error, prefer a RuntimeException over Error.
Class.forName("com.android.org.conscrypt.OpenSSLSocketImpl")

View File

@@ -16,6 +16,7 @@
package okhttp3.internal.platform
import okhttp3.Protocol
import okhttp3.internal.readFieldOrNull
import org.conscrypt.Conscrypt
import java.security.Provider
import javax.net.ssl.SSLContext
@@ -29,13 +30,9 @@ import javax.net.ssl.X509TrustManager
* Requires org.conscrypt:conscrypt-openjdk-uber >= 2.1.0 on the classpath.
*/
class ConscryptPlatform private constructor() : Platform() {
private val provider: Provider
get() {
// n.b. We should consider defaulting to OpenJDK 11 trust manager
// https://groups.google.com/forum/#!topic/conscrypt/3vYzbesjOb4
return Conscrypt.newProviderBuilder().provideTrustManager(true).build()
}
private val provider: Provider = Conscrypt.newProviderBuilder().provideTrustManager(true).build()
// See release notes https://groups.google.com/forum/#!forum/conscrypt
// for version differences

View File

@@ -0,0 +1,100 @@
/*
* Copyright (C) 2019 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.internal.platform
import okhttp3.Protocol
import java.security.KeyStore
import java.security.Provider
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocket
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
/**
* Platform using OpenJSSE (https://github.com/openjsse/openjsse) if installed as the first
* Security Provider.
*
* Requires org.openjsse:openjsse >= 1.1.0 on the classpath.
*/
class OpenJSSEPlatform private constructor() : Platform() {
private val provider: Provider = org.openjsse.net.ssl.OpenJSSE()
// Selects TLSv1.3 so we are specific about our intended version ranges (not just 1.3)
// and because it's a common pattern for VMs to have differences between supported and
// defaulted versions for TLS based on what is requested.
override fun newSSLContext(): SSLContext =
SSLContext.getInstance("TLSv1.3", provider)
override fun platformTrustManager(): X509TrustManager {
val factory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm(), provider)
factory.init(null as KeyStore?)
val trustManagers = factory.trustManagers!!
check(trustManagers.size == 1 && trustManagers[0] is X509TrustManager) {
"Unexpected default trust managers: ${trustManagers.contentToString()}"
}
return trustManagers[0] as X509TrustManager
}
public override fun trustManager(sslSocketFactory: SSLSocketFactory): X509TrustManager? =
throw UnsupportedOperationException(
"clientBuilder.sslSocketFactory(SSLSocketFactory) not supported with OpenJSSE")
override fun configureTlsExtensions(
sslSocket: SSLSocket,
hostname: String?,
protocols: List<Protocol>
) {
if (sslSocket is org.openjsse.javax.net.ssl.SSLSocket) {
val sslParameters = sslSocket.sslParameters
if (sslParameters is org.openjsse.javax.net.ssl.SSLParameters) {
// Enable ALPN.
val names = alpnProtocolNames(protocols)
sslParameters.applicationProtocols = names.toTypedArray()
sslSocket.sslParameters = sslParameters
}
} else {
super.configureTlsExtensions(sslSocket, hostname, protocols)
}
}
override fun getSelectedProtocol(sslSocket: SSLSocket): String? =
if (sslSocket is org.openjsse.javax.net.ssl.SSLSocket) {
when (val protocol = sslSocket.applicationProtocol) {
// Handles both un-configured and none selected.
null, "" -> null
else -> protocol
}
} else {
super.getSelectedProtocol(sslSocket)
}
companion object {
val isSupported: Boolean = try {
// Trigger an early exception over a fatal error, prefer a RuntimeException over Error.
Class.forName("org.openjsse.net.ssl.OpenJSSE")
true
} catch (_: ClassNotFoundException) {
false
}
fun buildIfSupported(): OpenJSSEPlatform? = if (isSupported) OpenJSSEPlatform() else null
}
}

View File

@@ -18,6 +18,7 @@ package okhttp3.internal.platform
import okhttp3.OkHttpClient
import okhttp3.Protocol
import okhttp3.internal.readFieldOrNull
import okhttp3.internal.tls.BasicCertificateChainCleaner
import okhttp3.internal.tls.BasicTrustRootIndex
import okhttp3.internal.tls.CertificateChainCleaner
@@ -193,12 +194,18 @@ open class Platform {
fun alpnProtocolNames(protocols: List<Protocol>) =
protocols.filter { it != Protocol.HTTP_1_0 }.map { it.toString() }
val isConscryptPreferred: Boolean
private val isConscryptPreferred: Boolean
get() {
val preferredProvider = Security.getProviders()[0].name
return "Conscrypt" == preferredProvider
}
private val isOpenJSSEPreferred: Boolean
get() {
val preferredProvider = Security.getProviders()[0].name
return "OpenJSSE" == preferredProvider
}
/** Attempt to match the host runtime to a capable Platform implementation. */
private fun findPlatform(): Platform {
val android = AndroidPlatform.buildIfSupported()
@@ -215,6 +222,14 @@ open class Platform {
}
}
if (isOpenJSSEPreferred) {
val openJSSE = OpenJSSEPlatform.buildIfSupported()
if (openJSSE != null) {
return openJSSE
}
}
val jdk9 = Jdk9Platform.buildIfSupported()
if (jdk9 != null) {
@@ -239,29 +254,5 @@ open class Platform {
}
return result.readByteArray()
}
fun <T> readFieldOrNull(instance: Any, fieldType: Class<T>, fieldName: String): T? {
var c: Class<*> = instance.javaClass
while (c != Any::class.java) {
try {
val field = c.getDeclaredField(fieldName)
field.isAccessible = true
val value = field.get(instance)
return if (!fieldType.isInstance(value)) null else fieldType.cast(value)
} catch (_: NoSuchFieldException) {
}
c = c.superclass
}
// Didn't find the field we wanted. As a last gasp attempt,
// try to find the value on a delegate.
if (fieldName != "delegate") {
val delegate = readFieldOrNull(instance, Any::class.java, "delegate")
if (delegate != null) return readFieldOrNull(delegate, fieldType, fieldName)
}
return null
}
}
}

View File

@@ -16,6 +16,7 @@
package okhttp3.internal.platform.android
import okhttp3.internal.platform.Platform
import okhttp3.internal.readFieldOrNull
import javax.net.ssl.SSLSocket
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.X509TrustManager
@@ -35,11 +36,11 @@ class StandardAndroidSocketAdapter(
override fun trustManager(sslSocketFactory: SSLSocketFactory): X509TrustManager? {
val context: Any? =
Platform.readFieldOrNull(sslSocketFactory, paramClass,
readFieldOrNull(sslSocketFactory, paramClass,
"sslParameters")
val x509TrustManager = Platform.readFieldOrNull(
val x509TrustManager = readFieldOrNull(
context!!, X509TrustManager::class.java, "x509TrustManager")
return x509TrustManager ?: Platform.readFieldOrNull(context,
return x509TrustManager ?: readFieldOrNull(context,
X509TrustManager::class.java,
"trustManager")
}

View File

@@ -40,6 +40,7 @@ import okhttp3.internal.platform.Platform;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import okhttp3.testing.PlatformRule;
import okhttp3.tls.HandshakeCertificates;
import okio.Buffer;
import okio.BufferedSink;
@@ -65,6 +66,7 @@ public final class CacheTest {
@Rule public MockWebServer server2 = new MockWebServer();
@Rule public InMemoryFileSystem fileSystem = new InMemoryFileSystem();
@Rule public final OkHttpClientTestRule clientTestRule = new OkHttpClientTestRule();
@Rule public final PlatformRule platform = new PlatformRule();
private final HandshakeCertificates handshakeCertificates = localhost();
private OkHttpClient client;
@@ -72,6 +74,8 @@ public final class CacheTest {
private final CookieManager cookieManager = new CookieManager();
@Before public void setUp() throws Exception {
platform.assumeNotOpenJSSE();
server.setProtocolNegotiationEnabled(false);
cache = new Cache(new File("/cache/"), Integer.MAX_VALUE, fileSystem);
client = clientTestRule.newClientBuilder()
@@ -82,8 +86,11 @@ public final class CacheTest {
@After public void tearDown() throws Exception {
ResponseCache.setDefault(null);
if (cache != null) {
cache.delete();
}
}
/**
* Test that response caching is consistent with the RI and the spec.

View File

@@ -117,6 +117,8 @@ public final class CallTest {
private Logger logger = Logger.getLogger(OkHttpClient.class.getName());
@Before public void setUp() {
platform.assumeNotOpenJSSE();
logger.addHandler(logHandler);
client = clientTestRule.newClientBuilder()
.eventListener(listener)

View File

@@ -15,6 +15,7 @@
*/
package okhttp3
import okhttp3.TestUtil.assumeNetwork
import okhttp3.internal.platform.ConscryptPlatform
import okhttp3.internal.platform.Platform
import okhttp3.testing.PlatformRule
@@ -22,13 +23,10 @@ import org.assertj.core.api.Assertions.assertThat
import org.conscrypt.Conscrypt
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Assume
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import java.net.InetAddress
import java.net.UnknownHostException
class ConscryptTest {
@Suppress("RedundantVisibilityModifier")
@@ -49,14 +47,6 @@ class ConscryptTest {
assertThat(Conscrypt.isConscrypt(Platform.get().platformTrustManager())).isTrue()
}
private fun assumeNetwork() {
try {
InetAddress.getByName("www.google.com")
} catch (uhe: UnknownHostException) {
Assume.assumeNoException(uhe)
}
}
@Test
@Ignore
fun testMozilla() {

View File

@@ -54,6 +54,7 @@ public final class DuplexTest {
private OkHttpClient client;
@Before public void setUp() {
platform.assumeNotOpenJSSE();
platform.assumeHttp2Support();
client = clientTestRule.newClientBuilder()
.eventListener(listener)

View File

@@ -86,6 +86,8 @@ public final class EventListenerTest {
private SocksProxy socksProxy;
@Before public void setUp() {
platform.assumeNotOpenJSSE();
client = clientTestRule.newClientBuilder()
.eventListener(listener)
.build();

View File

@@ -15,23 +15,22 @@
*/
package okhttp3
import okhttp3.Protocol.HTTP_2
import okhttp3.TestUtil.assumeNetwork
import okhttp3.internal.platform.OpenJSSEPlatform
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.testing.PlatformRule
import okhttp3.tls.HandshakeCertificates
import okhttp3.tls.HeldCertificate
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.openjsse.net.ssl.OpenJSSE
import org.openjsse.sun.security.ssl.SSLSocketFactoryImpl
import org.openjsse.sun.security.ssl.SSLSocketImpl
import java.net.InetAddress
import java.security.Security
class OpenJSSETest {
@JvmField @Rule var platform = PlatformRule()
@@ -41,17 +40,11 @@ class OpenJSSETest {
@Before
fun setUp() {
platform.assumeJdk8()
platform.assumeOpenJSSE()
Security.insertProviderAt(OpenJSSE(), 1)
client = clientTestRule.newClient()
}
@After
fun cleanup() {
Security.removeProvider("OpenJSSE")
}
@Test
fun testTlsv13Works() {
enableTls()
@@ -65,7 +58,7 @@ class OpenJSSETest {
response.use {
assertEquals(200, response.code)
assertEquals(TlsVersion.TLS_1_3, response.handshake?.tlsVersion)
assertEquals(Protocol.HTTP_1_1, response.protocol)
assertEquals(Protocol.HTTP_2, response.protocol)
assertThat(response.exchange?.connection()?.socket()).isInstanceOf(SSLSocketImpl::class.java)
}
@@ -76,14 +69,31 @@ class OpenJSSETest {
val factory = SSLSocketFactoryImpl()
val s = factory.createSocket() as SSLSocketImpl
// A Public API available is available to use in a custom Platform
s.setHandshakeApplicationProtocolSelector { _, _ -> HTTP_2.toString() }
assertEquals(listOf("TLSv1.3", "TLSv1.2", "TLSv1.1", "TLSv1"), s.enabledProtocols.toList())
}
@Test
@Ignore
fun testMozilla() {
assumeNetwork()
val request = Request.Builder().url("https://mozilla.org/robots.txt").build()
client.newCall(request).execute().use {
assertThat(it.protocol).isEqualTo(Protocol.HTTP_2)
assertThat(it.handshake!!.tlsVersion).isEqualTo(TlsVersion.TLS_1_3)
}
}
@Test
fun testBuildIfSupported() {
val actual = OpenJSSEPlatform.buildIfSupported()
assertThat(actual).isNotNull
}
private fun enableTls() {
// Generate a self-signed cert for the server to serve and the client to trust.
// can't use localhost with a non OpenJSSE trust manager
// can't use TlsUtil.localhost with a non OpenJSSE trust manager
val heldCertificate = HeldCertificate.Builder()
.commonName("localhost")
.addSubjectAlternativeName(InetAddress.getByName("localhost").canonicalHostName)

View File

@@ -129,6 +129,8 @@ public final class HttpOverHttp2Test {
}
@Before public void setUp() {
platform.assumeNotOpenJSSE();
if (protocol == Protocol.HTTP_2) {
platform.assumeHttp2Support();
server.useHttps(handshakeCertificates.sslSocketFactory(), false);

View File

@@ -65,6 +65,8 @@ public final class ClientAuthTest {
@Before
public void setUp() {
platform.assumeNotOpenJSSE();
serverRootCa = new HeldCertificate.Builder()
.serialNumber(1L)
.certificateAuthority(1)

View File

@@ -42,6 +42,7 @@ import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import okhttp3.mockwebserver.SocketPolicy;
import okhttp3.testing.Flaky;
import okhttp3.testing.PlatformRule;
import okhttp3.tls.HandshakeCertificates;
import okio.Buffer;
import okio.ByteString;
@@ -65,6 +66,7 @@ public final class WebSocketHttpTest {
@Rule public final MockWebServer webServer = new MockWebServer();
@Rule public final OkHttpClientTestRule clientTestRule = new OkHttpClientTestRule();
@Rule public final PlatformRule platform = new PlatformRule();
private final HandshakeCertificates handshakeCertificates = localhost();
private final WebSocketRecorder clientListener = new WebSocketRecorder("client");
@@ -73,6 +75,8 @@ public final class WebSocketHttpTest {
private OkHttpClient client;
@Before public void setUp() {
platform.assumeNotOpenJSSE();
client = clientTestRule.newClientBuilder()
.writeTimeout(500, TimeUnit.MILLISECONDS)
.readTimeout(500, TimeUnit.MILLISECONDS)