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 
17 package android.security.net.config;
18 
19 import android.util.ArraySet;
20 import android.util.Log;
21 
22 import libcore.io.IoUtils;
23 
24 import java.io.BufferedInputStream;
25 import java.io.File;
26 import java.io.FileInputStream;
27 import java.io.IOException;
28 import java.io.InputStream;
29 import java.security.MessageDigest;
30 import java.security.NoSuchAlgorithmException;
31 import java.security.cert.CertificateException;
32 import java.security.cert.CertificateFactory;
33 import java.security.cert.X509Certificate;
34 import java.util.Collections;
35 import java.util.Set;
36 
37 import javax.security.auth.x500.X500Principal;
38 
39 /**
40  * {@link CertificateSource} based on a directory where certificates are stored as individual files
41  * named after a hash of their SubjectName for more efficient lookups.
42  * @hide
43  */
44 abstract class DirectoryCertificateSource implements CertificateSource {
45     private static final String LOG_TAG = "DirectoryCertificateSrc";
46     private final File mDir;
47     private final Object mLock = new Object();
48     private final CertificateFactory mCertFactory;
49 
50     private Set<X509Certificate> mCertificates;
51 
DirectoryCertificateSource(File caDir)52     protected DirectoryCertificateSource(File caDir) {
53         mDir = caDir;
54         try {
55             mCertFactory = CertificateFactory.getInstance("X.509");
56         } catch (CertificateException e) {
57             throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
58         }
59     }
60 
isCertMarkedAsRemoved(String caFile)61     protected abstract boolean isCertMarkedAsRemoved(String caFile);
62 
63     @Override
getCertificates()64     public Set<X509Certificate> getCertificates() {
65         // TODO: loading all of these is wasteful, we should instead use a keystore style API.
66         synchronized (mLock) {
67             if (mCertificates != null) {
68                 return mCertificates;
69             }
70 
71             Set<X509Certificate> certs = new ArraySet<X509Certificate>();
72             if (mDir.isDirectory()) {
73                 for (String caFile : mDir.list()) {
74                     if (isCertMarkedAsRemoved(caFile)) {
75                         continue;
76                     }
77                     X509Certificate cert = readCertificate(caFile);
78                     if (cert != null) {
79                         certs.add(cert);
80                     }
81                 }
82             }
83             mCertificates = certs;
84             return mCertificates;
85         }
86     }
87 
88     @Override
findBySubjectAndPublicKey(final X509Certificate cert)89     public X509Certificate findBySubjectAndPublicKey(final X509Certificate cert) {
90         return findCert(cert.getSubjectX500Principal(), new CertSelector() {
91             @Override
92             public boolean match(X509Certificate ca) {
93                 return ca.getPublicKey().equals(cert.getPublicKey());
94             }
95         });
96     }
97 
98     @Override
99     public X509Certificate findByIssuerAndSignature(final X509Certificate cert) {
100         return findCert(cert.getIssuerX500Principal(), new CertSelector() {
101             @Override
102             public boolean match(X509Certificate ca) {
103                 try {
104                     cert.verify(ca.getPublicKey());
105                     return true;
106                 } catch (Exception e) {
107                     return false;
108                 }
109             }
110         });
111     }
112 
113     @Override
114     public Set<X509Certificate> findAllByIssuerAndSignature(final X509Certificate cert) {
115         return findCerts(cert.getIssuerX500Principal(), new CertSelector() {
116             @Override
117             public boolean match(X509Certificate ca) {
118                 try {
119                     cert.verify(ca.getPublicKey());
120                     return true;
121                 } catch (Exception e) {
122                     return false;
123                 }
124             }
125         });
126     }
127 
128     @Override
129     public void handleTrustStorageUpdate() {
130         synchronized (mLock) {
131             mCertificates = null;
132         }
133     }
134 
135     private static interface CertSelector {
136         boolean match(X509Certificate cert);
137     }
138 
139     private Set<X509Certificate> findCerts(X500Principal subj, CertSelector selector) {
140         String hash = getHash(subj);
141         Set<X509Certificate> certs = null;
142         for (int index = 0; index >= 0; index++) {
143             String fileName = hash + "." + index;
144             if (!new File(mDir, fileName).exists()) {
145                 break;
146             }
147             if (isCertMarkedAsRemoved(fileName)) {
148                 continue;
149             }
150             X509Certificate cert = readCertificate(fileName);
151             if (cert == null) {
152                 continue;
153             }
154             if (!subj.equals(cert.getSubjectX500Principal())) {
155                 continue;
156             }
157             if (selector.match(cert)) {
158                 if (certs == null) {
159                     certs = new ArraySet<X509Certificate>();
160                 }
161                 certs.add(cert);
162             }
163         }
164         return certs != null ? certs : Collections.<X509Certificate>emptySet();
165     }
166 
167     private X509Certificate findCert(X500Principal subj, CertSelector selector) {
168         String hash = getHash(subj);
169         for (int index = 0; index >= 0; index++) {
170             String fileName = hash + "." + index;
171             if (!new File(mDir, fileName).exists()) {
172                 break;
173             }
174             if (isCertMarkedAsRemoved(fileName)) {
175                 continue;
176             }
177             X509Certificate cert = readCertificate(fileName);
178             if (cert == null) {
179                 continue;
180             }
181             if (!subj.equals(cert.getSubjectX500Principal())) {
182                 continue;
183             }
184             if (selector.match(cert)) {
185                 return cert;
186             }
187         }
188         return null;
189     }
190 
191     private String getHash(X500Principal name) {
192         int hash = hashName(name);
193         return intToHexString(hash, 8);
194     }
195 
196     private static final char[] DIGITS = {
197             '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
198 
199     private static String intToHexString(int i, int minWidth) {
200         int bufLen = 8;  // Max number of hex digits in an int
201         char[] buf = new char[bufLen];
202         int cursor = bufLen;
203 
204         do {
205             buf[--cursor] = DIGITS[i & 0xf];
206         } while ((i >>>= 4) != 0 || (bufLen - cursor < minWidth));
207 
208         return new String(buf, cursor, bufLen - cursor);
209     }
210 
211     // This code matches the code in X509_NAME_hash_old() in Conscrypt, which in turn matches
212     // the names of certificate files.  It must be kept in sync.
213     private static int hashName(X500Principal principal) {
214         try {
215             byte[] digest = MessageDigest.getInstance("MD5").digest(principal.getEncoded());
216             int offset = 0;
217             return (((digest[offset++] & 0xff) << 0) | ((digest[offset++] & 0xff) << 8)
218                     | ((digest[offset++] & 0xff) << 16) | ((digest[offset] & 0xff) << 24));
219         } catch (NoSuchAlgorithmException e) {
220             throw new AssertionError(e);
221         }
222     }
223 
224     private X509Certificate readCertificate(String file) {
225         InputStream is = null;
226         try {
227             is = new BufferedInputStream(new FileInputStream(new File(mDir, file)));
228             return (X509Certificate) mCertFactory.generateCertificate(is);
229         } catch (CertificateException | IOException e) {
230             Log.e(LOG_TAG, "Failed to read certificate from " + file, e);
231             return null;
232         } finally {
233             IoUtils.closeQuietly(is);
234         }
235     }
236 }
237