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