blob: 4cdd8d2c3bdc24078981fb2e3d4b900ed206fdde [file] [log] [blame]
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.harmony.xnet.provider.jsse;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.net.ssl.DefaultHostnameVerifier;
import libcore.io.IoUtils;
import libcore.util.BasicLruCache;
/**
* This class provides a simple interface for cert pinning.
*/
public class CertPinManager {
private long lastModified;
private final Map<String, PinListEntry> entries = new HashMap<String, PinListEntry>();
private final BasicLruCache<String, String> hostnameCache = new BasicLruCache<String, String>(10);
private final DefaultHostnameVerifier verifier = new DefaultHostnameVerifier();
private boolean initialized = false;
private static final boolean DEBUG = false;
private final File pinFile;
private final TrustedCertificateStore certStore;
public CertPinManager(TrustedCertificateStore store) throws PinManagerException {
pinFile = new File("/data/misc/keychain/pins");
certStore = store;
rebuild();
}
/** Test only */
public CertPinManager(String path, TrustedCertificateStore store) throws PinManagerException {
if (path == null) {
throw new NullPointerException("path == null");
}
pinFile = new File(path);
certStore = store;
rebuild();
}
/**
* This is the public interface for cert pinning.
*
* Given a hostname and a certificate chain this verifies that the chain includes
* certs from the pinned list provided.
*
* If the chain doesn't include those certs and is in enforcing mode, then this method
* returns true and the certificate check should fail.
*/
public boolean chainIsNotPinned(String hostname, List<X509Certificate> chain)
throws PinManagerException {
// lookup the entry
PinListEntry entry = lookup(hostname);
// return its result or false if there's no pin
if (entry != null) {
return entry.chainIsNotPinned(chain);
}
return false;
}
private synchronized void rebuild() throws PinManagerException {
// reread the pin file
String pinFileContents = readPinFile();
if (pinFileContents != null) {
// rebuild the pinned certs
for (String entry : getPinFileEntries(pinFileContents)) {
try {
PinListEntry pin = new PinListEntry(entry, certStore);
entries.put(pin.getCommonName(), pin);
} catch (PinEntryException e) {
log("Pinlist contains a malformed pin: " + entry, e);
}
}
// clear the cache
hostnameCache.evictAll();
// set the last modified time
lastModified = pinFile.lastModified();
// we've been fully initialized and are ready to go
initialized = true;
}
}
private String readPinFile() throws PinManagerException {
try {
return IoUtils.readFileAsString(pinFile.getPath());
} catch (FileNotFoundException e) {
// there's no pin list, all certs are unpinned
return null;
} catch (IOException e) {
// this is unexpected, fail
throw new PinManagerException("Unexpected error reading pin list; failing.", e);
}
}
private static String[] getPinFileEntries(String pinFileContents) {
return pinFileContents.split("\n");
}
private synchronized PinListEntry lookup(String hostname) throws PinManagerException {
// if we don't have any data, don't bother
if (!initialized) {
return null;
}
// check to see if our cache is valid
if (cacheIsNotValid()) {
rebuild();
}
// if so, check the hostname cache
String cn = hostnameCache.get(hostname);
if (cn != null) {
// if we hit, return the corresponding entry
return entries.get(cn);
}
// otherwise, get the matching cn
cn = getMatchingCN(hostname);
if (cn != null) {
hostnameCache.put(hostname, cn);
// we have a matching CN, return that entry
return entries.get(cn);
}
// if we got here, we don't have a matching CN for this hostname
return null;
}
private boolean cacheIsNotValid() {
return pinFile.lastModified() != lastModified;
}
private String getMatchingCN(String hostname) {
String bestMatch = "";
for (String cn : entries.keySet()) {
// skip shorter CNs since they can't be better matches
if (cn.length() < bestMatch.length()) {
continue;
}
// now verify that the CN matches at all
if (verifier.verifyHostName(hostname, cn)) {
bestMatch = cn;
}
}
return bestMatch;
}
private static void log(String s, Exception e) {
if (DEBUG) {
System.out.println("PINFILE: " + s);
if (e != null) {
e.printStackTrace();
}
}
}
}