diff options
12 files changed, 299 insertions, 138 deletions
diff --git a/core/java/android/security/net/config/CertificateSource.java b/core/java/android/security/net/config/CertificateSource.java index 386354dc4d57..2b7829eb6a31 100644 --- a/core/java/android/security/net/config/CertificateSource.java +++ b/core/java/android/security/net/config/CertificateSource.java @@ -22,4 +22,5 @@ import java.security.cert.X509Certificate; /** @hide */ public interface CertificateSource { Set<X509Certificate> getCertificates(); + X509Certificate findBySubjectAndPublicKey(X509Certificate cert); } diff --git a/core/java/android/security/net/config/CertificatesEntryRef.java b/core/java/android/security/net/config/CertificatesEntryRef.java index 2ba38c21c330..1d15e19a99f2 100644 --- a/core/java/android/security/net/config/CertificatesEntryRef.java +++ b/core/java/android/security/net/config/CertificatesEntryRef.java @@ -30,6 +30,10 @@ public final class CertificatesEntryRef { mOverridesPins = overridesPins; } + boolean overridesPins() { + return mOverridesPins; + } + public Set<TrustAnchor> getTrustAnchors() { // TODO: cache this [but handle mutable sources] Set<TrustAnchor> anchors = new ArraySet<TrustAnchor>(); @@ -38,4 +42,13 @@ public final class CertificatesEntryRef { } return anchors; } + + public TrustAnchor findBySubjectAndPublicKey(X509Certificate cert) { + X509Certificate foundCert = mSource.findBySubjectAndPublicKey(cert); + if (foundCert == null) { + return null; + } + + return new TrustAnchor(foundCert, mOverridesPins); + } } diff --git a/core/java/android/security/net/config/DirectoryCertificateSource.java b/core/java/android/security/net/config/DirectoryCertificateSource.java new file mode 100644 index 000000000000..92c70920e040 --- /dev/null +++ b/core/java/android/security/net/config/DirectoryCertificateSource.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2015 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 android.security.net.config; + +import android.os.Environment; +import android.os.UserHandle; +import android.util.ArraySet; +import android.util.Pair; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.io.IOException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Set; +import libcore.io.IoUtils; + +import com.android.org.conscrypt.NativeCrypto; + +import javax.security.auth.x500.X500Principal; + +/** + * {@link CertificateSource} based on a directory where certificates are stored as individual files + * named after a hash of their SubjectName for more efficient lookups. + * @hide + */ +abstract class DirectoryCertificateSource implements CertificateSource { + private final File mDir; + private final Object mLock = new Object(); + private final CertificateFactory mCertFactory; + + private Set<X509Certificate> mCertificates; + + protected DirectoryCertificateSource(File caDir) { + mDir = caDir; + try { + mCertFactory = CertificateFactory.getInstance("X.509"); + } catch (CertificateException e) { + throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e); + } + } + + protected abstract boolean isCertMarkedAsRemoved(String caFile); + + @Override + public Set<X509Certificate> getCertificates() { + // TODO: loading all of these is wasteful, we should instead use a keystore style API. + synchronized (mLock) { + if (mCertificates != null) { + return mCertificates; + } + + Set<X509Certificate> certs = new ArraySet<X509Certificate>(); + if (mDir.isDirectory()) { + for (String caFile : mDir.list()) { + if (isCertMarkedAsRemoved(caFile)) { + continue; + } + X509Certificate cert = readCertificate(caFile); + if (cert != null) { + certs.add(cert); + } + } + } + mCertificates = certs; + return mCertificates; + } + } + + @Override + public X509Certificate findBySubjectAndPublicKey(final X509Certificate cert) { + return findCert(cert.getSubjectX500Principal(), new CertSelector() { + @Override + public boolean match(X509Certificate ca) { + return ca.getPublicKey().equals(cert.getPublicKey()); + } + }); + } + + private static interface CertSelector { + boolean match(X509Certificate cert); + } + + private X509Certificate findCert(X500Principal subj, CertSelector selector) { + String hash = getHash(subj); + for (int index = 0; index >= 0; index++) { + String fileName = hash + "." + index; + if (!new File(mDir, fileName).exists()) { + break; + } + if (isCertMarkedAsRemoved(fileName)) { + continue; + } + X509Certificate cert = readCertificate(fileName); + if (!subj.equals(cert.getSubjectX500Principal())) { + continue; + } + if (selector.match(cert)) { + return cert; + } + } + return null; + } + + private String getHash(X500Principal name) { + int hash = NativeCrypto.X509_NAME_hash_old(name); + return IntegralToString.intToHexString(hash, false, 8); + } + + private X509Certificate readCertificate(String file) { + InputStream is = null; + try { + is = new BufferedInputStream(new FileInputStream(new File(mDir, file))); + return (X509Certificate) mCertFactory.generateCertificate(is); + } catch (CertificateException | IOException e) { + return null; + } finally { + IoUtils.closeQuietly(is); + } + } +} diff --git a/core/java/android/security/net/config/KeyStoreCertificateSource.java b/core/java/android/security/net/config/KeyStoreCertificateSource.java index 9167a9007325..7a01a6488a04 100644 --- a/core/java/android/security/net/config/KeyStoreCertificateSource.java +++ b/core/java/android/security/net/config/KeyStoreCertificateSource.java @@ -24,6 +24,8 @@ import java.security.cert.X509Certificate; import java.util.Enumeration; import java.util.Set; +import com.android.org.conscrypt.TrustedCertificateIndex; + /** * {@link CertificateSource} which provides certificates from trusted certificate entries of a * {@link KeyStore}. @@ -31,6 +33,7 @@ import java.util.Set; class KeyStoreCertificateSource implements CertificateSource { private final Object mLock = new Object(); private final KeyStore mKeyStore; + private TrustedCertificateIndex mIndex; private Set<X509Certificate> mCertificates; public KeyStoreCertificateSource(KeyStore ks) { @@ -39,24 +42,42 @@ class KeyStoreCertificateSource implements CertificateSource { @Override public Set<X509Certificate> getCertificates() { + ensureInitialized(); + return mCertificates; + } + + private void ensureInitialized() { synchronized (mLock) { if (mCertificates != null) { - return mCertificates; + return; } + try { + TrustedCertificateIndex localIndex = new TrustedCertificateIndex(); Set<X509Certificate> certificates = new ArraySet<>(mKeyStore.size()); for (Enumeration<String> en = mKeyStore.aliases(); en.hasMoreElements();) { String alias = en.nextElement(); X509Certificate cert = (X509Certificate) mKeyStore.getCertificate(alias); if (cert != null) { certificates.add(cert); + localIndex.index(cert); } } + mIndex = localIndex; mCertificates = certificates; - return mCertificates; } catch (KeyStoreException e) { throw new RuntimeException("Failed to load certificates from KeyStore", e); } } } + + @Override + public X509Certificate findBySubjectAndPublicKey(X509Certificate cert) { + ensureInitialized(); + java.security.cert.TrustAnchor anchor = mIndex.findBySubjectAndPublicKey(cert); + if (anchor == null) { + return null; + } + return anchor.getTrustedCert(); + } } diff --git a/core/java/android/security/net/config/NetworkSecurityConfig.java b/core/java/android/security/net/config/NetworkSecurityConfig.java index 9eab80ca0771..2ab07b5abf18 100644 --- a/core/java/android/security/net/config/NetworkSecurityConfig.java +++ b/core/java/android/security/net/config/NetworkSecurityConfig.java @@ -22,6 +22,7 @@ import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Set; @@ -53,6 +54,19 @@ public final class NetworkSecurityConfig { mHstsEnforced = hstsEnforced; mPins = pins; mCertificatesEntryRefs = certificatesEntryRefs; + // Sort the certificates entry refs so that all entries that override pins come before + // non-override pin entries. This allows us to handle the case where a certificate is in + // multiple entry refs by returning the certificate from the first entry ref. + Collections.sort(mCertificatesEntryRefs, new Comparator<CertificatesEntryRef>() { + @Override + public int compare(CertificatesEntryRef lhs, CertificatesEntryRef rhs) { + if (lhs.overridesPins()) { + return rhs.overridesPins() ? 0 : -1; + } else { + return rhs.overridesPins() ? 1 : 0; + } + } + }); } public Set<TrustAnchor> getTrustAnchors() { @@ -63,14 +77,15 @@ public final class NetworkSecurityConfig { // Merge trust anchors based on the X509Certificate. // If we see the same certificate in two TrustAnchors, one with overridesPins and one // without, the one with overridesPins wins. + // Because mCertificatesEntryRefs is sorted with all overridesPins anchors coming first + // this can be simplified to just using the first occurrence of a certificate. Map<X509Certificate, TrustAnchor> anchorMap = new ArrayMap<>(); for (CertificatesEntryRef ref : mCertificatesEntryRefs) { Set<TrustAnchor> anchors = ref.getTrustAnchors(); for (TrustAnchor anchor : anchors) { - if (anchor.overridesPins) { - anchorMap.put(anchor.certificate, anchor); - } else if (!anchorMap.containsKey(anchor.certificate)) { - anchorMap.put(anchor.certificate, anchor); + X509Certificate cert = anchor.certificate; + if (!anchorMap.containsKey(cert)) { + anchorMap.put(cert, anchor); } } } @@ -108,6 +123,17 @@ public final class NetworkSecurityConfig { } } + /** @hide */ + public TrustAnchor findTrustAnchorBySubjectAndPublicKey(X509Certificate cert) { + for (CertificatesEntryRef ref : mCertificatesEntryRefs) { + TrustAnchor anchor = ref.findBySubjectAndPublicKey(cert); + if (anchor != null) { + return anchor; + } + } + return null; + } + /** * Return a {@link Builder} for the default {@code NetworkSecurityConfig}. * diff --git a/core/java/android/security/net/config/NetworkSecurityTrustManager.java b/core/java/android/security/net/config/NetworkSecurityTrustManager.java index 2b860fac45c1..6013c1e4023e 100644 --- a/core/java/android/security/net/config/NetworkSecurityTrustManager.java +++ b/core/java/android/security/net/config/NetworkSecurityTrustManager.java @@ -133,14 +133,8 @@ public class NetworkSecurityTrustManager implements X509TrustManager { return false; } X509Certificate anchorCert = chain.get(chain.size() - 1); - TrustAnchor chainAnchor = null; - // TODO: faster lookup - for (TrustAnchor anchor : mNetworkSecurityConfig.getTrustAnchors()) { - if (anchor.certificate.equals(anchorCert)) { - chainAnchor = anchor; - break; - } - } + TrustAnchor chainAnchor = + mNetworkSecurityConfig.findTrustAnchorBySubjectAndPublicKey(anchorCert); if (chainAnchor == null) { throw new CertificateException("Trusted chain does not end in a TrustAnchor"); } diff --git a/core/java/android/security/net/config/ResourceCertificateSource.java b/core/java/android/security/net/config/ResourceCertificateSource.java index 06dd9d42e364..b007f8f00a55 100644 --- a/core/java/android/security/net/config/ResourceCertificateSource.java +++ b/core/java/android/security/net/config/ResourceCertificateSource.java @@ -27,26 +27,29 @@ import java.security.cert.X509Certificate; import java.util.Collection; import java.util.Set; +import com.android.org.conscrypt.TrustedCertificateIndex; + /** * {@link CertificateSource} based on certificates contained in an application resource file. * @hide */ public class ResourceCertificateSource implements CertificateSource { - private Set<X509Certificate> mCertificates; + private final Object mLock = new Object(); private final int mResourceId; + + private Set<X509Certificate> mCertificates; private Context mContext; - private final Object mLock = new Object(); + private TrustedCertificateIndex mIndex; public ResourceCertificateSource(int resourceId, Context context) { mResourceId = resourceId; mContext = context.getApplicationContext(); } - @Override - public Set<X509Certificate> getCertificates() { + private void ensureInitialized() { synchronized (mLock) { if (mCertificates != null) { - return mCertificates; + return; } Set<X509Certificate> certificates = new ArraySet<X509Certificate>(); Collection<? extends Certificate> certs; @@ -61,12 +64,30 @@ public class ResourceCertificateSource implements CertificateSource { } finally { IoUtils.closeQuietly(in); } + TrustedCertificateIndex indexLocal = new TrustedCertificateIndex(); for (Certificate cert : certs) { - certificates.add((X509Certificate) cert); + certificates.add((X509Certificate) cert); + indexLocal.index((X509Certificate) cert); } mCertificates = certificates; + mIndex = indexLocal; mContext = null; - return mCertificates; } } + + @Override + public Set<X509Certificate> getCertificates() { + ensureInitialized(); + return mCertificates; + } + + @Override + public X509Certificate findBySubjectAndPublicKey(X509Certificate cert) { + ensureInitialized(); + java.security.cert.TrustAnchor anchor = mIndex.findBySubjectAndPublicKey(cert); + if (anchor == null) { + return null; + } + return anchor.getTrustedCert(); + } } diff --git a/core/java/android/security/net/config/SystemCertificateSource.java b/core/java/android/security/net/config/SystemCertificateSource.java index 7649a977ff5c..abef7b453c79 100644 --- a/core/java/android/security/net/config/SystemCertificateSource.java +++ b/core/java/android/security/net/config/SystemCertificateSource.java @@ -18,29 +18,20 @@ package android.security.net.config; import android.os.Environment; import android.os.UserHandle; -import android.util.ArraySet; -import java.io.BufferedInputStream; import java.io.File; -import java.io.FileInputStream; -import java.io.InputStream; -import java.io.IOException; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.util.Set; -import libcore.io.IoUtils; /** * {@link CertificateSource} based on the system trusted CA store. * @hide */ -public class SystemCertificateSource implements CertificateSource { +public final class SystemCertificateSource extends DirectoryCertificateSource { private static final SystemCertificateSource INSTANCE = new SystemCertificateSource(); - private Set<X509Certificate> mSystemCerts = null; - private final Object mLock = new Object(); + private final File mUserRemovedCaDir; private SystemCertificateSource() { + super(new File(System.getenv("ANDROID_ROOT") + "/etc/security/cacerts")); + File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId()); + mUserRemovedCaDir = new File(configDir, "cacerts-removed"); } public static SystemCertificateSource getInstance() { @@ -48,54 +39,7 @@ public class SystemCertificateSource implements CertificateSource { } @Override - public Set<X509Certificate> getCertificates() { - // TODO: loading all of these is wasteful, we should instead use a keystore style API. - synchronized (mLock) { - if (mSystemCerts != null) { - return mSystemCerts; - } - CertificateFactory certFactory; - try { - certFactory = CertificateFactory.getInstance("X.509"); - } catch (CertificateException e) { - throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e); - } - - final String ANDROID_ROOT = System.getenv("ANDROID_ROOT"); - final File systemCaDir = new File(ANDROID_ROOT + "/etc/security/cacerts"); - final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId()); - final File userRemovedCaDir = new File(configDir, "cacerts-removed"); - // Sanity check - if (!systemCaDir.isDirectory()) { - throw new AssertionError(systemCaDir + " is not a directory"); - } - - Set<X509Certificate> systemCerts = new ArraySet<X509Certificate>(); - for (String caFile : systemCaDir.list()) { - // Skip any CAs in the user's deleted directory. - if (new File(userRemovedCaDir, caFile).exists()) { - continue; - } - InputStream is = null; - try { - is = new BufferedInputStream( - new FileInputStream(new File(systemCaDir, caFile))); - systemCerts.add((X509Certificate) certFactory.generateCertificate(is)); - } catch (CertificateException | IOException e) { - // Don't rethrow to be consistent with conscrypt's cert loading code. - continue; - } finally { - IoUtils.closeQuietly(is); - } - } - mSystemCerts = systemCerts; - return mSystemCerts; - } - } - - public void onCertificateStorageChange() { - synchronized (mLock) { - mSystemCerts = null; - } + protected boolean isCertMarkedAsRemoved(String caFile) { + return new File(mUserRemovedCaDir, caFile).exists(); } } diff --git a/core/java/android/security/net/config/UserCertificateSource.java b/core/java/android/security/net/config/UserCertificateSource.java index e9d5aa1e2f34..1a7d92456a3d 100644 --- a/core/java/android/security/net/config/UserCertificateSource.java +++ b/core/java/android/security/net/config/UserCertificateSource.java @@ -18,29 +18,18 @@ package android.security.net.config; import android.os.Environment; import android.os.UserHandle; -import android.util.ArraySet; -import java.io.BufferedInputStream; import java.io.File; -import java.io.FileInputStream; -import java.io.InputStream; -import java.io.IOException; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.util.Set; -import libcore.io.IoUtils; /** * {@link CertificateSource} based on the user-installed trusted CA store. * @hide */ -public class UserCertificateSource implements CertificateSource { +public final class UserCertificateSource extends DirectoryCertificateSource { private static final UserCertificateSource INSTANCE = new UserCertificateSource(); - private Set<X509Certificate> mUserCerts = null; - private final Object mLock = new Object(); private UserCertificateSource() { + super(new File( + Environment.getUserConfigDirectory(UserHandle.myUserId()), "cacerts-added")); } public static UserCertificateSource getInstance() { @@ -48,45 +37,7 @@ public class UserCertificateSource implements CertificateSource { } @Override - public Set<X509Certificate> getCertificates() { - // TODO: loading all of these is wasteful, we should instead use a keystore style API. - synchronized (mLock) { - if (mUserCerts != null) { - return mUserCerts; - } - CertificateFactory certFactory; - try { - certFactory = CertificateFactory.getInstance("X.509"); - } catch (CertificateException e) { - throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e); - } - final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId()); - final File userCaDir = new File(configDir, "cacerts-added"); - Set<X509Certificate> userCerts = new ArraySet<X509Certificate>(); - // If the user hasn't added any certificates the directory may not exist. - if (userCaDir.isDirectory()) { - for (String caFile : userCaDir.list()) { - InputStream is = null; - try { - is = new BufferedInputStream( - new FileInputStream(new File(userCaDir, caFile))); - userCerts.add((X509Certificate) certFactory.generateCertificate(is)); - } catch (CertificateException | IOException e) { - // Don't rethrow to be consistent with conscrypt's cert loading code. - continue; - } finally { - IoUtils.closeQuietly(is); - } - } - } - mUserCerts = userCerts; - return mUserCerts; - } - } - - public void onCertificateStorageChange() { - synchronized (mLock) { - mUserCerts = null; - } + protected boolean isCertMarkedAsRemoved(String caFile) { + return false; } } diff --git a/tests/NetworkSecurityConfigTest/res/xml/override_dedup.xml b/tests/NetworkSecurityConfigTest/res/xml/override_dedup.xml new file mode 100644 index 000000000000..5ba56754e768 --- /dev/null +++ b/tests/NetworkSecurityConfigTest/res/xml/override_dedup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<network-security-config> + <!-- Entry with a bad pin. Connections to this will only succeed if overridePins is set. --> + <domain-config> + <domain>android.com</domain> + <pin-set> + <pin digest="SHA-256">aaaaaaaaIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin> + </pin-set> + <trust-anchors> + <certificates src="system" overridePins="false" /> + </trust-anchors> + </domain-config> + <!-- override that contains all of the system CA store. This should completely override the + anchors in the domain config-above with ones that have overridePins set. --> + <debug-overrides> + <trust-anchors> + <certificates src="system" /> + </trust-anchors> + </debug-overrides> +</network-security-config> diff --git a/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestCertificateSource.java b/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestCertificateSource.java index 92eadc06cd49..69b2a9d55642 100644 --- a/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestCertificateSource.java +++ b/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestCertificateSource.java @@ -19,15 +19,29 @@ package android.security.net.config; import java.util.Set; import java.security.cert.X509Certificate; +import com.android.org.conscrypt.TrustedCertificateIndex; + /** @hide */ public class TestCertificateSource implements CertificateSource { private final Set<X509Certificate> mCertificates; + private final TrustedCertificateIndex mIndex = new TrustedCertificateIndex(); public TestCertificateSource(Set<X509Certificate> certificates) { mCertificates = certificates; + for (X509Certificate cert : certificates) { + mIndex.index(cert); + } } public Set<X509Certificate> getCertificates() { return mCertificates; } + + public X509Certificate findBySubjectAndPublicKey(X509Certificate cert) { + java.security.cert.TrustAnchor anchor = mIndex.findBySubjectAndPublicKey(cert); + if (anchor == null) { + return null; + } + return anchor.getTrustedCert(); + } } diff --git a/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java b/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java index c6f3680f455c..998bb681dd24 100644 --- a/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java +++ b/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java @@ -402,4 +402,22 @@ public class XmlConfigTests extends AndroidTestCase { context.init(null, tms, null); TestUtils.assertConnectionSucceeds(context, "android.com" , 443); } + + public void testDebugDedup() throws Exception { + XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.override_dedup, true); + ApplicationConfig appConfig = new ApplicationConfig(source); + assertTrue(appConfig.hasPerDomainConfigs()); + // Check android.com. + NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com"); + PinSet pinSet = config.getPins(); + assertFalse(pinSet.pins.isEmpty()); + // Check that all TrustAnchors come from the override pins debug source. + for (TrustAnchor anchor : config.getTrustAnchors()) { + assertTrue(anchor.overridesPins); + } + // Try connections. + SSLContext context = TestUtils.getSSLContext(source); + TestUtils.assertConnectionSucceeds(context, "android.com", 443); + TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443); + } } |