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