summaryrefslogtreecommitdiffstats
path: root/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/CertPinManager.java
blob: 4cdd8d2c3bdc24078981fb2e3d4b900ed206fdde (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
/*
 * 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();
            }
        }
    }
}