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