Home | History | Annotate | Download | only in jsse
      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.apache.harmony.xnet.provider.jsse;
     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.Map;
     26 import javax.net.ssl.DefaultHostnameVerifier;
     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     private final DefaultHostnameVerifier verifier = new DefaultHostnameVerifier();
     40 
     41     private boolean initialized = false;
     42     private static final boolean DEBUG = false;
     43 
     44     private final File pinFile;
     45     private final TrustedCertificateStore certStore;
     46 
     47     public CertPinManager(TrustedCertificateStore store) throws PinManagerException {
     48         pinFile = new File("/data/misc/keychain/pins");
     49         certStore = store;
     50         rebuild();
     51     }
     52 
     53     /** Test only */
     54     public CertPinManager(String path, TrustedCertificateStore store) throws PinManagerException {
     55         if (path == null) {
     56             throw new NullPointerException("path == null");
     57         }
     58         pinFile = new File(path);
     59         certStore = store;
     60         rebuild();
     61     }
     62 
     63     /**
     64      * This is the public interface for cert pinning.
     65      *
     66      * Given a hostname and a certificate chain this verifies that the chain includes
     67      * certs from the pinned list provided.
     68      *
     69      * If the chain doesn't include those certs and is in enforcing mode, then this method
     70      * returns true and the certificate check should fail.
     71      */
     72     public boolean chainIsNotPinned(String hostname, List<X509Certificate> chain)
     73             throws PinManagerException {
     74         // lookup the entry
     75         PinListEntry entry = lookup(hostname);
     76 
     77         // return its result or false if there's no pin
     78         if (entry != null) {
     79             return entry.chainIsNotPinned(chain);
     80         }
     81         return false;
     82     }
     83 
     84     private synchronized void rebuild() throws PinManagerException {
     85         // reread the pin file
     86         String pinFileContents = readPinFile();
     87 
     88         if (pinFileContents != null) {
     89             // rebuild the pinned certs
     90             for (String entry : getPinFileEntries(pinFileContents)) {
     91                 try {
     92                     PinListEntry pin = new PinListEntry(entry, certStore);
     93                     entries.put(pin.getCommonName(), pin);
     94                 } catch (PinEntryException e) {
     95                     log("Pinlist contains a malformed pin: " + entry, e);
     96                 }
     97             }
     98 
     99             // clear the cache
    100             hostnameCache.evictAll();
    101 
    102             // set the last modified time
    103             lastModified = pinFile.lastModified();
    104 
    105             // we've been fully initialized and are ready to go
    106             initialized = true;
    107         }
    108     }
    109 
    110     private String readPinFile() throws PinManagerException {
    111         try {
    112             return IoUtils.readFileAsString(pinFile.getPath());
    113         } catch (FileNotFoundException e) {
    114             // there's no pin list, all certs are unpinned
    115             return null;
    116         } catch (IOException e) {
    117             // this is unexpected, fail
    118             throw new PinManagerException("Unexpected error reading pin list; failing.", e);
    119         }
    120     }
    121 
    122     private static String[] getPinFileEntries(String pinFileContents) {
    123         return pinFileContents.split("\n");
    124     }
    125 
    126     private synchronized PinListEntry lookup(String hostname) throws PinManagerException {
    127 
    128         // if we don't have any data, don't bother
    129         if (!initialized) {
    130             return null;
    131         }
    132 
    133         // check to see if our cache is valid
    134         if (cacheIsNotValid()) {
    135             rebuild();
    136         }
    137 
    138         // if so, check the hostname cache
    139         String cn = hostnameCache.get(hostname);
    140         if (cn != null) {
    141             // if we hit, return the corresponding entry
    142             return entries.get(cn);
    143         }
    144 
    145         // otherwise, get the matching cn
    146         cn = getMatchingCN(hostname);
    147         if (cn != null) {
    148             hostnameCache.put(hostname, cn);
    149             // we have a matching CN, return that entry
    150             return entries.get(cn);
    151         }
    152 
    153         // if we got here, we don't have a matching CN for this hostname
    154         return null;
    155     }
    156 
    157     private boolean cacheIsNotValid() {
    158         return pinFile.lastModified() != lastModified;
    159     }
    160 
    161     private String getMatchingCN(String hostname) {
    162         String bestMatch = "";
    163         for (String cn : entries.keySet()) {
    164             // skip shorter CNs since they can't be better matches
    165             if (cn.length() < bestMatch.length()) {
    166                 continue;
    167             }
    168             // now verify that the CN matches at all
    169             if (verifier.verifyHostName(hostname, cn)) {
    170                 bestMatch = cn;
    171             }
    172         }
    173         return bestMatch;
    174     }
    175 
    176     private static void log(String s, Exception e) {
    177         if (DEBUG) {
    178             System.out.println("PINFILE: " + s);
    179             if (e != null) {
    180                 e.printStackTrace();
    181             }
    182         }
    183     }
    184 }
    185