diff --git a/pom.xml b/pom.xml index a2977d991..88794b4a5 100644 --- a/pom.xml +++ b/pom.xml @@ -39,6 +39,7 @@ 1.6 8.1.2.v20120308 20120401 + 1.47 3.8.2 @@ -80,6 +81,12 @@ ${junit.version} test + + org.bouncycastle + bcprov-jdk15on + ${bouncycastle.version} + test + diff --git a/src/test/java/libcore/net/http/URLConnectionTest.java b/src/test/java/libcore/net/http/URLConnectionTest.java index f5faf2457..4c5398623 100644 --- a/src/test/java/libcore/net/http/URLConnectionTest.java +++ b/src/test/java/libcore/net/http/URLConnectionTest.java @@ -21,6 +21,7 @@ import com.google.mockwebserver.MockWebServer; import com.google.mockwebserver.RecordedRequest; import com.google.mockwebserver.SocketPolicy; import com.squareup.okhttp.OkHttpConnection; +import com.squareup.okhttp.OkHttpsConnection; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -30,6 +31,7 @@ import java.net.CacheRequest; import java.net.CacheResponse; import java.net.ConnectException; import java.net.HttpRetryException; +import java.net.InetAddress; import java.net.PasswordAuthentication; import java.net.ProtocolException; import java.net.Proxy; @@ -53,9 +55,12 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import javax.net.ssl.X509TrustManager; import junit.framework.TestCase; +import libcore.net.ssl.SslContextBuilder; + import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_END; import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_START; import static com.google.mockwebserver.SocketPolicy.SHUTDOWN_INPUT_AT_END; @@ -380,22 +385,24 @@ public final class URLConnectionTest extends TestCase { } } -// public void testConnectViaHttps() throws IOException, InterruptedException { -// TestSSLContext testSSLContext = TestSSLContext.create(); -// -// server.useHttps(testSSLContext.serverContext.getSocketFactory(), false); -// server.enqueue(new MockResponse().setBody("this response comes via HTTPS")); -// server.play(); -// -// HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/foo").openConnection(); -// connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); -// -// assertContent("this response comes via HTTPS", connection); -// -// RecordedRequest request = server.takeRequest(); -// assertEquals("GET /foo HTTP/1.1", request.getRequestLine()); -// } -// + public void testConnectViaHttps() throws Exception { + SSLContext sslContext = new SslContextBuilder(InetAddress.getLocalHost().getHostName()) + .build(); + + server.useHttps(sslContext.getSocketFactory(), false); + server.enqueue(new MockResponse().setBody("this response comes via HTTPS")); + server.play(); + + OkHttpsConnection connection = OkHttpsConnection.open(server.getUrl("/foo")); + connection.setHostnameVerifier(new RecordingHostnameVerifier()); + connection.setSSLSocketFactory(sslContext.getSocketFactory()); + + assertContent("this response comes via HTTPS", connection); + + RecordedRequest request = server.takeRequest(); + assertEquals("GET /foo HTTP/1.1", request.getRequestLine()); + } + // public void testConnectViaHttpsReusingConnections() throws IOException, InterruptedException { // TestSSLContext testSSLContext = TestSSLContext.create(); // diff --git a/src/test/java/libcore/net/ssl/SslContextBuilder.java b/src/test/java/libcore/net/ssl/SslContextBuilder.java new file mode 100644 index 000000000..a227a0f83 --- /dev/null +++ b/src/test/java/libcore/net/ssl/SslContextBuilder.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2012 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 libcore.net.ssl; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.security.Security; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.concurrent.TimeUnit; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import javax.security.auth.x500.X500Principal; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.x509.X509V3CertificateGenerator; + +/** + * Constructs an SSL context for testing. This uses Bouncy Castle to generate a + * self-signed certificate for a single hostname such as "localhost". + * + *

The crypto performed by this class is relatively slow. Clients should + * reuse SSL context instances where possible. + */ +public final class SslContextBuilder { + static { + Security.addProvider(new BouncyCastleProvider()); + } + + private final String hostName; + private long notBefore = System.currentTimeMillis(); + private long notAfter = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1); + + /** + * @param hostName the subject of the host. For TLS this should be the + * domain name that the client uses to identify the server. + */ + public SslContextBuilder(String hostName) { + this.hostName = hostName; + } + + public SSLContext build() throws GeneralSecurityException { + char[] password = "password".toCharArray(); + + // Generate public and private keys and use them to make a self-signed certificate. + KeyPair keyPair = generateKeyPair(); + X509Certificate certificate = selfSignedCertificate(keyPair); + + // Put 'em in a key store. + KeyStore keyStore = newEmptyKeyStore(password); + Certificate[] certificateChain = { + certificate + }; + keyStore.setKeyEntry("private", keyPair.getPrivate(), password, certificateChain); + keyStore.setCertificateEntry("cert", certificate); + + // Wrap it up in an SSL context. + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance( + KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keyStore, password); + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(keyStore); + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), + new SecureRandom()); + return sslContext; + } + + private KeyPair generateKeyPair() throws GeneralSecurityException { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC"); + keyPairGenerator.initialize(1024, new SecureRandom()); + return keyPairGenerator.generateKeyPair(); + } + + /** + * Generates a certificate for {@code hostName} containing {@code keyPair}'s + * public key, signed by {@code keyPair}'s private key. + */ + @SuppressWarnings("deprecation") // use the old Bouncy Castle APIs to reduce dependencies. + private X509Certificate selfSignedCertificate(KeyPair keyPair) throws GeneralSecurityException { + X509V3CertificateGenerator generator = new X509V3CertificateGenerator(); + X500Principal issuer = new X500Principal("CN=" + hostName); + X500Principal subject = new X500Principal("CN=" + hostName); + generator.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis())); + generator.setIssuerDN(issuer); + generator.setNotBefore(new Date(notBefore)); + generator.setNotAfter(new Date(notAfter)); + generator.setSubjectDN(subject); + generator.setPublicKey(keyPair.getPublic()); + generator.setSignatureAlgorithm("SHA256WithRSAEncryption"); + return generator.generateX509Certificate(keyPair.getPrivate(), "BC"); + } + + private KeyStore newEmptyKeyStore(char[] password) throws GeneralSecurityException { + try { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + InputStream in = null; // By convention, 'null' creates an empty key store. + keyStore.load(in, password); + return keyStore; + } catch (IOException e) { + throw new AssertionError(); + } + } +}