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