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