diff options
7 files changed, 229 insertions, 37 deletions
diff --git a/luni/src/main/java/libcore/io/DropBox.java b/luni/src/main/java/libcore/io/DropBox.java new file mode 100644 index 0000000..cf88106 --- /dev/null +++ b/luni/src/main/java/libcore/io/DropBox.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2012 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 libcore.io; + +public final class DropBox { + + /** + * Hook for customizing how events are reported. + */ + private static volatile Reporter REPORTER = new DefaultReporter(); + + /** + * Used to replace default Reporter for logging events. Must be non-null. + */ + public static void setReporter(Reporter reporter) { + if (reporter == null) { + throw new NullPointerException("reporter == null"); + } + REPORTER = reporter; + } + + /** + * Returns non-null Reporter. + */ + public static Reporter getReporter() { + return REPORTER; + } + + /** + * Interface to allow customization of reporting behavior. + */ + public static interface Reporter { + public void addData(String tag, byte[] data, int flags); + public void addText(String tag, String data); + } + + /** + * Default Reporter which reports events to the log. + */ + private static final class DefaultReporter implements Reporter { + + public void addData(String tag, byte[] data, int flags) { + System.out.println(tag + ": " + Base64.encode(data)); + } + + public void addText(String tag, String data) { + System.out.println(tag + ": " + data); + } + } + + public static void addData(String tag, byte[] data, int flags) { + getReporter().addData(tag, data, flags); + } + + public static void addText(String tag, String data) { + getReporter().addText(tag, data); + } +} diff --git a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/CertPinManager.java b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/CertPinManager.java index aa956dd..4cdd8d2 100644 --- a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/CertPinManager.java +++ b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/CertPinManager.java @@ -42,18 +42,21 @@ public class CertPinManager { private static final boolean DEBUG = false; private final File pinFile; + private final TrustedCertificateStore certStore; - public CertPinManager() throws PinManagerException { + public CertPinManager(TrustedCertificateStore store) throws PinManagerException { pinFile = new File("/data/misc/keychain/pins"); + certStore = store; rebuild(); } /** Test only */ - public CertPinManager(String path) throws PinManagerException { + public CertPinManager(String path, TrustedCertificateStore store) throws PinManagerException { if (path == null) { throw new NullPointerException("path == null"); } pinFile = new File(path); + certStore = store; rebuild(); } @@ -86,7 +89,7 @@ public class CertPinManager { // rebuild the pinned certs for (String entry : getPinFileEntries(pinFileContents)) { try { - PinListEntry pin = new PinListEntry(entry); + PinListEntry pin = new PinListEntry(entry, certStore); entries.put(pin.getCommonName(), pin); } catch (PinEntryException e) { log("Pinlist contains a malformed pin: " + entry, e); diff --git a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/PinFailureLogger.java b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/PinFailureLogger.java new file mode 100644 index 0000000..40b1838 --- /dev/null +++ b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/PinFailureLogger.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2012 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.apache.harmony.xnet.provider.jsse; + +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.List; +import libcore.io.Base64; +import libcore.io.DropBox; + +public class PinFailureLogger { + + private static final long LOG_INTERVAL_NANOS = 1000 * 1000 * 1000 * 60 * 60; + + private static long lastLoggedNanos = 0; + + public static synchronized void log(String cn, boolean chainContainsUserCert, + boolean pinIsEnforcing, + List<X509Certificate> chain) { + // if we've logged recently, don't do it again + if (!timeToLog()) { + return; + } + // otherwise, log the event + writeToLog(cn, chainContainsUserCert, pinIsEnforcing, chain); + // update the last logged time + lastLoggedNanos = System.nanoTime(); + } + + protected static synchronized void writeToLog(String cn, boolean chainContainsUserCert, + boolean pinIsEnforcing, + List<X509Certificate> chain) { + StringBuilder sb = new StringBuilder(); + sb.append(cn); + sb.append("|"); + sb.append(chainContainsUserCert); + sb.append("|"); + sb.append(pinIsEnforcing); + sb.append("|"); + for (X509Certificate cert : chain) { + try { + sb.append(Base64.encode(cert.getEncoded())); + } catch (CertificateEncodingException e) { + sb.append("Error: could not encode certificate"); + } + sb.append("|"); + } + DropBox.addText("cert_pin_failure", sb.toString()); + } + + protected static boolean timeToLog() { + long currentTimeNanos = System.nanoTime(); + return ((currentTimeNanos - lastLoggedNanos) > LOG_INTERVAL_NANOS); + } +} + diff --git a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/PinListEntry.java b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/PinListEntry.java index 15a07c1..c05a391 100644 --- a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/PinListEntry.java +++ b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/PinListEntry.java @@ -46,6 +46,8 @@ public class PinListEntry { private static final boolean DEBUG = false; + private final TrustedCertificateStore certStore; + public String getCommonName() { return cn; } @@ -54,10 +56,11 @@ public class PinListEntry { return enforcing; } - public PinListEntry(String entry) throws PinEntryException { + public PinListEntry(String entry, TrustedCertificateStore store) throws PinEntryException { if (entry == null) { throw new NullPointerException("entry == null"); } + certStore = store; // Examples: // *.google.com=true|34c8a0d...9e04ca05f,9e04ca05f...34c8a0d // *.android.com=true|ca05f...8a0d34c @@ -98,7 +101,7 @@ public class PinListEntry { return false; } } - logPinFailure(cn, chain); + logPinFailure(chain); return enforcing; } @@ -133,13 +136,20 @@ public class PinListEntry { } } - private void logPinFailure(String cn, List<X509Certificate> chain) { - Object[] values = new Object[chain.size() + 1]; - values[0] = (Object) cn; - for (int i=0; i < chain.size(); i++) { - values[i+1] = chain.get(i).toString(); + private boolean chainContainsUserCert(List<X509Certificate> chain) { + if (certStore == null) { + return false; } - EventLogger.writeEvent(90100, values); + for (X509Certificate cert : chain) { + if (certStore.isUserAddedCertificate(cert)) { + return true; + } + } + return false; + } + + private void logPinFailure(List<X509Certificate> chain) { + PinFailureLogger.log(cn, chainContainsUserCert(chain), enforcing, chain); } } diff --git a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/TrustManagerImpl.java b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/TrustManagerImpl.java index 48dbaba..0218249 100644 --- a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/TrustManagerImpl.java +++ b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/TrustManagerImpl.java @@ -129,7 +129,7 @@ public final class TrustManagerImpl implements X509TrustManager { this.pinManager = manager; } else { try { - pinManager = new CertPinManager(); + pinManager = new CertPinManager(trustedCertificateStoreLocal); } catch (PinManagerException e) { throw new SecurityException("Could not initialize CertPinManager", e); } @@ -214,17 +214,43 @@ public final class TrustManagerImpl implements X509TrustManager { throw new CertificateException(err); } - // get the cleaned up chain and trust anchors - Set<TrustAnchor> trustAnchors = new HashSet<TrustAnchor>(); - X509Certificate[] newChain = cleanupCertChainAndFindTrustAnchors(chain, trustAnchors); + // get the cleaned up chain and trust anchor + Set<TrustAnchor> trustAnchor = new HashSet<TrustAnchor>(); // there can only be one! + X509Certificate[] newChain = cleanupCertChainAndFindTrustAnchors(chain, trustAnchor); - // build the whole chain including all the trust anchors + // add the first trust anchor to the chain, which may be an intermediate List<X509Certificate> wholeChain = new ArrayList<X509Certificate>(); wholeChain.addAll(Arrays.asList(newChain)); - for (TrustAnchor trust : trustAnchors) { + // trustAnchor is actually just a single element + for (TrustAnchor trust : trustAnchor) { wholeChain.add(trust.getTrustedCert()); } + // add all the cached certificates from the cert index, avoiding loops + // this gives us a full chain from leaf to root, which we use for cert pinning and pass + // back out to callers when we return. + X509Certificate last = wholeChain.get(wholeChain.size() - 1); + while (true) { + TrustAnchor cachedTrust = trustedCertificateIndex.findByIssuerAndSignature(last); + // the cachedTrust can be null if there isn't anything in the index or if a user has + // trusted a non-self-signed cert. + if (cachedTrust == null) { + break; + } + + // at this point we have a cached trust anchor, but don't know if its one we got from + // the server. Extract the cert, compare it to the last element in the chain, and add it + // if we haven't seen it before. + X509Certificate next = cachedTrust.getTrustedCert(); + if (next != last) { + wholeChain.add(next); + last = next; + } else { + // if next == last then we found a self-signed cert and the chain is done + break; + } + } + // build the cert path from the array of certs sans trust anchors CertPath certPath = factory.generateCertPath(Arrays.asList(newChain)); @@ -236,7 +262,6 @@ public final class TrustManagerImpl implements X509TrustManager { throw new CertificateException(e); } if (chainIsNotPinned) { - EventLogger.writeEvent(90102, chainContainsUserCert(wholeChain)); throw new CertificateException(new CertPathValidatorException( "Certificate path is not properly pinned.", null, certPath, -1)); } @@ -247,13 +272,13 @@ public final class TrustManagerImpl implements X509TrustManager { return wholeChain; } - if (trustAnchors.isEmpty()) { + if (trustAnchor.isEmpty()) { throw new CertificateException(new CertPathValidatorException( "Trust anchor for certification path not found.", null, certPath, -1)); } try { - PKIXParameters params = new PKIXParameters(trustAnchors); + PKIXParameters params = new PKIXParameters(trustAnchor); params.setRevocationEnabled(false); validator.validate(certPath, params); // Add intermediate CAs to the index to tolerate sites @@ -398,13 +423,4 @@ public final class TrustManagerImpl implements X509TrustManager { @Override public X509Certificate[] getAcceptedIssuers() { return (acceptedIssuers != null) ? acceptedIssuers.clone() : acceptedIssuers(rootKeyStore); } - - private boolean chainContainsUserCert(List<X509Certificate> chain) { - for (X509Certificate cert : chain) { - if (trustedCertificateStore.isUserAddedCertificate(cert)) { - return true; - } - } - return false; - } } diff --git a/luni/src/test/java/org/apache/harmony/xnet/provider/jsse/CertPinManagerTest.java b/luni/src/test/java/org/apache/harmony/xnet/provider/jsse/CertPinManagerTest.java index 7d37492..8359c99 100644 --- a/luni/src/test/java/org/apache/harmony/xnet/provider/jsse/CertPinManagerTest.java +++ b/luni/src/test/java/org/apache/harmony/xnet/provider/jsse/CertPinManagerTest.java @@ -92,7 +92,7 @@ public class CertPinManagerTest extends TestCase { // create the pinFile String path = writeTmpPinFile(shortEntry + "\n" + longEntry); - CertPinManager pf = new CertPinManager(path); + CertPinManager pf = new CertPinManager(path, new TrustedCertificateStore()); // verify that the shorter chain doesn't work for a name matching the longer assertTrue("short chain long uri failed", @@ -112,7 +112,7 @@ public class CertPinManagerTest extends TestCase { // set up the pinEntry with a bogus entry String entry = "*.google.com="; try { - new PinListEntry(entry); + new PinListEntry(entry, new TrustedCertificateStore()); fail("Accepted an empty pin list entry."); } catch (PinEntryException expected) { } @@ -122,7 +122,7 @@ public class CertPinManagerTest extends TestCase { // set up the pinEntry with a bogus entry String entry = null; try { - new PinListEntry(entry); + new PinListEntry(entry, new TrustedCertificateStore()); fail("Accepted a basically wholly bogus entry."); } catch (NullPointerException expected) { } @@ -131,7 +131,7 @@ public class CertPinManagerTest extends TestCase { public void testPinEntryEmpty() throws Exception { // set up the pinEntry with a bogus entry try { - new PinListEntry(""); + new PinListEntry("", new TrustedCertificateStore()); fail("Accepted an empty entry."); } catch (PinEntryException expected) { } @@ -142,7 +142,7 @@ public class CertPinManagerTest extends TestCase { String shortEntry = "*.google.com=true|" + shortPin; // set up the pinEntry with a pinlist that doesn't match what we'll give it - PinListEntry e = new PinListEntry(shortEntry); + PinListEntry e = new PinListEntry(shortEntry, new TrustedCertificateStore()); assertTrue("Not enforcing!", e.getEnforcing()); // verify that it doesn't accept boolean retval = e.chainIsNotPinned(longChain); @@ -154,7 +154,7 @@ public class CertPinManagerTest extends TestCase { String shortEntry = "*.google.com=true|" + shortPin; // set up the pinEntry with a pinlist that matches what we'll give it - PinListEntry e = new PinListEntry(shortEntry); + PinListEntry e = new PinListEntry(shortEntry, new TrustedCertificateStore()); assertTrue("Not enforcing!", e.getEnforcing()); // verify that it accepts boolean retval = e.chainIsNotPinned(shortChain); @@ -166,7 +166,7 @@ public class CertPinManagerTest extends TestCase { String shortEntry = "*.google.com=false|" + shortPin; // set up the pinEntry with a pinlist that matches what we'll give it - PinListEntry e = new PinListEntry(shortEntry); + PinListEntry e = new PinListEntry(shortEntry, new TrustedCertificateStore()); assertFalse("Enforcing!", e.getEnforcing()); // verify that it accepts boolean retval = e.chainIsNotPinned(shortChain); diff --git a/luni/src/test/java/org/apache/harmony/xnet/provider/jsse/TrustManagerImplTest.java b/luni/src/test/java/org/apache/harmony/xnet/provider/jsse/TrustManagerImplTest.java index 97032d9..fe5f4f0 100644 --- a/luni/src/test/java/org/apache/harmony/xnet/provider/jsse/TrustManagerImplTest.java +++ b/luni/src/test/java/org/apache/harmony/xnet/provider/jsse/TrustManagerImplTest.java @@ -98,6 +98,27 @@ public class TrustManagerImplTest extends TestCase { assertValid(chain1, tm); } + 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", "purple.com"); + assertEquals(Arrays.asList(chain3), certs); + certs = tmi.checkServerTrusted(chain1, "RSA", "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"); @@ -151,7 +172,7 @@ public class TrustManagerImplTest extends TestCase { // write it to a pinfile String path = writeTmpPinFile(pinString); // build the certpinmanager - return new CertPinManager(path); + return new CertPinManager(path, new TrustedCertificateStore()); } private void assertValid(X509Certificate[] chain, X509TrustManager tm) throws Exception { |