Home | History | Annotate | Download | only in conscrypt
      1 /*
      2  * Copyright (C) 2012 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 org.conscrypt;
     18 
     19 import java.io.File;
     20 import java.io.FileNotFoundException;
     21 import java.io.IOException;
     22 import java.security.cert.X509Certificate;
     23 import java.util.HashMap;
     24 import java.util.List;
     25 import java.util.Locale;
     26 import java.util.Map;
     27 import libcore.io.IoUtils;
     28 import libcore.util.BasicLruCache;
     29 
     30 /**
     31  * This class provides a simple interface for cert pinning.
     32  */
     33 public class CertPinManager {
     34 
     35     private long lastModified;
     36 
     37     private final Map<String, PinListEntry> entries = new HashMap<String, PinListEntry>();
     38     private final BasicLruCache<String, String> hostnameCache = new BasicLruCache<String, String>(10);
     39 
     40     private boolean initialized = false;
     41     private static final boolean DEBUG = false;
     42 
     43     private final File pinFile;
     44     private final TrustedCertificateStore certStore;
     45 
     46     public CertPinManager(TrustedCertificateStore store) throws PinManagerException {
     47         pinFile = new File("/data/misc/keychain/pins");
     48         certStore = store;
     49         rebuild();
     50     }
     51 
     52     /** Test only */
     53     public CertPinManager(String path, TrustedCertificateStore store) throws PinManagerException {
     54         if (path == null) {
     55             throw new NullPointerException("path == null");
     56         }
     57         pinFile = new File(path);
     58         certStore = store;
     59         rebuild();
     60     }
     61 
     62     /**
     63      * This is the public interface for cert pinning.
     64      *
     65      * Given a hostname and a certificate chain this verifies that the chain includes
     66      * certs from the pinned list provided.
     67      *
     68      * If the chain doesn't include those certs and is in enforcing mode, then this method
     69      * returns true and the certificate check should fail.
     70      */
     71     public boolean chainIsNotPinned(String hostname, List<X509Certificate> chain)
     72             throws PinManagerException {
     73         // lookup the entry
     74         PinListEntry entry = lookup(hostname);
     75 
     76         // return its result or false if there's no pin
     77         if (entry != null) {
     78             return entry.chainIsNotPinned(chain);
     79         }
     80         return false;
     81     }
     82 
     83     private synchronized void rebuild() throws PinManagerException {
     84         // reread the pin file
     85         String pinFileContents = readPinFile();
     86 
     87         if (pinFileContents != null) {
     88             // rebuild the pinned certs
     89             for (String entry : getPinFileEntries(pinFileContents)) {
     90                 try {
     91                     PinListEntry pin = new PinListEntry(entry, certStore);
     92                     entries.put(pin.getCommonName(), pin);
     93                 } catch (PinEntryException e) {
     94                     log("Pinlist contains a malformed pin: " + entry, e);
     95                 }
     96             }
     97 
     98             // clear the cache
     99             hostnameCache.evictAll();
    100 
    101             // set the last modified time
    102             lastModified = pinFile.lastModified();
    103 
    104             // we've been fully initialized and are ready to go
    105             initialized = true;
    106         }
    107     }
    108 
    109     private String readPinFile() throws PinManagerException {
    110         try {
    111             return IoUtils.readFileAsString(pinFile.getPath());
    112         } catch (FileNotFoundException e) {
    113             // there's no pin list, all certs are unpinned
    114             return null;
    115         } catch (IOException e) {
    116             // this is unexpected, fail
    117             throw new PinManagerException("Unexpected error reading pin list; failing.", e);
    118         }
    119     }
    120 
    121     private static String[] getPinFileEntries(String pinFileContents) {
    122         return pinFileContents.split("\n");
    123     }
    124 
    125     private synchronized PinListEntry lookup(String hostname) throws PinManagerException {
    126 
    127         // if we don't have any data, don't bother
    128         if (!initialized) {
    129             return null;
    130         }
    131 
    132         // check to see if our cache is valid
    133         if (cacheIsNotValid()) {
    134             rebuild();
    135         }
    136 
    137         // if so, check the hostname cache
    138         String cn = hostnameCache.get(hostname);
    139         if (cn != null) {
    140             // if we hit, return the corresponding entry
    141             return entries.get(cn);
    142         }
    143 
    144         // otherwise, get the matching cn
    145         cn = getMatchingCN(hostname);
    146         if (cn != null) {
    147             hostnameCache.put(hostname, cn);
    148             // we have a matching CN, return that entry
    149             return entries.get(cn);
    150         }
    151 
    152         // if we got here, we don't have a matching CN for this hostname
    153         return null;
    154     }
    155 
    156     private boolean cacheIsNotValid() {
    157         return pinFile.lastModified() != lastModified;
    158     }
    159 
    160     private String getMatchingCN(String hostname) {
    161         String bestMatch = "";
    162         for (String cn : entries.keySet()) {
    163             // skip shorter CNs since they can't be better matches
    164             if (cn.length() < bestMatch.length()) {
    165                 continue;
    166             }
    167             // now verify that the CN matches at all
    168             if (isHostnameMatchedBy(hostname, cn)) {
    169                 bestMatch = cn;
    170             }
    171         }
    172         return bestMatch;
    173     }
    174 
    175     /**
    176      * Returns true if {@code hostName} matches the name or pattern {@code cn}.
    177      *
    178      * @param hostName lowercase host name.
    179      * @param cn certificate host name. May include wildcards like
    180      *            {@code *.android.com}.
    181      */
    182     private static boolean isHostnameMatchedBy(String hostName, String cn) {
    183         if (hostName == null || hostName.isEmpty() || cn == null || cn.isEmpty()) {
    184             return false;
    185         }
    186 
    187         cn = cn.toLowerCase(Locale.US);
    188 
    189         if (!cn.contains("*")) {
    190             return hostName.equals(cn);
    191         }
    192 
    193         if (cn.startsWith("*.") && hostName.regionMatches(0, cn, 2, cn.length() - 2)) {
    194             return true; // "*.foo.com" matches "foo.com"
    195         }
    196 
    197         int asterisk = cn.indexOf('*');
    198         int dot = cn.indexOf('.');
    199         if (asterisk > dot) {
    200             return false; // malformed; wildcard must be in the first part of
    201                           // the cn
    202         }
    203 
    204         if (!hostName.regionMatches(0, cn, 0, asterisk)) {
    205             return false; // prefix before '*' doesn't match
    206         }
    207 
    208         int suffixLength = cn.length() - (asterisk + 1);
    209         int suffixStart = hostName.length() - suffixLength;
    210         if (hostName.indexOf('.', asterisk) < suffixStart) {
    211             return false; // wildcard '*' can't match a '.'
    212         }
    213 
    214         if (!hostName.regionMatches(suffixStart, cn, asterisk + 1, suffixLength)) {
    215             return false; // suffix after '*' doesn't match
    216         }
    217 
    218         return true;
    219     }
    220 
    221     private static void log(String s, Exception e) {
    222         if (DEBUG) {
    223             System.out.println("PINFILE: " + s);
    224             if (e != null) {
    225                 e.printStackTrace();
    226             }
    227         }
    228     }
    229 }
    230