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