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  package org.jboss.netty.handler.ssl;
17  
18  import org.apache.tomcat.jni.Pool;
19  import org.apache.tomcat.jni.SSL;
20  import org.apache.tomcat.jni.SSLContext;
21  import org.jboss.netty.logging.InternalLogger;
22  import org.jboss.netty.logging.InternalLoggerFactory;
23  
24  import javax.net.ssl.SSLEngine;
25  import javax.net.ssl.SSLException;
26  import java.io.File;
27  import java.util.ArrayList;
28  import java.util.Collections;
29  import java.util.List;
30  
31  /**
32   * A server-side {@link SslContext} which uses OpenSSL's SSL/TLS implementation.
33   */
34  public final class OpenSslServerContext extends SslContext {
35  
36      private static final InternalLogger logger = InternalLoggerFactory.getInstance(OpenSslServerContext.class);
37      private static final List<String> DEFAULT_CIPHERS;
38  
39      static {
40          List<String> ciphers = new ArrayList<String>();
41          // XXX: Make sure to sync this list with JdkSslEngineFactory.
42          Collections.addAll(
43                  ciphers,
44                  "ECDHE-RSA-AES128-GCM-SHA256",
45                  "ECDHE-RSA-RC4-SHA",
46                  "ECDHE-RSA-AES128-SHA",
47                  "ECDHE-RSA-AES256-SHA",
48                  "AES128-GCM-SHA256",
49                  "RC4-SHA",
50                  "RC4-MD5",
51                  "AES128-SHA",
52                  "AES256-SHA",
53                  "DES-CBC3-SHA");
54          DEFAULT_CIPHERS = Collections.unmodifiableList(ciphers);
55  
56          if (logger.isDebugEnabled()) {
57              logger.debug("Default cipher suite (OpenSSL): " + ciphers);
58          }
59      }
60  
61      private final long aprPool;
62  
63      private final List<String> ciphers = new ArrayList<String>();
64      private final List<String> unmodifiableCiphers = Collections.unmodifiableList(ciphers);
65      private final long sessionCacheSize;
66      private final long sessionTimeout;
67      private final List<String> nextProtocols;
68  
69      /** The OpenSSL SSL_CTX object */
70      private final long ctx;
71      private final OpenSslSessionStats stats;
72  
73      /**
74       * Creates a new instance.
75       *
76       * @param certChainFile an X.509 certificate chain file in PEM format
77       * @param keyFile a PKCS#8 private key file in PEM format
78       */
79      public OpenSslServerContext(File certChainFile, File keyFile) throws SSLException {
80          this(certChainFile, keyFile, null);
81      }
82  
83      /**
84       * Creates a new instance.
85       *
86       * @param certChainFile an X.509 certificate chain file in PEM format
87       * @param keyFile a PKCS#8 private key file in PEM format
88       * @param keyPassword the password of the {@code keyFile}.
89       *                    {@code null} if it's not password-protected.
90       */
91      public OpenSslServerContext(File certChainFile, File keyFile, String keyPassword) throws SSLException {
92          this(null, certChainFile, keyFile, keyPassword, null, null, 0, 0);
93      }
94  
95      /**
96       * Creates a new instance.
97       *
98       * @param bufPool the buffer pool which will be used by this context.
99       *                {@code null} to use the default buffer pool.
100      * @param certChainFile an X.509 certificate chain file in PEM format
101      * @param keyFile a PKCS#8 private key file in PEM format
102      * @param keyPassword the password of the {@code keyFile}.
103      *                    {@code null} if it's not password-protected.
104      * @param ciphers the cipher suites to enable, in the order of preference.
105      *                {@code null} to use the default cipher suites.
106      * @param nextProtocols the application layer protocols to accept, in the order of preference.
107      *                      {@code null} to disable TLS NPN/ALPN extension.
108      * @param sessionCacheSize the size of the cache used for storing SSL session objects.
109      *                         {@code 0} to use the default value.
110      * @param sessionTimeout the timeout for the cached SSL session objects, in seconds.
111      *                       {@code 0} to use the default value.
112      */
113     public OpenSslServerContext(
114             SslBufferPool bufPool,
115             File certChainFile, File keyFile, String keyPassword,
116             Iterable<String> ciphers, Iterable<String> nextProtocols,
117             long sessionCacheSize, long sessionTimeout) throws SSLException {
118 
119         super(bufPool);
120 
121         OpenSsl.ensureAvailability();
122 
123         if (certChainFile == null) {
124             throw new NullPointerException("certChainFile");
125         }
126         if (!certChainFile.isFile()) {
127             throw new IllegalArgumentException("certChainFile is not a file: " + certChainFile);
128         }
129         if (keyFile == null) {
130             throw new NullPointerException("keyPath");
131         }
132         if (!keyFile.isFile()) {
133             throw new IllegalArgumentException("keyPath is not a file: " + keyFile);
134         }
135         if (ciphers == null) {
136             ciphers = DEFAULT_CIPHERS;
137         }
138 
139         if (keyPassword == null) {
140             keyPassword = "";
141         }
142         if (nextProtocols == null) {
143             nextProtocols = Collections.emptyList();
144         }
145 
146         for (String c: ciphers) {
147             if (c == null) {
148                 break;
149             }
150             this.ciphers.add(c);
151         }
152 
153         List<String> nextProtoList = new ArrayList<String>();
154         for (String p: nextProtocols) {
155             if (p == null) {
156                 break;
157             }
158             nextProtoList.add(p);
159         }
160         this.nextProtocols = Collections.unmodifiableList(nextProtoList);
161 
162         // Allocate a new APR pool.
163         aprPool = Pool.create(0);
164 
165         // Create a new SSL_CTX and configure it.
166         boolean success = false;
167         try {
168             synchronized (OpenSslServerContext.class) {
169                 try {
170                     ctx = SSLContext.make(aprPool, SSL.SSL_PROTOCOL_ALL, SSL.SSL_MODE_SERVER);
171                 } catch (Exception e) {
172                     throw new SSLException("failed to create an SSL_CTX", e);
173                 }
174 
175                 SSLContext.setOptions(ctx, SSL.SSL_OP_ALL);
176                 SSLContext.setOptions(ctx, SSL.SSL_OP_NO_SSLv2);
177                 SSLContext.setOptions(ctx, SSL.SSL_OP_CIPHER_SERVER_PREFERENCE);
178                 SSLContext.setOptions(ctx, SSL.SSL_OP_SINGLE_ECDH_USE);
179                 SSLContext.setOptions(ctx, SSL.SSL_OP_SINGLE_DH_USE);
180                 SSLContext.setOptions(ctx, SSL.SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION);
181 
182                 /* List the ciphers that the client is permitted to negotiate. */
183                 try {
184                     // Convert the cipher list into a colon-separated string.
185                     StringBuilder cipherBuf = new StringBuilder();
186                     for (String c: this.ciphers) {
187                         cipherBuf.append(c);
188                         cipherBuf.append(':');
189                     }
190                     cipherBuf.setLength(cipherBuf.length() - 1);
191 
192                     SSLContext.setCipherSuite(ctx, cipherBuf.toString());
193                 } catch (SSLException e) {
194                     throw e;
195                 } catch (Exception e) {
196                     throw new SSLException("failed to set cipher suite: " + this.ciphers, e);
197                 }
198 
199                 /* Set certificate verification policy. */
200                 SSLContext.setVerify(ctx, SSL.SSL_CVERIFY_NONE, 10);
201 
202                 /* Load the certificate file and private key. */
203                 try {
204                     if (!SSLContext.setCertificate(
205                             ctx, certChainFile.getPath(), keyFile.getPath(), keyPassword, SSL.SSL_AIDX_RSA)) {
206                         throw new SSLException("failed to set certificate: " +
207                                 certChainFile + " and " + keyFile + " (" + SSL.getLastError() + ')');
208                     }
209                 } catch (SSLException e) {
210                     throw e;
211                 } catch (Exception e) {
212                     throw new SSLException("failed to set certificate: " + certChainFile + " and " + keyFile, e);
213                 }
214 
215                 /* Load the certificate chain. We must skip the first cert since it was loaded above. */
216                 if (!SSLContext.setCertificateChainFile(ctx, certChainFile.getPath(), true)) {
217                     String error = SSL.getLastError();
218                     if (!error.startsWith(OpenSsl.IGNORABLE_ERROR_PREFIX)) {
219                         throw new SSLException(
220                                 "failed to set certificate chain: " + certChainFile + " (" + SSL.getLastError() + ')');
221                     }
222                 }
223 
224                 /* Set next protocols for next protocol negotiation extension, if specified */
225                 if (!nextProtoList.isEmpty()) {
226                     // Convert the protocol list into a comma-separated string.
227                     StringBuilder nextProtocolBuf = new StringBuilder();
228                     for (String p: nextProtoList) {
229                         nextProtocolBuf.append(p);
230                         nextProtocolBuf.append(',');
231                     }
232                     nextProtocolBuf.setLength(nextProtocolBuf.length() - 1);
233 
234                     SSLContext.setNextProtos(ctx, nextProtocolBuf.toString());
235                 }
236 
237                 /* Set session cache size, if specified */
238                 if (sessionCacheSize > 0) {
239                     this.sessionCacheSize = sessionCacheSize;
240                     SSLContext.setSessionCacheSize(ctx, sessionCacheSize);
241                 } else {
242                     // Get the default session cache size using SSLContext.setSessionCacheSize()
243                     this.sessionCacheSize = sessionCacheSize = SSLContext.setSessionCacheSize(ctx, 20480);
244                     // Revert the session cache size to the default value.
245                     SSLContext.setSessionCacheSize(ctx, sessionCacheSize);
246                 }
247 
248                 /* Set session timeout, if specified */
249                 if (sessionTimeout > 0) {
250                     this.sessionTimeout = sessionTimeout;
251                     SSLContext.setSessionCacheTimeout(ctx, sessionTimeout);
252                 } else {
253                     // Get the default session timeout using SSLContext.setSessionCacheTimeout()
254                     this.sessionTimeout = sessionTimeout = SSLContext.setSessionCacheTimeout(ctx, 300);
255                     // Revert the session timeout to the default value.
256                     SSLContext.setSessionCacheTimeout(ctx, sessionTimeout);
257                 }
258             }
259             success = true;
260         } finally {
261             if (!success) {
262                 destroyPools();
263             }
264         }
265 
266         stats = new OpenSslSessionStats(ctx);
267     }
268 
269     @Override
270     SslBufferPool newBufferPool() {
271         return new SslBufferPool(true, true);
272     }
273 
274     @Override
275     public boolean isClient() {
276         return false;
277     }
278 
279     @Override
280     public List<String> cipherSuites() {
281         return unmodifiableCiphers;
282     }
283 
284     @Override
285     public long sessionCacheSize() {
286         return sessionCacheSize;
287     }
288 
289     @Override
290     public long sessionTimeout() {
291         return sessionTimeout;
292     }
293 
294     @Override
295     public List<String> nextProtocols() {
296         return nextProtocols;
297     }
298 
299     /**
300      * Returns the {@code SSL_CTX} object of this context.
301      */
302     public long context() {
303         return ctx;
304     }
305 
306     /**
307      * Returns the stats of this context.
308      */
309     public OpenSslSessionStats stats() {
310         return stats;
311     }
312 
313     /**
314      * Returns a new server-side {@link SSLEngine} with the current configuration.
315      */
316     @Override
317     public SSLEngine newEngine() {
318         if (nextProtocols.isEmpty()) {
319             return new OpenSslEngine(ctx, bufferPool(), null);
320         } else {
321             return new OpenSslEngine(
322                     ctx, bufferPool(), nextProtocols.get(nextProtocols.size() - 1));
323         }
324     }
325 
326     @Override
327     public SSLEngine newEngine(String peerHost, int peerPort) {
328         throw new UnsupportedOperationException();
329     }
330 
331     /**
332      * Sets the SSL session ticket keys of this context.
333      */
334     public void setTicketKeys(byte[] keys) {
335         if (keys != null) {
336             throw new NullPointerException("keys");
337         }
338         SSLContext.setSessionTicketKeys(ctx, keys);
339     }
340 
341     @Override
342     @SuppressWarnings("FinalizeDeclaration")
343     protected void finalize() throws Throwable {
344         super.finalize();
345         synchronized (OpenSslServerContext.class) {
346             if (ctx != 0) {
347                 SSLContext.free(ctx);
348             }
349         }
350 
351         destroyPools();
352     }
353 
354     private void destroyPools() {
355         if (aprPool != 0) {
356             Pool.destroy(aprPool);
357         }
358     }
359 }