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