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