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 *   https://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
018package org.apache.commons.net.util;
019
020import java.io.File;
021import java.io.FileInputStream;
022import java.io.IOException;
023import java.io.InputStream;
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.Arrays;
033import java.util.Enumeration;
034
035import javax.net.ssl.KeyManager;
036import javax.net.ssl.X509ExtendedKeyManager;
037
038/**
039 * General KeyManager utilities
040 * <p>
041 * How to use with a client certificate:
042 *
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 * <p>
052 * If using the default store type and the key password is the same as the store password, these parameters can be omitted.
053 * </p>
054 * <p>
055 * If the desired key is the first or only key in the keystore, the keyAlias parameter can be omitted, in which case the code becomes:
056 * </p>
057 *
058 * <pre>
059 * KeyManager km = KeyManagerUtils.createClientKeyManager(
060 *     "/path/to/privatekeystore.jks","storepassword");
061 * FTPSClient cl = new FTPSClient();
062 * cl.setKeyManager(km);
063 * cl.connect(...);
064 * </pre>
065 *
066 * @since 3.0
067 */
068public final class KeyManagerUtils {
069
070    private static final class ClientKeyStore {
071
072        private final X509Certificate[] certChain;
073        private final PrivateKey key;
074        private final String keyAlias;
075
076        ClientKeyStore(final KeyStore ks, final String keyAlias, final String keyPass) throws GeneralSecurityException {
077            this.keyAlias = keyAlias;
078            this.key = (PrivateKey) ks.getKey(this.keyAlias, keyPass.toCharArray());
079            final Certificate[] certs = ks.getCertificateChain(this.keyAlias);
080            final X509Certificate[] x509certs = new X509Certificate[certs.length];
081            Arrays.setAll(x509certs, i -> (X509Certificate) certs[i]);
082            this.certChain = x509certs;
083        }
084
085        String getAlias() {
086            return keyAlias;
087        }
088
089        X509Certificate[] getCertificateChain() {
090            return certChain;
091        }
092
093        PrivateKey getPrivateKey() {
094            return key;
095        }
096    }
097
098    private static final class X509KeyManager extends X509ExtendedKeyManager {
099
100        private final ClientKeyStore keyStore;
101
102        X509KeyManager(final ClientKeyStore keyStore) {
103            this.keyStore = keyStore;
104        }
105
106        // Call sequence: 1
107        @Override
108        public String chooseClientAlias(final String[] keyType, final Principal[] issuers, final Socket socket) {
109            return keyStore.getAlias();
110        }
111
112        @Override
113        public String chooseServerAlias(final String keyType, final Principal[] issuers, final Socket socket) {
114            return null;
115        }
116
117        // Call sequence: 2
118        @Override
119        public X509Certificate[] getCertificateChain(final String alias) {
120            return keyStore.getCertificateChain();
121        }
122
123        @Override
124        public String[] getClientAliases(final String keyType, final Principal[] issuers) {
125            return new String[] { keyStore.getAlias()};
126        }
127
128        // Call sequence: 3
129        @Override
130        public PrivateKey getPrivateKey(final String alias) {
131            return keyStore.getPrivateKey();
132        }
133
134        @Override
135        public String[] getServerAliases(final String keyType, final Principal[] issuers) {
136            return null;
137        }
138
139    }
140
141    private static final String DEFAULT_STORE_TYPE = KeyStore.getDefaultType();
142
143    /**
144     * Create a client key manager which returns a particular key. Does not handle server keys. Uses the default store type and assumes the key password is the
145     * same as the store password. The key alias is found by searching the keystore for the first private key entry
146     *
147     * @param storePath the path to the keyStore
148     * @param storePass the keyStore password
149     * @return the customized KeyManager
150     * @throws IOException              if there is a problem creating the keystore
151     * @throws GeneralSecurityException if there is a problem creating the keystore
152     */
153    public static KeyManager createClientKeyManager(final File storePath, final String storePass) throws IOException, GeneralSecurityException {
154        return createClientKeyManager(DEFAULT_STORE_TYPE, storePath, storePass, null, storePass);
155    }
156
157    /**
158     * Create a client key manager which returns a particular key. Does not handle server keys. Uses the default store type and assumes the key password is the
159     * same as the store password
160     *
161     * @param storePath the path to the keyStore
162     * @param storePass the keyStore password
163     * @param keyAlias  the alias of the key to use, may be {@code null} in which case the first key entry alias is used
164     * @return the customized KeyManager
165     * @throws IOException              if there is a problem creating the keystore
166     * @throws GeneralSecurityException if there is a problem creating the keystore
167     */
168    public static KeyManager createClientKeyManager(final File storePath, final String storePass, final String keyAlias)
169            throws IOException, GeneralSecurityException {
170        return createClientKeyManager(DEFAULT_STORE_TYPE, storePath, storePass, keyAlias, storePass);
171    }
172
173    /**
174     * Create a client key manager which returns a particular key. Does not handle server keys.
175     *
176     * @param ks       the keystore to use
177     * @param keyAlias the alias of the key to use, may be {@code null} in which case the first key entry alias is used
178     * @param keyPass  the password of the key to use
179     * @return the customized KeyManager
180     * @throws GeneralSecurityException if there is a problem creating the keystore
181     */
182    public static KeyManager createClientKeyManager(final KeyStore ks, final String keyAlias, final String keyPass) throws GeneralSecurityException {
183        final ClientKeyStore cks = new ClientKeyStore(ks, keyAlias != null ? keyAlias : findAlias(ks), keyPass);
184        return new X509KeyManager(cks);
185    }
186
187    /**
188     * Create a client key manager which returns a particular key. Does not handle server keys.
189     *
190     * @param storeType the type of the keyStore, e.g. "JKS"
191     * @param storePath the path to the keyStore
192     * @param storePass the keyStore password
193     * @param keyAlias  the alias of the key to use, may be {@code null} in which case the first key entry alias is used
194     * @param keyPass   the password of the key to use
195     * @return the customized KeyManager
196     * @throws GeneralSecurityException if there is a problem creating the keystore
197     * @throws IOException              if there is a problem creating the keystore
198     */
199    public static KeyManager createClientKeyManager(final String storeType, final File storePath, final String storePass, final String keyAlias,
200            final String keyPass) throws IOException, GeneralSecurityException {
201        return createClientKeyManager(loadStore(storeType, storePath, storePass), keyAlias, keyPass);
202    }
203
204    private static String findAlias(final KeyStore ks) throws KeyStoreException {
205        final Enumeration<String> e = ks.aliases();
206        while (e.hasMoreElements()) {
207            final String entry = e.nextElement();
208            if (ks.isKeyEntry(entry)) {
209                return entry;
210            }
211        }
212        throw new KeyStoreException("Cannot find a private key entry");
213    }
214
215    private static KeyStore loadStore(final String storeType, final File storePath, final String storePass) throws IOException, GeneralSecurityException {
216        final KeyStore ks = KeyStore.getInstance(storeType);
217        try (InputStream stream = new FileInputStream(storePath)) {
218            ks.load(stream, storePass.toCharArray());
219        }
220        return ks;
221    }
222
223    private KeyManagerUtils() {
224        // Not instantiable
225    }
226
227}