1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.security.net.config;
18
19import android.os.Environment;
20import android.os.UserHandle;
21import android.util.ArraySet;
22import android.util.Log;
23import android.util.Pair;
24import java.io.BufferedInputStream;
25import java.io.File;
26import java.io.FileInputStream;
27import java.io.InputStream;
28import java.io.IOException;
29import java.security.cert.Certificate;
30import java.security.cert.CertificateException;
31import java.security.cert.CertificateFactory;
32import java.security.cert.X509Certificate;
33import java.util.Collections;
34import java.util.Set;
35import libcore.io.IoUtils;
36
37import com.android.org.conscrypt.Hex;
38import com.android.org.conscrypt.NativeCrypto;
39
40import javax.security.auth.x500.X500Principal;
41
42/**
43 * {@link CertificateSource} based on a directory where certificates are stored as individual files
44 * named after a hash of their SubjectName for more efficient lookups.
45 * @hide
46 */
47abstract class DirectoryCertificateSource implements CertificateSource {
48    private static final String LOG_TAG = "DirectoryCertificateSrc";
49    private final File mDir;
50    private final Object mLock = new Object();
51    private final CertificateFactory mCertFactory;
52
53    private Set<X509Certificate> mCertificates;
54
55    protected DirectoryCertificateSource(File caDir) {
56        mDir = caDir;
57        try {
58            mCertFactory = CertificateFactory.getInstance("X.509");
59        } catch (CertificateException e) {
60            throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
61        }
62    }
63
64    protected abstract boolean isCertMarkedAsRemoved(String caFile);
65
66    @Override
67    public Set<X509Certificate> getCertificates() {
68        // TODO: loading all of these is wasteful, we should instead use a keystore style API.
69        synchronized (mLock) {
70            if (mCertificates != null) {
71                return mCertificates;
72            }
73
74            Set<X509Certificate> certs = new ArraySet<X509Certificate>();
75            if (mDir.isDirectory()) {
76                for (String caFile : mDir.list()) {
77                    if (isCertMarkedAsRemoved(caFile)) {
78                        continue;
79                    }
80                    X509Certificate cert = readCertificate(caFile);
81                    if (cert != null) {
82                        certs.add(cert);
83                    }
84                }
85            }
86            mCertificates = certs;
87            return mCertificates;
88        }
89    }
90
91    @Override
92    public X509Certificate findBySubjectAndPublicKey(final X509Certificate cert) {
93        return findCert(cert.getSubjectX500Principal(), new CertSelector() {
94            @Override
95            public boolean match(X509Certificate ca) {
96                return ca.getPublicKey().equals(cert.getPublicKey());
97            }
98        });
99    }
100
101    @Override
102    public X509Certificate findByIssuerAndSignature(final X509Certificate cert) {
103        return findCert(cert.getIssuerX500Principal(), new CertSelector() {
104            @Override
105            public boolean match(X509Certificate ca) {
106                try {
107                    cert.verify(ca.getPublicKey());
108                    return true;
109                } catch (Exception e) {
110                    return false;
111                }
112            }
113        });
114    }
115
116    @Override
117    public Set<X509Certificate> findAllByIssuerAndSignature(final X509Certificate cert) {
118        return findCerts(cert.getIssuerX500Principal(), new CertSelector() {
119            @Override
120            public boolean match(X509Certificate ca) {
121                try {
122                    cert.verify(ca.getPublicKey());
123                    return true;
124                } catch (Exception e) {
125                    return false;
126                }
127            }
128        });
129    }
130
131    @Override
132    public void handleTrustStorageUpdate() {
133        synchronized (mLock) {
134            mCertificates = null;
135        }
136    }
137
138    private static interface CertSelector {
139        boolean match(X509Certificate cert);
140    }
141
142    private Set<X509Certificate> findCerts(X500Principal subj, CertSelector selector) {
143        String hash = getHash(subj);
144        Set<X509Certificate> certs = null;
145        for (int index = 0; index >= 0; index++) {
146            String fileName = hash + "." + index;
147            if (!new File(mDir, fileName).exists()) {
148                break;
149            }
150            if (isCertMarkedAsRemoved(fileName)) {
151                continue;
152            }
153            X509Certificate cert = readCertificate(fileName);
154            if (cert == null) {
155                continue;
156            }
157            if (!subj.equals(cert.getSubjectX500Principal())) {
158                continue;
159            }
160            if (selector.match(cert)) {
161                if (certs == null) {
162                    certs = new ArraySet<X509Certificate>();
163                }
164                certs.add(cert);
165            }
166        }
167        return certs != null ? certs : Collections.<X509Certificate>emptySet();
168    }
169
170    private X509Certificate findCert(X500Principal subj, CertSelector selector) {
171        String hash = getHash(subj);
172        for (int index = 0; index >= 0; index++) {
173            String fileName = hash + "." + index;
174            if (!new File(mDir, fileName).exists()) {
175                break;
176            }
177            if (isCertMarkedAsRemoved(fileName)) {
178                continue;
179            }
180            X509Certificate cert = readCertificate(fileName);
181            if (cert == null) {
182                continue;
183            }
184            if (!subj.equals(cert.getSubjectX500Principal())) {
185                continue;
186            }
187            if (selector.match(cert)) {
188                return cert;
189            }
190        }
191        return null;
192    }
193
194    private String getHash(X500Principal name) {
195        int hash = NativeCrypto.X509_NAME_hash_old(name);
196        return Hex.intToHexString(hash, 8);
197    }
198
199    private X509Certificate readCertificate(String file) {
200        InputStream is = null;
201        try {
202            is = new BufferedInputStream(new FileInputStream(new File(mDir, file)));
203            return (X509Certificate) mCertFactory.generateCertificate(is);
204        } catch (CertificateException | IOException e) {
205            Log.e(LOG_TAG, "Failed to read certificate from " + file, e);
206            return null;
207        } finally {
208            IoUtils.closeQuietly(is);
209        }
210    }
211}
212