View Javadoc
1   /*
2    * Copyright 2014 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    *   http://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  
17  package io.netty.handler.ssl;
18  
19  import io.netty.buffer.ByteBuf;
20  import io.netty.buffer.ByteBufAllocator;
21  import io.netty.handler.ssl.util.SelfSignedCertificate;
22  import io.netty.internal.tcnative.Buffer;
23  import io.netty.internal.tcnative.Library;
24  import io.netty.internal.tcnative.SSL;
25  import io.netty.internal.tcnative.SSLContext;
26  import io.netty.util.ReferenceCountUtil;
27  import io.netty.util.ReferenceCounted;
28  import io.netty.util.internal.NativeLibraryLoader;
29  import io.netty.util.internal.PlatformDependent;
30  import io.netty.util.internal.SystemPropertyUtil;
31  import io.netty.util.internal.logging.InternalLogger;
32  import io.netty.util.internal.logging.InternalLoggerFactory;
33  
34  import java.security.AccessController;
35  import java.security.PrivilegedAction;
36  import java.util.ArrayList;
37  import java.util.Collections;
38  import java.util.LinkedHashSet;
39  import java.util.List;
40  import java.util.Set;
41  
42  import static io.netty.handler.ssl.SslUtils.DEFAULT_CIPHER_SUITES;
43  import static io.netty.handler.ssl.SslUtils.addIfSupported;
44  import static io.netty.handler.ssl.SslUtils.useFallbackCiphersIfDefaultIsEmpty;
45  import static io.netty.handler.ssl.SslUtils.PROTOCOL_SSL_V2;
46  import static io.netty.handler.ssl.SslUtils.PROTOCOL_SSL_V2_HELLO;
47  import static io.netty.handler.ssl.SslUtils.PROTOCOL_SSL_V3;
48  import static io.netty.handler.ssl.SslUtils.PROTOCOL_TLS_V1;
49  import static io.netty.handler.ssl.SslUtils.PROTOCOL_TLS_V1_1;
50  import static io.netty.handler.ssl.SslUtils.PROTOCOL_TLS_V1_2;
51  
52  /**
53   * Tells if <a href="http://netty.io/wiki/forked-tomcat-native.html">{@code netty-tcnative}</a> and its OpenSSL support
54   * are available.
55   */
56  public final class OpenSsl {
57  
58      private static final InternalLogger logger = InternalLoggerFactory.getInstance(OpenSsl.class);
59      private static final Throwable UNAVAILABILITY_CAUSE;
60  
61      static final List<String> DEFAULT_CIPHERS;
62      static final Set<String> AVAILABLE_CIPHER_SUITES;
63      private static final Set<String> AVAILABLE_OPENSSL_CIPHER_SUITES;
64      private static final Set<String> AVAILABLE_JAVA_CIPHER_SUITES;
65      private static final boolean SUPPORTS_KEYMANAGER_FACTORY;
66      private static final boolean SUPPORTS_HOSTNAME_VALIDATION;
67      private static final boolean USE_KEYMANAGER_FACTORY;
68      private static final boolean SUPPORTS_OCSP;
69  
70      static final Set<String> SUPPORTED_PROTOCOLS_SET;
71  
72      static {
73          Throwable cause = null;
74  
75          if (SystemPropertyUtil.getBoolean("io.netty.handler.ssl.noOpenSsl", false)) {
76              cause = new UnsupportedOperationException(
77                      "OpenSSL was explicit disabled with -Dio.netty.handler.ssl.noOpenSsl=true");
78  
79              logger.debug(
80                      "netty-tcnative explicit disabled; " +
81                              OpenSslEngine.class.getSimpleName() + " will be unavailable.", cause);
82          } else {
83              // Test if netty-tcnative is in the classpath first.
84              try {
85                  Class.forName("io.netty.internal.tcnative.SSL", false, OpenSsl.class.getClassLoader());
86              } catch (ClassNotFoundException t) {
87                  cause = t;
88                  logger.debug(
89                          "netty-tcnative not in the classpath; " +
90                                  OpenSslEngine.class.getSimpleName() + " will be unavailable.");
91              }
92  
93              // If in the classpath, try to load the native library and initialize netty-tcnative.
94              if (cause == null) {
95                  try {
96                      // The JNI library was not already loaded. Load it now.
97                      loadTcNative();
98                  } catch (Throwable t) {
99                      cause = t;
100                     logger.debug(
101                             "Failed to load netty-tcnative; " +
102                                     OpenSslEngine.class.getSimpleName() + " will be unavailable, unless the " +
103                                     "application has already loaded the symbols by some other means. " +
104                                     "See http://netty.io/wiki/forked-tomcat-native.html for more information.", t);
105                 }
106 
107                 try {
108                     String engine = SystemPropertyUtil.get("io.netty.handler.ssl.openssl.engine", null);
109                     if (engine == null) {
110                         logger.debug("Initialize netty-tcnative using engine: 'default'");
111                     } else {
112                         logger.debug("Initialize netty-tcnative using engine: '{}'", engine);
113                     }
114                     initializeTcNative(engine);
115 
116                     // The library was initialized successfully. If loading the library failed above,
117                     // reset the cause now since it appears that the library was loaded by some other
118                     // means.
119                     cause = null;
120                 } catch (Throwable t) {
121                     if (cause == null) {
122                         cause = t;
123                     }
124                     logger.debug(
125                             "Failed to initialize netty-tcnative; " +
126                                     OpenSslEngine.class.getSimpleName() + " will be unavailable. " +
127                                     "See http://netty.io/wiki/forked-tomcat-native.html for more information.", t);
128                 }
129             }
130         }
131 
132         UNAVAILABILITY_CAUSE = cause;
133 
134         if (cause == null) {
135             logger.debug("netty-tcnative using native library: {}", SSL.versionString());
136 
137             final List<String> defaultCiphers = new ArrayList<String>();
138             final Set<String> availableOpenSslCipherSuites = new LinkedHashSet<String>(128);
139             boolean supportsKeyManagerFactory = false;
140             boolean useKeyManagerFactory = false;
141             boolean supportsHostNameValidation = false;
142             try {
143                 final long sslCtx = SSLContext.make(SSL.SSL_PROTOCOL_ALL, SSL.SSL_MODE_SERVER);
144                 long certBio = 0;
145                 SelfSignedCertificate cert = null;
146                 try {
147                     SSLContext.setCipherSuite(sslCtx, "ALL");
148                     final long ssl = SSL.newSSL(sslCtx, true);
149                     try {
150                         for (String c: SSL.getCiphers(ssl)) {
151                             // Filter out bad input.
152                             if (c == null || c.isEmpty() || availableOpenSslCipherSuites.contains(c)) {
153                                 continue;
154                             }
155                             availableOpenSslCipherSuites.add(c);
156                         }
157 
158                         try {
159                             SSL.setHostNameValidation(ssl, 0, "netty.io");
160                             supportsHostNameValidation = true;
161                         } catch (Throwable ignore) {
162                             logger.debug("Hostname Verification not supported.");
163                         }
164                         try {
165                             cert = new SelfSignedCertificate();
166                             certBio = ReferenceCountedOpenSslContext.toBIO(ByteBufAllocator.DEFAULT, cert.cert());
167                             SSL.setCertificateChainBio(ssl, certBio, false);
168                             supportsKeyManagerFactory = true;
169                             try {
170                                 useKeyManagerFactory = AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
171                                     @Override
172                                     public Boolean run() {
173                                         return SystemPropertyUtil.getBoolean(
174                                                 "io.netty.handler.ssl.openssl.useKeyManagerFactory", true);
175                                     }
176                                 });
177                             } catch (Throwable ignore) {
178                                 logger.debug("Failed to get useKeyManagerFactory system property.");
179                             }
180                         } catch (Throwable ignore) {
181                             logger.debug("KeyManagerFactory not supported.");
182                         }
183                     } finally {
184                         SSL.freeSSL(ssl);
185                         if (certBio != 0) {
186                             SSL.freeBIO(certBio);
187                         }
188                         if (cert != null) {
189                             cert.delete();
190                         }
191                     }
192                 } finally {
193                     SSLContext.free(sslCtx);
194                 }
195             } catch (Exception e) {
196                 logger.warn("Failed to get the list of available OpenSSL cipher suites.", e);
197             }
198             AVAILABLE_OPENSSL_CIPHER_SUITES = Collections.unmodifiableSet(availableOpenSslCipherSuites);
199             final Set<String> availableJavaCipherSuites = new LinkedHashSet<String>(
200                     AVAILABLE_OPENSSL_CIPHER_SUITES.size() * 2);
201             for (String cipher: AVAILABLE_OPENSSL_CIPHER_SUITES) {
202                 // Included converted but also openssl cipher name
203                 availableJavaCipherSuites.add(CipherSuiteConverter.toJava(cipher, "TLS"));
204                 availableJavaCipherSuites.add(CipherSuiteConverter.toJava(cipher, "SSL"));
205             }
206 
207             addIfSupported(availableJavaCipherSuites, defaultCiphers, DEFAULT_CIPHER_SUITES);
208             useFallbackCiphersIfDefaultIsEmpty(defaultCiphers, availableJavaCipherSuites);
209             DEFAULT_CIPHERS = Collections.unmodifiableList(defaultCiphers);
210 
211             AVAILABLE_JAVA_CIPHER_SUITES = Collections.unmodifiableSet(availableJavaCipherSuites);
212 
213             final Set<String> availableCipherSuites = new LinkedHashSet<String>(
214                     AVAILABLE_OPENSSL_CIPHER_SUITES.size() + AVAILABLE_JAVA_CIPHER_SUITES.size());
215             availableCipherSuites.addAll(AVAILABLE_OPENSSL_CIPHER_SUITES);
216             availableCipherSuites.addAll(AVAILABLE_JAVA_CIPHER_SUITES);
217 
218             AVAILABLE_CIPHER_SUITES = availableCipherSuites;
219             SUPPORTS_KEYMANAGER_FACTORY = supportsKeyManagerFactory;
220             SUPPORTS_HOSTNAME_VALIDATION = supportsHostNameValidation;
221             USE_KEYMANAGER_FACTORY = useKeyManagerFactory;
222 
223             Set<String> protocols = new LinkedHashSet<String>(6);
224             // Seems like there is no way to explicitly disable SSLv2Hello in openssl so it is always enabled
225             protocols.add(PROTOCOL_SSL_V2_HELLO);
226             if (doesSupportProtocol(SSL.SSL_PROTOCOL_SSLV2, SSL.SSL_OP_NO_SSLv2)) {
227                 protocols.add(PROTOCOL_SSL_V2);
228             }
229             if (doesSupportProtocol(SSL.SSL_PROTOCOL_SSLV3, SSL.SSL_OP_NO_SSLv3)) {
230                 protocols.add(PROTOCOL_SSL_V3);
231             }
232             if (doesSupportProtocol(SSL.SSL_PROTOCOL_TLSV1, SSL.SSL_OP_NO_TLSv1)) {
233                 protocols.add(PROTOCOL_TLS_V1);
234             }
235             if (doesSupportProtocol(SSL.SSL_PROTOCOL_TLSV1_1, SSL.SSL_OP_NO_TLSv1_1)) {
236                 protocols.add(PROTOCOL_TLS_V1_1);
237             }
238             if (doesSupportProtocol(SSL.SSL_PROTOCOL_TLSV1_2, SSL.SSL_OP_NO_TLSv1_2)) {
239                 protocols.add(PROTOCOL_TLS_V1_2);
240             }
241 
242             SUPPORTED_PROTOCOLS_SET = Collections.unmodifiableSet(protocols);
243             SUPPORTS_OCSP = doesSupportOcsp();
244 
245             if (logger.isDebugEnabled()) {
246                 logger.debug("Supported protocols (OpenSSL): {} ", SUPPORTED_PROTOCOLS_SET);
247                 logger.debug("Default cipher suites (OpenSSL): {}", DEFAULT_CIPHERS);
248             }
249         } else {
250             DEFAULT_CIPHERS = Collections.emptyList();
251             AVAILABLE_OPENSSL_CIPHER_SUITES = Collections.emptySet();
252             AVAILABLE_JAVA_CIPHER_SUITES = Collections.emptySet();
253             AVAILABLE_CIPHER_SUITES = Collections.emptySet();
254             SUPPORTS_KEYMANAGER_FACTORY = false;
255             SUPPORTS_HOSTNAME_VALIDATION = false;
256             USE_KEYMANAGER_FACTORY = false;
257             SUPPORTED_PROTOCOLS_SET = Collections.emptySet();
258             SUPPORTS_OCSP = false;
259         }
260     }
261 
262     private static boolean doesSupportOcsp() {
263         boolean supportsOcsp = false;
264         if (version() >= 0x10002000L) {
265             long sslCtx = -1;
266             try {
267                 sslCtx = SSLContext.make(SSL.SSL_PROTOCOL_TLSV1_2, SSL.SSL_MODE_SERVER);
268                 SSLContext.enableOcsp(sslCtx, false);
269                 supportsOcsp = true;
270             } catch (Exception ignore) {
271                 // ignore
272             } finally {
273                 if (sslCtx != -1) {
274                     SSLContext.free(sslCtx);
275                 }
276             }
277         }
278         return supportsOcsp;
279     }
280     private static boolean doesSupportProtocol(int protocol, int opt) {
281         if (opt == 0) {
282             // If the opt is 0 the protocol is not supported. This is for example the case with BoringSSL and SSLv2.
283             return false;
284         }
285         long sslCtx = -1;
286         try {
287             sslCtx = SSLContext.make(protocol, SSL.SSL_MODE_COMBINED);
288             return true;
289         } catch (Exception ignore) {
290             return false;
291         } finally {
292             if (sslCtx != -1) {
293                 SSLContext.free(sslCtx);
294             }
295         }
296     }
297 
298     /**
299      * Returns {@code true} if and only if
300      * <a href="http://netty.io/wiki/forked-tomcat-native.html">{@code netty-tcnative}</a> and its OpenSSL support
301      * are available.
302      */
303     public static boolean isAvailable() {
304         return UNAVAILABILITY_CAUSE == null;
305     }
306 
307     /**
308      * Returns {@code true} if the used version of openssl supports
309      * <a href="https://tools.ietf.org/html/rfc7301">ALPN</a>.
310      */
311     public static boolean isAlpnSupported() {
312         return version() >= 0x10002000L;
313     }
314 
315     /**
316      * Returns {@code true} if the used version of OpenSSL supports OCSP stapling.
317      */
318     public static boolean isOcspSupported() {
319       return SUPPORTS_OCSP;
320     }
321 
322     /**
323      * Returns the version of the used available OpenSSL library or {@code -1} if {@link #isAvailable()}
324      * returns {@code false}.
325      */
326     public static int version() {
327         return isAvailable() ? SSL.version() : -1;
328     }
329 
330     /**
331      * Returns the version string of the used available OpenSSL library or {@code null} if {@link #isAvailable()}
332      * returns {@code false}.
333      */
334     public static String versionString() {
335         return isAvailable() ? SSL.versionString() : null;
336     }
337 
338     /**
339      * Ensure that <a href="http://netty.io/wiki/forked-tomcat-native.html">{@code netty-tcnative}</a> and
340      * its OpenSSL support are available.
341      *
342      * @throws UnsatisfiedLinkError if unavailable
343      */
344     public static void ensureAvailability() {
345         if (UNAVAILABILITY_CAUSE != null) {
346             throw (Error) new UnsatisfiedLinkError(
347                     "failed to load the required native library").initCause(UNAVAILABILITY_CAUSE);
348         }
349     }
350 
351     /**
352      * Returns the cause of unavailability of
353      * <a href="http://netty.io/wiki/forked-tomcat-native.html">{@code netty-tcnative}</a> and its OpenSSL support.
354      *
355      * @return the cause if unavailable. {@code null} if available.
356      */
357     public static Throwable unavailabilityCause() {
358         return UNAVAILABILITY_CAUSE;
359     }
360 
361     /**
362      * @deprecated use {@link #availableOpenSslCipherSuites()}
363      */
364     @Deprecated
365     public static Set<String> availableCipherSuites() {
366         return availableOpenSslCipherSuites();
367     }
368 
369     /**
370      * Returns all the available OpenSSL cipher suites.
371      * Please note that the returned array may include the cipher suites that are insecure or non-functional.
372      */
373     public static Set<String> availableOpenSslCipherSuites() {
374         return AVAILABLE_OPENSSL_CIPHER_SUITES;
375     }
376 
377     /**
378      * Returns all the available cipher suites (Java-style).
379      * Please note that the returned array may include the cipher suites that are insecure or non-functional.
380      */
381     public static Set<String> availableJavaCipherSuites() {
382         return AVAILABLE_JAVA_CIPHER_SUITES;
383     }
384 
385     /**
386      * Returns {@code true} if and only if the specified cipher suite is available in OpenSSL.
387      * Both Java-style cipher suite and OpenSSL-style cipher suite are accepted.
388      */
389     public static boolean isCipherSuiteAvailable(String cipherSuite) {
390         String converted = CipherSuiteConverter.toOpenSsl(cipherSuite);
391         if (converted != null) {
392             cipherSuite = converted;
393         }
394         return AVAILABLE_OPENSSL_CIPHER_SUITES.contains(cipherSuite);
395     }
396 
397     /**
398      * Returns {@code true} if {@link javax.net.ssl.KeyManagerFactory} is supported when using OpenSSL.
399      */
400     public static boolean supportsKeyManagerFactory() {
401         return SUPPORTS_KEYMANAGER_FACTORY;
402     }
403 
404     /**
405      * Returns {@code true} if <a href="https://wiki.openssl.org/index.php/Hostname_validation">Hostname Validation</a>
406      * is supported when using OpenSSL.
407      */
408     public static boolean supportsHostnameValidation() {
409         return SUPPORTS_HOSTNAME_VALIDATION;
410     }
411 
412     static boolean useKeyManagerFactory() {
413         return USE_KEYMANAGER_FACTORY;
414     }
415 
416     static long memoryAddress(ByteBuf buf) {
417         assert buf.isDirect();
418         return buf.hasMemoryAddress() ? buf.memoryAddress() : Buffer.address(buf.nioBuffer());
419     }
420 
421     private OpenSsl() { }
422 
423     private static void loadTcNative() throws Exception {
424         String os = PlatformDependent.normalizedOs();
425         String arch = PlatformDependent.normalizedArch();
426 
427         Set<String> libNames = new LinkedHashSet<String>(4);
428         String staticLibName = "netty_tcnative";
429 
430         // First, try loading the platform-specific library. Platform-specific
431         // libraries will be available if using a tcnative uber jar.
432         libNames.add(staticLibName + "_" + os + '_' + arch);
433         if ("linux".equalsIgnoreCase(os)) {
434             // Fedora SSL lib so naming (libssl.so.10 vs libssl.so.1.0.0)..
435             libNames.add(staticLibName + "_" + os + '_' + arch + "_fedora");
436         }
437         libNames.add(staticLibName + "_" + arch);
438         libNames.add(staticLibName);
439 
440         NativeLibraryLoader.loadFirstAvailable(SSL.class.getClassLoader(),
441             libNames.toArray(new String[0]));
442     }
443 
444     private static boolean initializeTcNative(String engine) throws Exception {
445         return Library.initialize("provided", engine);
446     }
447 
448     static void releaseIfNeeded(ReferenceCounted counted) {
449         if (counted.refCnt() > 0) {
450             ReferenceCountUtil.safeRelease(counted);
451         }
452     }
453 }