1/*
2 * Copyright (C) 2011 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 org.conscrypt;
18
19import java.io.BufferedInputStream;
20import java.io.File;
21import java.io.FileInputStream;
22import java.io.FileOutputStream;
23import java.io.IOException;
24import java.io.InputStream;
25import java.io.OutputStream;
26import java.security.cert.Certificate;
27import java.security.cert.CertificateException;
28import java.security.cert.CertificateFactory;
29import java.security.cert.X509Certificate;
30import java.util.ArrayList;
31import java.util.Date;
32import java.util.HashSet;
33import java.util.List;
34import java.util.Set;
35import javax.security.auth.x500.X500Principal;
36import libcore.io.IoUtils;
37
38/**
39 * A source for trusted root certificate authority (CA) certificates
40 * supporting an immutable system CA directory along with mutable
41 * directories allowing the user addition of custom CAs and user
42 * removal of system CAs. This store supports the {@code
43 * TrustedCertificateKeyStoreSpi} wrapper to allow a traditional
44 * KeyStore interface for use with {@link
45 * javax.net.ssl.TrustManagerFactory.init}.
46 *
47 * <p>The CAs are accessed via {@code KeyStore} style aliases. Aliases
48 * are made up of a prefix identifying the source ("system:" vs
49 * "user:") and a suffix based on the OpenSSL X509_NAME_hash_old
50 * function of the CA's subject name. For example, the system CA for
51 * "C=US, O=VeriSign, Inc., OU=Class 3 Public Primary Certification
52 * Authority" could be represented as "system:7651b327.0". By using
53 * the subject hash, operations such as {@link #getCertificateAlias
54 * getCertificateAlias} can be implemented efficiently without
55 * scanning the entire store.
56 *
57 * <p>In addition to supporting the {@code
58 * TrustedCertificateKeyStoreSpi} implementation, {@code
59 * TrustedCertificateStore} also provides the additional public
60 * methods {@link #isTrustAnchor} and {@link #findIssuer} to allow
61 * efficient lookup operations for CAs again based on the file naming
62 * convention.
63 *
64 * <p>The KeyChainService users the {@link installCertificate} and
65 * {@link #deleteCertificateEntry} to install user CAs as well as
66 * delete those user CAs as well as system CAs. The deletion of system
67 * CAs is performed by placing an exact copy of that CA in the deleted
68 * directory. Such deletions are intended to persist across upgrades
69 * but not intended to mask a CA with a matching name or public key
70 * but is otherwise reissued in a system update. Reinstalling a
71 * deleted system certificate simply removes the copy from the deleted
72 * directory, reenabling the original in the system directory.
73 *
74 * <p>Note that the default mutable directory is created by init via
75 * configuration in the system/core/rootdir/init.rc file. The
76 * directive "mkdir /data/misc/keychain 0775 system system"
77 * ensures that its owner and group are the system uid and system
78 * gid and that it is world readable but only writable by the system
79 * user.
80 */
81public final class TrustedCertificateStore {
82
83    private static final String PREFIX_SYSTEM = "system:";
84    private static final String PREFIX_USER = "user:";
85
86    public static final boolean isSystem(String alias) {
87        return alias.startsWith(PREFIX_SYSTEM);
88    }
89    public static final boolean isUser(String alias) {
90        return alias.startsWith(PREFIX_USER);
91    }
92
93    private static final File CA_CERTS_DIR_SYSTEM;
94    private static final File CA_CERTS_DIR_ADDED;
95    private static final File CA_CERTS_DIR_DELETED;
96    private static final CertificateFactory CERT_FACTORY;
97    static {
98        String ANDROID_ROOT = System.getenv("ANDROID_ROOT");
99        String ANDROID_DATA = System.getenv("ANDROID_DATA");
100        CA_CERTS_DIR_SYSTEM = new File(ANDROID_ROOT + "/etc/security/cacerts");
101        CA_CERTS_DIR_ADDED = new File(ANDROID_DATA + "/misc/keychain/cacerts-added");
102        CA_CERTS_DIR_DELETED = new File(ANDROID_DATA + "/misc/keychain/cacerts-removed");
103
104        try {
105            CERT_FACTORY = CertificateFactory.getInstance("X509");
106        } catch (CertificateException e) {
107            throw new AssertionError(e);
108        }
109    }
110
111    private final File systemDir;
112    private final File addedDir;
113    private final File deletedDir;
114
115    public TrustedCertificateStore() {
116        this(CA_CERTS_DIR_SYSTEM, CA_CERTS_DIR_ADDED, CA_CERTS_DIR_DELETED);
117    }
118
119    public TrustedCertificateStore(File systemDir, File addedDir, File deletedDir) {
120        this.systemDir = systemDir;
121        this.addedDir = addedDir;
122        this.deletedDir = deletedDir;
123    }
124
125    public Certificate getCertificate(String alias) {
126        return getCertificate(alias, false);
127    }
128
129    public Certificate getCertificate(String alias, boolean includeDeletedSystem) {
130
131        File file = fileForAlias(alias);
132        if (file == null || (isUser(alias) && isTombstone(file))) {
133            return null;
134        }
135        X509Certificate cert = readCertificate(file);
136        if (cert == null || (isSystem(alias)
137                             && !includeDeletedSystem
138                             && isDeletedSystemCertificate(cert))) {
139            // skip malformed certs as well as deleted system ones
140            return null;
141        }
142        return cert;
143    }
144
145    private File fileForAlias(String alias) {
146        if (alias == null) {
147            throw new NullPointerException("alias == null");
148        }
149        File file;
150        if (isSystem(alias)) {
151            file = new File(systemDir, alias.substring(PREFIX_SYSTEM.length()));
152        } else if (isUser(alias)) {
153            file = new File(addedDir, alias.substring(PREFIX_USER.length()));
154        } else {
155            return null;
156        }
157        if (!file.exists() || isTombstone(file)) {
158            // silently elide tombstones
159            return null;
160        }
161        return file;
162    }
163
164    private boolean isTombstone(File file) {
165        return file.length() == 0;
166    }
167
168    private X509Certificate readCertificate(File file) {
169        if (!file.isFile()) {
170            return null;
171        }
172        InputStream is = null;
173        try {
174            is = new BufferedInputStream(new FileInputStream(file));
175            return (X509Certificate) CERT_FACTORY.generateCertificate(is);
176        } catch (IOException e) {
177            return null;
178        } catch (CertificateException e) {
179            // reading a cert while its being installed can lead to this.
180            // just pretend like its not available yet.
181            return null;
182        } finally {
183            IoUtils.closeQuietly(is);
184        }
185    }
186
187    private void writeCertificate(File file, X509Certificate cert)
188            throws IOException, CertificateException {
189        File dir = file.getParentFile();
190        dir.mkdirs();
191        dir.setReadable(true, false);
192        dir.setExecutable(true, false);
193        OutputStream os = null;
194        try {
195            os = new FileOutputStream(file);
196            os.write(cert.getEncoded());
197        } finally {
198            IoUtils.closeQuietly(os);
199        }
200        file.setReadable(true, false);
201    }
202
203    private boolean isDeletedSystemCertificate(X509Certificate x) {
204        return getCertificateFile(deletedDir, x).exists();
205    }
206
207    public Date getCreationDate(String alias) {
208        // containsAlias check ensures the later fileForAlias result
209        // was not a deleted system cert.
210        if (!containsAlias(alias)) {
211            return null;
212        }
213        File file = fileForAlias(alias);
214        if (file == null) {
215            return null;
216        }
217        long time = file.lastModified();
218        if (time == 0) {
219            return null;
220        }
221        return new Date(time);
222    }
223
224    public Set<String> aliases() {
225        Set<String> result = new HashSet<String>();
226        addAliases(result, PREFIX_USER, addedDir);
227        addAliases(result, PREFIX_SYSTEM, systemDir);
228        return result;
229    }
230
231    public Set<String> userAliases() {
232        Set<String> result = new HashSet<String>();
233        addAliases(result, PREFIX_USER, addedDir);
234        return result;
235    }
236
237    private void addAliases(Set<String> result, String prefix, File dir) {
238        String[] files = dir.list();
239        if (files == null) {
240            return;
241        }
242        for (String filename : files) {
243            String alias = prefix + filename;
244            if (containsAlias(alias)) {
245                result.add(alias);
246            }
247        }
248    }
249
250    public Set<String> allSystemAliases() {
251        Set<String> result = new HashSet<String>();
252        String[] files = systemDir.list();
253        if (files == null) {
254            return result;
255        }
256        for (String filename : files) {
257            String alias = PREFIX_SYSTEM + filename;
258            if (containsAlias(alias, true)) {
259                result.add(alias);
260            }
261        }
262        return result;
263    }
264
265    public boolean containsAlias(String alias) {
266        return containsAlias(alias, false);
267    }
268
269    private boolean containsAlias(String alias, boolean includeDeletedSystem) {
270        return getCertificate(alias, includeDeletedSystem) != null;
271    }
272
273    public String getCertificateAlias(Certificate c) {
274        if (c == null || !(c instanceof X509Certificate)) {
275            return null;
276        }
277        X509Certificate x = (X509Certificate) c;
278        File user = getCertificateFile(addedDir, x);
279        if (user.exists()) {
280            return PREFIX_USER + user.getName();
281        }
282        if (isDeletedSystemCertificate(x)) {
283            return null;
284        }
285        File system = getCertificateFile(systemDir, x);
286        if (system.exists()) {
287            return PREFIX_SYSTEM + system.getName();
288        }
289        return null;
290    }
291
292    /**
293     * Returns true to indicate that the certificate was added by the
294     * user, false otherwise.
295     */
296    public boolean isUserAddedCertificate(X509Certificate cert) {
297        return getCertificateFile(addedDir, cert).exists();
298    }
299
300    /**
301     * Returns a File for where the certificate is found if it exists
302     * or where it should be installed if it does not exist. The
303     * caller can disambiguate these cases by calling {@code
304     * File.exists()} on the result.
305     */
306    private File getCertificateFile(File dir, final X509Certificate x) {
307        // compare X509Certificate.getEncoded values
308        CertSelector selector = new CertSelector() {
309            @Override public boolean match(X509Certificate cert) {
310                return cert.equals(x);
311            }
312        };
313        return findCert(dir, x.getSubjectX500Principal(), selector, File.class);
314    }
315
316    /**
317     * This non-{@code KeyStoreSpi} public interface is used by {@code
318     * TrustManagerImpl} to locate a CA certificate with the same name
319     * and public key as the provided {@code X509Certificate}. We
320     * match on the name and public key and not the entire certificate
321     * since a CA may be reissued with the same name and PublicKey but
322     * with other differences (for example when switching signature
323     * from md2WithRSAEncryption to SHA1withRSA)
324     */
325    public boolean isTrustAnchor(final X509Certificate c) {
326        // compare X509Certificate.getPublicKey values
327        CertSelector selector = new CertSelector() {
328            @Override public boolean match(X509Certificate ca) {
329                return ca.getPublicKey().equals(c.getPublicKey());
330            }
331        };
332        boolean user = findCert(addedDir,
333                                c.getSubjectX500Principal(),
334                                selector,
335                                Boolean.class);
336        if (user) {
337            return true;
338        }
339        X509Certificate system = findCert(systemDir,
340                                          c.getSubjectX500Principal(),
341                                          selector,
342                                          X509Certificate.class);
343        return system != null && !isDeletedSystemCertificate(system);
344    }
345
346    /**
347     * This non-{@code KeyStoreSpi} public interface is used by {@code
348     * TrustManagerImpl} to locate the CA certificate that signed the
349     * provided {@code X509Certificate}.
350     */
351    public X509Certificate findIssuer(final X509Certificate c) {
352        // match on verified issuer of Certificate
353        CertSelector selector = new CertSelector() {
354            @Override public boolean match(X509Certificate ca) {
355                try {
356                    c.verify(ca.getPublicKey());
357                    return true;
358                } catch (Exception e) {
359                    return false;
360                }
361            }
362        };
363        X500Principal issuer = c.getIssuerX500Principal();
364        X509Certificate user = findCert(addedDir, issuer, selector, X509Certificate.class);
365        if (user != null) {
366            return user;
367        }
368        X509Certificate system = findCert(systemDir, issuer, selector, X509Certificate.class);
369        if (system != null && !isDeletedSystemCertificate(system)) {
370            return system;
371        }
372        return null;
373    }
374
375    private static boolean isSelfIssuedCertificate(OpenSSLX509Certificate cert) {
376        final long ctx = cert.getContext();
377        return NativeCrypto.X509_check_issued(ctx, ctx) == 0;
378    }
379
380    /**
381     * Converts the {@code cert} to the internal OpenSSL X.509 format so we can
382     * run {@link NativeCrypto} methods on it.
383     */
384    private static OpenSSLX509Certificate convertToOpenSSLIfNeeded(X509Certificate cert)
385            throws CertificateException {
386        if (cert == null) {
387            return null;
388        }
389
390        if (cert instanceof OpenSSLX509Certificate) {
391            return (OpenSSLX509Certificate) cert;
392        }
393
394        try {
395            return OpenSSLX509Certificate.fromX509Der(cert.getEncoded());
396        } catch (Exception e) {
397            throw new CertificateException(e);
398        }
399    }
400
401    /**
402     * Attempt to build a certificate chain from the supplied {@code leaf}
403     * argument through the chain of issuers as high up as known. If the chain
404     * can't be completed, the most complete chain available will be returned.
405     * This means that a list with only the {@code leaf} certificate is returned
406     * if no issuer certificates could be found.
407     *
408     * @throws CertificateException if there was a problem parsing the
409     *             certificates
410     */
411    public List<X509Certificate> getCertificateChain(X509Certificate leaf)
412            throws CertificateException {
413        final List<OpenSSLX509Certificate> chain = new ArrayList<OpenSSLX509Certificate>();
414        chain.add(convertToOpenSSLIfNeeded(leaf));
415
416        for (int i = 0; true; i++) {
417            OpenSSLX509Certificate cert = chain.get(i);
418            if (isSelfIssuedCertificate(cert)) {
419                break;
420            }
421            OpenSSLX509Certificate issuer = convertToOpenSSLIfNeeded(findIssuer(cert));
422            if (issuer == null) {
423                break;
424            }
425            chain.add(issuer);
426        }
427
428        return new ArrayList<X509Certificate>(chain);
429    }
430
431    // like java.security.cert.CertSelector but with X509Certificate and without cloning
432    private static interface CertSelector {
433        public boolean match(X509Certificate cert);
434    }
435
436    private <T> T findCert(
437            File dir, X500Principal subject, CertSelector selector, Class<T> desiredReturnType) {
438
439        String hash = hash(subject);
440        for (int index = 0; true; index++) {
441            File file = file(dir, hash, index);
442            if (!file.isFile()) {
443                // could not find a match, no file exists, bail
444                if (desiredReturnType == Boolean.class) {
445                    return (T) Boolean.FALSE;
446                }
447                if (desiredReturnType == File.class) {
448                    // we return file so that caller that wants to
449                    // write knows what the next available has
450                    // location is
451                    return (T) file;
452                }
453                return null;
454            }
455            if (isTombstone(file)) {
456                continue;
457            }
458            X509Certificate cert = readCertificate(file);
459            if (cert == null) {
460                // skip problem certificates
461                continue;
462            }
463            if (selector.match(cert)) {
464                if (desiredReturnType == X509Certificate.class) {
465                    return (T) cert;
466                }
467                if (desiredReturnType == Boolean.class) {
468                    return (T) Boolean.TRUE;
469                }
470                if (desiredReturnType == File.class) {
471                    return (T) file;
472                }
473                throw new AssertionError();
474            }
475        }
476    }
477
478    private String hash(X500Principal name) {
479        int hash = NativeCrypto.X509_NAME_hash_old(name);
480        return IntegralToString.intToHexString(hash, false, 8);
481    }
482
483    private File file(File dir, String hash, int index) {
484        return new File(dir, hash + '.' + index);
485    }
486
487    /**
488     * This non-{@code KeyStoreSpi} public interface is used by the
489     * {@code KeyChainService} to install new CA certificates. It
490     * silently ignores the certificate if it already exists in the
491     * store.
492     */
493    public void installCertificate(X509Certificate cert) throws IOException, CertificateException {
494        if (cert == null) {
495            throw new NullPointerException("cert == null");
496        }
497        File system = getCertificateFile(systemDir, cert);
498        if (system.exists()) {
499            File deleted = getCertificateFile(deletedDir, cert);
500            if (deleted.exists()) {
501                // we have a system cert that was marked deleted.
502                // remove the deleted marker to expose the original
503                if (!deleted.delete()) {
504                    throw new IOException("Could not remove " + deleted);
505                }
506                return;
507            }
508            // otherwise we just have a dup of an existing system cert.
509            // return taking no further action.
510            return;
511        }
512        File user = getCertificateFile(addedDir, cert);
513        if (user.exists()) {
514            // we have an already installed user cert, bail.
515            return;
516        }
517        // install the user cert
518        writeCertificate(user, cert);
519    }
520
521    /**
522     * This could be considered the implementation of {@code
523     * TrustedCertificateKeyStoreSpi.engineDeleteEntry} but we
524     * consider {@code TrustedCertificateKeyStoreSpi} to be read
525     * only. Instead, this is used by the {@code KeyChainService} to
526     * delete CA certificates.
527     */
528    public void deleteCertificateEntry(String alias) throws IOException, CertificateException {
529        if (alias == null) {
530            return;
531        }
532        File file = fileForAlias(alias);
533        if (file == null) {
534            return;
535        }
536        if (isSystem(alias)) {
537            X509Certificate cert = readCertificate(file);
538            if (cert == null) {
539                // skip problem certificates
540                return;
541            }
542            File deleted = getCertificateFile(deletedDir, cert);
543            if (deleted.exists()) {
544                // already deleted system certificate
545                return;
546            }
547            // write copy of system cert to marked as deleted
548            writeCertificate(deleted, cert);
549            return;
550        }
551        if (isUser(alias)) {
552            // truncate the file to make a tombstone by opening and closing.
553            // we need ensure that we don't leave a gap before a valid cert.
554            new FileOutputStream(file).close();
555            removeUnnecessaryTombstones(alias);
556            return;
557        }
558        // non-existant user cert, nothing to delete
559    }
560
561    private void removeUnnecessaryTombstones(String alias) throws IOException {
562        if (!isUser(alias)) {
563            throw new AssertionError(alias);
564        }
565        int dotIndex = alias.lastIndexOf('.');
566        if (dotIndex == -1) {
567            throw new AssertionError(alias);
568        }
569
570        String hash = alias.substring(PREFIX_USER.length(), dotIndex);
571        int lastTombstoneIndex = Integer.parseInt(alias.substring(dotIndex + 1));
572
573        if (file(addedDir, hash, lastTombstoneIndex + 1).exists()) {
574            return;
575        }
576        while (lastTombstoneIndex >= 0) {
577            File file = file(addedDir, hash, lastTombstoneIndex);
578            if (!isTombstone(file)) {
579                break;
580            }
581            if (!file.delete()) {
582                throw new IOException("Could not remove " + file);
583            }
584            lastTombstoneIndex--;
585        }
586    }
587}
588