/*
* Copyright (C) 2016 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.tls;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLPeerUnverifiedException;
import okhttp3.Call;
import okhttp3.CertificatePinner;
import okhttp3.OkHttpClient;
import okhttp3.RecordingHostnameVerifier;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.SocketPolicy;
import org.junit.Rule;
import org.junit.Test;
import static okhttp3.TestUtil.defaultClient;
import static okhttp3.internal.platform.PlatformTest.getPlatform;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public final class CertificatePinnerChainValidationTest {
@Rule public final MockWebServer server = new MockWebServer();
/** The pinner should pull the root certificate from the trust manager. */
@Test public void pinRootNotPresentInChain() throws Exception {
HeldCertificate rootCa = new HeldCertificate.Builder()
.serialNumber("1")
.ca(3)
.commonName("root")
.build();
HeldCertificate intermediateCa = new HeldCertificate.Builder()
.issuedBy(rootCa)
.ca(2)
.serialNumber("2")
.commonName("intermediate_ca")
.build();
HeldCertificate certificate = new HeldCertificate.Builder()
.issuedBy(intermediateCa)
.serialNumber("3")
.commonName(server.getHostName())
.build();
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add(server.getHostName(), CertificatePinner.pin(rootCa.certificate))
.build();
SslClient sslClient = new SslClient.Builder()
.addTrustedCertificate(rootCa.certificate)
.build();
OkHttpClient client = defaultClient().newBuilder()
.sslSocketFactory(sslClient.socketFactory, sslClient.trustManager)
.hostnameVerifier(new RecordingHostnameVerifier())
.certificatePinner(certificatePinner)
.build();
SslClient serverSslClient = new SslClient.Builder()
.certificateChain(certificate, intermediateCa)
.build();
server.useHttps(serverSslClient.socketFactory, false);
// The request should complete successfully.
server.enqueue(new MockResponse()
.setBody("abc")
.setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
Call call1 = client.newCall(new Request.Builder()
.url(server.url("/"))
.build());
Response response1 = call1.execute();
assertEquals("abc", response1.body().string());
// Confirm that a second request also succeeds. This should detect caching problems.
server.enqueue(new MockResponse()
.setBody("def")
.setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
Call call2 = client.newCall(new Request.Builder()
.url(server.url("/"))
.build());
Response response2 = call2.execute();
assertEquals("def", response2.body().string());
}
/** The pinner should accept an intermediate from the server's chain. */
@Test public void pinIntermediatePresentInChain() throws Exception {
HeldCertificate rootCa = new HeldCertificate.Builder()
.serialNumber("1")
.ca(3)
.commonName("root")
.build();
HeldCertificate intermediateCa = new HeldCertificate.Builder()
.issuedBy(rootCa)
.ca(2)
.serialNumber("2")
.commonName("intermediate_ca")
.build();
HeldCertificate certificate = new HeldCertificate.Builder()
.issuedBy(intermediateCa)
.serialNumber("3")
.commonName(server.getHostName())
.build();
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add(server.getHostName(), CertificatePinner.pin(intermediateCa.certificate))
.build();
SslClient contextBuilder = new SslClient.Builder()
.addTrustedCertificate(rootCa.certificate)
.build();
OkHttpClient client = defaultClient().newBuilder()
.sslSocketFactory(contextBuilder.socketFactory, contextBuilder.trustManager)
.hostnameVerifier(new RecordingHostnameVerifier())
.certificatePinner(certificatePinner)
.build();
SslClient serverSslContext = new SslClient.Builder()
.certificateChain(certificate.keyPair, certificate.certificate, intermediateCa.certificate)
.build();
server.useHttps(serverSslContext.socketFactory, false);
// The request should complete successfully.
server.enqueue(new MockResponse()
.setBody("abc")
.setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
Call call1 = client.newCall(new Request.Builder()
.url(server.url("/"))
.build());
Response response1 = call1.execute();
assertEquals("abc", response1.body().string());
response1.close();
// Force a fresh connection for the next request.
client.connectionPool().evictAll();
// Confirm that a second request also succeeds. This should detect caching problems.
server.enqueue(new MockResponse()
.setBody("def")
.setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
Call call2 = client.newCall(new Request.Builder()
.url(server.url("/"))
.build());
Response response2 = call2.execute();
assertEquals("def", response2.body().string());
response2.close();
}
@Test public void unrelatedPinnedLeafCertificateInChain() throws Exception {
// Start with a trusted root CA certificate.
HeldCertificate rootCa = new HeldCertificate.Builder()
.serialNumber("1")
.ca(3)
.commonName("root")
.build();
// Add a good intermediate CA, and have that issue a good certificate to localhost. Prepare an
// SSL context for an HTTP client under attack. It includes the trusted CA and a pinned
// certificate.
HeldCertificate goodIntermediateCa = new HeldCertificate.Builder()
.issuedBy(rootCa)
.ca(2)
.serialNumber("2")
.commonName("good_intermediate_ca")
.build();
HeldCertificate goodCertificate = new HeldCertificate.Builder()
.issuedBy(goodIntermediateCa)
.serialNumber("3")
.commonName(server.getHostName())
.build();
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add(server.getHostName(), CertificatePinner.pin(goodCertificate.certificate))
.build();
SslClient clientContextBuilder = new SslClient.Builder()
.addTrustedCertificate(rootCa.certificate)
.build();
OkHttpClient client = defaultClient().newBuilder()
.sslSocketFactory(clientContextBuilder.socketFactory, clientContextBuilder.trustManager)
.hostnameVerifier(new RecordingHostnameVerifier())
.certificatePinner(certificatePinner)
.build();
// Add a bad intermediate CA and have that issue a rogue certificate for localhost. Prepare
// an SSL context for an attacking webserver. It includes both these rogue certificates plus the
// trusted good certificate above. The attack is that by including the good certificate in the
// chain, we may trick the certificate pinner into accepting the rouge certificate.
HeldCertificate compromisedIntermediateCa = new HeldCertificate.Builder()
.issuedBy(rootCa)
.ca(2)
.serialNumber("4")
.commonName("bad_intermediate_ca")
.build();
HeldCertificate rogueCertificate = new HeldCertificate.Builder()
.serialNumber("5")
.issuedBy(compromisedIntermediateCa)
.commonName(server.getHostName())
.build();
SslClient.Builder sslBuilder = new SslClient.Builder();
// Test setup fails on JDK9
// java.security.KeyStoreException: Certificate chain is not valid
// at sun.security.pkcs12.PKCS12KeyStore.setKeyEntry
// http://openjdk.java.net/jeps/229
// http://hg.openjdk.java.net/jdk9/jdk9/jdk/file/2c1c21d11e58/src/share/classes/sun/security/pkcs12/PKCS12KeyStore.java#l596
if (getPlatform().equals("jdk9")) {
sslBuilder.keyStoreType("JKS");
}
SslClient serverSslContext = sslBuilder.certificateChain(
rogueCertificate.keyPair, rogueCertificate.certificate, compromisedIntermediateCa.certificate, goodCertificate.certificate, rootCa.certificate)
.build();
server.useHttps(serverSslContext.socketFactory, false);
server.enqueue(new MockResponse()
.setBody("abc")
.addHeader("Content-Type: text/plain"));
// Make a request from client to server. It should succeed certificate checks (unfortunately the
// rogue CA is trusted) but it should fail certificate pinning.
Request request = new Request.Builder()
.url(server.url("/"))
.build();
Call call = client.newCall(request);
try {
call.execute();
fail();
} catch (SSLPeerUnverifiedException expected) {
// Certificate pinning fails!
String message = expected.getMessage();
assertTrue(message, message.startsWith("Certificate pinning failure!"));
}
}
@Test public void unrelatedPinnedIntermediateCertificateInChain() throws Exception {
// Start with two root CA certificates, one is good and the other is compromised.
HeldCertificate rootCa = new HeldCertificate.Builder()
.serialNumber("1")
.ca(3)
.commonName("root")
.build();
HeldCertificate compromisedRootCa = new HeldCertificate.Builder()
.serialNumber("2")
.ca(3)
.commonName("compromised_root")
.build();
// Add a good intermediate CA, and have that issue a good certificate to localhost. Prepare an
// SSL context for an HTTP client under attack. It includes the trusted CA and a pinned
// certificate.
HeldCertificate goodIntermediateCa = new HeldCertificate.Builder()
.issuedBy(rootCa)
.ca(2)
.serialNumber("3")
.commonName("intermediate_ca")
.build();
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add(server.getHostName(), CertificatePinner.pin(goodIntermediateCa.certificate))
.build();
SslClient clientContextBuilder = new SslClient.Builder()
.addTrustedCertificate(rootCa.certificate)
.addTrustedCertificate(compromisedRootCa.certificate)
.build();
OkHttpClient client = defaultClient().newBuilder()
.sslSocketFactory(clientContextBuilder.socketFactory, clientContextBuilder.trustManager)
.hostnameVerifier(new RecordingHostnameVerifier())
.certificatePinner(certificatePinner)
.build();
// The attacker compromises the root CA, issues an intermediate with the same common name
// "intermediate_ca" as the good CA. This signs a rogue certificate for localhost. The server
// serves the good CAs certificate in the chain, which means the certificate pinner sees a
// different set of certificates than the SSL verifier.
HeldCertificate compromisedIntermediateCa = new HeldCertificate.Builder()
.issuedBy(compromisedRootCa)
.ca(2)
.serialNumber("4")
.commonName("intermediate_ca")
.build();
HeldCertificate rogueCertificate = new HeldCertificate.Builder()
.serialNumber("5")
.issuedBy(compromisedIntermediateCa)
.commonName(server.getHostName())
.build();
SslClient.Builder sslBuilder = new SslClient.Builder();
// Test setup fails on JDK9
// java.security.KeyStoreException: Certificate chain is not valid
// at sun.security.pkcs12.PKCS12KeyStore.setKeyEntry
// http://openjdk.java.net/jeps/229
// http://hg.openjdk.java.net/jdk9/jdk9/jdk/file/2c1c21d11e58/src/share/classes/sun/security/pkcs12/PKCS12KeyStore.java#l596
if (getPlatform().equals("jdk9")) {
sslBuilder.keyStoreType("JKS");
}
SslClient serverSslContext = sslBuilder.certificateChain(
rogueCertificate.keyPair, rogueCertificate.certificate, goodIntermediateCa.certificate, compromisedIntermediateCa.certificate, compromisedRootCa.certificate)
.build();
server.useHttps(serverSslContext.socketFactory, false);
server.enqueue(new MockResponse()
.setBody("abc")
.addHeader("Content-Type: text/plain"));
// Make a request from client to server. It should succeed certificate checks (unfortunately the
// rogue CA is trusted) but it should fail certificate pinning.
Request request = new Request.Builder()
.url(server.url("/"))
.build();
Call call = client.newCall(request);
try {
call.execute();
fail();
} catch (SSLHandshakeException expected) {
// On Android, the handshake fails before the certificate pinner runs.
String message = expected.getMessage();
assertTrue(message, message.contains("Could not validate certificate"));
} catch (SSLPeerUnverifiedException expected) {
// On OpenJDK, the handshake succeeds but the certificate pinner fails.
String message = expected.getMessage();
assertTrue(message, message.startsWith("Certificate pinning failure!"));
}
}
}