View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *   https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.net.util;
19  
20  import java.io.File;
21  import java.io.FileInputStream;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.net.Socket;
25  import java.security.GeneralSecurityException;
26  import java.security.KeyStore;
27  import java.security.KeyStoreException;
28  import java.security.Principal;
29  import java.security.PrivateKey;
30  import java.security.cert.Certificate;
31  import java.security.cert.X509Certificate;
32  import java.util.Arrays;
33  import java.util.Enumeration;
34  
35  import javax.net.ssl.KeyManager;
36  import javax.net.ssl.X509ExtendedKeyManager;
37  
38  /**
39   * General KeyManager utilities
40   * <p>
41   * How to use with a client certificate:
42   *
43   * <pre>
44   * KeyManager km = KeyManagerUtils.createClientKeyManager("JKS",
45   *     "/path/to/privatekeystore.jks","storepassword",
46   *     "privatekeyalias", "keypassword");
47   * FTPSClient cl = new FTPSClient();
48   * cl.setKeyManager(km);
49   * cl.connect(...);
50   * </pre>
51   * <p>
52   * If using the default store type and the key password is the same as the store password, these parameters can be omitted.
53   * </p>
54   * <p>
55   * 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:
56   * </p>
57   *
58   * <pre>
59   * KeyManager km = KeyManagerUtils.createClientKeyManager(
60   *     "/path/to/privatekeystore.jks","storepassword");
61   * FTPSClient cl = new FTPSClient();
62   * cl.setKeyManager(km);
63   * cl.connect(...);
64   * </pre>
65   *
66   * @since 3.0
67   */
68  public final class KeyManagerUtils {
69  
70      private static final class ClientKeyStore {
71  
72          private final X509Certificate[] certChain;
73          private final PrivateKey key;
74          private final String keyAlias;
75  
76          ClientKeyStore(final KeyStore ks, final String keyAlias, final String keyPass) throws GeneralSecurityException {
77              this.keyAlias = keyAlias;
78              this.key = (PrivateKey) ks.getKey(this.keyAlias, keyPass.toCharArray());
79              final Certificate[] certs = ks.getCertificateChain(this.keyAlias);
80              final X509Certificate[] x509certs = new X509Certificate[certs.length];
81              Arrays.setAll(x509certs, i -> (X509Certificate) certs[i]);
82              this.certChain = x509certs;
83          }
84  
85          String getAlias() {
86              return keyAlias;
87          }
88  
89          X509Certificate[] getCertificateChain() {
90              return certChain;
91          }
92  
93          PrivateKey getPrivateKey() {
94              return key;
95          }
96      }
97  
98      private static final class X509KeyManager extends X509ExtendedKeyManager {
99  
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 }