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                     for (Object material: materialMap.values()) {
229                         ReferenceCountUtil.release(material);
230                     }
231                     materialMap.clear();
232                 }
233             }
234         }
235     }
236 
237     /**
238      * Create a new initialized {@link OpenSslX509KeyManagerFactory} which loads its {@link PrivateKey} directly from
239      * an {@code OpenSSL engine} via the
240      * <a href="https://www.openssl.org/docs/man1.1.0/crypto/ENGINE_load_private_key.html">ENGINE_load_private_key</a>
241      * function.
242      */
243     public static OpenSslX509KeyManagerFactory newEngineBased(File certificateChain, String password)
244             throws CertificateException, IOException,
245                    KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
246         return newEngineBased(SslContext.toX509Certificates(certificateChain), password);
247     }
248 
249     /**
250      * Create a new initialized {@link OpenSslX509KeyManagerFactory} which loads its {@link PrivateKey} directly from
251      * an {@code OpenSSL engine} via the
252      * <a href="https://www.openssl.org/docs/man1.1.0/crypto/ENGINE_load_private_key.html">ENGINE_load_private_key</a>
253      * function.
254      */
255     public static OpenSslX509KeyManagerFactory newEngineBased(X509Certificate[] certificateChain, String password)
256             throws CertificateException, IOException,
257                    KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
258         checkNotNull(certificateChain, "certificateChain");
259         KeyStore store = new OpenSslKeyStore(certificateChain.clone(), false);
260         store.load(null, null);
261         OpenSslX509KeyManagerFactory factory = new OpenSslX509KeyManagerFactory();
262         factory.init(store, password == null ? null : password.toCharArray());
263         return factory;
264     }
265 
266     /**
267      * See {@link OpenSslX509KeyManagerFactory#newEngineBased(X509Certificate[], String)}.
268      */
269     public static OpenSslX509KeyManagerFactory newKeyless(File chain)
270             throws CertificateException, IOException,
271             KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
272         return newKeyless(SslContext.toX509Certificates(chain));
273     }
274 
275     /**
276      * See {@link OpenSslX509KeyManagerFactory#newEngineBased(X509Certificate[], String)}.
277      */
278     public static OpenSslX509KeyManagerFactory newKeyless(InputStream chain)
279             throws CertificateException, IOException,
280             KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
281         return newKeyless(SslContext.toX509Certificates(chain));
282     }
283 
284     /**
285      * Returns a new initialized {@link OpenSslX509KeyManagerFactory} which will provide its private key by using the
286      * {@link OpenSslPrivateKeyMethod}.
287      */
288     public static OpenSslX509KeyManagerFactory newKeyless(X509Certificate... certificateChain)
289             throws CertificateException, IOException,
290             KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
291         checkNotNull(certificateChain, "certificateChain");
292         KeyStore store = new OpenSslKeyStore(certificateChain.clone(), true);
293         store.load(null, null);
294         OpenSslX509KeyManagerFactory factory = new OpenSslX509KeyManagerFactory();
295         factory.init(store, null);
296         return factory;
297     }
298 
299     private static final class OpenSslKeyStore extends KeyStore {
300         private OpenSslKeyStore(final X509Certificate[] certificateChain, final boolean keyless) {
301             super(new KeyStoreSpi() {
302 
303                 private final Date creationDate = new Date();
304 
305                 @Override
306                 public Key engineGetKey(String alias, char[] password) throws UnrecoverableKeyException {
307                     if (engineContainsAlias(alias)) {
308                         final long privateKeyAddress;
309                         if (keyless) {
310                             privateKeyAddress = 0;
311                         } else {
312                             try {
313                                 privateKeyAddress = SSL.loadPrivateKeyFromEngine(
314                                         alias, password == null ? null : new String(password));
315                             } catch (Exception e) {
316                                 UnrecoverableKeyException keyException =
317                                         new UnrecoverableKeyException("Unable to load key from engine");
318                                 keyException.initCause(e);
319                                 throw keyException;
320                             }
321                         }
322                         return new OpenSslPrivateKey(privateKeyAddress);
323                     }
324                     return null;
325                 }
326 
327                 @Override
328                 public Certificate[] engineGetCertificateChain(String alias) {
329                     return engineContainsAlias(alias)? certificateChain.clone() : null;
330                 }
331 
332                 @Override
333                 public Certificate engineGetCertificate(String alias) {
334                     return engineContainsAlias(alias)? certificateChain[0] : null;
335                 }
336 
337                 @Override
338                 public Date engineGetCreationDate(String alias) {
339                     return engineContainsAlias(alias)? creationDate : null;
340                 }
341 
342                 @Override
343                 public void engineSetKeyEntry(String alias, Key key, char[] password, Certificate[] chain)
344                         throws KeyStoreException {
345                     throw new KeyStoreException("Not supported");
346                 }
347 
348                 @Override
349                 public void engineSetKeyEntry(String alias, byte[] key, Certificate[] chain) throws KeyStoreException {
350                     throw new KeyStoreException("Not supported");
351                 }
352 
353                 @Override
354                 public void engineSetCertificateEntry(String alias, Certificate cert) throws KeyStoreException {
355                     throw new KeyStoreException("Not supported");
356                 }
357 
358                 @Override
359                 public void engineDeleteEntry(String alias) throws KeyStoreException {
360                     throw new KeyStoreException("Not supported");
361                 }
362 
363                 @Override
364                 public Enumeration<String> engineAliases() {
365                     return Collections.enumeration(Collections.singleton(SslContext.ALIAS));
366                 }
367 
368                 @Override
369                 public boolean engineContainsAlias(String alias) {
370                     return SslContext.ALIAS.equals(alias);
371                 }
372 
373                 @Override
374                 public int engineSize() {
375                     return 1;
376                 }
377 
378                 @Override
379                 public boolean engineIsKeyEntry(String alias) {
380                     return engineContainsAlias(alias);
381                 }
382 
383                 @Override
384                 public boolean engineIsCertificateEntry(String alias) {
385                     return engineContainsAlias(alias);
386                 }
387 
388                 @Override
389                 public String engineGetCertificateAlias(Certificate cert) {
390                     if (cert instanceof X509Certificate) {
391                         for (X509Certificate x509Certificate : certificateChain) {
392                             if (x509Certificate.equals(cert)) {
393                                 return SslContext.ALIAS;
394                             }
395                         }
396                     }
397                     return null;
398                 }
399 
400                 @Override
401                 public void engineStore(OutputStream stream, char[] password) {
402                     throw new UnsupportedOperationException();
403                 }
404 
405                 @Override
406                 public void engineLoad(InputStream stream, char[] password) {
407                     if (stream != null && password != null) {
408                         throw new UnsupportedOperationException();
409                     }
410                 }
411             }, null, "native");
412 
413             OpenSsl.ensureAvailability();
414         }
415     }
416 }