Merge "Expose findTrustAnchorBySubjectAndPublicKey"

This commit is contained in:
Chad Brubaker
2015-12-01 20:13:40 +00:00
committed by Gerrit Code Review
12 changed files with 300 additions and 139 deletions

View File

@@ -22,4 +22,5 @@ import java.security.cert.X509Certificate;
/** @hide */
public interface CertificateSource {
Set<X509Certificate> getCertificates();
X509Certificate findBySubjectAndPublicKey(X509Certificate cert);
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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}.
*

View File

@@ -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");
}

View File

@@ -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 int mResourceId;
private Context mContext;
private final Object mLock = new Object();
private final int mResourceId;
private Set<X509Certificate> mCertificates;
private Context mContext;
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();
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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);
}
}