/* * Copyright (C) 2012-2016 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.facebook.nifty.ssl; import com.google.common.collect.ImmutableList; import org.apache.tomcat.jni.Pool; import org.apache.tomcat.jni.SSL; import org.apache.tomcat.jni.SSLContext; import org.apache.tomcat.jni.SessionTicketKey; import org.jboss.netty.handler.ssl.OpenSslEngine; import org.jboss.netty.handler.ssl.SslBufferPool; import org.jboss.netty.handler.ssl.SslHandler; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLException; import java.io.File; import java.util.Collections; import java.util.List; /** * A lot of this code is taken from Netty's OpenSslServerContext. * https://github.com/netty/netty/blob/3.10/src/main/java/org/jboss/netty/handler/ssl/OpenSslServerContext.java * NiftyOpenSslServerContext allows us to control this instantiation of SSLContext with custom options * missing in netty. We should be able to get rid of this when we move the dependency * to the latest version of netty. */ public final class NiftyOpenSslServerContext implements SslHandlerFactory { private static final String IGNORABLE_ERROR_PREFIX = "error:00000000:"; private static final int DEFAULT_CERT_DEPTH = 3; private final long aprPool; private final List<String> ciphers; private final long sessionCacheSize; private final long sessionTimeout; private final List<String> nextProtocols; private final OpenSslServerConfiguration sslServerConfiguration; private final SslBufferPool bufferPool; /** * The OpenSSL SSL_CTX object */ private final long ctx; public NiftyOpenSslServerContext(OpenSslServerConfiguration sslServerConfiguration) throws Exception { this.sslServerConfiguration = sslServerConfiguration; int sslVersion = this.sslServerConfiguration.sslVersion.getValue(); File certChainFile = sslServerConfiguration.certFile; File keyFile = sslServerConfiguration.keyFile; File clientCAFile = sslServerConfiguration.clientCAFile; OpenSslServerConfiguration.SSLVerification sslVerification = sslServerConfiguration.sslVerification; if (certChainFile == null) { throw new NullPointerException("certChainFile"); } if (!certChainFile.isFile()) { throw new IllegalArgumentException("certChainFile is not a file: " + certChainFile); } if (keyFile == null) { throw new NullPointerException("keyPath"); } if (!keyFile.isFile()) { throw new IllegalArgumentException("keyPath is not a file: " + keyFile); } if (clientCAFile != null && !clientCAFile.isFile()) { throw new IllegalArgumentException("clientCAFile is not a file " + clientCAFile); } if (sslServerConfiguration.ciphers == null) { ciphers = SslDefaults.SERVER_DEFAULTS; } else { ciphers = ImmutableList.copyOf(sslServerConfiguration.ciphers); } String keyPassword = sslServerConfiguration.keyPassword == null ? "" : sslServerConfiguration.keyPassword; if (sslServerConfiguration.nextProtocols == null) { nextProtocols = Collections.emptyList(); } else { nextProtocols = ImmutableList.copyOf(sslServerConfiguration.nextProtocols); } // Allocate a new APR pool. aprPool = Pool.create(0); int maxSslBufferBytes = sslServerConfiguration.maxSslBufferBytes; boolean preallocateSslBuffer = sslServerConfiguration.preallocateSslBuffer; boolean threadLocalSslBuffer = sslServerConfiguration.threadLocalSslBuffer; if (threadLocalSslBuffer) { bufferPool = new ThreadLocalSslBufferPool(maxSslBufferBytes, preallocateSslBuffer, true); } else { bufferPool = new SslBufferPool(maxSslBufferBytes, preallocateSslBuffer, true); } // Create a new SSL_CTX and configure it. boolean success = false; try { synchronized (NiftyOpenSslServerContext.class) { try { ctx = SSLContext.make(aprPool, sslVersion, SSL.SSL_MODE_SERVER); } catch (Exception e) { throw new SSLException("failed to create an SSL_CTX", e); } SSLContext.setOptions(ctx, SSL.SSL_OP_ALL); SSLContext.setOptions(ctx, SSL.SSL_OP_NO_SSLv2); SSLContext.setOptions(ctx, SSL.SSL_OP_NO_SSLv3); SSLContext.setOptions(ctx, SSL.SSL_OP_CIPHER_SERVER_PREFERENCE); SSLContext.setOptions(ctx, SSL.SSL_OP_SINGLE_ECDH_USE); SSLContext.setOptions(ctx, SSL.SSL_OP_SINGLE_DH_USE); SSLContext.setOptions(ctx, SSL.SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION); SSLContext.setOptions(ctx, SSL.SSL_OP_NO_COMPRESSION); if (!this.sslServerConfiguration.enableStatefulSessionCache) { SSLContext.setSessionCacheMode(ctx, OpenSSLConstants.SSL_SESS_CACHE_NO_INTERNAL); } // We need to enable SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER as the memory address may change between // calling OpenSSLEngine.wrap(...). // See https://github.com/netty/netty-tcnative/issues/100 SSLContext.setMode(ctx, SSLContext.getMode(ctx) | SSL.SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); /* List the ciphers that the client is permitted to negotiate. */ try { // Convert the cipher list into a colon-separated string. StringBuilder cipherBuf = new StringBuilder(); for (String c : ciphers) { cipherBuf.append(c); cipherBuf.append(':'); } cipherBuf.setLength(cipherBuf.length() - 1); SSLContext.setCipherSuite(ctx, cipherBuf.toString()); } catch (SSLException e) { throw e; } catch (Exception e) { throw new SSLException("failed to set cipher suite: " + ciphers, e); } /* Load the certificate file and private key. */ try { if (!SSLContext.setCertificate( ctx, certChainFile.getPath(), keyFile.getPath(), keyPassword, SSL.SSL_AIDX_RSA)) { throw new SSLException("failed to set certificate: " + certChainFile + " and " + keyFile + " (" + SSL.getLastError() + ')'); } } catch (SSLException e) { throw e; } catch (Exception e) { throw new SSLException("failed to set certificate: " + certChainFile + " and " + keyFile, e); } /* Load the certificate chain. We must skip the first cert since it was loaded above. */ if (!SSLContext.setCertificateChainFile(ctx, certChainFile.getPath(), true)) { String error = SSL.getLastError(); if (!error.startsWith(IGNORABLE_ERROR_PREFIX)) { throw new SSLException( "failed to set certificate chain: " + certChainFile + " (" + SSL.getLastError() + ')'); } } if (clientCAFile != null && !SSLContext.setCACertificate(ctx, clientCAFile.getPath(), null)) { String error = SSL.getLastError(); if (!error.startsWith(IGNORABLE_ERROR_PREFIX)) { throw new SSLException( "failed to set ca cert: " + clientCAFile + " (" + SSL.getLastError() + ')'); } } SSLContext.setVerify(ctx, sslVerification.getValue(), DEFAULT_CERT_DEPTH); /* Set next protocols for next protocol negotiation extension, if specified */ if (!nextProtocols.isEmpty()) { // Convert the protocol list into a comma-separated string. StringBuilder nextProtocolBuf = new StringBuilder(); for (String p : nextProtocols) { nextProtocolBuf.append(p); nextProtocolBuf.append(','); } nextProtocolBuf.setLength(nextProtocolBuf.length() - 1); SSLContext.setNextProtos(ctx, nextProtocolBuf.toString()); } if (nextProtocols != null && !nextProtocols.isEmpty()) { String[] alpnArray = nextProtocols.toArray(new String[0]); SSLContext.setAlpnProtos(ctx, alpnArray, SSL.SSL_SELECTOR_FAILURE_CHOOSE_MY_LAST_PROTOCOL); } /* Set session cache size, if specified */ if (sslServerConfiguration.sessionCacheSize > 0) { sessionCacheSize = sslServerConfiguration.sessionCacheSize; SSLContext.setSessionCacheSize(ctx, sessionCacheSize); } else { // Get the default session cache size using SSLContext.setSessionCacheSize() sessionCacheSize = SSLContext.setSessionCacheSize(ctx, 20480); // Revert the session cache size to the default value. SSLContext.setSessionCacheSize(ctx, sessionCacheSize); } /* Set session timeout, if specified */ if (sslServerConfiguration.sessionTimeoutSeconds > 0) { sessionTimeout = sslServerConfiguration.sessionTimeoutSeconds; SSLContext.setSessionCacheTimeout(ctx, sslServerConfiguration.sessionTimeoutSeconds); } else { // Get the default session timeout using SSLContext.setSessionCacheTimeout() sessionTimeout = SSLContext.setSessionCacheTimeout(ctx, 300); // Revert the session timeout to the default value. SSLContext.setSessionCacheTimeout(ctx, sessionTimeout); } } success = true; } finally { if (!success) { destroyPools(); } } } public List<String> cipherSuites() { return ImmutableList.copyOf(ciphers); } public long sessionCacheSize() { return sessionCacheSize; } public long sessionTimeout() { return sessionTimeout; } public List<String> nextProtocols() { return nextProtocols; } /** * Returns the {@code SSL_CTX} object of this context. */ public long context() { return ctx; } /** * Returns a new server-side {@link SSLEngine} with the current configuration. */ public SSLEngine newEngine() { if (nextProtocols.isEmpty()) { return new OpenSslEngine(ctx, bufferPool, null); } else { return new OpenSslEngine( ctx, bufferPool, nextProtocols.get(nextProtocols.size() - 1)); } } /** * Sets the SSL session ticket keys of this context. */ public void setTicketKeys(SessionTicketKey[] keys) { if (keys == null) { throw new NullPointerException("keys"); } SSLContext.setSessionTicketKeys(ctx, keys); } public void setSessionIdContext(byte[] sessionIdContext) { SSLContext.setSessionIdContext(ctx, sessionIdContext); } public void setSessionCacheTimeout(long sessionTimeoutSeconds) { SSLContext.setSessionCacheTimeout(ctx, sessionTimeoutSeconds); } @Override public SslHandler newHandler() { SslHandler handler = new BetterSslHandler(newEngine(), bufferPool, sslServerConfiguration); handler.setCloseOnSSLException(true); return handler; } @Override @SuppressWarnings("FinalizeDeclaration") protected void finalize() throws Throwable { super.finalize(); synchronized (NiftyOpenSslServerContext.class) { if (ctx != 0) { SSLContext.free(ctx); } } destroyPools(); } private void destroyPools() { if (aprPool != 0) { Pool.destroy(aprPool); } } }