View Javadoc
1   /*
2    * Copyright 2018 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty5.handler.ssl;
17  
18  import io.netty.internal.tcnative.SSL;
19  import io.netty5.buffer.api.BufferAllocator;
20  import io.netty5.buffer.api.DefaultBufferAllocators;
21  import io.netty5.util.Resource;
22  
23  import javax.net.ssl.KeyManager;
24  import javax.net.ssl.KeyManagerFactory;
25  import javax.net.ssl.KeyManagerFactorySpi;
26  import javax.net.ssl.ManagerFactoryParameters;
27  import javax.net.ssl.X509KeyManager;
28  import java.io.File;
29  import java.io.IOException;
30  import java.io.InputStream;
31  import java.io.OutputStream;
32  import java.security.InvalidAlgorithmParameterException;
33  import java.security.Key;
34  import java.security.KeyStore;
35  import java.security.KeyStoreException;
36  import java.security.KeyStoreSpi;
37  import java.security.NoSuchAlgorithmException;
38  import java.security.PrivateKey;
39  import java.security.Provider;
40  import java.security.UnrecoverableKeyException;
41  import java.security.cert.Certificate;
42  import java.security.cert.CertificateException;
43  import java.security.cert.X509Certificate;
44  import java.util.Collections;
45  import java.util.Date;
46  import java.util.Enumeration;
47  import java.util.HashMap;
48  import java.util.Map;
49  
50  import static io.netty5.util.internal.ObjectUtil.checkNonEmpty;
51  import static java.util.Objects.requireNonNull;
52  
53  /**
54   * Special {@link KeyManagerFactory} that pre-compute the keymaterial used when {@link SslProvider#OPENSSL} or
55   * {@link SslProvider#OPENSSL_REFCNT} is used and so will improve handshake times and its performance.
56   *
57   *
58   *
59   * Because the keymaterial is pre-computed any modification to the {@link KeyStore} is ignored after
60   * {@link #init(KeyStore, char[])} is called.
61   *
62   * {@link #init(ManagerFactoryParameters)} is not supported by this implementation and so a call to it will always
63   * result in an {@link InvalidAlgorithmParameterException}.
64   */
65  public final class OpenSslX509KeyManagerFactory extends KeyManagerFactory {
66  
67      private final OpenSslKeyManagerFactorySpi spi;
68  
69      public OpenSslX509KeyManagerFactory() {
70          this(newOpenSslKeyManagerFactorySpi(null));
71      }
72  
73      public OpenSslX509KeyManagerFactory(Provider provider) {
74          this(newOpenSslKeyManagerFactorySpi(provider));
75      }
76  
77      public OpenSslX509KeyManagerFactory(String algorithm, Provider provider) throws NoSuchAlgorithmException {
78          this(newOpenSslKeyManagerFactorySpi(algorithm, provider));
79      }
80  
81      private OpenSslX509KeyManagerFactory(OpenSslKeyManagerFactorySpi spi) {
82          super(spi, spi.kmf.getProvider(), spi.kmf.getAlgorithm());
83          this.spi = spi;
84      }
85  
86      private static OpenSslKeyManagerFactorySpi newOpenSslKeyManagerFactorySpi(Provider provider) {
87          try {
88              return newOpenSslKeyManagerFactorySpi(null, provider);
89          } catch (NoSuchAlgorithmException e) {
90              // This should never happen as we use the default algorithm.
91              throw new IllegalStateException(e);
92          }
93      }
94  
95      private static OpenSslKeyManagerFactorySpi newOpenSslKeyManagerFactorySpi(String algorithm, Provider provider)
96              throws NoSuchAlgorithmException {
97          if (algorithm == null) {
98              algorithm = KeyManagerFactory.getDefaultAlgorithm();
99          }
100         return new OpenSslKeyManagerFactorySpi(
101                 provider == null ? KeyManagerFactory.getInstance(algorithm) :
102                         KeyManagerFactory.getInstance(algorithm, provider));
103     }
104 
105     OpenSslKeyMaterialProvider newProvider() {
106         return spi.newProvider();
107     }
108 
109     private static final class OpenSslKeyManagerFactorySpi extends KeyManagerFactorySpi {
110         final KeyManagerFactory kmf;
111         private volatile ProviderFactory providerFactory;
112 
113         OpenSslKeyManagerFactorySpi(KeyManagerFactory kmf) {
114             this.kmf = requireNonNull(kmf, "kmf");
115         }
116 
117         @Override
118         protected synchronized void engineInit(KeyStore keyStore, char[] chars)
119                 throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
120             if (providerFactory != null) {
121                 throw new KeyStoreException("Already initialized");
122             }
123             if (!keyStore.aliases().hasMoreElements()) {
124                 throw new KeyStoreException("No aliases found");
125             }
126 
127             kmf.init(keyStore, chars);
128             providerFactory = new ProviderFactory(ReferenceCountedOpenSslContext.chooseX509KeyManager(
129                     kmf.getKeyManagers()), password(chars), Collections.list(keyStore.aliases()));
130         }
131 
132         private static String password(char[] password) {
133             if (password == null || password.length == 0) {
134                 return null;
135             }
136             return new String(password);
137         }
138 
139         @Override
140         protected void engineInit(ManagerFactoryParameters managerFactoryParameters)
141                 throws InvalidAlgorithmParameterException {
142             throw new InvalidAlgorithmParameterException("Not supported");
143         }
144 
145         @Override
146         protected KeyManager[] engineGetKeyManagers() {
147             ProviderFactory providerFactory = this.providerFactory;
148             if (providerFactory == null) {
149                 throw new IllegalStateException("engineInit(...) not called yet");
150             }
151             return new KeyManager[] { providerFactory.keyManager };
152         }
153 
154         OpenSslKeyMaterialProvider newProvider() {
155             ProviderFactory providerFactory = this.providerFactory;
156             if (providerFactory == null) {
157                 throw new IllegalStateException("engineInit(...) not called yet");
158             }
159             return providerFactory.newProvider();
160         }
161 
162         private static final class ProviderFactory {
163             private final X509KeyManager keyManager;
164             private final String password;
165             private final Iterable<String> aliases;
166 
167             ProviderFactory(X509KeyManager keyManager, String password, Iterable<String> aliases) {
168                 this.keyManager = keyManager;
169                 this.password = password;
170                 this.aliases = aliases;
171             }
172 
173             OpenSslKeyMaterialProvider newProvider() {
174                 return new OpenSslPopulatedKeyMaterialProvider(keyManager,
175                         password, aliases);
176             }
177 
178             /**
179              * {@link OpenSslKeyMaterialProvider} implementation that pre-compute the {@link OpenSslKeyMaterial} for
180              * all aliases.
181              */
182             private static final class OpenSslPopulatedKeyMaterialProvider extends OpenSslKeyMaterialProvider {
183                 private final Map<String, Object> materialMap;
184 
185                 OpenSslPopulatedKeyMaterialProvider(
186                         X509KeyManager keyManager, String password, Iterable<String> aliases) {
187                     super(keyManager, password);
188                     materialMap = new HashMap<>();
189                     boolean initComplete = false;
190                     try {
191                         for (String alias: aliases) {
192                             if (alias != null && !materialMap.containsKey(alias)) {
193                                 try {
194                                     materialMap.put(alias, super.chooseKeyMaterial(
195                                             DefaultBufferAllocators.offHeapAllocator(), alias));
196                                 } catch (Exception e) {
197                                     // Just store the exception and rethrow it when we try to choose the keymaterial
198                                     // for this alias later on.
199                                     materialMap.put(alias, e);
200                                 }
201                             }
202                         }
203                         initComplete = true;
204                     } finally {
205                         if (!initComplete) {
206                             destroy();
207                         }
208                     }
209                     checkNonEmpty(materialMap, "materialMap");
210                 }
211 
212                 @Override
213                 OpenSslKeyMaterial chooseKeyMaterial(BufferAllocator allocator, String alias) throws Exception {
214                     Object value = materialMap.get(alias);
215                     if (value == null) {
216                         // There is no keymaterial for the requested alias, return null
217                         return null;
218                     }
219                     if (value instanceof OpenSslKeyMaterial) {
220                         return ((OpenSslKeyMaterial) value).retain();
221                     }
222                     throw (Exception) value;
223                 }
224 
225                 @Override
226                 void destroy() {
227                     for (Object material: materialMap.values()) {
228                         Resource.dispose(material);
229                     }
230                     materialMap.clear();
231                 }
232             }
233         }
234     }
235 
236     /**
237      * Create a new initialized {@link OpenSslX509KeyManagerFactory} which loads its {@link PrivateKey} directly from
238      * an {@code OpenSSL engine} via the
239      * <a href="https://www.openssl.org/docs/man1.1.0/crypto/ENGINE_load_private_key.html">ENGINE_load_private_key</a>
240      * function.
241      */
242     public static OpenSslX509KeyManagerFactory newEngineBased(File certificateChain, String password)
243             throws CertificateException, IOException,
244                    KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
245         return newEngineBased(SslContext.toX509Certificates(certificateChain), password);
246     }
247 
248     /**
249      * Create a new initialized {@link OpenSslX509KeyManagerFactory} which loads its {@link PrivateKey} directly from
250      * an {@code OpenSSL engine} via the
251      * <a href="https://www.openssl.org/docs/man1.1.0/crypto/ENGINE_load_private_key.html">ENGINE_load_private_key</a>
252      * function.
253      */
254     public static OpenSslX509KeyManagerFactory newEngineBased(X509Certificate[] certificateChain, String password)
255             throws CertificateException, IOException,
256                    KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
257         requireNonNull(certificateChain, "certificateChain");
258         KeyStore store = new OpenSslKeyStore(certificateChain.clone(), false);
259         store.load(null, null);
260         OpenSslX509KeyManagerFactory factory = new OpenSslX509KeyManagerFactory();
261         factory.init(store, password == null ? null : password.toCharArray());
262         return factory;
263     }
264 
265     /**
266      * See {@link OpenSslX509KeyManagerFactory#newEngineBased(X509Certificate[], String)}.
267      */
268     public static OpenSslX509KeyManagerFactory newKeyless(File chain)
269             throws CertificateException, IOException,
270             KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
271         return newKeyless(SslContext.toX509Certificates(chain));
272     }
273 
274     /**
275      * See {@link OpenSslX509KeyManagerFactory#newEngineBased(X509Certificate[], String)}.
276      */
277     public static OpenSslX509KeyManagerFactory newKeyless(InputStream chain)
278             throws CertificateException, IOException,
279             KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
280         return newKeyless(SslContext.toX509Certificates(chain));
281     }
282 
283     /**
284      * Returns a new initialized {@link OpenSslX509KeyManagerFactory} which will provide its private key by using the
285      * {@link OpenSslPrivateKeyMethod}.
286      */
287     public static OpenSslX509KeyManagerFactory newKeyless(X509Certificate... certificateChain)
288             throws CertificateException, IOException,
289             KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
290         requireNonNull(certificateChain, "certificateChain");
291         KeyStore store = new OpenSslKeyStore(certificateChain.clone(), true);
292         store.load(null, null);
293         OpenSslX509KeyManagerFactory factory = new OpenSslX509KeyManagerFactory();
294         factory.init(store, null);
295         return factory;
296     }
297 
298     private static final class OpenSslKeyStore extends KeyStore {
299         private OpenSslKeyStore(final X509Certificate[] certificateChain, final boolean keyless) {
300             super(new KeyStoreSpi() {
301 
302                 private final Date creationDate = new Date();
303 
304                 @Override
305                 public Key engineGetKey(String alias, char[] password) throws UnrecoverableKeyException {
306                     if (engineContainsAlias(alias)) {
307                         final long privateKeyAddress;
308                         if (keyless) {
309                             privateKeyAddress = 0;
310                         } else {
311                             try {
312                                 privateKeyAddress = SSL.loadPrivateKeyFromEngine(
313                                         alias, password == null ? null : new String(password));
314                             } catch (Exception e) {
315                                 UnrecoverableKeyException keyException =
316                                         new UnrecoverableKeyException("Unable to load key from engine");
317                                 keyException.initCause(e);
318                                 throw keyException;
319                             }
320                         }
321                         return new OpenSslPrivateKey(privateKeyAddress);
322                     }
323                     return null;
324                 }
325 
326                 @Override
327                 public Certificate[] engineGetCertificateChain(String alias) {
328                     return engineContainsAlias(alias)? certificateChain.clone() : null;
329                 }
330 
331                 @Override
332                 public Certificate engineGetCertificate(String alias) {
333                     return engineContainsAlias(alias)? certificateChain[0] : null;
334                 }
335 
336                 @Override
337                 public Date engineGetCreationDate(String alias) {
338                     return engineContainsAlias(alias)? creationDate : null;
339                 }
340 
341                 @Override
342                 public void engineSetKeyEntry(String alias, Key key, char[] password, Certificate[] chain)
343                         throws KeyStoreException {
344                     throw new KeyStoreException("Not supported");
345                 }
346 
347                 @Override
348                 public void engineSetKeyEntry(String alias, byte[] key, Certificate[] chain) throws KeyStoreException {
349                     throw new KeyStoreException("Not supported");
350                 }
351 
352                 @Override
353                 public void engineSetCertificateEntry(String alias, Certificate cert) throws KeyStoreException {
354                     throw new KeyStoreException("Not supported");
355                 }
356 
357                 @Override
358                 public void engineDeleteEntry(String alias) throws KeyStoreException {
359                     throw new KeyStoreException("Not supported");
360                 }
361 
362                 @Override
363                 public Enumeration<String> engineAliases() {
364                     return Collections.enumeration(Collections.singleton(SslContext.ALIAS));
365                 }
366 
367                 @Override
368                 public boolean engineContainsAlias(String alias) {
369                     return SslContext.ALIAS.equals(alias);
370                 }
371 
372                 @Override
373                 public int engineSize() {
374                     return 1;
375                 }
376 
377                 @Override
378                 public boolean engineIsKeyEntry(String alias) {
379                     return engineContainsAlias(alias);
380                 }
381 
382                 @Override
383                 public boolean engineIsCertificateEntry(String alias) {
384                     return engineContainsAlias(alias);
385                 }
386 
387                 @Override
388                 public String engineGetCertificateAlias(Certificate cert) {
389                     if (cert instanceof X509Certificate) {
390                         for (X509Certificate x509Certificate : certificateChain) {
391                             if (x509Certificate.equals(cert)) {
392                                 return SslContext.ALIAS;
393                             }
394                         }
395                     }
396                     return null;
397                 }
398 
399                 @Override
400                 public void engineStore(OutputStream stream, char[] password) {
401                     throw new UnsupportedOperationException();
402                 }
403 
404                 @Override
405                 public void engineLoad(InputStream stream, char[] password) {
406                     if (stream != null && password != null) {
407                         throw new UnsupportedOperationException();
408                     }
409                 }
410             }, null, "native");
411 
412             OpenSsl.ensureAvailability();
413         }
414     }
415 }