diff --git a/okhttp-tests/src/test/java/okhttp3/CallTest.java b/okhttp-tests/src/test/java/okhttp3/CallTest.java index ad16010d1db1..860e447bf356 100644 --- a/okhttp-tests/src/test/java/okhttp3/CallTest.java +++ b/okhttp-tests/src/test/java/okhttp3/CallTest.java @@ -84,6 +84,7 @@ import org.junit.rules.Timeout; import static java.net.CookiePolicy.ACCEPT_ORIGINAL_SERVER; +import static okhttp3.CipherSuite.TLS_DH_anon_WITH_AES_128_GCM_SHA256; import static okhttp3.TestUtil.awaitGarbageCollection; import static okhttp3.TestUtil.defaultClient; import static okhttp3.tls.internal.TlsUtil.localhost; @@ -1180,6 +1181,96 @@ private void postBodyRetransmittedAfterAuthorizationFail(String body) throws Exc } } + /** + * When the server doesn't present any certificates we fail the TLS handshake. This test requires + * that the client and server are each configured with a cipher suite that permits the server to + * be unauthenticated. + */ + @Test public void tlsSuccessWithNoPeerCertificates() throws Exception { + server.enqueue(new MockResponse() + .setBody("abc")); + + // The _anon_ cipher suites don't require server certificates. + CipherSuite cipherSuite = TLS_DH_anon_WITH_AES_128_GCM_SHA256; + + HandshakeCertificates clientCertificates = new HandshakeCertificates.Builder() + .build(); + client = client.newBuilder() + .sslSocketFactory( + socketFactoryWithCipherSuite(clientCertificates.sslSocketFactory(), cipherSuite), + clientCertificates.trustManager()) + .connectionSpecs(Arrays.asList(new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + .cipherSuites(cipherSuite) + .build())) + .hostnameVerifier(new RecordingHostnameVerifier()) + .build(); + + HandshakeCertificates serverCertificates = new HandshakeCertificates.Builder() + .build(); + server.useHttps(socketFactoryWithCipherSuite( + serverCertificates.sslSocketFactory(), cipherSuite), false); + + Call call = client.newCall(new Request.Builder() + .url(server.url("/")) + .build()); + Response response = call.execute(); + assertEquals("abc", response.body().string()); + assertNull(response.handshake().peerPrincipal()); + assertEquals(Collections.emptyList(), response.handshake().peerCertificates()); + assertEquals(cipherSuite, response.handshake().cipherSuite()); + } + + @Test public void tlsHostnameVerificationFailure() throws Exception { + server.enqueue(new MockResponse()); + + HeldCertificate serverCertificate = new HeldCertificate.Builder() + .commonName("localhost") // Unusued for hostname verification. + .addSubjectAlternativeName("wronghostname") + .build(); + + HandshakeCertificates serverCertificates = new HandshakeCertificates.Builder() + .heldCertificate(serverCertificate) + .build(); + + HandshakeCertificates clientCertificates = new HandshakeCertificates.Builder() + .addTrustedCertificate(serverCertificate.certificate()) + .build(); + + client = client.newBuilder() + .sslSocketFactory(clientCertificates.sslSocketFactory(), clientCertificates.trustManager()) + .build(); + server.useHttps(serverCertificates.sslSocketFactory(), false); + + executeSynchronously("/") + .assertFailureMatches("(?s)Hostname localhost not verified.*"); + } + + @Test public void tlsHostnameVerificationFailureNoPeerCertificates() throws Exception { + server.enqueue(new MockResponse()); + + // The _anon_ cipher suites don't require server certificates. + CipherSuite cipherSuite = TLS_DH_anon_WITH_AES_128_GCM_SHA256; + + HandshakeCertificates clientCertificates = new HandshakeCertificates.Builder() + .build(); + client = client.newBuilder() + .sslSocketFactory( + socketFactoryWithCipherSuite(clientCertificates.sslSocketFactory(), cipherSuite), + clientCertificates.trustManager()) + .connectionSpecs(Arrays.asList(new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + .cipherSuites(cipherSuite) + .build())) + .build(); + + HandshakeCertificates serverCertificates = new HandshakeCertificates.Builder() + .build(); + server.useHttps(socketFactoryWithCipherSuite( + serverCertificates.sslSocketFactory(), cipherSuite), false); + + executeSynchronously("/") + .assertFailure("Hostname localhost not verified (no certificates)"); + } + @Test public void cleartextCallsFailWhenCleartextIsDisabled() throws Exception { // Configure the client with only TLS configurations. No cleartext! client = client.newBuilder() @@ -3564,6 +3655,16 @@ private Thread cancelLater(final Call call, final long delay) { return thread; } + private SSLSocketFactory socketFactoryWithCipherSuite( + final SSLSocketFactory sslSocketFactory, final CipherSuite cipherSuite) { + return new DelegatingSSLSocketFactory(sslSocketFactory) { + @Override protected SSLSocket configureSocket(SSLSocket sslSocket) throws IOException { + sslSocket.setEnabledCipherSuites(new String[] { cipherSuite.javaName() }); + return super.configureSocket(sslSocket); + } + }; + } + private static class RecordingSSLSocketFactory extends DelegatingSSLSocketFactory { private List socketsCreated = new ArrayList<>(); diff --git a/okhttp/src/main/java/okhttp3/internal/connection/RealConnection.java b/okhttp/src/main/java/okhttp3/internal/connection/RealConnection.java index 25445fac5066..cca07e9df1ae 100644 --- a/okhttp/src/main/java/okhttp3/internal/connection/RealConnection.java +++ b/okhttp/src/main/java/okhttp3/internal/connection/RealConnection.java @@ -26,6 +26,7 @@ import java.net.SocketException; import java.net.SocketTimeoutException; import java.net.UnknownServiceException; +import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; @@ -323,11 +324,18 @@ private void connectTls(ConnectionSpecSelector connectionSpecSelector) throws IO // Verify that the socket's certificates are acceptable for the target host. if (!address.hostnameVerifier().verify(address.url().host(), sslSocketSession)) { - X509Certificate cert = (X509Certificate) unverifiedHandshake.peerCertificates().get(0); - throw new SSLPeerUnverifiedException("Hostname " + address.url().host() + " not verified:" - + "\n certificate: " + CertificatePinner.pin(cert) - + "\n DN: " + cert.getSubjectDN().getName() - + "\n subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert)); + List peerCertificates = unverifiedHandshake.peerCertificates(); + if (!peerCertificates.isEmpty()) { + X509Certificate cert = (X509Certificate) peerCertificates.get(0); + throw new SSLPeerUnverifiedException( + "Hostname " + address.url().host() + " not verified:" + + "\n certificate: " + CertificatePinner.pin(cert) + + "\n DN: " + cert.getSubjectDN().getName() + + "\n subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert)); + } else { + throw new SSLPeerUnverifiedException( + "Hostname " + address.url().host() + " not verified (no certificates)"); + } } // Check that the certificate pinner is satisfied by the certificates presented.