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.conscrypt;
18
19import java.math.BigInteger;
20import java.security.MessageDigest;
21import java.security.NoSuchAlgorithmException;
22import java.security.cert.X509Certificate;
23import java.util.Arrays;
24import java.util.Collections;
25import java.util.HashSet;
26import java.util.List;
27import java.util.Set;
28
29/**
30 * This class represents a single entry in the pin file.
31 */
32// public for testing by CertPinManagerTest
33public class PinListEntry {
34
35    /** The Common Name (CN) as used on the SSL certificate */
36    private final String cn;
37
38    /**
39     * Determines whether a failed match here will prevent the chain from being accepted. If true,
40     *  an unpinned chain will log and cause a match failure. If false, it will merely log.
41     */
42    private final boolean enforcing;
43
44    private final Set<String> pinnedFingerprints = new HashSet<String>();
45
46    private final TrustedCertificateStore certStore;
47
48    public String getCommonName() {
49        return cn;
50    }
51
52    public boolean getEnforcing() {
53        return enforcing;
54    }
55
56    public PinListEntry(String entry, TrustedCertificateStore store) throws PinEntryException {
57        if (entry == null) {
58            throw new NullPointerException("entry == null");
59        }
60        certStore = store;
61        // Examples:
62        // *.google.com=true|34c8a0d...9e04ca05f,9e04ca05f...34c8a0d
63        // *.android.com=true|ca05f...8a0d34c
64        // clients.google.com=false|9e04ca05f...34c8a0d,34c8a0d...9e04ca05f
65        String[] values = entry.split("[=,|]");
66        // entry must have a CN, an enforcement value, and at least one pin
67        if (values.length < 3) {
68            throw new PinEntryException("Received malformed pin entry");
69        }
70        // get the cn
71        cn = values[0];        // is there more validation we can do here?
72        enforcing = enforcementValueFromString(values[1]);
73        // the remainder should be pins
74        addPins(Arrays.copyOfRange(values, 2, values.length));
75    }
76
77    private static boolean enforcementValueFromString(String val) throws PinEntryException {
78        if (val.equals("true")) {
79            return true;
80        } else if (val.equals("false")) {
81            return false;
82        } else {
83            throw new PinEntryException("Enforcement status is not a valid value");
84        }
85    }
86
87    /**
88     * Checks the given chain against the pin list corresponding to this entry.
89     *
90     * <p>If enforcing is on and the given {@code chain} does not include the
91     * expected pinned certificate, this will return {@code false} indicating
92     * the chain is not valid unless the {@code chain} chains up to an user-installed
93     * CA cert. Otherwise this will return {@code true} indicating the {@code chain}
94     * is valid.
95     */
96    public boolean isChainValid(List<X509Certificate> chain) {
97        boolean containsUserCert = chainContainsUserCert(chain);
98        if (!containsUserCert) {
99            for (X509Certificate cert : chain) {
100                String fingerprint = getFingerprint(cert);
101                if (pinnedFingerprints.contains(fingerprint)) {
102                    return true;
103                }
104            }
105        }
106        logPinFailure(chain, containsUserCert);
107        return !enforcing || containsUserCert;
108    }
109
110    private static String getFingerprint(X509Certificate cert) {
111        try {
112            MessageDigest dgst = MessageDigest.getInstance("SHA512");
113            byte[] encoded = cert.getPublicKey().getEncoded();
114            byte[] fingerprint = dgst.digest(encoded);
115            return IntegralToString.bytesToHexString(fingerprint, false);
116        } catch (NoSuchAlgorithmException e) {
117            throw new AssertionError(e);
118        }
119    }
120
121    private void addPins(String[] pins) {
122        for (String pin : pins) {
123            validatePin(pin);
124        }
125        Collections.addAll(pinnedFingerprints, pins);
126    }
127
128    private static void validatePin(String pin) {
129        // check to make sure the length is correct
130        if (pin.length() != 128) {
131            throw new IllegalArgumentException("Pin is not a valid length");
132        }
133        // check to make sure that it's a valid hex string
134        try {
135            new BigInteger(pin, 16);
136        } catch (NumberFormatException e) {
137            throw new IllegalArgumentException("Pin is not a valid hex string", e);
138        }
139    }
140
141    private boolean chainContainsUserCert(List<X509Certificate> chain) {
142        if (certStore == null) {
143            return false;
144        }
145        for (X509Certificate cert : chain) {
146            if (certStore.isUserAddedCertificate(cert)) {
147                return true;
148            }
149        }
150        return false;
151    }
152
153    private void logPinFailure(List<X509Certificate> chain, boolean containsUserCert) {
154        PinFailureLogger.log(cn, containsUserCert, enforcing, chain);
155    }
156}
157
158