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    *   http://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.net.Socket;
24  import java.security.GeneralSecurityException;
25  import java.security.KeyStore;
26  import java.security.KeyStoreException;
27  import java.security.Principal;
28  import java.security.PrivateKey;
29  import java.security.cert.Certificate;
30  import java.security.cert.X509Certificate;
31  import java.util.Arrays;
32  import java.util.Enumeration;
33  
34  import javax.net.ssl.KeyManager;
35  import javax.net.ssl.X509ExtendedKeyManager;
36  
37  import org.apache.commons.net.io.Util;
38  
39  /**
40   * General KeyManager utilities
41   * <p>
42   * How to use with a client certificate:
43   *
44   * <pre>
45   * KeyManager km = KeyManagerUtils.createClientKeyManager("JKS",
46   *     "/path/to/privatekeystore.jks","storepassword",
47   *     "privatekeyalias", "keypassword");
48   * FTPSClient cl = new FTPSClient();
49   * cl.setKeyManager(km);
50   * cl.connect(...);
51   * </pre>
52   *
53   * If using the default store type and the key password is the same as the store password, these parameters can be omitted. <br>
54   * 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:
55   *
56   * <pre>
57   * KeyManager km = KeyManagerUtils.createClientKeyManager(
58   *     "/path/to/privatekeystore.jks","storepassword");
59   * FTPSClient cl = new FTPSClient();
60   * cl.setKeyManager(km);
61   * cl.connect(...);
62   * </pre>
63   *
64   * @since 3.0
65   */
66  public final class KeyManagerUtils {
67  
68      private static class ClientKeyStore {
69  
70          private final X509Certificate[] certChain;
71          private final PrivateKey key;
72          private final String keyAlias;
73  
74          ClientKeyStore(final KeyStore ks, final String keyAlias, final String keyPass) throws GeneralSecurityException {
75              this.keyAlias = keyAlias;
76              this.key = (PrivateKey) ks.getKey(this.keyAlias, keyPass.toCharArray());
77              final Certificate[] certs = ks.getCertificateChain(this.keyAlias);
78              final X509Certificate[] x509certs = new X509Certificate[certs.length];
79              Arrays.setAll(x509certs, i -> (X509Certificate) certs[i]);
80              this.certChain = x509certs;
81          }
82  
83          final String getAlias() {
84              return this.keyAlias;
85          }
86  
87          final X509Certificate[] getCertificateChain() {
88              return this.certChain;
89          }
90  
91          final PrivateKey getPrivateKey() {
92              return this.key;
93          }
94      }
95  
96      private static class X509KeyManager extends X509ExtendedKeyManager {
97  
98          private final ClientKeyStore keyStore;
99  
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 }