/* * 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 tmpFiles = new ArrayList(); 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 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 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(); } } }