    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *   http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 *
    017 */
    018
    019package org.apache.commons.net.util;
    020
    021import java.io.File;
    022import java.io.FileInputStream;
    023import java.io.IOException;
    024import java.net.Socket;
    025import java.security.GeneralSecurityException;
    026import java.security.KeyStore;
    027import java.security.KeyStoreException;
    028import java.security.Principal;
    029import java.security.PrivateKey;
    030import java.security.cert.Certificate;
    031import java.security.cert.X509Certificate;
    032import java.util.Enumeration;
    033
    034import javax.net.ssl.KeyManager;
    035import javax.net.ssl.X509ExtendedKeyManager;
    036
    037import org.apache.commons.net.io.Util;
    038
    039/**
    040 * General KeyManager utilities
    041 * <p>
    042 * How to use with a client certificate:
    043 * <pre>
    044 * KeyManager km = KeyManagerUtils.createClientKeyManager("JKS",
    045 *     "/path/to/privatekeystore.jks","storepassword",
    046 *     "privatekeyalias", "keypassword");
    047 * FTPSClient cl = new FTPSClient();
    048 * cl.setKeyManager(km);
    049 * cl.connect(...);
    050 * </pre>
    051 * If using the default store type and the key password is the same as the
    052 * store password, these parameters can be omitted. <br>
    053 * If the desired key is the first or only key in the keystore, the keyAlias parameter
    054 * can be omitted, in which case the code becomes:
    055 * <pre>
    056 * KeyManager km = KeyManagerUtils.createClientKeyManager(
    057 *     "/path/to/privatekeystore.jks","storepassword");
    058 * FTPSClient cl = new FTPSClient();
    059 * cl.setKeyManager(km);
    060 * cl.connect(...);
    061 * </pre>
    062 *
    063 * @since 3.0
    064 */
    065public final class KeyManagerUtils {
    066
    067    private static final String DEFAULT_STORE_TYPE = KeyStore.getDefaultType();
    068
    069    private KeyManagerUtils(){
    070        // Not instantiable
    071    }
    072
    073    /**
    074     * Create a client key manager which returns a particular key.
    075     * Does not handle server keys.
    076     *
    077     * @param ks the keystore to use
    078     * @param keyAlias the alias of the key to use, may be {@code null} in which case the first key entry alias is used
    079     * @param keyPass the password of the key to use
    080     * @return the customised KeyManager
    081     * @throws GeneralSecurityException if there is a problem creating the keystore
    082     */
    083    public static KeyManager createClientKeyManager(KeyStore ks, String keyAlias, String keyPass)
    084        throws GeneralSecurityException
    085    {
    086        ClientKeyStore cks = new ClientKeyStore(ks, keyAlias != null ? keyAlias : findAlias(ks), keyPass);
    087        return new X509KeyManager(cks);
    088    }
    089
    090    /**
    091     * Create a client key manager which returns a particular key.
    092     * Does not handle server keys.
    093     *
    094     * @param storeType the type of the keyStore, e.g. "JKS"
    095     * @param storePath the path to the keyStore
    096     * @param storePass the keyStore password
    097     * @param keyAlias the alias of the key to use, may be {@code null} in which case the first key entry alias is used
    098     * @param keyPass the password of the key to use
    099     * @return the customised KeyManager
    100     * @throws GeneralSecurityException if there is a problem creating the keystore
    101     * @throws IOException if there is a problem creating the keystore
    102     */
    103    public static KeyManager createClientKeyManager(
    104            String storeType, File storePath, String storePass, String keyAlias, String keyPass)
    105        throws IOException, GeneralSecurityException
    106    {
    107        KeyStore ks = loadStore(storeType, storePath, storePass);
    108        return createClientKeyManager(ks, keyAlias, keyPass);
    109    }
    110
    111    /**
    112     * Create a client key manager which returns a particular key.
    113     * Does not handle server keys.
    114     * Uses the default store type and assumes the key password is the same as the store password
    115     *
    116     * @param storePath the path to the keyStore
    117     * @param storePass the keyStore password
    118     * @param keyAlias the alias of the key to use, may be {@code null} in which case the first key entry alias is used
    119     * @return the customised KeyManager
    120     * @throws IOException if there is a problem creating the keystore
    121     * @throws GeneralSecurityException if there is a problem creating the keystore
    122     */
    123    public static KeyManager createClientKeyManager(File storePath, String storePass, String keyAlias)
    124        throws IOException, GeneralSecurityException
    125    {
    126        return createClientKeyManager(DEFAULT_STORE_TYPE, storePath, storePass, keyAlias, storePass);
    127    }
    128
    129    /**
    130     * Create a client key manager which returns a particular key.
    131     * Does not handle server keys.
    132     * Uses the default store type and assumes the key password is the same as the store password.
    133     * The key alias is found by searching the keystore for the first private key entry
    134     *
    135     * @param storePath the path to the keyStore
    136     * @param storePass the keyStore password
    137     * @return the customised KeyManager
    138     * @throws IOException if there is a problem creating the keystore
    139     * @throws GeneralSecurityException if there is a problem creating the keystore
    140     */
    141    public static KeyManager createClientKeyManager(File storePath, String storePass)
    142        throws IOException, GeneralSecurityException
    143    {
    144        return createClientKeyManager(DEFAULT_STORE_TYPE, storePath, storePass, null, storePass);
    145    }
    146
    147    private static KeyStore loadStore(String storeType, File storePath, String storePass)
    148        throws KeyStoreException,  IOException, GeneralSecurityException {
    149        KeyStore ks = KeyStore.getInstance(storeType);
    150        FileInputStream stream = null;
    151        try {
    152            stream = new FileInputStream(storePath);
    153            ks.load(stream, storePass.toCharArray());
    154        } finally {
    155            Util.closeQuietly(stream);
    156        }
    157        return ks;
    158    }
    159
    160    private static String findAlias(KeyStore ks) throws KeyStoreException {
    161        Enumeration<String> e = ks.aliases();
    162        while(e.hasMoreElements()) {
    163            String entry = e.nextElement();
    164            if (ks.isKeyEntry(entry)) {
    165                return entry;
    166            }
    167        }
    168        throw new KeyStoreException("Cannot find a private key entry");
    169    }
    170
    171    private static class ClientKeyStore {
    172
    173        private final X509Certificate[] certChain;
    174        private final PrivateKey key;
    175        private final String keyAlias;
    176
    177        ClientKeyStore(KeyStore ks, String keyAlias, String keyPass) throws GeneralSecurityException
    178        {
    179            this.keyAlias = keyAlias;
    180            this.key = (PrivateKey) ks.getKey(this.keyAlias, keyPass.toCharArray());
    181            Certificate[] certs = ks.getCertificateChain(this.keyAlias);
    182            X509Certificate[] X509certs = new X509Certificate[certs.length];
    183            for (int i=0; i < certs.length; i++) {
    184                X509certs[i] = (X509Certificate) certs[i];
    185            }
    186            this.certChain = X509certs;
    187        }
    188
    189        final X509Certificate[] getCertificateChain() {
    190            return this.certChain;
    191        }
    192
    193        final PrivateKey getPrivateKey() {
    194            return this.key;
    195        }
    196
    197        final String getAlias() {
    198            return this.keyAlias;
    199        }
    200    }
    201
    202    private static class X509KeyManager extends X509ExtendedKeyManager  {
    203
    204        private final ClientKeyStore keyStore;
    205
    206        X509KeyManager(final ClientKeyStore keyStore) {
    207            this.keyStore = keyStore;
    208        }
    209
    210        // Call sequence: 1
    211        @Override
    212        public String chooseClientAlias(String[] keyType, Principal[] issuers,
    213                Socket socket) {
    214            return keyStore.getAlias();
    215        }
    216
    217        // Call sequence: 2
    218        @Override
    219        public X509Certificate[] getCertificateChain(String alias) {
    220            return keyStore.getCertificateChain();
    221        }
    222
    223        @Override
    224        public String[] getClientAliases(String keyType, Principal[] issuers) {
    225            return new String[]{ keyStore.getAlias()};
    226        }
    227
    228        // Call sequence: 3
    229        @Override
    230        public PrivateKey getPrivateKey(String alias) {
    231            return keyStore.getPrivateKey();
    232        }
    233
    234        @Override
    235        public String[] getServerAliases(String keyType, Principal[] issuers) {
    236            return null;
    237        }
    238
    239        @Override
    240        public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
    241            return null;
    242        }
    243
    244    }
    245
    246}