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 }