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