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 }