/*
 * Copyright (c) 2004, 2017, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package sun.security.ssl;

import java.lang.ref.*;
import java.util.*;
import static java.util.Locale.ENGLISH;
import java.util.concurrent.atomic.AtomicLong;
import java.net.Socket;

import java.security.*;
import java.security.KeyStore.*;
import java.security.cert.*;
import java.security.cert.Certificate;

import javax.net.ssl.*;

import sun.security.validator.Validator;

The new X509 key manager implementation. The main differences to the old SunX509 key manager are: . it is based around the KeyStore.Builder API. This allows it to use other forms of KeyStore protection or password input (e.g. a CallbackHandler) or to have keys within one KeyStore protected by different keys. . it can use multiple KeyStores at the same time. . it is explicitly designed to accomodate KeyStores that change over the lifetime of the process. . it makes an effort to choose the key that matches best, i.e. one that is not expired and has the appropriate certificate extensions. Note that this code is not explicitly performance optimzied yet.
Author: Andreas Sterbenz
/** * The new X509 key manager implementation. The main differences to the * old SunX509 key manager are: * . it is based around the KeyStore.Builder API. This allows it to use * other forms of KeyStore protection or password input (e.g. a * CallbackHandler) or to have keys within one KeyStore protected by * different keys. * . it can use multiple KeyStores at the same time. * . it is explicitly designed to accomodate KeyStores that change over * the lifetime of the process. * . it makes an effort to choose the key that matches best, i.e. one that * is not expired and has the appropriate certificate extensions. * * Note that this code is not explicitly performance optimzied yet. * * @author Andreas Sterbenz */
final class X509KeyManagerImpl extends X509ExtendedKeyManager implements X509KeyManager { private static final Debug debug = Debug.getInstance("ssl"); private static final boolean useDebug = (debug != null) && Debug.isOn("keymanager"); // for unit testing only, set via privileged reflection private static Date verificationDate; // list of the builders private final List<Builder> builders; // counter to generate unique ids for the aliases private final AtomicLong uidCounter; // cached entries private final Map<String,Reference<PrivateKeyEntry>> entryCacheMap; X509KeyManagerImpl(Builder builder) { this(Collections.singletonList(builder)); } X509KeyManagerImpl(List<Builder> builders) { this.builders = builders; uidCounter = new AtomicLong(); entryCacheMap = Collections.synchronizedMap (new SizedMap<String,Reference<PrivateKeyEntry>>()); } // LinkedHashMap with a max size of 10 // see LinkedHashMap JavaDocs private static class SizedMap<K,V> extends LinkedHashMap<K,V> { private static final long serialVersionUID = -8211222668790986062L; @Override protected boolean removeEldestEntry(Map.Entry<K,V> eldest) { return size() > 10; } } // // public methods // public X509Certificate[] getCertificateChain(String alias) { PrivateKeyEntry entry = getEntry(alias); return entry == null ? null : (X509Certificate[])entry.getCertificateChain(); } public PrivateKey getPrivateKey(String alias) { PrivateKeyEntry entry = getEntry(alias); return entry == null ? null : entry.getPrivateKey(); } public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) { return chooseAlias(getKeyTypes(keyTypes), issuers, CheckType.CLIENT); } public String chooseEngineClientAlias(String[] keyTypes, Principal[] issuers, SSLEngine engine) { return chooseAlias(getKeyTypes(keyTypes), issuers, CheckType.CLIENT); } public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { return chooseAlias(getKeyTypes(keyType), issuers, CheckType.SERVER); } public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) { return chooseAlias(getKeyTypes(keyType), issuers, CheckType.SERVER); } public String[] getClientAliases(String keyType, Principal[] issuers) { return getAliases(keyType, issuers, CheckType.CLIENT); } public String[] getServerAliases(String keyType, Principal[] issuers) { return getAliases(keyType, issuers, CheckType.SERVER); } // // implementation private methods // // we construct the alias we return to JSSE as seen in the code below // a unique id is included to allow us to reliably cache entries // between the calls to getCertificateChain() and getPrivateKey() // even if tokens are inserted or removed private String makeAlias(EntryStatus entry) { return uidCounter.incrementAndGet() + "." + entry.builderIndex + "." + entry.alias; } private PrivateKeyEntry getEntry(String alias) { // if the alias is null, return immediately if (alias == null) { return null; } // try to get the entry from cache Reference<PrivateKeyEntry> ref = entryCacheMap.get(alias); PrivateKeyEntry entry = (ref != null) ? ref.get() : null; if (entry != null) { return entry; } // parse the alias int firstDot = alias.indexOf('.'); int secondDot = alias.indexOf('.', firstDot + 1); if ((firstDot == -1) || (secondDot == firstDot)) { // invalid alias return null; } try { int builderIndex = Integer.parseInt (alias.substring(firstDot + 1, secondDot)); String keyStoreAlias = alias.substring(secondDot + 1); Builder builder = builders.get(builderIndex); KeyStore ks = builder.getKeyStore(); Entry newEntry = ks.getEntry (keyStoreAlias, builder.getProtectionParameter(alias)); if (newEntry instanceof PrivateKeyEntry == false) { // unexpected type of entry return null; } entry = (PrivateKeyEntry)newEntry; entryCacheMap.put(alias, new SoftReference<PrivateKeyEntry>(entry)); return entry; } catch (Exception e) { // ignore return null; } } // Class to help verify that the public key algorithm (and optionally // the signature algorithm) of a certificate matches what we need. private static class KeyType { final String keyAlgorithm; final String sigKeyAlgorithm; KeyType(String algorithm) { int k = algorithm.indexOf("_"); if (k == -1) { keyAlgorithm = algorithm; sigKeyAlgorithm = null; } else { keyAlgorithm = algorithm.substring(0, k); sigKeyAlgorithm = algorithm.substring(k + 1); } } boolean matches(Certificate[] chain) { if (!chain[0].getPublicKey().getAlgorithm().equals(keyAlgorithm)) { return false; } if (sigKeyAlgorithm == null) { return true; } if (chain.length > 1) { // if possible, check the public key in the issuer cert return sigKeyAlgorithm.equals(chain[1].getPublicKey().getAlgorithm()); } else { // Check the signature algorithm of the certificate itself. // Look for the "withRSA" in "SHA1withRSA", etc. X509Certificate issuer = (X509Certificate)chain[0]; String sigAlgName = issuer.getSigAlgName().toUpperCase(ENGLISH); String pattern = "WITH" + sigKeyAlgorithm.toUpperCase(ENGLISH); return sigAlgName.contains(pattern); } } } private static List<KeyType> getKeyTypes(String ... keyTypes) { if ((keyTypes == null) || (keyTypes.length == 0) || (keyTypes[0] == null)) { return null; } List<KeyType> list = new ArrayList<KeyType>(keyTypes.length); for (String keyType : keyTypes) { list.add(new KeyType(keyType)); } return list; } /* * Return the best alias that fits the given parameters. * The algorithm we use is: * . scan through all the aliases in all builders in order * . as soon as we find a perfect match, return * (i.e. a match with a cert that has appropriate key usage * and is not expired). * . if we do not find a perfect match, keep looping and remember * the imperfect matches * . at the end, sort the imperfect matches. we prefer expired certs * with appropriate key usage to certs with the wrong key usage. * return the first one of them. */ private String chooseAlias(List<KeyType> keyTypeList, Principal[] issuers, CheckType checkType) { if (keyTypeList == null || keyTypeList.isEmpty()) { return null; } Set<Principal> issuerSet = getIssuerSet(issuers); List<EntryStatus> allResults = null; for (int i = 0, n = builders.size(); i < n; i++) { try { List<EntryStatus> results = getAliases(i, keyTypeList, issuerSet, false, checkType); if (results != null) { // the results will either be a single perfect match // or 1 or more imperfect matches // if it's a perfect match, return immediately EntryStatus status = results.get(0); if (status.checkResult == CheckResult.OK) { if (useDebug) { debug.println("KeyMgr: choosing key: " + status); } return makeAlias(status); } if (allResults == null) { allResults = new ArrayList<EntryStatus>(); } allResults.addAll(results); } } catch (Exception e) { // ignore } } if (allResults == null) { if (useDebug) { debug.println("KeyMgr: no matching key found"); } return null; } Collections.sort(allResults); if (useDebug) { debug.println("KeyMgr: no good matching key found, " + "returning best match out of:"); debug.println(allResults.toString()); } return makeAlias(allResults.get(0)); } /* * Return all aliases that (approximately) fit the parameters. * These are perfect matches plus imperfect matches (expired certificates * and certificates with the wrong extensions). * The perfect matches will be first in the array. */ public String[] getAliases(String keyType, Principal[] issuers, CheckType checkType) { if (keyType == null) { return null; } Set<Principal> issuerSet = getIssuerSet(issuers); List<KeyType> keyTypeList = getKeyTypes(keyType); List<EntryStatus> allResults = null; for (int i = 0, n = builders.size(); i < n; i++) { try { List<EntryStatus> results = getAliases(i, keyTypeList, issuerSet, true, checkType); if (results != null) { if (allResults == null) { allResults = new ArrayList<EntryStatus>(); } allResults.addAll(results); } } catch (Exception e) { // ignore } } if (allResults == null || allResults.isEmpty()) { if (useDebug) { debug.println("KeyMgr: no matching alias found"); } return null; } Collections.sort(allResults); if (useDebug) { debug.println("KeyMgr: getting aliases: " + allResults); } return toAliases(allResults); } // turn candidate entries into unique aliases we can return to JSSE private String[] toAliases(List<EntryStatus> results) { String[] s = new String[results.size()]; int i = 0; for (EntryStatus result : results) { s[i++] = makeAlias(result); } return s; } // make a Set out of the array private Set<Principal> getIssuerSet(Principal[] issuers) { if ((issuers != null) && (issuers.length != 0)) { return new HashSet<Principal>(Arrays.asList(issuers)); } else { return null; } } // a candidate match // identifies the entry by builder and alias // and includes the result of the certificate check private static class EntryStatus implements Comparable<EntryStatus> { final int builderIndex; final int keyIndex; final String alias; final CheckResult checkResult; EntryStatus(int builderIndex, int keyIndex, String alias, Certificate[] chain, CheckResult checkResult) { this.builderIndex = builderIndex; this.keyIndex = keyIndex; this.alias = alias; this.checkResult = checkResult; } public int compareTo(EntryStatus other) { int result = this.checkResult.compareTo(other.checkResult); return (result == 0) ? (this.keyIndex - other.keyIndex) : result; } public String toString() { String s = alias + " (verified: " + checkResult + ")"; if (builderIndex == 0) { return s; } else { return "Builder #" + builderIndex + ", alias: " + s; } } } // enum for the type of certificate check we want to perform // (client or server) // also includes the check code itself private static enum CheckType { // enum constant for "no check" (currently not used) NONE(Collections.<String>emptySet()), // enum constant for "tls client" check // valid EKU for TLS client: any, tls_client CLIENT(new HashSet<String>(Arrays.asList(new String[] { "2.5.29.37.0", "1.3.6.1.5.5.7.3.2" }))), // enum constant for "tls server" check // valid EKU for TLS server: any, tls_server, ns_sgc, ms_sgc SERVER(new HashSet<String>(Arrays.asList(new String[] { "2.5.29.37.0", "1.3.6.1.5.5.7.3.1", "2.16.840.1.113730.4.1", "1.3.6.1.4.1.311.10.3.3" }))); // set of valid EKU values for this type final Set<String> validEku; CheckType(Set<String> validEku) { this.validEku = validEku; } private static boolean getBit(boolean[] keyUsage, int bit) { return (bit < keyUsage.length) && keyUsage[bit]; } // check if this certificate is appropriate for this type of use // first check extensions, if they match, check expiration // note: we may want to move this code into the sun.security.validator // package CheckResult check(X509Certificate cert, Date date) { if (this == NONE) { return CheckResult.OK; } // check extensions try { // check extended key usage List<String> certEku = cert.getExtendedKeyUsage(); if ((certEku != null) && Collections.disjoint(validEku, certEku)) { // if extension present and it does not contain any of // the valid EKU OIDs, return extension_mismatch return CheckResult.EXTENSION_MISMATCH; } // check key usage boolean[] ku = cert.getKeyUsage(); if (ku != null) { String algorithm = cert.getPublicKey().getAlgorithm(); boolean kuSignature = getBit(ku, 0); if ("RSA".equals(algorithm)) { // require either signature bit // or if server also allow key encipherment bit if (kuSignature == false) { if ((this == CLIENT) || (getBit(ku, 2) == false)) { return CheckResult.EXTENSION_MISMATCH; } } } else if ("DSA".equals(algorithm)) { // require signature bit if (kuSignature == false) { return CheckResult.EXTENSION_MISMATCH; } } else if ("DH".equals(algorithm)) { // require keyagreement bit if (getBit(ku, 4) == false) { return CheckResult.EXTENSION_MISMATCH; } } else if ("EC".equals(algorithm)) { // require signature bit if (kuSignature == false) { return CheckResult.EXTENSION_MISMATCH; } // For servers, also require key agreement. // This is not totally accurate as the keyAgreement bit // is only necessary for static ECDH key exchange and // not ephemeral ECDH. We leave it in for now until // there are signs that this check causes problems // for real world EC certificates. if ((this == SERVER) && (getBit(ku, 4) == false)) { return CheckResult.EXTENSION_MISMATCH; } } } } catch (CertificateException e) { // extensions unparseable, return failure return CheckResult.EXTENSION_MISMATCH; } try { cert.checkValidity(date); return CheckResult.OK; } catch (CertificateException e) { return CheckResult.EXPIRED; } } public String getValidator() { if (this == CLIENT) { return Validator.VAR_TLS_CLIENT; } else if (this == SERVER) { return Validator.VAR_TLS_SERVER; } return Validator.VAR_GENERIC; } } // enum for the result of the extension check // NOTE: the order of the constants is important as they are used // for sorting, i.e. OK is best, followed by EXPIRED and EXTENSION_MISMATCH private static enum CheckResult { OK, // ok or not checked EXPIRED, // extensions valid but cert expired EXTENSION_MISMATCH, // extensions invalid (expiration not checked) } /* * Return a List of all candidate matches in the specified builder * that fit the parameters. * We exclude entries in the KeyStore if they are not: * . private key entries * . the certificates are not X509 certificates * . the algorithm of the key in the EE cert doesn't match one of keyTypes * . none of the certs is issued by a Principal in issuerSet * Using those entries would not be possible or they would almost * certainly be rejected by the peer. * * In addition to those checks, we also check the extensions in the EE * cert and its expiration. Even if there is a mismatch, we include * such certificates because they technically work and might be accepted * by the peer. This leads to more graceful failure and better error * messages if the cert expires from one day to the next. * * The return values are: * . null, if there are no matching entries at all * . if 'findAll' is 'false' and there is a perfect match, a List * with a single element (early return) * . if 'findAll' is 'false' and there is NO perfect match, a List * with all the imperfect matches (expired, wrong extensions) * . if 'findAll' is 'true', a List with all perfect and imperfect * matches */ private List<EntryStatus> getAliases(int builderIndex, List<KeyType> keyTypes, Set<Principal> issuerSet, boolean findAll, CheckType checkType) throws Exception { Builder builder = builders.get(builderIndex); KeyStore ks = builder.getKeyStore(); List<EntryStatus> results = null; Date date = verificationDate; boolean preferred = false; for (Enumeration<String> e = ks.aliases(); e.hasMoreElements(); ) { String alias = e.nextElement(); // check if it is a key entry (private key or secret key) if (ks.isKeyEntry(alias) == false) { continue; } Certificate[] chain = ks.getCertificateChain(alias); if ((chain == null) || (chain.length == 0)) { // must be secret key entry, ignore continue; } // check keytype int keyIndex = -1; int j = 0; for (KeyType keyType : keyTypes) { if (keyType.matches(chain)) { keyIndex = j; break; } j++; } if (keyIndex == -1) { if (useDebug) { debug.println("Ignoring alias " + alias + ": key algorithm does not match"); } continue; } // check issuers if (issuerSet != null) { boolean found = false; for (Certificate cert : chain) { if (cert instanceof X509Certificate == false) { // not an X509Certificate, ignore this entry break; } X509Certificate xcert = (X509Certificate)cert; if (issuerSet.contains(xcert.getIssuerX500Principal())) { found = true; break; } } if (found == false) { if (useDebug) { debug.println("Ignoring alias " + alias + ": issuers do not match"); } continue; } } if (date == null) { date = new Date(); } CheckResult checkResult = checkType.check((X509Certificate)chain[0], date); EntryStatus status = new EntryStatus(builderIndex, keyIndex, alias, chain, checkResult); if (!preferred && checkResult == CheckResult.OK && keyIndex == 0) { preferred = true; } if (preferred && (findAll == false)) { // if we have a good match and do not need all matches, // return immediately return Collections.singletonList(status); } else { if (results == null) { results = new ArrayList<EntryStatus>(); } results.add(status); } } return results; } }