blob: 035d4f2ce73fd3817b1f67815ee68bff20d07239 [file] [log] [blame]
/*
* Copyright (C) 2011 The Android Open Source Project
*
* 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 org.conscrypt;
import java.io.File;
import java.io.FileWriter;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSessionContext;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import junit.framework.TestCase;
import libcore.java.security.TestKeyStore;
public class TrustManagerImplTest extends TestCase {
private List<File> tmpFiles = new ArrayList<File>();
private String getFingerprint(X509Certificate cert) throws Exception {
MessageDigest dgst = MessageDigest.getInstance("SHA512");
byte[] encoded = cert.getPublicKey().getEncoded();
byte[] fingerprint = dgst.digest(encoded);
return IntegralToString.bytesToHexString(fingerprint, false);
}
private String writeTmpPinFile(String text) throws Exception {
File tmp = File.createTempFile("pins", null);
FileWriter fstream = new FileWriter(tmp);
fstream.write(text);
fstream.close();
tmpFiles.add(tmp);
return tmp.getPath();
}
@Override
public void tearDown() throws Exception {
try {
for (File f : tmpFiles) {
f.delete();
}
tmpFiles.clear();
} finally {
super.tearDown();
}
}
/**
* Ensure that our non-standard behavior of learning to trust new
* intermediate CAs does not regress. http://b/3404902
*/
public void testLearnIntermediate() throws Exception {
// chain3 should be server/intermediate/root
KeyStore.PrivateKeyEntry pke = TestKeyStore.getServer().getPrivateKey("RSA", "RSA");
X509Certificate[] chain3 = (X509Certificate[])pke.getCertificateChain();
X509Certificate root = chain3[2];
X509Certificate intermediate = chain3[1];
X509Certificate server = chain3[0];
X509Certificate[] chain2 = new X509Certificate[] { server, intermediate };
X509Certificate[] chain1 = new X509Certificate[] { server };
// Normal behavior
assertValid(chain3, trustManager(root));
assertValid(chain2, trustManager(root));
assertInvalid(chain1, trustManager(root));
assertValid(chain3, trustManager(intermediate));
assertValid(chain2, trustManager(intermediate));
assertValid(chain1, trustManager(intermediate));
assertValid(chain3, trustManager(server));
assertValid(chain2, trustManager(server));
assertValid(chain1, trustManager(server));
// non-standard behavior
X509TrustManager tm = trustManager(root);
// fail on short chain with only root trusted
assertInvalid(chain1, tm);
// succeed on longer chain, learn intermediate
assertValid(chain2, tm);
// now we can validate the short chain
assertValid(chain1, tm);
}
// We should ignore duplicate cruft in the certificate chain
// See https://code.google.com/p/android/issues/detail?id=52295 http://b/8313312
public void testDuplicateInChain() throws Exception {
// chain3 should be server/intermediate/root
KeyStore.PrivateKeyEntry pke = TestKeyStore.getServer().getPrivateKey("RSA", "RSA");
X509Certificate[] chain3 = (X509Certificate[])pke.getCertificateChain();
X509Certificate root = chain3[2];
X509Certificate intermediate = chain3[1];
X509Certificate server = chain3[0];
X509Certificate[] chain4 = new X509Certificate[] { server, intermediate,
server, intermediate
};
assertValid(chain4, trustManager(root));
}
public void testGetFullChain() throws Exception {
// build the trust manager
KeyStore.PrivateKeyEntry pke = TestKeyStore.getServer().getPrivateKey("RSA", "RSA");
X509Certificate[] chain3 = (X509Certificate[])pke.getCertificateChain();
X509Certificate root = chain3[2];
X509TrustManager tm = trustManager(root);
// build the chains we'll use for testing
X509Certificate intermediate = chain3[1];
X509Certificate server = chain3[0];
X509Certificate[] chain2 = new X509Certificate[] { server, intermediate };
X509Certificate[] chain1 = new X509Certificate[] { server };
assertTrue(tm instanceof TrustManagerImpl);
TrustManagerImpl tmi = (TrustManagerImpl) tm;
List<X509Certificate> certs = tmi.checkServerTrusted(chain2, "RSA", new MySSLSession(
"purple.com"));
assertEquals(Arrays.asList(chain3), certs);
certs = tmi.checkServerTrusted(chain1, "RSA", new MySSLSession("purple.com"));
assertEquals(Arrays.asList(chain3), certs);
}
public void testCertPinning() throws Exception {
// chain3 should be server/intermediate/root
KeyStore.PrivateKeyEntry pke = TestKeyStore.getServer().getPrivateKey("RSA", "RSA");
X509Certificate[] chain3 = (X509Certificate[]) pke.getCertificateChain();
X509Certificate root = chain3[2];
X509Certificate intermediate = chain3[1];
X509Certificate server = chain3[0];
X509Certificate[] chain2 = new X509Certificate[] { server, intermediate };
X509Certificate[] chain1 = new X509Certificate[] { server };
// test without a hostname, expecting failure
assertInvalidPinned(chain1, trustManager(root, "gugle.com", root), null);
// test without a hostname, expecting success
assertValidPinned(chain3, trustManager(root, "gugle.com", root), null, chain3);
// test an unpinned hostname that should fail
assertInvalidPinned(chain1, trustManager(root, "gugle.com", root), "purple.com");
// test an unpinned hostname that should succeed
assertValidPinned(chain3, trustManager(root, "gugle.com", root), "purple.com", chain3);
// test a pinned hostname that should fail
assertInvalidPinned(chain1, trustManager(intermediate, "gugle.com", root), "gugle.com");
// test a pinned hostname that should succeed
assertValidPinned(chain2, trustManager(intermediate, "gugle.com", server), "gugle.com",
chain2);
// test a pinned hostname that chains to user installed that should succeed
assertValidPinned(chain2, trustManagerUserInstalled(
(X509Certificate)TestKeyStore.getIntermediateCa2().getPrivateKey("RSA", "RSA")
.getCertificateChain()[1], intermediate, "gugle.com", server), "gugle.com",
chain2, true);
}
private X509TrustManager trustManager(X509Certificate ca) throws Exception {
KeyStore keyStore = TestKeyStore.createKeyStore();
keyStore.setCertificateEntry("alias", ca);
String algorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(algorithm);
tmf.init(keyStore);
return (X509TrustManager) tmf.getTrustManagers()[0];
}
private TrustManagerImpl trustManager(X509Certificate ca, String hostname, X509Certificate pin)
throws Exception {
// build the cert pin manager
CertPinManager cm = certManager(hostname, pin);
// insert it into the trust manager
KeyStore keyStore = TestKeyStore.createKeyStore();
keyStore.setCertificateEntry("alias", ca);
return new TrustManagerImpl(keyStore, cm);
}
private TrustManagerImpl trustManagerUserInstalled(
X509Certificate caKeyStore, X509Certificate caUserStore, String hostname,
X509Certificate pin) throws Exception {
// build the cert pin manager
CertPinManager cm = certManager(hostname, pin);
// install at least one cert in the store (requirement)
KeyStore keyStore = TestKeyStore.createKeyStore();
keyStore.setCertificateEntry("alias", caKeyStore);
// install a cert into the user installed store
final File DIR_TEMP = new File(System.getProperty("java.io.tmpdir"));
final File DIR_TEST = new File(DIR_TEMP, "test");
final File system = new File(DIR_TEST, "system-test");
final File added = new File(DIR_TEST, "added-test");
final File deleted = new File(DIR_TEST, "deleted-test");
TrustedCertificateStore tcs = new TrustedCertificateStore(system, added, deleted);
added.mkdirs();
tcs.installCertificate(caUserStore);
return new TrustManagerImpl(keyStore, cm, tcs);
}
private CertPinManager certManager(String hostname, X509Certificate pin) throws Exception {
String pinString = "";
if (pin != null) {
pinString = hostname + "=true|" + getFingerprint(pin);
}
// write it to a pinfile
String path = writeTmpPinFile(pinString);
// build the certpinmanager
return new CertPinManager(path, new TrustedCertificateStore());
}
private void assertValid(X509Certificate[] chain, X509TrustManager tm) throws Exception {
if (tm instanceof TrustManagerImpl) {
TrustManagerImpl tmi = (TrustManagerImpl) tm;
tmi.checkServerTrusted(chain, "RSA");
}
tm.checkServerTrusted(chain, "RSA");
}
private void assertValidPinned(X509Certificate[] chain, X509TrustManager tm, String hostname,
X509Certificate[] fullChain) throws Exception {
assertValidPinned(chain, tm, hostname, fullChain, false);
}
private void assertValidPinned(X509Certificate[] chain, X509TrustManager tm, String hostname,
X509Certificate[] fullChain, boolean expectUserInstalled)
throws Exception {
if (tm instanceof TrustManagerImpl) {
TrustManagerImpl tmi = (TrustManagerImpl) tm;
List<X509Certificate> checkedChain = tmi.checkServerTrusted(chain, "RSA",
new MySSLSession(hostname));
assertEquals(checkedChain, Arrays.asList(fullChain));
boolean chainContainsUserInstalled = false;
for (X509Certificate cert : checkedChain) {
if (tmi.isUserAddedCertificate(cert)) {
chainContainsUserInstalled = true;
}
}
assertEquals(expectUserInstalled, chainContainsUserInstalled);
}
tm.checkServerTrusted(chain, "RSA");
}
private void assertInvalid(X509Certificate[] chain, X509TrustManager tm) {
try {
tm.checkClientTrusted(chain, "RSA");
fail();
} catch (CertificateException expected) {
}
try {
tm.checkServerTrusted(chain, "RSA");
fail();
} catch (CertificateException expected) {
}
}
private void assertInvalidPinned(X509Certificate[] chain, X509TrustManager tm, String hostname)
throws Exception {
assertTrue(tm.getClass().getName(), tm instanceof TrustManagerImpl);
try {
TrustManagerImpl tmi = (TrustManagerImpl) tm;
tmi.checkServerTrusted(chain, "RSA", new MySSLSession(hostname));
fail();
} catch (CertificateException expected) {
}
}
private class MySSLSession implements SSLSession {
private final String hostname;
public MySSLSession(String hostname) {
this.hostname = hostname;
}
@Override
public int getApplicationBufferSize() {
throw new UnsupportedOperationException();
}
@Override
public String getCipherSuite() {
throw new UnsupportedOperationException();
}
@Override
public long getCreationTime() {
throw new UnsupportedOperationException();
}
@Override
public byte[] getId() {
throw new UnsupportedOperationException();
}
@Override
public long getLastAccessedTime() {
throw new UnsupportedOperationException();
}
@Override
public Certificate[] getLocalCertificates() {
throw new UnsupportedOperationException();
}
@Override
public Principal getLocalPrincipal() {
throw new UnsupportedOperationException();
}
@Override
public int getPacketBufferSize() {
throw new UnsupportedOperationException();
}
@Override
public javax.security.cert.X509Certificate[] getPeerCertificateChain()
throws SSLPeerUnverifiedException {
throw new UnsupportedOperationException();
}
@Override
public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException {
throw new UnsupportedOperationException();
}
@Override
public String getPeerHost() {
return hostname;
}
@Override
public int getPeerPort() {
throw new UnsupportedOperationException();
}
@Override
public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
throw new UnsupportedOperationException();
}
@Override
public String getProtocol() {
throw new UnsupportedOperationException();
}
@Override
public SSLSessionContext getSessionContext() {
throw new UnsupportedOperationException();
}
@Override
public Object getValue(String name) {
throw new UnsupportedOperationException();
}
@Override
public String[] getValueNames() {
throw new UnsupportedOperationException();
}
@Override
public void invalidate() {
throw new UnsupportedOperationException();
}
@Override
public boolean isValid() {
throw new UnsupportedOperationException();
}
@Override
public void putValue(String name, Object value) {
throw new UnsupportedOperationException();
}
@Override
public void removeValue(String name) {
throw new UnsupportedOperationException();
}
}
}