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