diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslClientContext.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslClientContext.java index b04a3485c2..bd7a28dd12 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslClientContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslClientContext.java @@ -15,34 +15,25 @@ */ package io.netty.handler.ssl; -import io.netty.util.internal.logging.InternalLogger; -import io.netty.util.internal.logging.InternalLoggerFactory; -import org.apache.tomcat.jni.CertificateRequestedCallback; import org.apache.tomcat.jni.SSL; -import org.apache.tomcat.jni.SSLContext; + +import java.io.File; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLException; -import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509ExtendedKeyManager; -import javax.net.ssl.X509ExtendedTrustManager; -import javax.net.ssl.X509KeyManager; -import javax.net.ssl.X509TrustManager; -import javax.security.auth.x500.X500Principal; -import java.io.File; -import java.security.KeyStore; -import java.security.PrivateKey; -import java.security.cert.X509Certificate; -import java.util.HashSet; -import java.util.Set; + +import static io.netty.handler.ssl.ReferenceCountedOpenSslClientContext.newSessionContext; /** * A client-side {@link SslContext} which uses OpenSSL's SSL/TLS implementation. + *

This class will use a finalizer to ensure native resources are automatically cleaned up. To avoid finalizers + * and manually release the native memory see {@link ReferenceCountedOpenSslClientContext}. */ public final class OpenSslClientContext extends OpenSslContext { - private static final InternalLogger logger = InternalLoggerFactory.getInstance(OpenSslClientContext.class); private final OpenSslSessionContext sessionContext; /** @@ -187,7 +178,6 @@ public final class OpenSslClientContext extends OpenSslContext { keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout); } - @SuppressWarnings("deprecation") OpenSslClientContext(X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory, X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, Iterable ciphers, @@ -198,73 +188,12 @@ public final class OpenSslClientContext extends OpenSslContext { ClientAuth.NONE); boolean success = false; try { - if (key == null && keyCertChain != null || key != null && keyCertChain == null) { - throw new IllegalArgumentException( - "Either both keyCertChain and key needs to be null or none of them"); - } - synchronized (OpenSslContext.class) { - try { - if (!OpenSsl.useKeyManagerFactory()) { - if (keyManagerFactory != null) { - throw new IllegalArgumentException( - "KeyManagerFactory not supported"); - } - if (keyCertChain != null && key != null) { - setKeyMaterial(ctx, keyCertChain, key, keyPassword); - } - } else { - if (keyCertChain != null) { - keyManagerFactory = buildKeyManagerFactory( - keyCertChain, key, keyPassword, keyManagerFactory); - } - if (keyManagerFactory != null) { - X509KeyManager keyManager = chooseX509KeyManager(keyManagerFactory.getKeyManagers()); - OpenSslKeyMaterialManager materialManager = useExtendedKeyManager(keyManager) ? - new OpenSslExtendedKeyMaterialManager( - (X509ExtendedKeyManager) keyManager, keyPassword) : - new OpenSslKeyMaterialManager(keyManager, keyPassword); - SSLContext.setCertRequestedCallback(ctx, new OpenSslCertificateRequestedCallback( - engineMap, materialManager)); - } - } - } catch (Exception e) { - throw new SSLException("failed to set certificate and key", e); - } - - SSLContext.setVerify(ctx, SSL.SSL_VERIFY_NONE, VERIFY_DEPTH); - - try { - if (trustCertCollection != null) { - trustManagerFactory = buildTrustManagerFactory(trustCertCollection, trustManagerFactory); - } else if (trustManagerFactory == null) { - trustManagerFactory = TrustManagerFactory.getInstance( - TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init((KeyStore) null); - } - final X509TrustManager manager = chooseTrustManager(trustManagerFactory.getTrustManagers()); - - // IMPORTANT: The callbacks set for verification must be static to prevent memory leak as - // otherwise the context can never be collected. This is because the JNI code holds - // a global reference to the callbacks. - // - // See https://github.com/netty/netty/issues/5372 - - // Use this to prevent an error when running on java < 7 - if (useExtendedTrustManager(manager)) { - SSLContext.setCertVerifyCallback(ctx, - new ExtendedTrustManagerVerifyCallback(engineMap, (X509ExtendedTrustManager) manager)); - } else { - SSLContext.setCertVerifyCallback(ctx, new TrustManagerVerifyCallback(engineMap, manager)); - } - } catch (Exception e) { - throw new SSLException("unable to setup trustmanager", e); - } - } - sessionContext = new OpenSslClientSessionContext(this); + sessionContext = newSessionContext(this, ctx, engineMap, trustCertCollection, trustManagerFactory, + keyCertChain, key, keyPassword, keyManagerFactory); success = true; } finally { if (!success) { - destroy(); + release(); } } } @@ -278,148 +207,4 @@ public final class OpenSslClientContext extends OpenSslContext { OpenSslKeyMaterialManager keyMaterialManager() { return null; } - - // No cache is currently supported for client side mode. - private static final class OpenSslClientSessionContext extends OpenSslSessionContext { - private OpenSslClientSessionContext(OpenSslContext context) { - super(context); - } - - @Override - public void setSessionTimeout(int seconds) { - if (seconds < 0) { - throw new IllegalArgumentException(); - } - } - - @Override - public int getSessionTimeout() { - return 0; - } - - @Override - public void setSessionCacheSize(int size) { - if (size < 0) { - throw new IllegalArgumentException(); - } - } - - @Override - public int getSessionCacheSize() { - return 0; - } - - @Override - public void setSessionCacheEnabled(boolean enabled) { - // ignored - } - - @Override - public boolean isSessionCacheEnabled() { - return false; - } - } - - private static final class TrustManagerVerifyCallback extends AbstractCertificateVerifier { - private final X509TrustManager manager; - - TrustManagerVerifyCallback(OpenSslEngineMap engineMap, X509TrustManager manager) { - super(engineMap); - this.manager = manager; - } - - @Override - void verify(OpenSslEngine engine, X509Certificate[] peerCerts, String auth) - throws Exception { - manager.checkServerTrusted(peerCerts, auth); - } - } - - private static final class ExtendedTrustManagerVerifyCallback extends AbstractCertificateVerifier { - private final X509ExtendedTrustManager manager; - - ExtendedTrustManagerVerifyCallback(OpenSslEngineMap engineMap, X509ExtendedTrustManager manager) { - super(engineMap); - this.manager = manager; - } - - @Override - void verify(OpenSslEngine engine, X509Certificate[] peerCerts, String auth) - throws Exception { - manager.checkServerTrusted(peerCerts, auth, engine); - } - } - - private static final class OpenSslCertificateRequestedCallback implements CertificateRequestedCallback { - private final OpenSslEngineMap engineMap; - private final OpenSslKeyMaterialManager keyManagerHolder; - - OpenSslCertificateRequestedCallback(OpenSslEngineMap engineMap, OpenSslKeyMaterialManager keyManagerHolder) { - this.engineMap = engineMap; - this.keyManagerHolder = keyManagerHolder; - } - - @Override - public void requested(long ssl, byte[] keyTypeBytes, byte[][] asn1DerEncodedPrincipals) { - final OpenSslEngine engine = engineMap.get(ssl); - try { - final Set keyTypesSet = supportedClientKeyTypes(keyTypeBytes); - final String[] keyTypes = keyTypesSet.toArray(new String[keyTypesSet.size()]); - final X500Principal[] issuers; - if (asn1DerEncodedPrincipals == null) { - issuers = null; - } else { - issuers = new X500Principal[asn1DerEncodedPrincipals.length]; - for (int i = 0; i < asn1DerEncodedPrincipals.length; i++) { - issuers[i] = new X500Principal(asn1DerEncodedPrincipals[i]); - } - } - keyManagerHolder.setKeyMaterial(engine, keyTypes, issuers); - } catch (Throwable cause) { - logger.debug("request of key failed", cause); - SSLHandshakeException e = new SSLHandshakeException("General OpenSslEngine problem"); - e.initCause(cause); - engine.handshakeException = e; - } - } - - /** - * Gets the supported key types for client certificates. - * - * @param clientCertificateTypes {@code ClientCertificateType} values provided by the server. - * See https://www.ietf.org/assignments/tls-parameters/tls-parameters.xml. - * @return supported key types that can be used in {@code X509KeyManager.chooseClientAlias} and - * {@code X509ExtendedKeyManager.chooseEngineClientAlias}. - */ - private static Set supportedClientKeyTypes(byte[] clientCertificateTypes) { - Set result = new HashSet(clientCertificateTypes.length); - for (byte keyTypeCode : clientCertificateTypes) { - String keyType = clientKeyType(keyTypeCode); - if (keyType == null) { - // Unsupported client key type -- ignore - continue; - } - result.add(keyType); - } - return result; - } - - private static String clientKeyType(byte clientCertificateType) { - // See also http://www.ietf.org/assignments/tls-parameters/tls-parameters.xml - switch (clientCertificateType) { - case CertificateRequestedCallback.TLS_CT_RSA_SIGN: - return OpenSslKeyMaterialManager.KEY_TYPE_RSA; // RFC rsa_sign - case CertificateRequestedCallback.TLS_CT_RSA_FIXED_DH: - return OpenSslKeyMaterialManager.KEY_TYPE_DH_RSA; // RFC rsa_fixed_dh - case CertificateRequestedCallback.TLS_CT_ECDSA_SIGN: - return OpenSslKeyMaterialManager.KEY_TYPE_EC; // RFC ecdsa_sign - case CertificateRequestedCallback.TLS_CT_RSA_FIXED_ECDH: - return OpenSslKeyMaterialManager.KEY_TYPE_EC_RSA; // RFC rsa_fixed_ecdh - case CertificateRequestedCallback.TLS_CT_ECDSA_FIXED_ECDH: - return OpenSslKeyMaterialManager.KEY_TYPE_EC_EC; // RFC ecdsa_fixed_ecdh - default: - return null; - } - } - } } diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslContext.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslContext.java index f85a910e8f..5e9d36c7e1 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslContext.java @@ -15,661 +15,46 @@ */ package io.netty.handler.ssl; -import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -import io.netty.util.internal.PlatformDependent; -import io.netty.util.internal.StringUtil; -import io.netty.util.internal.SystemPropertyUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; -import org.apache.tomcat.jni.CertificateVerifier; -import org.apache.tomcat.jni.Pool; -import org.apache.tomcat.jni.SSL; -import org.apache.tomcat.jni.SSLContext; -import javax.net.ssl.KeyManager; +import java.security.cert.Certificate; + import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLException; -import javax.net.ssl.SSLHandshakeException; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509ExtendedKeyManager; -import javax.net.ssl.X509ExtendedTrustManager; -import javax.net.ssl.X509KeyManager; -import javax.net.ssl.X509TrustManager; -import java.security.AccessController; -import java.security.PrivateKey; -import java.security.PrivilegedAction; -import java.security.cert.Certificate; -import java.security.cert.CertificateExpiredException; -import java.security.cert.CertificateNotYetValidException; -import java.security.cert.CertificateRevokedException; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import static io.netty.util.internal.ObjectUtil.checkNotNull; -import static io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior; -import static io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior; - -public abstract class OpenSslContext extends SslContext { - private static final InternalLogger logger = InternalLoggerFactory.getInstance(OpenSslContext.class); - /** - * To make it easier for users to replace JDK implemention with OpenSsl version we also use - * {@code jdk.tls.rejectClientInitiatedRenegotiation} to allow disabling client initiated renegotiation. - * Java8+ uses this system property as well. - *

- * See also - * Significant SSL/TLS improvements in Java 8 - */ - private static final boolean JDK_REJECT_CLIENT_INITIATED_RENEGOTIATION = - SystemPropertyUtil.getBoolean("jdk.tls.rejectClientInitiatedRenegotiation", false); - private static final List DEFAULT_CIPHERS; - private static final Integer DH_KEY_LENGTH; - - // TODO: Maybe make configurable ? - protected static final int VERIFY_DEPTH = 10; - - /** - * The OpenSSL SSL_CTX object - */ - protected volatile long ctx; - long aprPool; - @SuppressWarnings({ "unused", "FieldMayBeFinal" }) - private volatile int aprPoolDestroyed; - private final List unmodifiableCiphers; - private final long sessionCacheSize; - private final long sessionTimeout; - private final OpenSslApplicationProtocolNegotiator apn; - private final int mode; - - final Certificate[] keyCertChain; - final ClientAuth clientAuth; - final OpenSslEngineMap engineMap = new DefaultOpenSslEngineMap(); - volatile boolean rejectRemoteInitiatedRenegotiation; - - static final OpenSslApplicationProtocolNegotiator NONE_PROTOCOL_NEGOTIATOR = - new OpenSslApplicationProtocolNegotiator() { - @Override - public ApplicationProtocolConfig.Protocol protocol() { - return ApplicationProtocolConfig.Protocol.NONE; - } - - @Override - public List protocols() { - return Collections.emptyList(); - } - - @Override - public SelectorFailureBehavior selectorFailureBehavior() { - return SelectorFailureBehavior.CHOOSE_MY_LAST_PROTOCOL; - } - - @Override - public SelectedListenerFailureBehavior selectedListenerFailureBehavior() { - return SelectedListenerFailureBehavior.ACCEPT; - } - }; - - static { - List ciphers = new ArrayList(); - // XXX: Make sure to sync this list with JdkSslEngineFactory. - Collections.addAll( - ciphers, - "ECDHE-RSA-AES128-GCM-SHA256", - "ECDHE-RSA-AES128-SHA", - "ECDHE-RSA-AES256-SHA", - "AES128-GCM-SHA256", - "AES128-SHA", - "AES256-SHA", - "DES-CBC3-SHA"); - DEFAULT_CIPHERS = Collections.unmodifiableList(ciphers); - - if (logger.isDebugEnabled()) { - logger.debug("Default cipher suite (OpenSSL): " + ciphers); - } - - Integer dhLen = null; - - try { - String dhKeySize = AccessController.doPrivileged(new PrivilegedAction() { - @Override - public String run() { - return SystemPropertyUtil.get("jdk.tls.ephemeralDHKeySize"); - } - }); - if (dhKeySize != null) { - try { - dhLen = Integer.parseInt(dhKeySize); - } catch (NumberFormatException e) { - logger.debug("OpenSslContext only support -Djdk.tls.ephemeralDHKeySize={int}, but got: " - + dhKeySize); - } - } - } catch (Throwable ignore) { - // ignore - } - DH_KEY_LENGTH = dhLen; - } +import static io.netty.util.ReferenceCountUtil.safeRelease; +/** + * This class will use a finalizer to ensure native resources are automatically cleaned up. To avoid finalizers + * and manually release the native memory see {@link ReferenceCountedOpenSslContext}. + */ +public abstract class OpenSslContext extends ReferenceCountedOpenSslContext { OpenSslContext(Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apnCfg, long sessionCacheSize, long sessionTimeout, int mode, Certificate[] keyCertChain, ClientAuth clientAuth) throws SSLException { - this(ciphers, cipherFilter, toNegotiator(apnCfg), sessionCacheSize, sessionTimeout, mode, keyCertChain, - clientAuth); + super(ciphers, cipherFilter, apnCfg, sessionCacheSize, sessionTimeout, mode, keyCertChain, + clientAuth, false); } OpenSslContext(Iterable ciphers, CipherSuiteFilter cipherFilter, OpenSslApplicationProtocolNegotiator apn, long sessionCacheSize, long sessionTimeout, int mode, Certificate[] keyCertChain, ClientAuth clientAuth) throws SSLException { - OpenSsl.ensureAvailability(); - - if (mode != SSL.SSL_MODE_SERVER && mode != SSL.SSL_MODE_CLIENT) { - throw new IllegalArgumentException("mode most be either SSL.SSL_MODE_SERVER or SSL.SSL_MODE_CLIENT"); - } - this.mode = mode; - this.clientAuth = isServer() ? checkNotNull(clientAuth, "clientAuth") : ClientAuth.NONE; - - if (mode == SSL.SSL_MODE_SERVER) { - rejectRemoteInitiatedRenegotiation = - JDK_REJECT_CLIENT_INITIATED_RENEGOTIATION; - } - this.keyCertChain = keyCertChain == null ? null : keyCertChain.clone(); - final List convertedCiphers; - if (ciphers == null) { - convertedCiphers = null; - } else { - convertedCiphers = new ArrayList(); - for (String c : ciphers) { - if (c == null) { - break; - } - - String converted = CipherSuiteConverter.toOpenSsl(c); - if (converted != null) { - c = converted; - } - convertedCiphers.add(c); - } - } - - unmodifiableCiphers = Arrays.asList(checkNotNull(cipherFilter, "cipherFilter").filterCipherSuites( - convertedCiphers, DEFAULT_CIPHERS, OpenSsl.availableCipherSuites())); - - this.apn = checkNotNull(apn, "apn"); - - // Allocate a new APR pool. - aprPool = Pool.create(0); - - // Create a new SSL_CTX and configure it. - boolean success = false; - try { - synchronized (OpenSslContext.class) { - try { - ctx = SSLContext.make(aprPool, SSL.SSL_PROTOCOL_ALL, mode); - } 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); - // Disable ticket support by default to be more inline with SSLEngineImpl of the JDK. - // This also let SSLSession.getId() work the same way for the JDK implementation and the OpenSSLEngine. - // If tickets are supported SSLSession.getId() will only return an ID on the server-side if it could - // make use of tickets. - SSLContext.setOptions(ctx, SSL.SSL_OP_NO_TICKET); - - // 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); - - if (DH_KEY_LENGTH != null) { - SSLContext.setTmpDHLength(ctx, DH_KEY_LENGTH); - } - - /* List the ciphers that are permitted to negotiate. */ - try { - SSLContext.setCipherSuite(ctx, CipherSuiteConverter.toOpenSsl(unmodifiableCiphers)); - } catch (SSLException e) { - throw e; - } catch (Exception e) { - throw new SSLException("failed to set cipher suite: " + unmodifiableCiphers, e); - } - - List nextProtoList = apn.protocols(); - /* Set next protocols for next protocol negotiation extension, if specified */ - if (!nextProtoList.isEmpty()) { - String[] protocols = nextProtoList.toArray(new String[nextProtoList.size()]); - int selectorBehavior = opensslSelectorFailureBehavior(apn.selectorFailureBehavior()); - - switch (apn.protocol()) { - case NPN: - SSLContext.setNpnProtos(ctx, protocols, selectorBehavior); - break; - case ALPN: - SSLContext.setAlpnProtos(ctx, protocols, selectorBehavior); - break; - case NPN_AND_ALPN: - SSLContext.setNpnProtos(ctx, protocols, selectorBehavior); - SSLContext.setAlpnProtos(ctx, protocols, selectorBehavior); - break; - default: - throw new Error(); - } - } - - /* Set session cache size, if specified */ - if (sessionCacheSize > 0) { - this.sessionCacheSize = sessionCacheSize; - SSLContext.setSessionCacheSize(ctx, sessionCacheSize); - } else { - // Get the default session cache size using SSLContext.setSessionCacheSize() - this.sessionCacheSize = sessionCacheSize = SSLContext.setSessionCacheSize(ctx, 20480); - // Revert the session cache size to the default value. - SSLContext.setSessionCacheSize(ctx, sessionCacheSize); - } - - /* Set session timeout, if specified */ - if (sessionTimeout > 0) { - this.sessionTimeout = sessionTimeout; - SSLContext.setSessionCacheTimeout(ctx, sessionTimeout); - } else { - // Get the default session timeout using SSLContext.setSessionCacheTimeout() - this.sessionTimeout = sessionTimeout = SSLContext.setSessionCacheTimeout(ctx, 300); - // Revert the session timeout to the default value. - SSLContext.setSessionCacheTimeout(ctx, sessionTimeout); - } - } - success = true; - } finally { - if (!success) { - destroy(); - } - } - } - - private static int opensslSelectorFailureBehavior(SelectorFailureBehavior behavior) { - switch (behavior) { - case NO_ADVERTISE: - return SSL.SSL_SELECTOR_FAILURE_NO_ADVERTISE; - case CHOOSE_MY_LAST_PROTOCOL: - return SSL.SSL_SELECTOR_FAILURE_CHOOSE_MY_LAST_PROTOCOL; - default: - throw new Error(); - } + super(ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, mode, keyCertChain, clientAuth, false); } @Override - public final List cipherSuites() { - return unmodifiableCiphers; - } - - @Override - public final long sessionCacheSize() { - return sessionCacheSize; - } - - @Override - public final long sessionTimeout() { - return sessionTimeout; - } - - @Override - public ApplicationProtocolNegotiator applicationProtocolNegotiator() { - return apn; - } - - @Override - public final boolean isClient() { - return mode == SSL.SSL_MODE_CLIENT; - } - - @Override - public final SSLEngine newEngine(ByteBufAllocator alloc, String peerHost, int peerPort) { + final SSLEngine newEngine0(ByteBufAllocator alloc, String peerHost, int peerPort) { return new OpenSslEngine(this, alloc, peerHost, peerPort); } - abstract OpenSslKeyMaterialManager keyMaterialManager(); - - /** - * Returns a new server-side {@link SSLEngine} with the current configuration. - */ - @Override - public final SSLEngine newEngine(ByteBufAllocator alloc) { - return newEngine(alloc, null, -1); - } - - /** - * Returns the pointer to the {@code SSL_CTX} object for this {@link OpenSslContext}. - * Be aware that it is freed as soon as the {@link #finalize()} method is called. - * At this point {@code 0} will be returned. - * - * @deprecated use {@link #sslCtxPointer()} - */ - @Deprecated - public final long context() { - return ctx; - } - - /** - * Returns the stats of this context. - * - * @deprecated use {@link #sessionContext#stats()} - */ - @Deprecated - public final OpenSslSessionStats stats() { - return sessionContext().stats(); - } - - /** - * Specify if remote initiated renegotiation is supported or not. If not supported and the remote side tries - * to initiate a renegotiation a {@link SSLHandshakeException} will be thrown during decoding. - */ - public void setRejectRemoteInitiatedRenegotiation(boolean rejectRemoteInitiatedRenegotiation) { - this.rejectRemoteInitiatedRenegotiation = rejectRemoteInitiatedRenegotiation; - } - @Override @SuppressWarnings("FinalizeDeclaration") protected final void finalize() throws Throwable { super.finalize(); - destroy(); - } - - /** - * Sets the SSL session ticket keys of this context. - * - * @deprecated use {@link OpenSslSessionContext#setTicketKeys(byte[])} - */ - @Deprecated - public final void setTicketKeys(byte[] keys) { - sessionContext().setTicketKeys(keys); - } - - @Override - public abstract OpenSslSessionContext sessionContext(); - - /** - * Returns the pointer to the {@code SSL_CTX} object for this {@link OpenSslContext}. - * Be aware that it is freed as soon as the {@link #finalize()} method is called. - * At this point {@code 0} will be returned. - */ - public final long sslCtxPointer() { - return ctx; - } - - // IMPORTANT: This method must only be called from either the constructor or the finalizer as a user MUST never - // get access to an OpenSslSessionContext after this method was called to prevent the user from - // producing a segfault. - final void destroy() { - synchronized (OpenSslContext.class) { - if (ctx != 0) { - SSLContext.free(ctx); - ctx = 0; - } - - // Guard against multiple destroyPools() calls triggered by construction exception and finalize() later - if (aprPool != 0) { - Pool.destroy(aprPool); - aprPool = 0; - } - } - } - - protected static X509Certificate[] certificates(byte[][] chain) { - X509Certificate[] peerCerts = new X509Certificate[chain.length]; - for (int i = 0; i < peerCerts.length; i++) { - peerCerts[i] = new OpenSslX509Certificate(chain[i]); - } - return peerCerts; - } - - protected static X509TrustManager chooseTrustManager(TrustManager[] managers) { - for (TrustManager m : managers) { - if (m instanceof X509TrustManager) { - return (X509TrustManager) m; - } - } - throw new IllegalStateException("no X509TrustManager found"); - } - - protected static X509KeyManager chooseX509KeyManager(KeyManager[] kms) { - for (KeyManager km : kms) { - if (km instanceof X509KeyManager) { - return (X509KeyManager) km; - } - } - throw new IllegalStateException("no X509KeyManager found"); - } - - /** - * Translate a {@link ApplicationProtocolConfig} object to a - * {@link OpenSslApplicationProtocolNegotiator} object. - * - * @param config The configuration which defines the translation - * @return The results of the translation - */ - static OpenSslApplicationProtocolNegotiator toNegotiator(ApplicationProtocolConfig config) { - if (config == null) { - return NONE_PROTOCOL_NEGOTIATOR; - } - - switch (config.protocol()) { - case NONE: - return NONE_PROTOCOL_NEGOTIATOR; - case ALPN: - case NPN: - case NPN_AND_ALPN: - switch (config.selectedListenerFailureBehavior()) { - case CHOOSE_MY_LAST_PROTOCOL: - case ACCEPT: - switch (config.selectorFailureBehavior()) { - case CHOOSE_MY_LAST_PROTOCOL: - case NO_ADVERTISE: - return new OpenSslDefaultApplicationProtocolNegotiator( - config); - default: - throw new UnsupportedOperationException( - new StringBuilder("OpenSSL provider does not support ") - .append(config.selectorFailureBehavior()) - .append(" behavior").toString()); - } - default: - throw new UnsupportedOperationException( - new StringBuilder("OpenSSL provider does not support ") - .append(config.selectedListenerFailureBehavior()) - .append(" behavior").toString()); - } - default: - throw new Error(); - } - } - - static boolean useExtendedTrustManager(X509TrustManager trustManager) { - return PlatformDependent.javaVersion() >= 7 && trustManager instanceof X509ExtendedTrustManager; - } - - static boolean useExtendedKeyManager(X509KeyManager keyManager) { - return PlatformDependent.javaVersion() >= 7 && keyManager instanceof X509ExtendedKeyManager; - } - - abstract static class AbstractCertificateVerifier implements CertificateVerifier { - private final OpenSslEngineMap engineMap; - - AbstractCertificateVerifier(OpenSslEngineMap engineMap) { - this.engineMap = engineMap; - } - - @Override - public final int verify(long ssl, byte[][] chain, String auth) { - X509Certificate[] peerCerts = certificates(chain); - final OpenSslEngine engine = engineMap.get(ssl); - try { - verify(engine, peerCerts, auth); - return CertificateVerifier.X509_V_OK; - } catch (Throwable cause) { - logger.debug("verification of certificate failed", cause); - SSLHandshakeException e = new SSLHandshakeException("General OpenSslEngine problem"); - e.initCause(cause); - engine.handshakeException = e; - - if (cause instanceof OpenSslCertificateException) { - return ((OpenSslCertificateException) cause).errorCode(); - } - if (cause instanceof CertificateExpiredException) { - return CertificateVerifier.X509_V_ERR_CERT_HAS_EXPIRED; - } - if (cause instanceof CertificateNotYetValidException) { - return CertificateVerifier.X509_V_ERR_CERT_NOT_YET_VALID; - } - if (PlatformDependent.javaVersion() >= 7 && cause instanceof CertificateRevokedException) { - return CertificateVerifier.X509_V_ERR_CERT_REVOKED; - } - return CertificateVerifier.X509_V_ERR_UNSPECIFIED; - } - } - - abstract void verify(OpenSslEngine engine, X509Certificate[] peerCerts, String auth) throws Exception; - } - - private static final class DefaultOpenSslEngineMap implements OpenSslEngineMap { - private final Map engines = PlatformDependent.newConcurrentHashMap(); - - @Override - public OpenSslEngine remove(long ssl) { - return engines.remove(ssl); - } - - @Override - public void add(OpenSslEngine engine) { - engines.put(engine.sslPointer(), engine); - } - - @Override - public OpenSslEngine get(long ssl) { - return engines.get(ssl); - } - } - - static void setKeyMaterial(long ctx, X509Certificate[] keyCertChain, PrivateKey key, String keyPassword) - throws SSLException { - /* Load the certificate file and private key. */ - long keyBio = 0; - long keyCertChainBio = 0; - - try { - keyCertChainBio = toBIO(keyCertChain); - keyBio = toBIO(key); - - SSLContext.setCertificateBio( - ctx, keyCertChainBio, keyBio, - keyPassword == null ? StringUtil.EMPTY_STRING : keyPassword, SSL.SSL_AIDX_RSA); - // We may have more then one cert in the chain so add all of them now. - SSLContext.setCertificateChainBio(ctx, keyCertChainBio, false); - } catch (SSLException e) { - throw e; - } catch (Exception e) { - throw new SSLException("failed to set certificate and key", e); - } finally { - if (keyBio != 0) { - SSL.freeBIO(keyBio); - } - if (keyCertChainBio != 0) { - SSL.freeBIO(keyCertChainBio); - } - } - } - /** - * Return the pointer to a in-memory BIO - * or {@code 0} if the {@code key} is {@code null}. The BIO contains the content of the {@code key}. - */ - static long toBIO(PrivateKey key) throws Exception { - if (key == null) { - return 0; - } - - ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; - PemEncoded pem = PemPrivateKey.toPEM(allocator, true, key); - try { - return toBIO(allocator, pem.retain()); - } finally { - pem.release(); - } - } - - /** - * Return the pointer to a in-memory BIO - * or {@code 0} if the {@code certChain} is {@code null}. The BIO contains the content of the {@code certChain}. - */ - static long toBIO(X509Certificate... certChain) throws Exception { - if (certChain == null) { - return 0; - } - - if (certChain.length == 0) { - throw new IllegalArgumentException("certChain can't be empty"); - } - - ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; - PemEncoded pem = PemX509Certificate.toPEM(allocator, true, certChain); - try { - return toBIO(allocator, pem.retain()); - } finally { - pem.release(); - } - } - - private static long toBIO(ByteBufAllocator allocator, PemEncoded pem) throws Exception { - try { - // We can turn direct buffers straight into BIOs. No need to - // make a yet another copy. - ByteBuf content = pem.content(); - - if (content.isDirect()) { - return newBIO(content.retainedSlice()); - } - - ByteBuf buffer = allocator.directBuffer(content.readableBytes()); - try { - buffer.writeBytes(content, content.readerIndex(), content.readableBytes()); - return newBIO(buffer.retainedSlice()); - } finally { - try { - // If the contents of the ByteBuf is sensitive (e.g. a PrivateKey) we - // need to zero out the bytes of the copy before we're releasing it. - if (pem.isSensitive()) { - SslUtils.zeroout(buffer); - } - } finally { - buffer.release(); - } - } - } finally { - pem.release(); - } - } - - private static long newBIO(ByteBuf buffer) throws Exception { - try { - long bio = SSL.newMemBIO(); - int readable = buffer.readableBytes(); - if (SSL.writeToBIO(bio, OpenSsl.memoryAddress(buffer) + buffer.readerIndex(), readable) != readable) { - SSL.freeBIO(bio); - throw new IllegalStateException("Could not write data to memory BIO"); - } - return bio; - } finally { - buffer.release(); - } + safeRelease(this); } } diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslEngine.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslEngine.java index 3e62b2924d..3ab4232629 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslEngine.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslEngine.java @@ -15,1865 +15,30 @@ */ package io.netty.handler.ssl; -import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.Unpooled; -import io.netty.util.internal.EmptyArrays; -import io.netty.util.internal.InternalThreadLocalMap; -import io.netty.util.internal.PlatformDependent; -import io.netty.util.internal.StringUtil; -import io.netty.util.internal.ThrowableUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; -import org.apache.tomcat.jni.Buffer; -import org.apache.tomcat.jni.SSL; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.nio.ByteBuffer; -import java.nio.ReadOnlyBufferException; -import java.security.Principal; -import java.security.cert.Certificate; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLEngineResult; -import javax.net.ssl.SSLEngineResult.HandshakeStatus; -import javax.net.ssl.SSLEngineResult.Status; -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLHandshakeException; -import javax.net.ssl.SSLParameters; -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.net.ssl.SSLSession; -import javax.net.ssl.SSLSessionBindingEvent; -import javax.net.ssl.SSLSessionBindingListener; -import javax.net.ssl.SSLSessionContext; -import javax.security.cert.X509Certificate; -import static io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior; -import static io.netty.handler.ssl.OpenSsl.memoryAddress; -import static io.netty.util.internal.ObjectUtil.checkNotNull; -import static javax.net.ssl.SSLEngineResult.HandshakeStatus.FINISHED; -import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NEED_UNWRAP; -import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NEED_WRAP; -import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING; -import static javax.net.ssl.SSLEngineResult.Status.BUFFER_OVERFLOW; -import static javax.net.ssl.SSLEngineResult.Status.CLOSED; -import static javax.net.ssl.SSLEngineResult.Status.OK; +import static io.netty.util.ReferenceCountUtil.safeRelease; /** * Implements a {@link SSLEngine} using * OpenSSL BIO abstractions. + *

+ * This class will use a finalizer to ensure native resources are automatically cleaned up. To avoid finalizers + * and manually release the native memory see {@link ReferenceCountedOpenSslEngine}. */ -public final class OpenSslEngine extends SSLEngine { - - private static final InternalLogger logger = InternalLoggerFactory.getInstance(OpenSslEngine.class); - - private static final Certificate[] EMPTY_CERTIFICATES = EmptyArrays.EMPTY_CERTIFICATES; - private static final X509Certificate[] EMPTY_X509_CERTIFICATES = EmptyArrays.EMPTY_JAVAX_X509_CERTIFICATES; - - private static final SSLException BEGIN_HANDSHAKE_ENGINE_CLOSED = ThrowableUtil.unknownStackTrace( - new SSLException("engine closed"), OpenSslEngine.class, "beginHandshake()"); - private static final SSLException HANDSHAKE_ENGINE_CLOSED = ThrowableUtil.unknownStackTrace( - new SSLException("engine closed"), OpenSslEngine.class, "handshake()"); - private static final SSLException RENEGOTIATION_UNSUPPORTED = ThrowableUtil.unknownStackTrace( - new SSLException("renegotiation unsupported"), OpenSslEngine.class, "beginHandshake()"); - private static final SSLException ENCRYPTED_PACKET_OVERSIZED = ThrowableUtil.unknownStackTrace( - new SSLException("encrypted packet oversized"), OpenSslEngine.class, "unwrap(...)"); - private static final Class SNI_HOSTNAME_CLASS; - private static final Method GET_SERVER_NAMES_METHOD; - private static final Method SET_SERVER_NAMES_METHOD; - private static final Method GET_ASCII_NAME_METHOD; - private static final Method GET_USE_CIPHER_SUITES_ORDER_METHOD; - private static final Method SET_USE_CIPHER_SUITES_ORDER_METHOD; - - static { - AtomicIntegerFieldUpdater destroyedUpdater = - PlatformDependent.newAtomicIntegerFieldUpdater(OpenSslEngine.class, "destroyed"); - if (destroyedUpdater == null) { - destroyedUpdater = AtomicIntegerFieldUpdater.newUpdater(OpenSslEngine.class, "destroyed"); - } - DESTROYED_UPDATER = destroyedUpdater; - - Method getUseCipherSuitesOrderMethod = null; - Method setUseCipherSuitesOrderMethod = null; - Class sniHostNameClass = null; - Method getAsciiNameMethod = null; - Method getServerNamesMethod = null; - Method setServerNamesMethod = null; - if (PlatformDependent.javaVersion() >= 8) { - try { - getUseCipherSuitesOrderMethod = SSLParameters.class.getDeclaredMethod("getUseCipherSuitesOrder"); - SSLParameters parameters = new SSLParameters(); - @SuppressWarnings("unused") - Boolean order = (Boolean) getUseCipherSuitesOrderMethod.invoke(parameters); - setUseCipherSuitesOrderMethod = SSLParameters.class.getDeclaredMethod("setUseCipherSuitesOrder", - boolean.class); - setUseCipherSuitesOrderMethod.invoke(parameters, true); - } catch (Throwable ignore) { - getUseCipherSuitesOrderMethod = null; - setUseCipherSuitesOrderMethod = null; - } - try { - sniHostNameClass = Class.forName("javax.net.ssl.SNIHostName", false, - PlatformDependent.getClassLoader(OpenSslEngine.class)); - Object sniHostName = sniHostNameClass.getConstructor(String.class).newInstance("netty.io"); - getAsciiNameMethod = sniHostNameClass.getDeclaredMethod("getAsciiName"); - @SuppressWarnings("unused") - String name = (String) getAsciiNameMethod.invoke(sniHostName); - - getServerNamesMethod = SSLParameters.class.getDeclaredMethod("getServerNames"); - setServerNamesMethod = SSLParameters.class.getDeclaredMethod("setServerNames", List.class); - SSLParameters parameters = new SSLParameters(); - @SuppressWarnings({ "rawtypes", "unused" }) - List serverNames = (List) getServerNamesMethod.invoke(parameters); - setServerNamesMethod.invoke(parameters, Collections.emptyList()); - } catch (Throwable ingore) { - sniHostNameClass = null; - getAsciiNameMethod = null; - getServerNamesMethod = null; - setServerNamesMethod = null; - } - } - GET_USE_CIPHER_SUITES_ORDER_METHOD = getUseCipherSuitesOrderMethod; - SET_USE_CIPHER_SUITES_ORDER_METHOD = setUseCipherSuitesOrderMethod; - SNI_HOSTNAME_CLASS = sniHostNameClass; - GET_ASCII_NAME_METHOD = getAsciiNameMethod; - GET_SERVER_NAMES_METHOD = getServerNamesMethod; - SET_SERVER_NAMES_METHOD = setServerNamesMethod; - } - - private static final int MAX_PLAINTEXT_LENGTH = 16 * 1024; // 2^14 - private static final int MAX_COMPRESSED_LENGTH = MAX_PLAINTEXT_LENGTH + 1024; - private static final int MAX_CIPHERTEXT_LENGTH = MAX_COMPRESSED_LENGTH + 1024; - - // Header (5) + Data (2^14) + Compression (1024) + Encryption (1024) + MAC (20) + Padding (256) - static final int MAX_ENCRYPTED_PACKET_LENGTH = MAX_CIPHERTEXT_LENGTH + 5 + 20 + 256; - - static final int MAX_ENCRYPTION_OVERHEAD_LENGTH = MAX_ENCRYPTED_PACKET_LENGTH - MAX_PLAINTEXT_LENGTH; - - private static final AtomicIntegerFieldUpdater DESTROYED_UPDATER; - - private static final String INVALID_CIPHER = "SSL_NULL_WITH_NULL_NULL"; - - private static final long EMPTY_ADDR = Buffer.address(Unpooled.EMPTY_BUFFER.nioBuffer()); - - private static final SSLEngineResult NEED_UNWRAP_OK = new SSLEngineResult(OK, NEED_UNWRAP, 0, 0); - private static final SSLEngineResult NEED_UNWRAP_CLOSED = new SSLEngineResult(CLOSED, NEED_UNWRAP, 0, 0); - private static final SSLEngineResult NEED_WRAP_OK = new SSLEngineResult(OK, NEED_WRAP, 0, 0); - private static final SSLEngineResult NEED_WRAP_CLOSED = new SSLEngineResult(CLOSED, NEED_WRAP, 0, 0); - private static final SSLEngineResult CLOSED_NOT_HANDSHAKING = new SSLEngineResult(CLOSED, NOT_HANDSHAKING, 0, 0); - - // OpenSSL state - private long ssl; - private long networkBIO; - private boolean certificateSet; - - private enum HandshakeState { - /** - * Not started yet. - */ - NOT_STARTED, - /** - * Started via unwrap/wrap. - */ - STARTED_IMPLICITLY, - /** - * Started via {@link #beginHandshake()}. - */ - STARTED_EXPLICITLY, - - /** - * Handshake is finished. - */ - FINISHED - } - - private HandshakeState handshakeState = HandshakeState.NOT_STARTED; - private boolean receivedShutdown; - private volatile int destroyed; - - private volatile ClientAuth clientAuth = ClientAuth.NONE; - - // Updated once a new handshake is started and so the SSLSession reused. - private volatile long lastAccessed = -1; - - private String endPointIdentificationAlgorithm; - // Store as object as AlgorithmConstraints only exists since java 7. - private Object algorithmConstraints; - private List sniHostNames; - - // SSL Engine status variables - private boolean isInboundDone; - private boolean isOutboundDone; - private boolean engineClosed; - - private final boolean clientMode; - private final ByteBufAllocator alloc; - private final OpenSslEngineMap engineMap; - private final OpenSslApplicationProtocolNegotiator apn; - private final boolean rejectRemoteInitiatedRenegation; - private final OpenSslSession session; - private final Certificate[] localCerts; - private final ByteBuffer[] singleSrcBuffer = new ByteBuffer[1]; - private final ByteBuffer[] singleDstBuffer = new ByteBuffer[1]; - private final OpenSslKeyMaterialManager keyMaterialManager; - - // This is package-private as we set it from OpenSslContext if an exception is thrown during - // the verification step. - SSLHandshakeException handshakeException; - +public final class OpenSslEngine extends ReferenceCountedOpenSslEngine { OpenSslEngine(OpenSslContext context, ByteBufAllocator alloc, String peerHost, int peerPort) { - super(peerHost, peerPort); - OpenSsl.ensureAvailability(); - this.alloc = checkNotNull(alloc, "alloc"); - apn = (OpenSslApplicationProtocolNegotiator) context.applicationProtocolNegotiator(); - ssl = SSL.newSSL(context.ctx, !context.isClient()); - session = new OpenSslSession(context.sessionContext()); - networkBIO = SSL.makeNetworkBIO(ssl); - clientMode = context.isClient(); - engineMap = context.engineMap; - rejectRemoteInitiatedRenegation = context.rejectRemoteInitiatedRenegotiation; - localCerts = context.keyCertChain; - - // Set the client auth mode, this needs to be done via setClientAuth(...) method so we actually call the - // needed JNI methods. - setClientAuth(clientMode ? ClientAuth.NONE : context.clientAuth); - - // Use SNI if peerHost was specified - // See https://github.com/netty/netty/issues/4746 - if (clientMode && peerHost != null) { - SSL.setTlsExtHostName(ssl, peerHost); - } - keyMaterialManager = context.keyMaterialManager(); - } - - @Override - public synchronized SSLSession getHandshakeSession() { - // Javadocs state return value should be: - // null if this instance is not currently handshaking, or if the current handshake has not - // progressed far enough to create a basic SSLSession. Otherwise, this method returns the - // SSLSession currently being negotiated. - switch(handshakeState) { - case NOT_STARTED: - case FINISHED: - return null; - default: - return session; - } - } - - /** - * Returns the pointer to the {@code SSL} object for this {@link OpenSslEngine}. - * Be aware that it is freed as soon as the {@link #finalize()} or {@link #shutdown} method is called. - * At this point {@code 0} will be returned. - */ - public synchronized long sslPointer() { - return ssl; - } - - /** - * Destroys this engine. - */ - public synchronized void shutdown() { - if (DESTROYED_UPDATER.compareAndSet(this, 0, 1)) { - engineMap.remove(ssl); - SSL.freeSSL(ssl); - SSL.freeBIO(networkBIO); - ssl = networkBIO = 0; - - // internal errors can cause shutdown without marking the engine closed - isInboundDone = isOutboundDone = engineClosed = true; - } - - // On shutdown clear all errors - SSL.clearError(); - } - - /** - * Write plaintext data to the OpenSSL internal BIO - * - * Calling this function with src.remaining == 0 is undefined. - */ - private int writePlaintextData(final ByteBuffer src) { - final int pos = src.position(); - final int limit = src.limit(); - final int len = Math.min(limit - pos, MAX_PLAINTEXT_LENGTH); - final int sslWrote; - - if (src.isDirect()) { - final long addr = Buffer.address(src) + pos; - sslWrote = SSL.writeToSSL(ssl, addr, len); - if (sslWrote > 0) { - src.position(pos + sslWrote); - } - } else { - ByteBuf buf = alloc.directBuffer(len); - try { - final long addr = memoryAddress(buf); - - src.limit(pos + len); - - buf.setBytes(0, src); - src.limit(limit); - - sslWrote = SSL.writeToSSL(ssl, addr, len); - if (sslWrote > 0) { - src.position(pos + sslWrote); - } else { - src.position(pos); - } - } finally { - buf.release(); - } - } - return sslWrote; - } - - /** - * Write encrypted data to the OpenSSL network BIO. - */ - private int writeEncryptedData(final ByteBuffer src) { - final int pos = src.position(); - final int len = src.remaining(); - final int netWrote; - if (src.isDirect()) { - final long addr = Buffer.address(src) + pos; - netWrote = SSL.writeToBIO(networkBIO, addr, len); - if (netWrote >= 0) { - src.position(pos + netWrote); - } - } else { - final ByteBuf buf = alloc.directBuffer(len); - try { - final long addr = memoryAddress(buf); - - buf.setBytes(0, src); - - netWrote = SSL.writeToBIO(networkBIO, addr, len); - if (netWrote >= 0) { - src.position(pos + netWrote); - } else { - src.position(pos); - } - } finally { - buf.release(); - } - } - - return netWrote; - } - - /** - * Read plaintext data from the OpenSSL internal BIO - */ - private int readPlaintextData(final ByteBuffer dst) { - final int sslRead; - if (dst.isDirect()) { - final int pos = dst.position(); - final long addr = Buffer.address(dst) + pos; - final int len = dst.limit() - pos; - sslRead = SSL.readFromSSL(ssl, addr, len); - if (sslRead > 0) { - dst.position(pos + sslRead); - } - } else { - final int pos = dst.position(); - final int limit = dst.limit(); - final int len = Math.min(MAX_ENCRYPTED_PACKET_LENGTH, limit - pos); - final ByteBuf buf = alloc.directBuffer(len); - try { - final long addr = memoryAddress(buf); - - sslRead = SSL.readFromSSL(ssl, addr, len); - if (sslRead > 0) { - dst.limit(pos + sslRead); - buf.getBytes(0, dst); - dst.limit(limit); - } - } finally { - buf.release(); - } - } - - return sslRead; - } - - /** - * Read encrypted data from the OpenSSL network BIO - */ - private int readEncryptedData(final ByteBuffer dst, final int pending) { - final int bioRead; - - if (dst.isDirect() && dst.remaining() >= pending) { - final int pos = dst.position(); - final long addr = Buffer.address(dst) + pos; - bioRead = SSL.readFromBIO(networkBIO, addr, pending); - if (bioRead > 0) { - dst.position(pos + bioRead); - return bioRead; - } - } else { - final ByteBuf buf = alloc.directBuffer(pending); - try { - final long addr = memoryAddress(buf); - - bioRead = SSL.readFromBIO(networkBIO, addr, pending); - if (bioRead > 0) { - int oldLimit = dst.limit(); - dst.limit(dst.position() + bioRead); - buf.getBytes(0, dst); - dst.limit(oldLimit); - return bioRead; - } - } finally { - buf.release(); - } - } - - return bioRead; - } - - private SSLEngineResult readPendingBytesFromBIO( - ByteBuffer dst, int bytesConsumed, int bytesProduced, HandshakeStatus status) throws SSLException { - // Check to see if the engine wrote data into the network BIO - int pendingNet = SSL.pendingWrittenBytesInBIO(networkBIO); - if (pendingNet > 0) { - - // Do we have enough room in dst to write encrypted data? - int capacity = dst.remaining(); - if (capacity < pendingNet) { - return new SSLEngineResult(BUFFER_OVERFLOW, - mayFinishHandshake(status != FINISHED ? getHandshakeStatus(pendingNet) : status), - bytesConsumed, bytesProduced); - } - - // Write the pending data from the network BIO into the dst buffer - int produced = readEncryptedData(dst, pendingNet); - - if (produced <= 0) { - // We ignore BIO_* errors here as we use in memory BIO anyway and will do another SSL_* call later - // on in which we will produce an exception in case of an error - SSL.clearError(); - } else { - bytesProduced += produced; - pendingNet -= produced; - } - // If isOuboundDone is set, then the data from the network BIO - // was the close_notify message -- we are not required to wait - // for the receipt the peer's close_notify message -- shutdown. - if (isOutboundDone) { - shutdown(); - } - - return new SSLEngineResult(getEngineStatus(), - mayFinishHandshake(status != FINISHED ? getHandshakeStatus(pendingNet) : status), - bytesConsumed, bytesProduced); - } - return null; - } - - @Override - public SSLEngineResult wrap( - final ByteBuffer[] srcs, final int offset, final int length, final ByteBuffer dst) throws SSLException { - // Throw required runtime exceptions - if (srcs == null) { - throw new IllegalArgumentException("srcs is null"); - } - if (dst == null) { - throw new IllegalArgumentException("dst is null"); - } - - if (offset >= srcs.length || offset + length > srcs.length) { - throw new IndexOutOfBoundsException( - "offset: " + offset + ", length: " + length + - " (expected: offset <= offset + length <= srcs.length (" + srcs.length + "))"); - } - - if (dst.isReadOnly()) { - throw new ReadOnlyBufferException(); - } - - synchronized (this) { - // Check to make sure the engine has not been closed - if (isDestroyed()) { - return CLOSED_NOT_HANDSHAKING; - } - - HandshakeStatus status = NOT_HANDSHAKING; - // Prepare OpenSSL to work in server mode and receive handshake - if (handshakeState != HandshakeState.FINISHED) { - if (handshakeState != HandshakeState.STARTED_EXPLICITLY) { - // Update accepted so we know we triggered the handshake via wrap - handshakeState = HandshakeState.STARTED_IMPLICITLY; - } - - status = handshake(); - if (status == NEED_UNWRAP) { - return NEED_UNWRAP_OK; - } - - if (engineClosed) { - return NEED_UNWRAP_CLOSED; - } - } - - // There was no pending data in the network BIO -- encrypt any application data - int bytesProduced = 0; - int bytesConsumed = 0; - int endOffset = offset + length; - for (int i = offset; i < endOffset; ++i) { - final ByteBuffer src = srcs[i]; - if (src == null) { - throw new IllegalArgumentException("srcs[" + i + "] is null"); - } - while (src.hasRemaining()) { - final SSLEngineResult pendingNetResult; - // Write plaintext application data to the SSL engine - int result = writePlaintextData(src); - if (result > 0) { - bytesConsumed += result; - - pendingNetResult = readPendingBytesFromBIO(dst, bytesConsumed, bytesProduced, status); - if (pendingNetResult != null) { - if (pendingNetResult.getStatus() != OK) { - return pendingNetResult; - } - bytesProduced = pendingNetResult.bytesProduced(); - } - } else { - int sslError = SSL.getError(ssl, result); - switch (sslError) { - case SSL.SSL_ERROR_ZERO_RETURN: - // This means the connection was shutdown correctly, close inbound and outbound - if (!receivedShutdown) { - closeAll(); - } - pendingNetResult = readPendingBytesFromBIO(dst, bytesConsumed, bytesProduced, status); - return pendingNetResult != null ? pendingNetResult : CLOSED_NOT_HANDSHAKING; - case SSL.SSL_ERROR_WANT_READ: - // If there is no pending data to read from BIO we should go back to event loop and try - // to read more data [1]. It is also possible that event loop will detect the socket - // has been closed. [1] https://www.openssl.org/docs/manmaster/ssl/SSL_write.html - pendingNetResult = readPendingBytesFromBIO(dst, bytesConsumed, bytesProduced, status); - return pendingNetResult != null ? pendingNetResult : - new SSLEngineResult(getEngineStatus(), - NEED_UNWRAP, bytesConsumed, bytesProduced); - case SSL.SSL_ERROR_WANT_WRITE: - // SSL_ERROR_WANT_WRITE typically means that the underlying transport is not writable - // and we should set the "want write" flag on the selector and try again when the - // underlying transport is writable [1]. However we are not directly writing to the - // underlying transport and instead writing to a BIO buffer. The OpenSsl documentation - // says we should do the following [1]: - // - // "When using a buffering BIO, like a BIO pair, data must be written into or retrieved - // out of the BIO before being able to continue." - // - // So we attempt to drain the BIO buffer below, but if there is no data this condition - // is undefined and we assume their is a fatal error with the openssl engine and close. - // [1] https://www.openssl.org/docs/manmaster/ssl/SSL_write.html - pendingNetResult = readPendingBytesFromBIO(dst, bytesConsumed, bytesProduced, status); - return pendingNetResult != null ? pendingNetResult : NEED_WRAP_CLOSED; - default: - // Everything else is considered as error - throw shutdownWithError("SSL_write"); - } - } - } - } - // We need to check if pendingWrittenBytesInBIO was checked yet, as we may not checked if the srcs was - // empty, or only contained empty buffers. - if (bytesConsumed == 0) { - SSLEngineResult pendingNetResult = readPendingBytesFromBIO(dst, 0, bytesProduced, status); - if (pendingNetResult != null) { - return pendingNetResult; - } - } - - return newResult(bytesConsumed, bytesProduced, status); - } - } - - /** - * Log the error, shutdown the engine and throw an exception. - */ - private SSLException shutdownWithError(String operations) { - String err = SSL.getLastError(); - return shutdownWithError(operations, err); - } - - private SSLException shutdownWithError(String operation, String err) { - if (logger.isDebugEnabled()) { - logger.debug("{} failed: OpenSSL error: {}", operation, err); - } - - // There was an internal error -- shutdown - shutdown(); - if (handshakeState == HandshakeState.FINISHED) { - return new SSLException(err); - } - return new SSLHandshakeException(err); - } - - public SSLEngineResult unwrap( - final ByteBuffer[] srcs, int srcsOffset, final int srcsLength, - final ByteBuffer[] dsts, final int dstsOffset, final int dstsLength) throws SSLException { - - // Throw required runtime exceptions - if (srcs == null) { - throw new NullPointerException("srcs"); - } - if (srcsOffset >= srcs.length - || srcsOffset + srcsLength > srcs.length) { - throw new IndexOutOfBoundsException( - "offset: " + srcsOffset + ", length: " + srcsLength + - " (expected: offset <= offset + length <= srcs.length (" + srcs.length + "))"); - } - if (dsts == null) { - throw new IllegalArgumentException("dsts is null"); - } - if (dstsOffset >= dsts.length || dstsOffset + dstsLength > dsts.length) { - throw new IndexOutOfBoundsException( - "offset: " + dstsOffset + ", length: " + dstsLength + - " (expected: offset <= offset + length <= dsts.length (" + dsts.length + "))"); - } - long capacity = 0; - final int endOffset = dstsOffset + dstsLength; - for (int i = dstsOffset; i < endOffset; i ++) { - ByteBuffer dst = dsts[i]; - if (dst == null) { - throw new IllegalArgumentException("dsts[" + i + "] is null"); - } - if (dst.isReadOnly()) { - throw new ReadOnlyBufferException(); - } - capacity += dst.remaining(); - } - - final int srcsEndOffset = srcsOffset + srcsLength; - long len = 0; - for (int i = srcsOffset; i < srcsEndOffset; i++) { - ByteBuffer src = srcs[i]; - if (src == null) { - throw new IllegalArgumentException("srcs[" + i + "] is null"); - } - len += src.remaining(); - } - - synchronized (this) { - // Check to make sure the engine has not been closed - if (isDestroyed()) { - return CLOSED_NOT_HANDSHAKING; - } - - // protect against protocol overflow attack vector - if (len > MAX_ENCRYPTED_PACKET_LENGTH) { - isInboundDone = true; - isOutboundDone = true; - engineClosed = true; - shutdown(); - throw ENCRYPTED_PACKET_OVERSIZED; - } - - HandshakeStatus status = NOT_HANDSHAKING; - // Prepare OpenSSL to work in server mode and receive handshake - if (handshakeState != HandshakeState.FINISHED) { - if (handshakeState != HandshakeState.STARTED_EXPLICITLY) { - // Update accepted so we know we triggered the handshake via wrap - handshakeState = HandshakeState.STARTED_IMPLICITLY; - } - - status = handshake(); - if (status == NEED_WRAP) { - return NEED_WRAP_OK; - } - if (engineClosed) { - return NEED_WRAP_CLOSED; - } - } - - // Write encrypted data to network BIO - int bytesConsumed = 0; - if (srcsOffset < srcsEndOffset) { - do { - ByteBuffer src = srcs[srcsOffset]; - int remaining = src.remaining(); - if (remaining == 0) { - // We must skip empty buffers as BIO_write will return 0 if asked to write something - // with length 0. - srcsOffset++; - continue; - } - int written = writeEncryptedData(src); - if (written > 0) { - bytesConsumed += written; - - if (written == remaining) { - srcsOffset++; - } else { - // We were not able to write everything into the BIO so break the write loop as otherwise - // we will produce an error on the next write attempt, which will trigger a SSL.clearError() - // later. - break; - } - } else { - // BIO_write returned a negative or zero number, this means we could not complete the write - // operation and should retry later. - // We ignore BIO_* errors here as we use in memory BIO anyway and will do another SSL_* call - // later on in which we will produce an exception in case of an error - SSL.clearError(); - break; - } - } while (srcsOffset < srcsEndOffset); - } - - // Number of produced bytes - int bytesProduced = 0; - - if (capacity > 0) { - // Write decrypted data to dsts buffers - int idx = dstsOffset; - while (idx < endOffset) { - ByteBuffer dst = dsts[idx]; - if (!dst.hasRemaining()) { - idx++; - continue; - } - - int bytesRead = readPlaintextData(dst); - - // TODO: We may want to consider if we move this check and only do it in a less often called place - // at the price of not being 100% accurate, like for example when calling SSL.getError(...). - rejectRemoteInitiatedRenegation(); - - if (bytesRead > 0) { - bytesProduced += bytesRead; - - if (!dst.hasRemaining()) { - idx++; - } else { - // We read everything return now. - return newResult(bytesConsumed, bytesProduced, status); - } - } else { - int sslError = SSL.getError(ssl, bytesRead); - switch (sslError) { - case SSL.SSL_ERROR_ZERO_RETURN: - // This means the connection was shutdown correctly, close inbound and outbound - if (!receivedShutdown) { - closeAll(); - } - // fall-trough! - case SSL.SSL_ERROR_WANT_READ: - case SSL.SSL_ERROR_WANT_WRITE: - // break to the outer loop - return newResult(bytesConsumed, bytesProduced, status); - default: - return sslReadErrorResult(SSL.getLastErrorNumber(), bytesConsumed, bytesProduced); - } - } - } - } else { - // If the capacity of all destination buffers is 0 we need to trigger a SSL_read anyway to ensure - // everything is flushed in the BIO pair and so we can detect it in the pendingAppData() call. - if (SSL.readFromSSL(ssl, EMPTY_ADDR, 0) <= 0) { - // We do not check SSL_get_error as we are not interested in any error that is not fatal. - int err = SSL.getLastErrorNumber(); - if (OpenSsl.isError(err)) { - return sslReadErrorResult(err, bytesConsumed, bytesProduced); - } - } - } - if (pendingAppData() > 0) { - // We filled all buffers but there is still some data pending in the BIO buffer, return BUFFER_OVERFLOW. - return new SSLEngineResult( - BUFFER_OVERFLOW, mayFinishHandshake(status != FINISHED ? getHandshakeStatus() : status), - bytesConsumed, bytesProduced); - } - - // Check to see if we received a close_notify message from the peer. - if (!receivedShutdown && (SSL.getShutdown(ssl) & SSL.SSL_RECEIVED_SHUTDOWN) == SSL.SSL_RECEIVED_SHUTDOWN) { - closeAll(); - } - - return newResult(bytesConsumed, bytesProduced, status); - } - } - - private SSLEngineResult sslReadErrorResult(int err, int bytesConsumed, int bytesProduced) throws SSLException { - String errStr = SSL.getErrorString(err); - - // Check if we have a pending handshakeException and if so see if we need to consume all pending data from the - // BIO first or can just shutdown and throw it now. - // This is needed so we ensure close_notify etc is correctly send to the remote peer. - // See https://github.com/netty/netty/issues/3900 - if (SSL.pendingWrittenBytesInBIO(networkBIO) > 0) { - if (handshakeException == null && handshakeState != HandshakeState.FINISHED) { - // we seems to have data left that needs to be transfered and so the user needs - // call wrap(...). Store the error so we can pick it up later. - handshakeException = new SSLHandshakeException(errStr); - } - return new SSLEngineResult(OK, NEED_WRAP, bytesConsumed, bytesProduced); - } - throw shutdownWithError("SSL_read", errStr); - } - - private int pendingAppData() { - // There won't be any application data until we're done handshaking. - // We first check handshakeFinished to eliminate the overhead of extra JNI call if possible. - return handshakeState == HandshakeState.FINISHED ? SSL.pendingReadableBytesInSSL(ssl) : 0; - } - - private SSLEngineResult newResult( - int bytesConsumed, int bytesProduced, HandshakeStatus status) throws SSLException { - return new SSLEngineResult( - getEngineStatus(), mayFinishHandshake(status != FINISHED ? getHandshakeStatus() : status) - , bytesConsumed, bytesProduced); - } - - private void closeAll() throws SSLException { - receivedShutdown = true; - closeOutbound(); - closeInbound(); - } - - private void rejectRemoteInitiatedRenegation() throws SSLHandshakeException { - if (rejectRemoteInitiatedRenegation && SSL.getHandshakeCount(ssl) > 1) { - // TODO: In future versions me may also want to send a fatal_alert to the client and so notify it - // that the renegotiation failed. - shutdown(); - throw new SSLHandshakeException("remote-initiated renegotation not allowed"); - } - } - - public SSLEngineResult unwrap(final ByteBuffer[] srcs, final ByteBuffer[] dsts) throws SSLException { - return unwrap(srcs, 0, srcs.length, dsts, 0, dsts.length); - } - - private ByteBuffer[] singleSrcBuffer(ByteBuffer src) { - singleSrcBuffer[0] = src; - return singleSrcBuffer; - } - - private void resetSingleSrcBuffer() { - singleSrcBuffer[0] = null; - } - - private ByteBuffer[] singleDstBuffer(ByteBuffer src) { - singleDstBuffer[0] = src; - return singleDstBuffer; - } - - private void resetSingleDstBuffer() { - singleDstBuffer[0] = null; - } - - @Override - public synchronized SSLEngineResult unwrap( - final ByteBuffer src, final ByteBuffer[] dsts, final int offset, final int length) throws SSLException { - try { - return unwrap(singleSrcBuffer(src), 0, 1, dsts, offset, length); - } finally { - resetSingleSrcBuffer(); - } - } - - @Override - public synchronized SSLEngineResult wrap(ByteBuffer src, ByteBuffer dst) throws SSLException { - try { - return wrap(singleSrcBuffer(src), dst); - } finally { - resetSingleSrcBuffer(); - } - } - - @Override - public synchronized SSLEngineResult unwrap(ByteBuffer src, ByteBuffer dst) throws SSLException { - try { - return unwrap(singleSrcBuffer(src), singleDstBuffer(dst)); - } finally { - resetSingleSrcBuffer(); - resetSingleDstBuffer(); - } - } - - @Override - public synchronized SSLEngineResult unwrap(ByteBuffer src, ByteBuffer[] dsts) throws SSLException { - try { - return unwrap(singleSrcBuffer(src), dsts); - } finally { - resetSingleSrcBuffer(); - } - } - - @Override - public Runnable getDelegatedTask() { - // Currently, we do not delegate SSL computation tasks - // TODO: in the future, possibly create tasks to do encrypt / decrypt async - - return null; - } - - @Override - public synchronized void closeInbound() throws SSLException { - if (isInboundDone) { - return; - } - - isInboundDone = true; - engineClosed = true; - - shutdown(); - - if (handshakeState != HandshakeState.NOT_STARTED && !receivedShutdown) { - throw new SSLException( - "Inbound closed before receiving peer's close_notify: possible truncation attack?"); - } - } - - @Override - public synchronized boolean isInboundDone() { - return isInboundDone || engineClosed; - } - - @Override - public synchronized void closeOutbound() { - if (isOutboundDone) { - return; - } - - isOutboundDone = true; - engineClosed = true; - - if (handshakeState != HandshakeState.NOT_STARTED && !isDestroyed()) { - int mode = SSL.getShutdown(ssl); - if ((mode & SSL.SSL_SENT_SHUTDOWN) != SSL.SSL_SENT_SHUTDOWN) { - int err = SSL.shutdownSSL(ssl); - if (err < 0) { - int sslErr = SSL.getError(ssl, err); - switch (sslErr) { - case SSL.SSL_ERROR_NONE: - case SSL.SSL_ERROR_WANT_ACCEPT: - case SSL.SSL_ERROR_WANT_CONNECT: - case SSL.SSL_ERROR_WANT_WRITE: - case SSL.SSL_ERROR_WANT_READ: - case SSL.SSL_ERROR_WANT_X509_LOOKUP: - case SSL.SSL_ERROR_ZERO_RETURN: - // Nothing to do here - break; - case SSL.SSL_ERROR_SYSCALL: - case SSL.SSL_ERROR_SSL: - if (logger.isDebugEnabled()) { - logger.debug("SSL_shutdown failed: OpenSSL error: {}", SSL.getLastError()); - } - // There was an internal error -- shutdown - shutdown(); - break; - default: - SSL.clearError(); - break; - } - } - } - } else { - // engine closing before initial handshake - shutdown(); - } - } - - @Override - public synchronized boolean isOutboundDone() { - return isOutboundDone; - } - - @Override - public String[] getSupportedCipherSuites() { - return OpenSsl.AVAILABLE_CIPHER_SUITES.toArray(new String[OpenSsl.AVAILABLE_CIPHER_SUITES.size()]); - } - - @Override - public String[] getEnabledCipherSuites() { - final String[] enabled; - synchronized (this) { - if (!isDestroyed()) { - enabled = SSL.getCiphers(ssl); - } else { - return EmptyArrays.EMPTY_STRINGS; - } - } - if (enabled == null) { - return EmptyArrays.EMPTY_STRINGS; - } else { - for (int i = 0; i < enabled.length; i++) { - String mapped = toJavaCipherSuite(enabled[i]); - if (mapped != null) { - enabled[i] = mapped; - } - } - return enabled; - } - } - - @Override - public void setEnabledCipherSuites(String[] cipherSuites) { - checkNotNull(cipherSuites, "cipherSuites"); - - final StringBuilder buf = new StringBuilder(); - for (String c: cipherSuites) { - if (c == null) { - break; - } - - String converted = CipherSuiteConverter.toOpenSsl(c); - if (converted == null) { - converted = c; - } - - if (!OpenSsl.isCipherSuiteAvailable(converted)) { - throw new IllegalArgumentException("unsupported cipher suite: " + c + '(' + converted + ')'); - } - - buf.append(converted); - buf.append(':'); - } - - if (buf.length() == 0) { - throw new IllegalArgumentException("empty cipher suites"); - } - buf.setLength(buf.length() - 1); - - final String cipherSuiteSpec = buf.toString(); - - synchronized (this) { - if (!isDestroyed()) { - try { - SSL.setCipherSuites(ssl, cipherSuiteSpec); - } catch (Exception e) { - throw new IllegalStateException("failed to enable cipher suites: " + cipherSuiteSpec, e); - } - } else { - throw new IllegalStateException("failed to enable cipher suites: " + cipherSuiteSpec); - } - } - } - - @Override - public String[] getSupportedProtocols() { - return OpenSsl.SUPPORTED_PROTOCOLS_SET.toArray(new String[OpenSsl.SUPPORTED_PROTOCOLS_SET.size()]); - } - - @Override - public String[] getEnabledProtocols() { - List enabled = InternalThreadLocalMap.get().arrayList(); - // Seems like there is no way to explict disable SSLv2Hello in openssl so it is always enabled - enabled.add(OpenSsl.PROTOCOL_SSL_V2_HELLO); - - int opts; - synchronized (this) { - if (!isDestroyed()) { - opts = SSL.getOptions(ssl); - } else { - return enabled.toArray(new String[1]); - } - } - if ((opts & SSL.SSL_OP_NO_TLSv1) == 0) { - enabled.add(OpenSsl.PROTOCOL_TLS_V1); - } - if ((opts & SSL.SSL_OP_NO_TLSv1_1) == 0) { - enabled.add(OpenSsl.PROTOCOL_TLS_V1_1); - } - if ((opts & SSL.SSL_OP_NO_TLSv1_2) == 0) { - enabled.add(OpenSsl.PROTOCOL_TLS_V1_2); - } - if ((opts & SSL.SSL_OP_NO_SSLv2) == 0) { - enabled.add(OpenSsl.PROTOCOL_SSL_V2); - } - if ((opts & SSL.SSL_OP_NO_SSLv3) == 0) { - enabled.add(OpenSsl.PROTOCOL_SSL_V3); - } - return enabled.toArray(new String[enabled.size()]); - } - - @Override - public void setEnabledProtocols(String[] protocols) { - if (protocols == null) { - // This is correct from the API docs - throw new IllegalArgumentException(); - } - boolean sslv2 = false; - boolean sslv3 = false; - boolean tlsv1 = false; - boolean tlsv1_1 = false; - boolean tlsv1_2 = false; - for (String p: protocols) { - if (!OpenSsl.SUPPORTED_PROTOCOLS_SET.contains(p)) { - throw new IllegalArgumentException("Protocol " + p + " is not supported."); - } - if (p.equals(OpenSsl.PROTOCOL_SSL_V2)) { - sslv2 = true; - } else if (p.equals(OpenSsl.PROTOCOL_SSL_V3)) { - sslv3 = true; - } else if (p.equals(OpenSsl.PROTOCOL_TLS_V1)) { - tlsv1 = true; - } else if (p.equals(OpenSsl.PROTOCOL_TLS_V1_1)) { - tlsv1_1 = true; - } else if (p.equals(OpenSsl.PROTOCOL_TLS_V1_2)) { - tlsv1_2 = true; - } - } - synchronized (this) { - if (!isDestroyed()) { - // Enable all and then disable what we not want - SSL.setOptions(ssl, SSL.SSL_OP_ALL); - - // Clear out options which disable protocols - SSL.clearOptions(ssl, SSL.SSL_OP_NO_SSLv2 | SSL.SSL_OP_NO_SSLv3 | SSL.SSL_OP_NO_TLSv1 | - SSL.SSL_OP_NO_TLSv1_1 | SSL.SSL_OP_NO_TLSv1_2); - - int opts = 0; - if (!sslv2) { - opts |= SSL.SSL_OP_NO_SSLv2; - } - if (!sslv3) { - opts |= SSL.SSL_OP_NO_SSLv3; - } - if (!tlsv1) { - opts |= SSL.SSL_OP_NO_TLSv1; - } - if (!tlsv1_1) { - opts |= SSL.SSL_OP_NO_TLSv1_1; - } - if (!tlsv1_2) { - opts |= SSL.SSL_OP_NO_TLSv1_2; - } - - // Disable protocols we do not want - SSL.setOptions(ssl, opts); - } else { - throw new IllegalStateException("failed to enable protocols: " + Arrays.asList(protocols)); - } - } - } - - @Override - public SSLSession getSession() { - return session; - } - - @Override - public synchronized void beginHandshake() throws SSLException { - switch (handshakeState) { - case STARTED_IMPLICITLY: - checkEngineClosed(BEGIN_HANDSHAKE_ENGINE_CLOSED); - - // A user did not start handshake by calling this method by him/herself, - // but handshake has been started already by wrap() or unwrap() implicitly. - // Because it's the user's first time to call this method, it is unfair to - // raise an exception. From the user's standpoint, he or she never asked - // for renegotiation. - - handshakeState = HandshakeState.STARTED_EXPLICITLY; // Next time this method is invoked by the user, - // we should raise an exception. - break; - case STARTED_EXPLICITLY: - // Nothing to do as the handshake is not done yet. - break; - case FINISHED: - if (clientMode) { - // Only supported for server mode at the moment. - throw RENEGOTIATION_UNSUPPORTED; - } - // For renegotiate on the server side we need to issue the following command sequence with openssl: - // - // SSL_renegotiate(ssl) - // SSL_do_handshake(ssl) - // ssl->state = SSL_ST_ACCEPT - // SSL_do_handshake(ssl) - // - // Bcause of this we fall-through to call handshake() after setting the state, as this will also take - // care of updating the internal OpenSslSession object. - // - // See also: - // https://github.com/apache/httpd/blob/2.4.16/modules/ssl/ssl_engine_kernel.c#L812 - // http://h71000.www7.hp.com/doc/83final/ba554_90007/ch04s03.html - if (SSL.renegotiate(ssl) != 1 || SSL.doHandshake(ssl) != 1) { - throw shutdownWithError("renegotiation failed"); - } - - SSL.setState(ssl, SSL.SSL_ST_ACCEPT); - - lastAccessed = System.currentTimeMillis(); - - // fall-through - case NOT_STARTED: - handshakeState = HandshakeState.STARTED_EXPLICITLY; - handshake(); - break; - default: - throw new Error(); - } - } - - private void checkEngineClosed(SSLException cause) throws SSLException { - if (engineClosed || isDestroyed()) { - throw cause; - } - } - - private static HandshakeStatus pendingStatus(int pendingStatus) { - // Depending on if there is something left in the BIO we need to WRAP or UNWRAP - return pendingStatus > 0 ? NEED_WRAP : NEED_UNWRAP; - } - - private HandshakeStatus handshake() throws SSLException { - if (handshakeState == HandshakeState.FINISHED) { - return FINISHED; - } - checkEngineClosed(HANDSHAKE_ENGINE_CLOSED); - - // Check if we have a pending handshakeException and if so see if we need to consume all pending data from the - // BIO first or can just shutdown and throw it now. - // This is needed so we ensure close_notify etc is correctly send to the remote peer. - // See https://github.com/netty/netty/issues/3900 - SSLHandshakeException exception = handshakeException; - if (exception != null) { - if (SSL.pendingWrittenBytesInBIO(networkBIO) > 0) { - // There is something pending, we need to consume it first via a WRAP so we not loose anything. - return NEED_WRAP; - } - // No more data left to send to the remote peer, so null out the exception field, shutdown and throw - // the exception. - handshakeException = null; - shutdown(); - throw exception; - } - - // Adding the OpenSslEngine to the OpenSslEngineMap so it can be used in the AbstractCertificateVerifier. - engineMap.add(this); - if (lastAccessed == -1) { - lastAccessed = System.currentTimeMillis(); - } - - if (!certificateSet && keyMaterialManager != null) { - certificateSet = true; - keyMaterialManager.setKeyMaterial(this); - } - - int code = SSL.doHandshake(ssl); - if (code <= 0) { - // Check if we have a pending exception that was created during the handshake and if so throw it after - // shutdown the connection. - if (handshakeException != null) { - exception = handshakeException; - handshakeException = null; - shutdown(); - throw exception; - } - - int sslError = SSL.getError(ssl, code); - - switch (sslError) { - case SSL.SSL_ERROR_WANT_READ: - case SSL.SSL_ERROR_WANT_WRITE: - return pendingStatus(SSL.pendingWrittenBytesInBIO(networkBIO)); - default: - // Everything else is considered as error - throw shutdownWithError("SSL_do_handshake"); - } - } - // if SSL_do_handshake returns > 0 or sslError == SSL.SSL_ERROR_NAME it means the handshake was finished. - session.handshakeFinished(); - engineMap.remove(ssl); - return FINISHED; - } - - private Status getEngineStatus() { - return engineClosed? CLOSED : OK; - } - - private SSLEngineResult.HandshakeStatus mayFinishHandshake(SSLEngineResult.HandshakeStatus status) - throws SSLException { - if (status == NOT_HANDSHAKING && handshakeState != HandshakeState.FINISHED) { - // If the status was NOT_HANDSHAKING and we not finished the handshake we need to call - // SSL_do_handshake() again - return handshake(); - } - return status; - } - - @Override - public synchronized SSLEngineResult.HandshakeStatus getHandshakeStatus() { - // Check if we are in the initial handshake phase or shutdown phase - return needPendingStatus() ? pendingStatus(SSL.pendingWrittenBytesInBIO(networkBIO)) : NOT_HANDSHAKING; - } - - private SSLEngineResult.HandshakeStatus getHandshakeStatus(int pending) { - // Check if we are in the initial handshake phase or shutdown phase - return needPendingStatus() ? pendingStatus(pending) : NOT_HANDSHAKING; - } - - private boolean needPendingStatus() { - return handshakeState != HandshakeState.NOT_STARTED && !isDestroyed() - && (handshakeState != HandshakeState.FINISHED || engineClosed); - } - - /** - * Converts the specified OpenSSL cipher suite to the Java cipher suite. - */ - private String toJavaCipherSuite(String openSslCipherSuite) { - if (openSslCipherSuite == null) { - return null; - } - - String prefix = toJavaCipherSuitePrefix(SSL.getVersion(ssl)); - return CipherSuiteConverter.toJava(openSslCipherSuite, prefix); - } - - /** - * Converts the protocol version string returned by {@link SSL#getVersion(long)} to protocol family string. - */ - private static String toJavaCipherSuitePrefix(String protocolVersion) { - final char c; - if (protocolVersion == null || protocolVersion.length() == 0) { - c = 0; - } else { - c = protocolVersion.charAt(0); - } - - switch (c) { - case 'T': - return "TLS"; - case 'S': - return "SSL"; - default: - return "UNKNOWN"; - } - } - - @Override - public void setUseClientMode(boolean clientMode) { - if (clientMode != this.clientMode) { - throw new UnsupportedOperationException(); - } - } - - @Override - public boolean getUseClientMode() { - return clientMode; - } - - @Override - public void setNeedClientAuth(boolean b) { - setClientAuth(b ? ClientAuth.REQUIRE : ClientAuth.NONE); - } - - @Override - public boolean getNeedClientAuth() { - return clientAuth == ClientAuth.REQUIRE; - } - - @Override - public void setWantClientAuth(boolean b) { - setClientAuth(b ? ClientAuth.OPTIONAL : ClientAuth.NONE); - } - - @Override - public boolean getWantClientAuth() { - return clientAuth == ClientAuth.OPTIONAL; - } - - private void setClientAuth(ClientAuth mode) { - if (clientMode) { - return; - } - synchronized (this) { - if (clientAuth == mode) { - // No need to issue any JNI calls if the mode is the same - return; - } - switch (mode) { - case NONE: - SSL.setVerify(ssl, SSL.SSL_CVERIFY_NONE, OpenSslContext.VERIFY_DEPTH); - break; - case REQUIRE: - SSL.setVerify(ssl, SSL.SSL_CVERIFY_REQUIRE, OpenSslContext.VERIFY_DEPTH); - break; - case OPTIONAL: - SSL.setVerify(ssl, SSL.SSL_CVERIFY_OPTIONAL, OpenSslContext.VERIFY_DEPTH); - break; - } - clientAuth = mode; - } - } - - @Override - public void setEnableSessionCreation(boolean b) { - if (b) { - throw new UnsupportedOperationException(); - } - } - - @Override - public boolean getEnableSessionCreation() { - return false; - } - - @Override - public synchronized SSLParameters getSSLParameters() { - SSLParameters sslParameters = super.getSSLParameters(); - - int version = PlatformDependent.javaVersion(); - if (version >= 7) { - sslParameters.setEndpointIdentificationAlgorithm(endPointIdentificationAlgorithm); - SslParametersUtils.setAlgorithmConstraints(sslParameters, algorithmConstraints); - if (version >= 8) { - if (SET_SERVER_NAMES_METHOD != null && sniHostNames != null) { - try { - SET_SERVER_NAMES_METHOD.invoke(sslParameters, sniHostNames); - } catch (IllegalAccessException e) { - throw new Error(e); - } catch (InvocationTargetException e) { - throw new Error(e); - } - } - if (SET_USE_CIPHER_SUITES_ORDER_METHOD != null && !isDestroyed()) { - try { - SET_USE_CIPHER_SUITES_ORDER_METHOD.invoke(sslParameters, - (SSL.getOptions(ssl) & SSL.SSL_OP_CIPHER_SERVER_PREFERENCE) != 0); - } catch (IllegalAccessException e) { - throw new Error(e); - } catch (InvocationTargetException e) { - throw new Error(e); - } - } - } - } - return sslParameters; - } - - @Override - public synchronized void setSSLParameters(SSLParameters sslParameters) { - super.setSSLParameters(sslParameters); - - int version = PlatformDependent.javaVersion(); - if (version >= 7) { - endPointIdentificationAlgorithm = sslParameters.getEndpointIdentificationAlgorithm(); - algorithmConstraints = sslParameters.getAlgorithmConstraints(); - if (version >= 8) { - if (SNI_HOSTNAME_CLASS != null && clientMode && !isDestroyed()) { - assert GET_SERVER_NAMES_METHOD != null; - assert GET_ASCII_NAME_METHOD != null; - try { - List servernames = (List) GET_SERVER_NAMES_METHOD.invoke(sslParameters); - if (servernames != null) { - for (Object serverName : servernames) { - if (SNI_HOSTNAME_CLASS.isInstance(serverName)) { - SSL.setTlsExtHostName(ssl, (String) GET_ASCII_NAME_METHOD.invoke(serverName)); - } else { - throw new IllegalArgumentException("Only " + SNI_HOSTNAME_CLASS.getName() - + " instances are supported, but found: " + - serverName); - } - } - } - sniHostNames = servernames; - } catch (IllegalAccessException e) { - throw new Error(e); - } catch (InvocationTargetException e) { - throw new Error(e); - } - } - if (GET_USE_CIPHER_SUITES_ORDER_METHOD != null && !isDestroyed()) { - try { - if ((Boolean) GET_USE_CIPHER_SUITES_ORDER_METHOD.invoke(sslParameters)) { - SSL.setOptions(ssl, SSL.SSL_OP_CIPHER_SERVER_PREFERENCE); - } else { - SSL.clearOptions(ssl, SSL.SSL_OP_CIPHER_SERVER_PREFERENCE); - } - } catch (IllegalAccessException e) { - throw new Error(e); - } catch (InvocationTargetException e) { - throw new Error(e); - } - } - } - } + super(context, alloc, peerHost, peerPort, false); } @Override @SuppressWarnings("FinalizeDeclaration") protected void finalize() throws Throwable { super.finalize(); - // Call shutdown as the user may have created the OpenSslEngine and not used it at all. - shutdown(); - } - - private boolean isDestroyed() { - return destroyed != 0; - } - - private final class OpenSslSession implements SSLSession, ApplicationProtocolAccessor { - private final OpenSslSessionContext sessionContext; - - // These are guarded by synchronized(OpenSslEngine.this) as handshakeFinished() may be triggered by any - // thread. - private X509Certificate[] x509PeerCerts; - private String protocol; - private String applicationProtocol; - private Certificate[] peerCerts; - private String cipher; - private byte[] id; - private long creationTime; - - // lazy init for memory reasons - private Map values; - - OpenSslSession(OpenSslSessionContext sessionContext) { - this.sessionContext = sessionContext; - } - - @Override - public byte[] getId() { - synchronized (OpenSslEngine.this) { - if (id == null) { - return EmptyArrays.EMPTY_BYTES; - } - return id.clone(); - } - } - - @Override - public SSLSessionContext getSessionContext() { - return sessionContext; - } - - @Override - public long getCreationTime() { - synchronized (OpenSslEngine.this) { - if (creationTime == 0 && !isDestroyed()) { - creationTime = SSL.getTime(ssl) * 1000L; - } - } - return creationTime; - } - - @Override - public long getLastAccessedTime() { - long lastAccessed = OpenSslEngine.this.lastAccessed; - // if lastAccessed is -1 we will just return the creation time as the handshake was not started yet. - return lastAccessed == -1 ? getCreationTime() : lastAccessed; - } - - @Override - public void invalidate() { - synchronized (OpenSslEngine.this) { - if (!isDestroyed()) { - SSL.setTimeout(ssl, 0); - } - } - } - - @Override - public boolean isValid() { - synchronized (OpenSslEngine.this) { - if (!isDestroyed()) { - return System.currentTimeMillis() - (SSL.getTimeout(ssl) * 1000L) < (SSL.getTime(ssl) * 1000L); - } - } - return false; - } - - @Override - public void putValue(String name, Object value) { - if (name == null) { - throw new NullPointerException("name"); - } - if (value == null) { - throw new NullPointerException("value"); - } - Map values = this.values; - if (values == null) { - // Use size of 2 to keep the memory overhead small - values = this.values = new HashMap(2); - } - Object old = values.put(name, value); - if (value instanceof SSLSessionBindingListener) { - ((SSLSessionBindingListener) value).valueBound(new SSLSessionBindingEvent(this, name)); - } - notifyUnbound(old, name); - } - - @Override - public Object getValue(String name) { - if (name == null) { - throw new NullPointerException("name"); - } - if (values == null) { - return null; - } - return values.get(name); - } - - @Override - public void removeValue(String name) { - if (name == null) { - throw new NullPointerException("name"); - } - Map values = this.values; - if (values == null) { - return; - } - Object old = values.remove(name); - notifyUnbound(old, name); - } - - @Override - public String[] getValueNames() { - Map values = this.values; - if (values == null || values.isEmpty()) { - return EmptyArrays.EMPTY_STRINGS; - } - return values.keySet().toArray(new String[values.size()]); - } - - private void notifyUnbound(Object value, String name) { - if (value instanceof SSLSessionBindingListener) { - ((SSLSessionBindingListener) value).valueUnbound(new SSLSessionBindingEvent(this, name)); - } - } - - /** - * Finish the handshake and so init everything in the {@link OpenSslSession} that should be accessable by - * the user. - */ - void handshakeFinished() throws SSLException { - synchronized (OpenSslEngine.this) { - if (!isDestroyed()) { - id = SSL.getSessionId(ssl); - cipher = toJavaCipherSuite(SSL.getCipherForSSL(ssl)); - protocol = SSL.getVersion(ssl); - - initPeerCerts(); - selectApplicationProtocol(); - - handshakeState = HandshakeState.FINISHED; - } else { - throw new SSLException("Already closed"); - } - } - } - - /** - * Init peer certificates that can be obtained via {@link #getPeerCertificateChain()} - * and {@link #getPeerCertificates()}. - */ - private void initPeerCerts() { - // Return the full chain from the JNI layer. - byte[][] chain = SSL.getPeerCertChain(ssl); - final byte[] clientCert; - if (!clientMode) { - // if used on the server side SSL_get_peer_cert_chain(...) will not include the remote peer - // certificate. We use SSL_get_peer_certificate to get it in this case and add it to our - // array later. - // - // See https://www.openssl.org/docs/ssl/SSL_get_peer_cert_chain.html - clientCert = SSL.getPeerCertificate(ssl); - } else { - clientCert = null; - } - - if (chain == null && clientCert == null) { - peerCerts = EMPTY_CERTIFICATES; - x509PeerCerts = EMPTY_X509_CERTIFICATES; - } else { - int len = chain != null ? chain.length : 0; - - int i = 0; - Certificate[] peerCerts; - if (clientCert != null) { - len++; - peerCerts = new Certificate[len]; - peerCerts[i++] = new OpenSslX509Certificate(clientCert); - } else { - peerCerts = new Certificate[len]; - } - if (chain != null) { - X509Certificate[] pCerts = new X509Certificate[chain.length]; - - for (int a = 0; a < pCerts.length; ++i, ++a) { - byte[] bytes = chain[a]; - pCerts[a] = new OpenSslJavaxX509Certificate(bytes); - peerCerts[i] = new OpenSslX509Certificate(bytes); - } - x509PeerCerts = pCerts; - } else { - x509PeerCerts = EMPTY_X509_CERTIFICATES; - } - this.peerCerts = peerCerts; - } - } - - /** - * Select the application protocol used. - */ - private void selectApplicationProtocol() throws SSLException { - SelectedListenerFailureBehavior behavior = apn.selectedListenerFailureBehavior(); - List protocols = apn.protocols(); - String applicationProtocol; - switch (apn.protocol()) { - case NONE: - break; - // We always need to check for applicationProtocol == null as the remote peer may not support - // the TLS extension or may have returned an empty selection. - case ALPN: - applicationProtocol = SSL.getAlpnSelected(ssl); - if (applicationProtocol != null) { - this.applicationProtocol = selectApplicationProtocol( - protocols, behavior, applicationProtocol); - } - break; - case NPN: - applicationProtocol = SSL.getNextProtoNegotiated(ssl); - if (applicationProtocol != null) { - this.applicationProtocol = selectApplicationProtocol( - protocols, behavior, applicationProtocol); - } - break; - case NPN_AND_ALPN: - applicationProtocol = SSL.getAlpnSelected(ssl); - if (applicationProtocol == null) { - applicationProtocol = SSL.getNextProtoNegotiated(ssl); - } - if (applicationProtocol != null) { - this.applicationProtocol = selectApplicationProtocol( - protocols, behavior, applicationProtocol); - } - break; - default: - throw new Error(); - } - } - - private String selectApplicationProtocol(List protocols, - SelectedListenerFailureBehavior behavior, - String applicationProtocol) throws SSLException { - if (behavior == SelectedListenerFailureBehavior.ACCEPT) { - return applicationProtocol; - } else { - int size = protocols.size(); - assert size > 0; - if (protocols.contains(applicationProtocol)) { - return applicationProtocol; - } else { - if (behavior == SelectedListenerFailureBehavior.CHOOSE_MY_LAST_PROTOCOL) { - return protocols.get(size - 1); - } else { - throw new SSLException("unknown protocol " + applicationProtocol); - } - } - } - } - - @Override - public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException { - synchronized (OpenSslEngine.this) { - if (peerCerts == null || peerCerts.length == 0) { - throw new SSLPeerUnverifiedException("peer not verified"); - } - return peerCerts; - } - } - - @Override - public Certificate[] getLocalCertificates() { - if (localCerts == null) { - return null; - } - return localCerts.clone(); - } - - @Override - public X509Certificate[] getPeerCertificateChain() throws SSLPeerUnverifiedException { - synchronized (OpenSslEngine.this) { - if (x509PeerCerts == null || x509PeerCerts.length == 0) { - throw new SSLPeerUnverifiedException("peer not verified"); - } - return x509PeerCerts; - } - } - - @Override - public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { - Certificate[] peer = getPeerCertificates(); - // No need for null or length > 0 is needed as this is done in getPeerCertificates() - // already. - return ((java.security.cert.X509Certificate) peer[0]).getSubjectX500Principal(); - } - - @Override - public Principal getLocalPrincipal() { - Certificate[] local = localCerts; - if (local == null || local.length == 0) { - return null; - } - return ((java.security.cert.X509Certificate) local[0]).getIssuerX500Principal(); - } - - @Override - public String getCipherSuite() { - synchronized (OpenSslEngine.this) { - if (cipher == null) { - return INVALID_CIPHER; - } - return cipher; - } - } - - @Override - public String getProtocol() { - String protocol = this.protocol; - if (protocol == null) { - synchronized (OpenSslEngine.this) { - if (!isDestroyed()) { - protocol = SSL.getVersion(ssl); - } else { - protocol = StringUtil.EMPTY_STRING; - } - } - } - return protocol; - } - - @Override - public String getApplicationProtocol() { - synchronized (OpenSslEngine.this) { - return applicationProtocol; - } - } - - @Override - public String getPeerHost() { - return OpenSslEngine.this.getPeerHost(); - } - - @Override - public int getPeerPort() { - return OpenSslEngine.this.getPeerPort(); - } - - @Override - public int getPacketBufferSize() { - return MAX_ENCRYPTED_PACKET_LENGTH; - } - - @Override - public int getApplicationBufferSize() { - return MAX_PLAINTEXT_LENGTH; - } + safeRelease(this); } } diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslEngineMap.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslEngineMap.java index 14e9f02bce..02131b4b26 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslEngineMap.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslEngineMap.java @@ -21,15 +21,15 @@ interface OpenSslEngineMap { * Remove the {@link OpenSslEngine} with the given {@code ssl} address and * return it. */ - OpenSslEngine remove(long ssl); + ReferenceCountedOpenSslEngine remove(long ssl); /** * Add a {@link OpenSslEngine} to this {@link OpenSslEngineMap}. */ - void add(OpenSslEngine engine); + void add(ReferenceCountedOpenSslEngine engine); /** * Get the {@link OpenSslEngine} for the given {@code ssl} address. */ - OpenSslEngine get(long ssl); + ReferenceCountedOpenSslEngine get(long ssl); } diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslExtendedKeyMaterialManager.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslExtendedKeyMaterialManager.java index 6ade0ae9d0..38f6a7f723 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslExtendedKeyMaterialManager.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslExtendedKeyMaterialManager.java @@ -28,12 +28,13 @@ final class OpenSslExtendedKeyMaterialManager extends OpenSslKeyMaterialManager } @Override - protected String chooseClientAlias(OpenSslEngine engine, String[] keyTypes, X500Principal[] issuer) { + protected String chooseClientAlias(ReferenceCountedOpenSslEngine engine, String[] keyTypes, + X500Principal[] issuer) { return keyManager.chooseEngineClientAlias(keyTypes, issuer, engine); } @Override - protected String chooseServerAlias(OpenSslEngine engine, String type) { + protected String chooseServerAlias(ReferenceCountedOpenSslEngine engine, String type) { return keyManager.chooseEngineServerAlias(type, null, engine); } } diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslKeyMaterialManager.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslKeyMaterialManager.java index e5f5fbe1de..5a297614a9 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslKeyMaterialManager.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslKeyMaterialManager.java @@ -65,7 +65,7 @@ class OpenSslKeyMaterialManager { this.password = password; } - void setKeyMaterial(OpenSslEngine engine) throws SSLException { + void setKeyMaterial(ReferenceCountedOpenSslEngine engine) throws SSLException { long ssl = engine.sslPointer(); String[] authMethods = SSL.authenticationMethods(ssl); Set aliases = new HashSet(authMethods.length); @@ -80,7 +80,8 @@ class OpenSslKeyMaterialManager { } } - void setKeyMaterial(OpenSslEngine engine, String[] keyTypes, X500Principal[] issuer) throws SSLException { + void setKeyMaterial(ReferenceCountedOpenSslEngine engine, String[] keyTypes, + X500Principal[] issuer) throws SSLException { setKeyMaterial(engine.sslPointer(), chooseClientAlias(engine, keyTypes, issuer)); } @@ -116,12 +117,12 @@ class OpenSslKeyMaterialManager { } } - protected String chooseClientAlias(@SuppressWarnings("unused") OpenSslEngine engine, + protected String chooseClientAlias(@SuppressWarnings("unused") ReferenceCountedOpenSslEngine engine, String[] keyTypes, X500Principal[] issuer) { return keyManager.chooseClientAlias(keyTypes, issuer, null); } - protected String chooseServerAlias(@SuppressWarnings("unused") OpenSslEngine engine, String type) { + protected String chooseServerAlias(@SuppressWarnings("unused") ReferenceCountedOpenSslEngine engine, String type) { return keyManager.chooseServerAlias(type, null, null); } } diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java index 82ba578c46..a71d5d35dd 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java @@ -15,30 +15,27 @@ */ package io.netty.handler.ssl; +import io.netty.handler.ssl.ReferenceCountedOpenSslServerContext.ServerContext; import org.apache.tomcat.jni.SSL; -import org.apache.tomcat.jni.SSLContext; + +import java.io.File; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLException; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509ExtendedKeyManager; -import javax.net.ssl.X509ExtendedTrustManager; -import javax.net.ssl.X509KeyManager; -import javax.net.ssl.X509TrustManager; -import java.io.File; -import java.security.KeyStore; -import java.security.PrivateKey; -import java.security.cert.X509Certificate; -import static io.netty.util.internal.ObjectUtil.*; +import static io.netty.handler.ssl.ReferenceCountedOpenSslServerContext.newSessionContext; /** * A server-side {@link SslContext} which uses OpenSSL's SSL/TLS implementation. + *

This class will use a finalizer to ensure native resources are automatically cleaned up. To avoid finalizers + * and manually release the native memory see {@link ReferenceCountedOpenSslServerContext}. */ public final class OpenSslServerContext extends OpenSslContext { - private static final byte[] ID = new byte[] {'n', 'e', 't', 't', 'y'}; private final OpenSslServerSessionContext sessionContext; private final OpenSslKeyMaterialManager keyMaterialManager; @@ -349,74 +346,14 @@ public final class OpenSslServerContext extends OpenSslContext { // Create a new SSL_CTX and configure it. boolean success = false; try { - synchronized (OpenSslContext.class) { - try { - SSLContext.setVerify(ctx, SSL.SSL_CVERIFY_NONE, VERIFY_DEPTH); - if (!OpenSsl.useKeyManagerFactory()) { - if (keyManagerFactory != null) { - throw new IllegalArgumentException( - "KeyManagerFactory not supported"); - } - - /* Set certificate verification policy. */ - SSLContext.setVerify(ctx, SSL.SSL_CVERIFY_NONE, VERIFY_DEPTH); - - setKeyMaterial(ctx, keyCertChain, key, keyPassword); - keyMaterialManager = null; - } else { - if (keyCertChain != null) { - keyManagerFactory = buildKeyManagerFactory( - keyCertChain, key, keyPassword, keyManagerFactory); - } - - if (keyManagerFactory != null) { - X509KeyManager keyManager = chooseX509KeyManager(keyManagerFactory.getKeyManagers()); - keyMaterialManager = useExtendedKeyManager(keyManager) ? - new OpenSslExtendedKeyMaterialManager( - (X509ExtendedKeyManager) keyManager, keyPassword) : - new OpenSslKeyMaterialManager(keyManager, keyPassword); - } else { - keyMaterialManager = null; - } - } - } catch (Exception e) { - throw new SSLException("failed to set certificate and key", e); - } - try { - if (trustCertCollection != null) { - trustManagerFactory = buildTrustManagerFactory(trustCertCollection, trustManagerFactory); - } else if (trustManagerFactory == null) { - // Mimic the way SSLContext.getInstance(KeyManager[], null, null) works - trustManagerFactory = TrustManagerFactory.getInstance( - TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init((KeyStore) null); - } - - final X509TrustManager manager = chooseTrustManager(trustManagerFactory.getTrustManagers()); - - // IMPORTANT: The callbacks set for verification must be static to prevent memory leak as - // otherwise the context can never be collected. This is because the JNI code holds - // a global reference to the callbacks. - // - // See https://github.com/netty/netty/issues/5372 - - // Use this to prevent an error when running on java < 7 - if (useExtendedTrustManager(manager)) { - SSLContext.setCertVerifyCallback(ctx, - new ExtendedTrustManagerVerifyCallback(engineMap, (X509ExtendedTrustManager) manager)); - } else { - SSLContext.setCertVerifyCallback(ctx, new TrustManagerVerifyCallback(engineMap, manager)); - } - } catch (Exception e) { - throw new SSLException("unable to setup trustmanager", e); - } - } - sessionContext = new OpenSslServerSessionContext(this); - sessionContext.setSessionIdContext(ID); + ServerContext context = newSessionContext(this, ctx, engineMap, trustCertCollection, trustManagerFactory, + keyCertChain, key, keyPassword, keyManagerFactory); + sessionContext = context.sessionContext; + keyMaterialManager = context.keyMaterialManager; success = true; } finally { if (!success) { - destroy(); + release(); } } } @@ -430,34 +367,4 @@ public final class OpenSslServerContext extends OpenSslContext { OpenSslKeyMaterialManager keyMaterialManager() { return keyMaterialManager; } - - private static final class TrustManagerVerifyCallback extends AbstractCertificateVerifier { - private final X509TrustManager manager; - - TrustManagerVerifyCallback(OpenSslEngineMap engineMap, X509TrustManager manager) { - super(engineMap); - this.manager = manager; - } - - @Override - void verify(OpenSslEngine engine, X509Certificate[] peerCerts, String auth) - throws Exception { - manager.checkClientTrusted(peerCerts, auth); - } - } - - private static final class ExtendedTrustManagerVerifyCallback extends AbstractCertificateVerifier { - private final X509ExtendedTrustManager manager; - - ExtendedTrustManagerVerifyCallback(OpenSslEngineMap engineMap, X509ExtendedTrustManager manager) { - super(engineMap); - this.manager = manager; - } - - @Override - void verify(OpenSslEngine engine, X509Certificate[] peerCerts, String auth) - throws Exception { - manager.checkClientTrusted(peerCerts, auth, engine); - } - } } diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslServerSessionContext.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslServerSessionContext.java index 7634897d87..c6687f6668 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslServerSessionContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslServerSessionContext.java @@ -23,7 +23,7 @@ import org.apache.tomcat.jni.SSLContext; * {@link OpenSslSessionContext} implementation which offers extra methods which are only useful for the server-side. */ public final class OpenSslServerSessionContext extends OpenSslSessionContext { - OpenSslServerSessionContext(OpenSslContext context) { + OpenSslServerSessionContext(ReferenceCountedOpenSslContext context) { super(context); } diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionContext.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionContext.java index bcc76159ff..df13d6ab4c 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionContext.java @@ -32,13 +32,13 @@ public abstract class OpenSslSessionContext implements SSLSessionContext { private static final Enumeration EMPTY = new EmptyEnumeration(); private final OpenSslSessionStats stats; - final OpenSslContext context; + final ReferenceCountedOpenSslContext context; // IMPORTANT: We take the OpenSslContext and not just the long (which points the native instance) to prevent // the GC to collect OpenSslContext as this would also free the pointer and so could result in a // segfault when the user calls any of the methods here that try to pass the pointer down to the native // level. - OpenSslSessionContext(OpenSslContext context) { + OpenSslSessionContext(ReferenceCountedOpenSslContext context) { this.context = context; stats = new OpenSslSessionStats(context); } diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionStats.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionStats.java index 26d0d33e99..ff93a04b81 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionStats.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionStats.java @@ -25,13 +25,13 @@ import org.apache.tomcat.jni.SSLContext; */ public final class OpenSslSessionStats { - private final OpenSslContext context; + private final ReferenceCountedOpenSslContext context; // IMPORTANT: We take the OpenSslContext and not just the long (which points the native instance) to prevent // the GC to collect OpenSslContext as this would also free the pointer and so could result in a // segfault when the user calls any of the methods here that try to pass the pointer down to the native // level. - OpenSslSessionStats(OpenSslContext context) { + OpenSslSessionStats(ReferenceCountedOpenSslContext context) { this.context = context; } diff --git a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslClientContext.java b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslClientContext.java new file mode 100644 index 0000000000..43c79c2002 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslClientContext.java @@ -0,0 +1,295 @@ +/* + * Copyright 2016 The Netty Project + * + * The Netty Project licenses this file to you 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 io.netty.handler.ssl; + +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; +import org.apache.tomcat.jni.CertificateRequestedCallback; +import org.apache.tomcat.jni.SSL; +import org.apache.tomcat.jni.SSLContext; + +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.HashSet; +import java.util.Set; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509ExtendedTrustManager; +import javax.net.ssl.X509KeyManager; +import javax.net.ssl.X509TrustManager; +import javax.security.auth.x500.X500Principal; + +/** + * A client-side {@link SslContext} which uses OpenSSL's SSL/TLS implementation. + *

Instances of this class must be {@link #release() released} or else native memory will leak! + */ +public final class ReferenceCountedOpenSslClientContext extends ReferenceCountedOpenSslContext { + private static final InternalLogger logger = + InternalLoggerFactory.getInstance(ReferenceCountedOpenSslClientContext.class); + private final OpenSslSessionContext sessionContext; + + ReferenceCountedOpenSslClientContext(X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory, + X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, + KeyManagerFactory keyManagerFactory, Iterable ciphers, + CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, + long sessionCacheSize, long sessionTimeout) + throws SSLException { + super(ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, SSL.SSL_MODE_CLIENT, keyCertChain, + ClientAuth.NONE, true); + boolean success = false; + try { + sessionContext = newSessionContext(this, ctx, engineMap, trustCertCollection, trustManagerFactory, + keyCertChain, key, keyPassword, keyManagerFactory); + success = true; + } finally { + if (!success) { + release(); + } + } + } + + @Override + OpenSslKeyMaterialManager keyMaterialManager() { + return null; + } + + @Override + public OpenSslSessionContext sessionContext() { + return sessionContext; + } + + static OpenSslSessionContext newSessionContext(ReferenceCountedOpenSslContext thiz, long ctx, + OpenSslEngineMap engineMap, + X509Certificate[] trustCertCollection, + TrustManagerFactory trustManagerFactory, + X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, + KeyManagerFactory keyManagerFactory) throws SSLException { + if (key == null && keyCertChain != null || key != null && keyCertChain == null) { + throw new IllegalArgumentException( + "Either both keyCertChain and key needs to be null or none of them"); + } + synchronized (ReferenceCountedOpenSslContext.class) { + try { + if (!OpenSsl.useKeyManagerFactory()) { + if (keyManagerFactory != null) { + throw new IllegalArgumentException( + "KeyManagerFactory not supported"); + } + if (keyCertChain != null/* && key != null*/) { + setKeyMaterial(ctx, keyCertChain, key, keyPassword); + } + } else { + // javadocs state that keyManagerFactory has precedent over keyCertChain + if (keyManagerFactory == null && keyCertChain != null) { + keyManagerFactory = buildKeyManagerFactory( + keyCertChain, key, keyPassword, keyManagerFactory); + } + + if (keyManagerFactory != null) { + X509KeyManager keyManager = chooseX509KeyManager(keyManagerFactory.getKeyManagers()); + OpenSslKeyMaterialManager materialManager = useExtendedKeyManager(keyManager) ? + new OpenSslExtendedKeyMaterialManager( + (X509ExtendedKeyManager) keyManager, keyPassword) : + new OpenSslKeyMaterialManager(keyManager, keyPassword); + SSLContext.setCertRequestedCallback(ctx, new OpenSslCertificateRequestedCallback( + engineMap, materialManager)); + } + } + } catch (Exception e) { + throw new SSLException("failed to set certificate and key", e); + } + + SSLContext.setVerify(ctx, SSL.SSL_VERIFY_NONE, VERIFY_DEPTH); + + try { + if (trustCertCollection != null) { + trustManagerFactory = buildTrustManagerFactory(trustCertCollection, trustManagerFactory); + } else if (trustManagerFactory == null) { + trustManagerFactory = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init((KeyStore) null); + } + final X509TrustManager manager = chooseTrustManager(trustManagerFactory.getTrustManagers()); + + // IMPORTANT: The callbacks set for verification must be static to prevent memory leak as + // otherwise the context can never be collected. This is because the JNI code holds + // a global reference to the callbacks. + // + // See https://github.com/netty/netty/issues/5372 + + // Use this to prevent an error when running on java < 7 + if (useExtendedTrustManager(manager)) { + SSLContext.setCertVerifyCallback(ctx, + new ExtendedTrustManagerVerifyCallback(engineMap, (X509ExtendedTrustManager) manager)); + } else { + SSLContext.setCertVerifyCallback(ctx, new TrustManagerVerifyCallback(engineMap, manager)); + } + } catch (Exception e) { + throw new SSLException("unable to setup trustmanager", e); + } + } + return new OpenSslClientSessionContext(thiz); + } + + // No cache is currently supported for client side mode. + static final class OpenSslClientSessionContext extends OpenSslSessionContext { + OpenSslClientSessionContext(ReferenceCountedOpenSslContext context) { + super(context); + } + + @Override + public void setSessionTimeout(int seconds) { + if (seconds < 0) { + throw new IllegalArgumentException(); + } + } + + @Override + public int getSessionTimeout() { + return 0; + } + + @Override + public void setSessionCacheSize(int size) { + if (size < 0) { + throw new IllegalArgumentException(); + } + } + + @Override + public int getSessionCacheSize() { + return 0; + } + + @Override + public void setSessionCacheEnabled(boolean enabled) { + // ignored + } + + @Override + public boolean isSessionCacheEnabled() { + return false; + } + } + + private static final class TrustManagerVerifyCallback extends AbstractCertificateVerifier { + private final X509TrustManager manager; + + TrustManagerVerifyCallback(OpenSslEngineMap engineMap, X509TrustManager manager) { + super(engineMap); + this.manager = manager; + } + + @Override + void verify(ReferenceCountedOpenSslEngine engine, X509Certificate[] peerCerts, String auth) + throws Exception { + manager.checkServerTrusted(peerCerts, auth); + } + } + + private static final class ExtendedTrustManagerVerifyCallback extends AbstractCertificateVerifier { + private final X509ExtendedTrustManager manager; + + ExtendedTrustManagerVerifyCallback(OpenSslEngineMap engineMap, X509ExtendedTrustManager manager) { + super(engineMap); + this.manager = manager; + } + + @Override + void verify(ReferenceCountedOpenSslEngine engine, X509Certificate[] peerCerts, String auth) + throws Exception { + manager.checkServerTrusted(peerCerts, auth, engine); + } + } + + private static final class OpenSslCertificateRequestedCallback implements CertificateRequestedCallback { + private final OpenSslEngineMap engineMap; + private final OpenSslKeyMaterialManager keyManagerHolder; + + OpenSslCertificateRequestedCallback(OpenSslEngineMap engineMap, OpenSslKeyMaterialManager keyManagerHolder) { + this.engineMap = engineMap; + this.keyManagerHolder = keyManagerHolder; + } + + @Override + public void requested(long ssl, byte[] keyTypeBytes, byte[][] asn1DerEncodedPrincipals) { + final ReferenceCountedOpenSslEngine engine = engineMap.get(ssl); + try { + final Set keyTypesSet = supportedClientKeyTypes(keyTypeBytes); + final String[] keyTypes = keyTypesSet.toArray(new String[keyTypesSet.size()]); + final X500Principal[] issuers; + if (asn1DerEncodedPrincipals == null) { + issuers = null; + } else { + issuers = new X500Principal[asn1DerEncodedPrincipals.length]; + for (int i = 0; i < asn1DerEncodedPrincipals.length; i++) { + issuers[i] = new X500Principal(asn1DerEncodedPrincipals[i]); + } + } + keyManagerHolder.setKeyMaterial(engine, keyTypes, issuers); + } catch (Throwable cause) { + logger.debug("request of key failed", cause); + SSLHandshakeException e = new SSLHandshakeException("General OpenSslEngine problem"); + e.initCause(cause); + engine.handshakeException = e; + } + } + + /** + * Gets the supported key types for client certificates. + * + * @param clientCertificateTypes {@code ClientCertificateType} values provided by the server. + * See https://www.ietf.org/assignments/tls-parameters/tls-parameters.xml. + * @return supported key types that can be used in {@code X509KeyManager.chooseClientAlias} and + * {@code X509ExtendedKeyManager.chooseEngineClientAlias}. + */ + private static Set supportedClientKeyTypes(byte[] clientCertificateTypes) { + Set result = new HashSet(clientCertificateTypes.length); + for (byte keyTypeCode : clientCertificateTypes) { + String keyType = clientKeyType(keyTypeCode); + if (keyType == null) { + // Unsupported client key type -- ignore + continue; + } + result.add(keyType); + } + return result; + } + + private static String clientKeyType(byte clientCertificateType) { + // See also http://www.ietf.org/assignments/tls-parameters/tls-parameters.xml + switch (clientCertificateType) { + case CertificateRequestedCallback.TLS_CT_RSA_SIGN: + return OpenSslKeyMaterialManager.KEY_TYPE_RSA; // RFC rsa_sign + case CertificateRequestedCallback.TLS_CT_RSA_FIXED_DH: + return OpenSslKeyMaterialManager.KEY_TYPE_DH_RSA; // RFC rsa_fixed_dh + case CertificateRequestedCallback.TLS_CT_ECDSA_SIGN: + return OpenSslKeyMaterialManager.KEY_TYPE_EC; // RFC ecdsa_sign + case CertificateRequestedCallback.TLS_CT_RSA_FIXED_ECDH: + return OpenSslKeyMaterialManager.KEY_TYPE_EC_RSA; // RFC rsa_fixed_ecdh + case CertificateRequestedCallback.TLS_CT_ECDSA_FIXED_ECDH: + return OpenSslKeyMaterialManager.KEY_TYPE_EC_EC; // RFC ecdsa_fixed_ecdh + default: + return null; + } + } + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslContext.java b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslContext.java new file mode 100644 index 0000000000..1d1dbfdbc6 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslContext.java @@ -0,0 +1,746 @@ +/* + * Copyright 2016 The Netty Project + * + * The Netty Project licenses this file to you 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 io.netty.handler.ssl; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.util.AbstractReferenceCounted; +import io.netty.util.ReferenceCounted; +import io.netty.util.ResourceLeak; +import io.netty.util.ResourceLeakDetector; +import io.netty.util.ResourceLeakDetectorFactory; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.StringUtil; +import io.netty.util.internal.SystemPropertyUtil; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; +import org.apache.tomcat.jni.CertificateVerifier; +import org.apache.tomcat.jni.Pool; +import org.apache.tomcat.jni.SSL; +import org.apache.tomcat.jni.SSLContext; + +import java.security.AccessController; +import java.security.PrivateKey; +import java.security.PrivilegedAction; +import java.security.cert.Certificate; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.CertificateRevokedException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509ExtendedTrustManager; +import javax.net.ssl.X509KeyManager; +import javax.net.ssl.X509TrustManager; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +/** + * An implementation of {@link SslContext} which works with libraries that support the + * OpenSsl C library API. + *

Instances of this class must be {@link #release() released} or else native memory will leak! + */ +public abstract class ReferenceCountedOpenSslContext extends SslContext implements ReferenceCounted { + private static final InternalLogger logger = + InternalLoggerFactory.getInstance(ReferenceCountedOpenSslContext.class); + /** + * To make it easier for users to replace JDK implemention with OpenSsl version we also use + * {@code jdk.tls.rejectClientInitiatedRenegotiation} to allow disabling client initiated renegotiation. + * Java8+ uses this system property as well. + *

+ * See also + * Significant SSL/TLS improvements in Java 8 + */ + private static final boolean JDK_REJECT_CLIENT_INITIATED_RENEGOTIATION = + SystemPropertyUtil.getBoolean("jdk.tls.rejectClientInitiatedRenegotiation", false); + private static final List DEFAULT_CIPHERS; + private static final Integer DH_KEY_LENGTH; + private static final ResourceLeakDetector leakDetector = + ResourceLeakDetectorFactory.instance().newResourceLeakDetector(ReferenceCountedOpenSslContext.class); + + // TODO: Maybe make configurable ? + protected static final int VERIFY_DEPTH = 10; + + /** + * The OpenSSL SSL_CTX object + */ + protected volatile long ctx; + long aprPool; + @SuppressWarnings({ "unused", "FieldMayBeFinal" }) + private volatile int aprPoolDestroyed; + private final List unmodifiableCiphers; + private final long sessionCacheSize; + private final long sessionTimeout; + private final OpenSslApplicationProtocolNegotiator apn; + private final int mode; + + // Reference Counting + private final ResourceLeak leak; + private final AbstractReferenceCounted refCnt = new AbstractReferenceCounted() { + @Override + public ReferenceCounted touch(Object hint) { + if (leak != null) { + leak.record(hint); + } + + return ReferenceCountedOpenSslContext.this; + } + + @Override + protected void deallocate() { + destroy(); + if (leak != null) { + leak.close(); + } + } + }; + + final Certificate[] keyCertChain; + final ClientAuth clientAuth; + final OpenSslEngineMap engineMap = new DefaultOpenSslEngineMap(); + volatile boolean rejectRemoteInitiatedRenegotiation; + + static final OpenSslApplicationProtocolNegotiator NONE_PROTOCOL_NEGOTIATOR = + new OpenSslApplicationProtocolNegotiator() { + @Override + public ApplicationProtocolConfig.Protocol protocol() { + return ApplicationProtocolConfig.Protocol.NONE; + } + + @Override + public List protocols() { + return Collections.emptyList(); + } + + @Override + public ApplicationProtocolConfig.SelectorFailureBehavior selectorFailureBehavior() { + return ApplicationProtocolConfig.SelectorFailureBehavior.CHOOSE_MY_LAST_PROTOCOL; + } + + @Override + public ApplicationProtocolConfig.SelectedListenerFailureBehavior selectedListenerFailureBehavior() { + return ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT; + } + }; + + static { + List ciphers = new ArrayList(); + // XXX: Make sure to sync this list with JdkSslEngineFactory. + Collections.addAll( + ciphers, + "ECDHE-RSA-AES128-GCM-SHA256", + "ECDHE-RSA-AES128-SHA", + "ECDHE-RSA-AES256-SHA", + "AES128-GCM-SHA256", + "AES128-SHA", + "AES256-SHA", + "DES-CBC3-SHA"); + DEFAULT_CIPHERS = Collections.unmodifiableList(ciphers); + + if (logger.isDebugEnabled()) { + logger.debug("Default cipher suite (OpenSSL): " + ciphers); + } + + Integer dhLen = null; + + try { + String dhKeySize = AccessController.doPrivileged(new PrivilegedAction() { + @Override + public String run() { + return SystemPropertyUtil.get("jdk.tls.ephemeralDHKeySize"); + } + }); + if (dhKeySize != null) { + try { + dhLen = Integer.valueOf(dhKeySize); + } catch (NumberFormatException e) { + logger.debug("ReferenceCountedOpenSslContext supports -Djdk.tls.ephemeralDHKeySize={int}, but got: " + + dhKeySize); + } + } + } catch (Throwable ignore) { + // ignore + } + DH_KEY_LENGTH = dhLen; + } + + ReferenceCountedOpenSslContext(Iterable ciphers, CipherSuiteFilter cipherFilter, + ApplicationProtocolConfig apnCfg, long sessionCacheSize, long sessionTimeout, + int mode, Certificate[] keyCertChain, ClientAuth clientAuth, boolean leakDetection) + throws SSLException { + this(ciphers, cipherFilter, toNegotiator(apnCfg), sessionCacheSize, sessionTimeout, mode, keyCertChain, + clientAuth, leakDetection); + } + + ReferenceCountedOpenSslContext(Iterable ciphers, CipherSuiteFilter cipherFilter, + OpenSslApplicationProtocolNegotiator apn, long sessionCacheSize, + long sessionTimeout, int mode, Certificate[] keyCertChain, + ClientAuth clientAuth, boolean leakDetection) throws SSLException { + OpenSsl.ensureAvailability(); + + if (mode != SSL.SSL_MODE_SERVER && mode != SSL.SSL_MODE_CLIENT) { + throw new IllegalArgumentException("mode most be either SSL.SSL_MODE_SERVER or SSL.SSL_MODE_CLIENT"); + } + leak = leakDetection ? leakDetector.open(this) : null; + this.mode = mode; + this.clientAuth = isServer() ? checkNotNull(clientAuth, "clientAuth") : ClientAuth.NONE; + + if (mode == SSL.SSL_MODE_SERVER) { + rejectRemoteInitiatedRenegotiation = + JDK_REJECT_CLIENT_INITIATED_RENEGOTIATION; + } + this.keyCertChain = keyCertChain == null ? null : keyCertChain.clone(); + final List convertedCiphers; + if (ciphers == null) { + convertedCiphers = null; + } else { + convertedCiphers = new ArrayList(); + for (String c : ciphers) { + if (c == null) { + break; + } + + String converted = CipherSuiteConverter.toOpenSsl(c); + if (converted != null) { + c = converted; + } + convertedCiphers.add(c); + } + } + + unmodifiableCiphers = Arrays.asList(checkNotNull(cipherFilter, "cipherFilter").filterCipherSuites( + convertedCiphers, DEFAULT_CIPHERS, OpenSsl.availableCipherSuites())); + + this.apn = checkNotNull(apn, "apn"); + + // Allocate a new APR pool. + aprPool = Pool.create(0); + + // Create a new SSL_CTX and configure it. + boolean success = false; + try { + synchronized (ReferenceCountedOpenSslContext.class) { + try { + ctx = SSLContext.make(aprPool, SSL.SSL_PROTOCOL_ALL, mode); + } 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); + // Disable ticket support by default to be more inline with SSLEngineImpl of the JDK. + // This also let SSLSession.getId() work the same way for the JDK implementation and the OpenSSLEngine. + // If tickets are supported SSLSession.getId() will only return an ID on the server-side if it could + // make use of tickets. + SSLContext.setOptions(ctx, SSL.SSL_OP_NO_TICKET); + + // 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); + + if (DH_KEY_LENGTH != null) { + SSLContext.setTmpDHLength(ctx, DH_KEY_LENGTH); + } + + /* List the ciphers that are permitted to negotiate. */ + try { + SSLContext.setCipherSuite(ctx, CipherSuiteConverter.toOpenSsl(unmodifiableCiphers)); + } catch (SSLException e) { + throw e; + } catch (Exception e) { + throw new SSLException("failed to set cipher suite: " + unmodifiableCiphers, e); + } + + List nextProtoList = apn.protocols(); + /* Set next protocols for next protocol negotiation extension, if specified */ + if (!nextProtoList.isEmpty()) { + String[] protocols = nextProtoList.toArray(new String[nextProtoList.size()]); + int selectorBehavior = opensslSelectorFailureBehavior(apn.selectorFailureBehavior()); + + switch (apn.protocol()) { + case NPN: + SSLContext.setNpnProtos(ctx, protocols, selectorBehavior); + break; + case ALPN: + SSLContext.setAlpnProtos(ctx, protocols, selectorBehavior); + break; + case NPN_AND_ALPN: + SSLContext.setNpnProtos(ctx, protocols, selectorBehavior); + SSLContext.setAlpnProtos(ctx, protocols, selectorBehavior); + break; + default: + throw new Error(); + } + } + + /* Set session cache size, if specified */ + if (sessionCacheSize > 0) { + this.sessionCacheSize = sessionCacheSize; + SSLContext.setSessionCacheSize(ctx, sessionCacheSize); + } else { + // Get the default session cache size using SSLContext.setSessionCacheSize() + this.sessionCacheSize = sessionCacheSize = SSLContext.setSessionCacheSize(ctx, 20480); + // Revert the session cache size to the default value. + SSLContext.setSessionCacheSize(ctx, sessionCacheSize); + } + + /* Set session timeout, if specified */ + if (sessionTimeout > 0) { + this.sessionTimeout = sessionTimeout; + SSLContext.setSessionCacheTimeout(ctx, sessionTimeout); + } else { + // Get the default session timeout using SSLContext.setSessionCacheTimeout() + this.sessionTimeout = sessionTimeout = SSLContext.setSessionCacheTimeout(ctx, 300); + // Revert the session timeout to the default value. + SSLContext.setSessionCacheTimeout(ctx, sessionTimeout); + } + } + success = true; + } finally { + if (!success) { + release(); + } + } + } + + private static int opensslSelectorFailureBehavior(ApplicationProtocolConfig.SelectorFailureBehavior behavior) { + switch (behavior) { + case NO_ADVERTISE: + return SSL.SSL_SELECTOR_FAILURE_NO_ADVERTISE; + case CHOOSE_MY_LAST_PROTOCOL: + return SSL.SSL_SELECTOR_FAILURE_CHOOSE_MY_LAST_PROTOCOL; + default: + throw new Error(); + } + } + + @Override + public final List cipherSuites() { + return unmodifiableCiphers; + } + + @Override + public final long sessionCacheSize() { + return sessionCacheSize; + } + + @Override + public final long sessionTimeout() { + return sessionTimeout; + } + + @Override + public ApplicationProtocolNegotiator applicationProtocolNegotiator() { + return apn; + } + + @Override + public final boolean isClient() { + return mode == SSL.SSL_MODE_CLIENT; + } + + @Override + public final SSLEngine newEngine(ByteBufAllocator alloc, String peerHost, int peerPort) { + return newEngine0(alloc, peerHost, peerPort); + } + + SSLEngine newEngine0(ByteBufAllocator alloc, String peerHost, int peerPort) { + return new ReferenceCountedOpenSslEngine(this, alloc, peerHost, peerPort, true); + } + + abstract OpenSslKeyMaterialManager keyMaterialManager(); + + /** + * Returns a new server-side {@link SSLEngine} with the current configuration. + */ + @Override + public final SSLEngine newEngine(ByteBufAllocator alloc) { + return newEngine(alloc, null, -1); + } + + /** + * Returns the pointer to the {@code SSL_CTX} object for this {@link ReferenceCountedOpenSslContext}. + * Be aware that it is freed as soon as the {@link #finalize()} method is called. + * At this point {@code 0} will be returned. + * + * @deprecated use {@link #sslCtxPointer()} + */ + @Deprecated + public final long context() { + return ctx; + } + + /** + * Returns the stats of this context. + * + * @deprecated use {@link #sessionContext#stats()} + */ + @Deprecated + public final OpenSslSessionStats stats() { + return sessionContext().stats(); + } + + /** + * Specify if remote initiated renegotiation is supported or not. If not supported and the remote side tries + * to initiate a renegotiation a {@link SSLHandshakeException} will be thrown during decoding. + */ + public void setRejectRemoteInitiatedRenegotiation(boolean rejectRemoteInitiatedRenegotiation) { + this.rejectRemoteInitiatedRenegotiation = rejectRemoteInitiatedRenegotiation; + } + + /** + * Sets the SSL session ticket keys of this context. + * + * @deprecated use {@link OpenSslSessionContext#setTicketKeys(byte[])} + */ + @Deprecated + public final void setTicketKeys(byte[] keys) { + sessionContext().setTicketKeys(keys); + } + + @Override + public abstract OpenSslSessionContext sessionContext(); + + /** + * Returns the pointer to the {@code SSL_CTX} object for this {@link ReferenceCountedOpenSslContext}. + * Be aware that it is freed as soon as the {@link #release()} method is called. + * At this point {@code 0} will be returned. + */ + public final long sslCtxPointer() { + return ctx; + } + + // IMPORTANT: This method must only be called from either the constructor or the finalizer as a user MUST never + // get access to an OpenSslSessionContext after this method was called to prevent the user from + // producing a segfault. + final void destroy() { + synchronized (ReferenceCountedOpenSslContext.class) { + if (ctx != 0) { + SSLContext.free(ctx); + ctx = 0; + } + + // Guard against multiple destroyPools() calls triggered by construction exception and finalize() later + if (aprPool != 0) { + Pool.destroy(aprPool); + aprPool = 0; + } + } + } + + protected static X509Certificate[] certificates(byte[][] chain) { + X509Certificate[] peerCerts = new X509Certificate[chain.length]; + for (int i = 0; i < peerCerts.length; i++) { + peerCerts[i] = new OpenSslX509Certificate(chain[i]); + } + return peerCerts; + } + + protected static X509TrustManager chooseTrustManager(TrustManager[] managers) { + for (TrustManager m : managers) { + if (m instanceof X509TrustManager) { + return (X509TrustManager) m; + } + } + throw new IllegalStateException("no X509TrustManager found"); + } + + protected static X509KeyManager chooseX509KeyManager(KeyManager[] kms) { + for (KeyManager km : kms) { + if (km instanceof X509KeyManager) { + return (X509KeyManager) km; + } + } + throw new IllegalStateException("no X509KeyManager found"); + } + + /** + * Translate a {@link ApplicationProtocolConfig} object to a + * {@link OpenSslApplicationProtocolNegotiator} object. + * + * @param config The configuration which defines the translation + * @return The results of the translation + */ + static OpenSslApplicationProtocolNegotiator toNegotiator(ApplicationProtocolConfig config) { + if (config == null) { + return NONE_PROTOCOL_NEGOTIATOR; + } + + switch (config.protocol()) { + case NONE: + return NONE_PROTOCOL_NEGOTIATOR; + case ALPN: + case NPN: + case NPN_AND_ALPN: + switch (config.selectedListenerFailureBehavior()) { + case CHOOSE_MY_LAST_PROTOCOL: + case ACCEPT: + switch (config.selectorFailureBehavior()) { + case CHOOSE_MY_LAST_PROTOCOL: + case NO_ADVERTISE: + return new OpenSslDefaultApplicationProtocolNegotiator( + config); + default: + throw new UnsupportedOperationException( + new StringBuilder("OpenSSL provider does not support ") + .append(config.selectorFailureBehavior()) + .append(" behavior").toString()); + } + default: + throw new UnsupportedOperationException( + new StringBuilder("OpenSSL provider does not support ") + .append(config.selectedListenerFailureBehavior()) + .append(" behavior").toString()); + } + default: + throw new Error(); + } + } + + static boolean useExtendedTrustManager(X509TrustManager trustManager) { + return PlatformDependent.javaVersion() >= 7 && trustManager instanceof X509ExtendedTrustManager; + } + + static boolean useExtendedKeyManager(X509KeyManager keyManager) { + return PlatformDependent.javaVersion() >= 7 && keyManager instanceof X509ExtendedKeyManager; + } + + @Override + public final int refCnt() { + return refCnt.refCnt(); + } + + @Override + public final ReferenceCounted retain() { + refCnt.retain(); + return this; + } + + @Override + public final ReferenceCounted retain(int increment) { + refCnt.retain(increment); + return this; + } + + @Override + public final ReferenceCounted touch() { + refCnt.touch(); + return this; + } + + @Override + public final ReferenceCounted touch(Object hint) { + refCnt.touch(hint); + return this; + } + + @Override + public final boolean release() { + return refCnt.release(); + } + + @Override + public final boolean release(int decrement) { + return refCnt.release(decrement); + } + + abstract static class AbstractCertificateVerifier implements CertificateVerifier { + private final OpenSslEngineMap engineMap; + + AbstractCertificateVerifier(OpenSslEngineMap engineMap) { + this.engineMap = engineMap; + } + + @Override + public final int verify(long ssl, byte[][] chain, String auth) { + X509Certificate[] peerCerts = certificates(chain); + final ReferenceCountedOpenSslEngine engine = engineMap.get(ssl); + try { + verify(engine, peerCerts, auth); + return CertificateVerifier.X509_V_OK; + } catch (Throwable cause) { + logger.debug("verification of certificate failed", cause); + SSLHandshakeException e = new SSLHandshakeException("General OpenSslEngine problem"); + e.initCause(cause); + engine.handshakeException = e; + + if (cause instanceof OpenSslCertificateException) { + return ((OpenSslCertificateException) cause).errorCode(); + } + if (cause instanceof CertificateExpiredException) { + return CertificateVerifier.X509_V_ERR_CERT_HAS_EXPIRED; + } + if (cause instanceof CertificateNotYetValidException) { + return CertificateVerifier.X509_V_ERR_CERT_NOT_YET_VALID; + } + if (PlatformDependent.javaVersion() >= 7 && cause instanceof CertificateRevokedException) { + return CertificateVerifier.X509_V_ERR_CERT_REVOKED; + } + return CertificateVerifier.X509_V_ERR_UNSPECIFIED; + } + } + + abstract void verify(ReferenceCountedOpenSslEngine engine, X509Certificate[] peerCerts, + String auth) throws Exception; + } + + private static final class DefaultOpenSslEngineMap implements OpenSslEngineMap { + private final Map engines = PlatformDependent.newConcurrentHashMap(); + + @Override + public ReferenceCountedOpenSslEngine remove(long ssl) { + return engines.remove(ssl); + } + + @Override + public void add(ReferenceCountedOpenSslEngine engine) { + engines.put(engine.sslPointer(), engine); + } + + @Override + public ReferenceCountedOpenSslEngine get(long ssl) { + return engines.get(ssl); + } + } + + static void setKeyMaterial(long ctx, X509Certificate[] keyCertChain, PrivateKey key, String keyPassword) + throws SSLException { + /* Load the certificate file and private key. */ + long keyBio = 0; + long keyCertChainBio = 0; + + try { + keyCertChainBio = toBIO(keyCertChain); + keyBio = toBIO(key); + + SSLContext.setCertificateBio( + ctx, keyCertChainBio, keyBio, + keyPassword == null ? StringUtil.EMPTY_STRING : keyPassword, SSL.SSL_AIDX_RSA); + // We may have more then one cert in the chain so add all of them now. + SSLContext.setCertificateChainBio(ctx, keyCertChainBio, false); + } catch (SSLException e) { + throw e; + } catch (Exception e) { + throw new SSLException("failed to set certificate and key", e); + } finally { + if (keyBio != 0) { + SSL.freeBIO(keyBio); + } + if (keyCertChainBio != 0) { + SSL.freeBIO(keyCertChainBio); + } + } + } + /** + * Return the pointer to a in-memory BIO + * or {@code 0} if the {@code key} is {@code null}. The BIO contains the content of the {@code key}. + */ + static long toBIO(PrivateKey key) throws Exception { + if (key == null) { + return 0; + } + + ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; + PemEncoded pem = PemPrivateKey.toPEM(allocator, true, key); + try { + return toBIO(allocator, pem.retain()); + } finally { + pem.release(); + } + } + + /** + * Return the pointer to a in-memory BIO + * or {@code 0} if the {@code certChain} is {@code null}. The BIO contains the content of the {@code certChain}. + */ + static long toBIO(X509Certificate... certChain) throws Exception { + if (certChain == null) { + return 0; + } + + if (certChain.length == 0) { + throw new IllegalArgumentException("certChain can't be empty"); + } + + ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; + PemEncoded pem = PemX509Certificate.toPEM(allocator, true, certChain); + try { + return toBIO(allocator, pem.retain()); + } finally { + pem.release(); + } + } + + private static long toBIO(ByteBufAllocator allocator, PemEncoded pem) throws Exception { + try { + // We can turn direct buffers straight into BIOs. No need to + // make a yet another copy. + ByteBuf content = pem.content(); + + if (content.isDirect()) { + return newBIO(content.retainedSlice()); + } + + ByteBuf buffer = allocator.directBuffer(content.readableBytes()); + try { + buffer.writeBytes(content, content.readerIndex(), content.readableBytes()); + return newBIO(buffer.retainedSlice()); + } finally { + try { + // If the contents of the ByteBuf is sensitive (e.g. a PrivateKey) we + // need to zero out the bytes of the copy before we're releasing it. + if (pem.isSensitive()) { + SslUtils.zeroout(buffer); + } + } finally { + buffer.release(); + } + } + } finally { + pem.release(); + } + } + + private static long newBIO(ByteBuf buffer) throws Exception { + try { + long bio = SSL.newMemBIO(); + int readable = buffer.readableBytes(); + if (SSL.writeToBIO(bio, OpenSsl.memoryAddress(buffer) + buffer.readerIndex(), readable) != readable) { + SSL.freeBIO(bio); + throw new IllegalStateException("Could not write data to memory BIO"); + } + return bio; + } finally { + buffer.release(); + } + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngine.java b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngine.java new file mode 100644 index 0000000000..cf84b5a099 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngine.java @@ -0,0 +1,1951 @@ +/* + * Copyright 2016 The Netty Project + * + * The Netty Project licenses this file to you 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 io.netty.handler.ssl; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.util.AbstractReferenceCounted; +import io.netty.util.ReferenceCounted; +import io.netty.util.ResourceLeak; +import io.netty.util.ResourceLeakDetector; +import io.netty.util.ResourceLeakDetectorFactory; +import io.netty.util.internal.EmptyArrays; +import io.netty.util.internal.InternalThreadLocalMap; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.StringUtil; +import io.netty.util.internal.ThrowableUtil; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; +import org.apache.tomcat.jni.Buffer; +import org.apache.tomcat.jni.SSL; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.ByteBuffer; +import java.nio.ReadOnlyBufferException; +import java.security.Principal; +import java.security.cert.Certificate; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSessionBindingEvent; +import javax.net.ssl.SSLSessionBindingListener; +import javax.net.ssl.SSLSessionContext; +import javax.security.cert.X509Certificate; + +import static io.netty.handler.ssl.OpenSsl.memoryAddress; +import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static javax.net.ssl.SSLEngineResult.HandshakeStatus.FINISHED; +import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NEED_UNWRAP; +import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NEED_WRAP; +import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING; +import static javax.net.ssl.SSLEngineResult.Status.BUFFER_OVERFLOW; +import static javax.net.ssl.SSLEngineResult.Status.CLOSED; +import static javax.net.ssl.SSLEngineResult.Status.OK; + +/** + * Implements a {@link SSLEngine} using + * OpenSSL BIO abstractions. + *

Instances of this class must be {@link #release() released} or else native memory will leak! + */ +public class ReferenceCountedOpenSslEngine extends SSLEngine implements ReferenceCounted { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(ReferenceCountedOpenSslEngine.class); + + private static final Certificate[] EMPTY_CERTIFICATES = EmptyArrays.EMPTY_CERTIFICATES; + private static final X509Certificate[] EMPTY_X509_CERTIFICATES = EmptyArrays.EMPTY_JAVAX_X509_CERTIFICATES; + + private static final SSLException BEGIN_HANDSHAKE_ENGINE_CLOSED = ThrowableUtil.unknownStackTrace( + new SSLException("engine closed"), ReferenceCountedOpenSslEngine.class, "beginHandshake()"); + private static final SSLException HANDSHAKE_ENGINE_CLOSED = ThrowableUtil.unknownStackTrace( + new SSLException("engine closed"), ReferenceCountedOpenSslEngine.class, "handshake()"); + private static final SSLException RENEGOTIATION_UNSUPPORTED = ThrowableUtil.unknownStackTrace( + new SSLException("renegotiation unsupported"), ReferenceCountedOpenSslEngine.class, "beginHandshake()"); + private static final SSLException ENCRYPTED_PACKET_OVERSIZED = ThrowableUtil.unknownStackTrace( + new SSLException("encrypted packet oversized"), ReferenceCountedOpenSslEngine.class, "unwrap(...)"); + private static final Class SNI_HOSTNAME_CLASS; + private static final Method GET_SERVER_NAMES_METHOD; + private static final Method SET_SERVER_NAMES_METHOD; + private static final Method GET_ASCII_NAME_METHOD; + private static final Method GET_USE_CIPHER_SUITES_ORDER_METHOD; + private static final Method SET_USE_CIPHER_SUITES_ORDER_METHOD; + private static final ResourceLeakDetector leakDetector = + ResourceLeakDetectorFactory.instance().newResourceLeakDetector(ReferenceCountedOpenSslEngine.class); + + static { + AtomicIntegerFieldUpdater destroyedUpdater = + PlatformDependent.newAtomicIntegerFieldUpdater(ReferenceCountedOpenSslEngine.class, "destroyed"); + if (destroyedUpdater == null) { + destroyedUpdater = AtomicIntegerFieldUpdater.newUpdater(ReferenceCountedOpenSslEngine.class, "destroyed"); + } + DESTROYED_UPDATER = destroyedUpdater; + + Method getUseCipherSuitesOrderMethod = null; + Method setUseCipherSuitesOrderMethod = null; + Class sniHostNameClass = null; + Method getAsciiNameMethod = null; + Method getServerNamesMethod = null; + Method setServerNamesMethod = null; + if (PlatformDependent.javaVersion() >= 8) { + try { + getUseCipherSuitesOrderMethod = SSLParameters.class.getDeclaredMethod("getUseCipherSuitesOrder"); + SSLParameters parameters = new SSLParameters(); + @SuppressWarnings("unused") + Boolean order = (Boolean) getUseCipherSuitesOrderMethod.invoke(parameters); + setUseCipherSuitesOrderMethod = SSLParameters.class.getDeclaredMethod("setUseCipherSuitesOrder", + boolean.class); + setUseCipherSuitesOrderMethod.invoke(parameters, true); + } catch (Throwable ignore) { + getUseCipherSuitesOrderMethod = null; + setUseCipherSuitesOrderMethod = null; + } + try { + sniHostNameClass = Class.forName("javax.net.ssl.SNIHostName", false, + PlatformDependent.getClassLoader(ReferenceCountedOpenSslEngine.class)); + Object sniHostName = sniHostNameClass.getConstructor(String.class).newInstance("netty.io"); + getAsciiNameMethod = sniHostNameClass.getDeclaredMethod("getAsciiName"); + @SuppressWarnings("unused") + String name = (String) getAsciiNameMethod.invoke(sniHostName); + + getServerNamesMethod = SSLParameters.class.getDeclaredMethod("getServerNames"); + setServerNamesMethod = SSLParameters.class.getDeclaredMethod("setServerNames", List.class); + SSLParameters parameters = new SSLParameters(); + @SuppressWarnings({ "rawtypes", "unused" }) + List serverNames = (List) getServerNamesMethod.invoke(parameters); + setServerNamesMethod.invoke(parameters, Collections.emptyList()); + } catch (Throwable ingore) { + sniHostNameClass = null; + getAsciiNameMethod = null; + getServerNamesMethod = null; + setServerNamesMethod = null; + } + } + GET_USE_CIPHER_SUITES_ORDER_METHOD = getUseCipherSuitesOrderMethod; + SET_USE_CIPHER_SUITES_ORDER_METHOD = setUseCipherSuitesOrderMethod; + SNI_HOSTNAME_CLASS = sniHostNameClass; + GET_ASCII_NAME_METHOD = getAsciiNameMethod; + GET_SERVER_NAMES_METHOD = getServerNamesMethod; + SET_SERVER_NAMES_METHOD = setServerNamesMethod; + } + + private static final int MAX_PLAINTEXT_LENGTH = 16 * 1024; // 2^14 + private static final int MAX_COMPRESSED_LENGTH = MAX_PLAINTEXT_LENGTH + 1024; + private static final int MAX_CIPHERTEXT_LENGTH = MAX_COMPRESSED_LENGTH + 1024; + + // Header (5) + Data (2^14) + Compression (1024) + Encryption (1024) + MAC (20) + Padding (256) + static final int MAX_ENCRYPTED_PACKET_LENGTH = MAX_CIPHERTEXT_LENGTH + 5 + 20 + 256; + + static final int MAX_ENCRYPTION_OVERHEAD_LENGTH = MAX_ENCRYPTED_PACKET_LENGTH - MAX_PLAINTEXT_LENGTH; + + private static final AtomicIntegerFieldUpdater DESTROYED_UPDATER; + + private static final String INVALID_CIPHER = "SSL_NULL_WITH_NULL_NULL"; + + private static final long EMPTY_ADDR = Buffer.address(Unpooled.EMPTY_BUFFER.nioBuffer()); + + private static final SSLEngineResult NEED_UNWRAP_OK = new SSLEngineResult(OK, NEED_UNWRAP, 0, 0); + private static final SSLEngineResult NEED_UNWRAP_CLOSED = new SSLEngineResult(CLOSED, NEED_UNWRAP, 0, 0); + private static final SSLEngineResult NEED_WRAP_OK = new SSLEngineResult(OK, NEED_WRAP, 0, 0); + private static final SSLEngineResult NEED_WRAP_CLOSED = new SSLEngineResult(CLOSED, NEED_WRAP, 0, 0); + private static final SSLEngineResult CLOSED_NOT_HANDSHAKING = new SSLEngineResult(CLOSED, NOT_HANDSHAKING, 0, 0); + + // OpenSSL state + private long ssl; + private long networkBIO; + private boolean certificateSet; + + private enum HandshakeState { + /** + * Not started yet. + */ + NOT_STARTED, + /** + * Started via unwrap/wrap. + */ + STARTED_IMPLICITLY, + /** + * Started via {@link #beginHandshake()}. + */ + STARTED_EXPLICITLY, + + /** + * Handshake is finished. + */ + FINISHED + } + + private HandshakeState handshakeState = HandshakeState.NOT_STARTED; + private boolean receivedShutdown; + private volatile int destroyed; + + // Reference Counting + private final ResourceLeak leak; + private final AbstractReferenceCounted refCnt = new AbstractReferenceCounted() { + @Override + public ReferenceCounted touch(Object hint) { + if (leak != null) { + leak.record(hint); + } + + return ReferenceCountedOpenSslEngine.this; + } + + @Override + protected void deallocate() { + shutdown(); + if (leak != null) { + leak.close(); + } + } + }; + + private volatile ClientAuth clientAuth = ClientAuth.NONE; + + // Updated once a new handshake is started and so the SSLSession reused. + private volatile long lastAccessed = -1; + + private String endPointIdentificationAlgorithm; + // Store as object as AlgorithmConstraints only exists since java 7. + private Object algorithmConstraints; + private List sniHostNames; + + // SSL Engine status variables + private boolean isInboundDone; + private boolean isOutboundDone; + private boolean engineClosed; + + private final boolean clientMode; + private final ByteBufAllocator alloc; + private final OpenSslEngineMap engineMap; + private final OpenSslApplicationProtocolNegotiator apn; + private final boolean rejectRemoteInitiatedRenegation; + private final ReferenceCountedOpenSslEngine.OpenSslSession session; + private final Certificate[] localCerts; + private final ByteBuffer[] singleSrcBuffer = new ByteBuffer[1]; + private final ByteBuffer[] singleDstBuffer = new ByteBuffer[1]; + private final OpenSslKeyMaterialManager keyMaterialManager; + + // This is package-private as we set it from OpenSslContext if an exception is thrown during + // the verification step. + SSLHandshakeException handshakeException; + + /** + * Create a new instance. + * @param context Reference count release responsibility is not transferred! The callee still owns this object. + * @param alloc The allocator to use. + * @param peerHost The peer host name. + * @param peerPort The peer port. + * @param leakDetection {@code true} to enable leak detection of this object. + */ + ReferenceCountedOpenSslEngine(ReferenceCountedOpenSslContext context, ByteBufAllocator alloc, String peerHost, + int peerPort, boolean leakDetection) { + super(peerHost, peerPort); + OpenSsl.ensureAvailability(); + leak = leakDetection ? leakDetector.open(this) : null; + this.alloc = checkNotNull(alloc, "alloc"); + apn = (OpenSslApplicationProtocolNegotiator) context.applicationProtocolNegotiator(); + ssl = SSL.newSSL(context.ctx, !context.isClient()); + session = new ReferenceCountedOpenSslEngine.OpenSslSession(context.sessionContext()); + networkBIO = SSL.makeNetworkBIO(ssl); + clientMode = context.isClient(); + engineMap = context.engineMap; + rejectRemoteInitiatedRenegation = context.rejectRemoteInitiatedRenegotiation; + localCerts = context.keyCertChain; + + // Set the client auth mode, this needs to be done via setClientAuth(...) method so we actually call the + // needed JNI methods. + setClientAuth(clientMode ? ClientAuth.NONE : context.clientAuth); + + // Use SNI if peerHost was specified + // See https://github.com/netty/netty/issues/4746 + if (clientMode && peerHost != null) { + SSL.setTlsExtHostName(ssl, peerHost); + } + keyMaterialManager = context.keyMaterialManager(); + } + + @Override + public final int refCnt() { + return refCnt.refCnt(); + } + + @Override + public final ReferenceCounted retain() { + refCnt.retain(); + return this; + } + + @Override + public final ReferenceCounted retain(int increment) { + refCnt.retain(increment); + return this; + } + + @Override + public final ReferenceCounted touch() { + refCnt.touch(); + return this; + } + + @Override + public final ReferenceCounted touch(Object hint) { + refCnt.touch(hint); + return this; + } + + @Override + public final boolean release() { + return refCnt.release(); + } + + @Override + public final boolean release(int decrement) { + return refCnt.release(decrement); + } + + @Override + public final synchronized SSLSession getHandshakeSession() { + // Javadocs state return value should be: + // null if this instance is not currently handshaking, or if the current handshake has not + // progressed far enough to create a basic SSLSession. Otherwise, this method returns the + // SSLSession currently being negotiated. + switch(handshakeState) { + case NOT_STARTED: + case FINISHED: + return null; + default: + return session; + } + } + + /** + * Returns the pointer to the {@code SSL} object for this {@link ReferenceCountedOpenSslEngine}. + * Be aware that it is freed as soon as the {@link #release()} or {@link #shutdown()} methods are called. + * At this point {@code 0} will be returned. + */ + public final synchronized long sslPointer() { + return ssl; + } + + /** + * Destroys this engine. + */ + public final synchronized void shutdown() { + if (DESTROYED_UPDATER.compareAndSet(this, 0, 1)) { + engineMap.remove(ssl); + SSL.freeSSL(ssl); + SSL.freeBIO(networkBIO); + ssl = networkBIO = 0; + + // internal errors can cause shutdown without marking the engine closed + isInboundDone = isOutboundDone = engineClosed = true; + } + + // On shutdown clear all errors + SSL.clearError(); + } + + /** + * Write plaintext data to the OpenSSL internal BIO + * + * Calling this function with src.remaining == 0 is undefined. + */ + private int writePlaintextData(final ByteBuffer src) { + final int pos = src.position(); + final int limit = src.limit(); + final int len = Math.min(limit - pos, MAX_PLAINTEXT_LENGTH); + final int sslWrote; + + if (src.isDirect()) { + final long addr = Buffer.address(src) + pos; + sslWrote = SSL.writeToSSL(ssl, addr, len); + if (sslWrote > 0) { + src.position(pos + sslWrote); + } + } else { + ByteBuf buf = alloc.directBuffer(len); + try { + final long addr = memoryAddress(buf); + + src.limit(pos + len); + + buf.setBytes(0, src); + src.limit(limit); + + sslWrote = SSL.writeToSSL(ssl, addr, len); + if (sslWrote > 0) { + src.position(pos + sslWrote); + } else { + src.position(pos); + } + } finally { + buf.release(); + } + } + return sslWrote; + } + + /** + * Write encrypted data to the OpenSSL network BIO. + */ + private int writeEncryptedData(final ByteBuffer src) { + final int pos = src.position(); + final int len = src.remaining(); + final int netWrote; + if (src.isDirect()) { + final long addr = Buffer.address(src) + pos; + netWrote = SSL.writeToBIO(networkBIO, addr, len); + if (netWrote >= 0) { + src.position(pos + netWrote); + } + } else { + final ByteBuf buf = alloc.directBuffer(len); + try { + final long addr = memoryAddress(buf); + + buf.setBytes(0, src); + + netWrote = SSL.writeToBIO(networkBIO, addr, len); + if (netWrote >= 0) { + src.position(pos + netWrote); + } else { + src.position(pos); + } + } finally { + buf.release(); + } + } + + return netWrote; + } + + /** + * Read plaintext data from the OpenSSL internal BIO + */ + private int readPlaintextData(final ByteBuffer dst) { + final int sslRead; + if (dst.isDirect()) { + final int pos = dst.position(); + final long addr = Buffer.address(dst) + pos; + final int len = dst.limit() - pos; + sslRead = SSL.readFromSSL(ssl, addr, len); + if (sslRead > 0) { + dst.position(pos + sslRead); + } + } else { + final int pos = dst.position(); + final int limit = dst.limit(); + final int len = Math.min(MAX_ENCRYPTED_PACKET_LENGTH, limit - pos); + final ByteBuf buf = alloc.directBuffer(len); + try { + final long addr = memoryAddress(buf); + + sslRead = SSL.readFromSSL(ssl, addr, len); + if (sslRead > 0) { + dst.limit(pos + sslRead); + buf.getBytes(0, dst); + dst.limit(limit); + } + } finally { + buf.release(); + } + } + + return sslRead; + } + + /** + * Read encrypted data from the OpenSSL network BIO + */ + private int readEncryptedData(final ByteBuffer dst, final int pending) { + final int bioRead; + + if (dst.isDirect() && dst.remaining() >= pending) { + final int pos = dst.position(); + final long addr = Buffer.address(dst) + pos; + bioRead = SSL.readFromBIO(networkBIO, addr, pending); + if (bioRead > 0) { + dst.position(pos + bioRead); + return bioRead; + } + } else { + final ByteBuf buf = alloc.directBuffer(pending); + try { + final long addr = memoryAddress(buf); + + bioRead = SSL.readFromBIO(networkBIO, addr, pending); + if (bioRead > 0) { + int oldLimit = dst.limit(); + dst.limit(dst.position() + bioRead); + buf.getBytes(0, dst); + dst.limit(oldLimit); + return bioRead; + } + } finally { + buf.release(); + } + } + + return bioRead; + } + + private SSLEngineResult readPendingBytesFromBIO(ByteBuffer dst, int bytesConsumed, int bytesProduced, + SSLEngineResult.HandshakeStatus status) throws SSLException { + // Check to see if the engine wrote data into the network BIO + int pendingNet = SSL.pendingWrittenBytesInBIO(networkBIO); + if (pendingNet > 0) { + + // Do we have enough room in dst to write encrypted data? + int capacity = dst.remaining(); + if (capacity < pendingNet) { + return new SSLEngineResult(BUFFER_OVERFLOW, + mayFinishHandshake(status != FINISHED ? getHandshakeStatus(pendingNet) : status), + bytesConsumed, bytesProduced); + } + + // Write the pending data from the network BIO into the dst buffer + int produced = readEncryptedData(dst, pendingNet); + + if (produced <= 0) { + // We ignore BIO_* errors here as we use in memory BIO anyway and will do another SSL_* call later + // on in which we will produce an exception in case of an error + SSL.clearError(); + } else { + bytesProduced += produced; + pendingNet -= produced; + } + // If isOuboundDone is set, then the data from the network BIO + // was the close_notify message -- we are not required to wait + // for the receipt the peer's close_notify message -- shutdown. + if (isOutboundDone) { + shutdown(); + } + + return new SSLEngineResult(getEngineStatus(), + mayFinishHandshake(status != FINISHED ? getHandshakeStatus(pendingNet) : status), + bytesConsumed, bytesProduced); + } + return null; + } + + @Override + public final SSLEngineResult wrap( + final ByteBuffer[] srcs, final int offset, final int length, final ByteBuffer dst) throws SSLException { + // Throw required runtime exceptions + if (srcs == null) { + throw new IllegalArgumentException("srcs is null"); + } + if (dst == null) { + throw new IllegalArgumentException("dst is null"); + } + + if (offset >= srcs.length || offset + length > srcs.length) { + throw new IndexOutOfBoundsException( + "offset: " + offset + ", length: " + length + + " (expected: offset <= offset + length <= srcs.length (" + srcs.length + "))"); + } + + if (dst.isReadOnly()) { + throw new ReadOnlyBufferException(); + } + + synchronized (this) { + // Check to make sure the engine has not been closed + if (isDestroyed()) { + return CLOSED_NOT_HANDSHAKING; + } + + SSLEngineResult.HandshakeStatus status = NOT_HANDSHAKING; + // Prepare OpenSSL to work in server mode and receive handshake + if (handshakeState != HandshakeState.FINISHED) { + if (handshakeState != HandshakeState.STARTED_EXPLICITLY) { + // Update accepted so we know we triggered the handshake via wrap + handshakeState = HandshakeState.STARTED_IMPLICITLY; + } + + status = handshake(); + if (status == NEED_UNWRAP) { + return NEED_UNWRAP_OK; + } + + if (engineClosed) { + return NEED_UNWRAP_CLOSED; + } + } + + // There was no pending data in the network BIO -- encrypt any application data + int bytesProduced = 0; + int bytesConsumed = 0; + int endOffset = offset + length; + for (int i = offset; i < endOffset; ++i) { + final ByteBuffer src = srcs[i]; + if (src == null) { + throw new IllegalArgumentException("srcs[" + i + "] is null"); + } + while (src.hasRemaining()) { + final SSLEngineResult pendingNetResult; + // Write plaintext application data to the SSL engine + int result = writePlaintextData(src); + if (result > 0) { + bytesConsumed += result; + + pendingNetResult = readPendingBytesFromBIO(dst, bytesConsumed, bytesProduced, status); + if (pendingNetResult != null) { + if (pendingNetResult.getStatus() != OK) { + return pendingNetResult; + } + bytesProduced = pendingNetResult.bytesProduced(); + } + } else { + int sslError = SSL.getError(ssl, result); + switch (sslError) { + case SSL.SSL_ERROR_ZERO_RETURN: + // This means the connection was shutdown correctly, close inbound and outbound + if (!receivedShutdown) { + closeAll(); + } + pendingNetResult = readPendingBytesFromBIO(dst, bytesConsumed, bytesProduced, status); + return pendingNetResult != null ? pendingNetResult : CLOSED_NOT_HANDSHAKING; + case SSL.SSL_ERROR_WANT_READ: + // If there is no pending data to read from BIO we should go back to event loop and try + // to read more data [1]. It is also possible that event loop will detect the socket + // has been closed. [1] https://www.openssl.org/docs/manmaster/ssl/SSL_write.html + pendingNetResult = readPendingBytesFromBIO(dst, bytesConsumed, bytesProduced, status); + return pendingNetResult != null ? pendingNetResult : + new SSLEngineResult(getEngineStatus(), + NEED_UNWRAP, bytesConsumed, bytesProduced); + case SSL.SSL_ERROR_WANT_WRITE: + // SSL_ERROR_WANT_WRITE typically means that the underlying transport is not writable + // and we should set the "want write" flag on the selector and try again when the + // underlying transport is writable [1]. However we are not directly writing to the + // underlying transport and instead writing to a BIO buffer. The OpenSsl documentation + // says we should do the following [1]: + // + // "When using a buffering BIO, like a BIO pair, data must be written into or retrieved + // out of the BIO before being able to continue." + // + // So we attempt to drain the BIO buffer below, but if there is no data this condition + // is undefined and we assume their is a fatal error with the openssl engine and close. + // [1] https://www.openssl.org/docs/manmaster/ssl/SSL_write.html + pendingNetResult = readPendingBytesFromBIO(dst, bytesConsumed, bytesProduced, status); + return pendingNetResult != null ? pendingNetResult : NEED_WRAP_CLOSED; + default: + // Everything else is considered as error + throw shutdownWithError("SSL_write"); + } + } + } + } + // We need to check if pendingWrittenBytesInBIO was checked yet, as we may not checked if the srcs was + // empty, or only contained empty buffers. + if (bytesConsumed == 0) { + SSLEngineResult pendingNetResult = readPendingBytesFromBIO(dst, 0, bytesProduced, status); + if (pendingNetResult != null) { + return pendingNetResult; + } + } + + return newResult(bytesConsumed, bytesProduced, status); + } + } + + /** + * Log the error, shutdown the engine and throw an exception. + */ + private SSLException shutdownWithError(String operations) { + String err = SSL.getLastError(); + return shutdownWithError(operations, err); + } + + private SSLException shutdownWithError(String operation, String err) { + if (logger.isDebugEnabled()) { + logger.debug("{} failed: OpenSSL error: {}", operation, err); + } + + // There was an internal error -- shutdown + shutdown(); + if (handshakeState == HandshakeState.FINISHED) { + return new SSLException(err); + } + return new SSLHandshakeException(err); + } + + public final SSLEngineResult unwrap( + final ByteBuffer[] srcs, int srcsOffset, final int srcsLength, + final ByteBuffer[] dsts, final int dstsOffset, final int dstsLength) throws SSLException { + + // Throw required runtime exceptions + if (srcs == null) { + throw new NullPointerException("srcs"); + } + if (srcsOffset >= srcs.length + || srcsOffset + srcsLength > srcs.length) { + throw new IndexOutOfBoundsException( + "offset: " + srcsOffset + ", length: " + srcsLength + + " (expected: offset <= offset + length <= srcs.length (" + srcs.length + "))"); + } + if (dsts == null) { + throw new IllegalArgumentException("dsts is null"); + } + if (dstsOffset >= dsts.length || dstsOffset + dstsLength > dsts.length) { + throw new IndexOutOfBoundsException( + "offset: " + dstsOffset + ", length: " + dstsLength + + " (expected: offset <= offset + length <= dsts.length (" + dsts.length + "))"); + } + long capacity = 0; + final int endOffset = dstsOffset + dstsLength; + for (int i = dstsOffset; i < endOffset; i ++) { + ByteBuffer dst = dsts[i]; + if (dst == null) { + throw new IllegalArgumentException("dsts[" + i + "] is null"); + } + if (dst.isReadOnly()) { + throw new ReadOnlyBufferException(); + } + capacity += dst.remaining(); + } + + final int srcsEndOffset = srcsOffset + srcsLength; + long len = 0; + for (int i = srcsOffset; i < srcsEndOffset; i++) { + ByteBuffer src = srcs[i]; + if (src == null) { + throw new IllegalArgumentException("srcs[" + i + "] is null"); + } + len += src.remaining(); + } + + synchronized (this) { + // Check to make sure the engine has not been closed + if (isDestroyed()) { + return CLOSED_NOT_HANDSHAKING; + } + + // protect against protocol overflow attack vector + if (len > MAX_ENCRYPTED_PACKET_LENGTH) { + isInboundDone = true; + isOutboundDone = true; + engineClosed = true; + shutdown(); + throw ENCRYPTED_PACKET_OVERSIZED; + } + + SSLEngineResult.HandshakeStatus status = NOT_HANDSHAKING; + // Prepare OpenSSL to work in server mode and receive handshake + if (handshakeState != HandshakeState.FINISHED) { + if (handshakeState != HandshakeState.STARTED_EXPLICITLY) { + // Update accepted so we know we triggered the handshake via wrap + handshakeState = HandshakeState.STARTED_IMPLICITLY; + } + + status = handshake(); + if (status == NEED_WRAP) { + return NEED_WRAP_OK; + } + if (engineClosed) { + return NEED_WRAP_CLOSED; + } + } + + // Write encrypted data to network BIO + int bytesConsumed = 0; + if (srcsOffset < srcsEndOffset) { + do { + ByteBuffer src = srcs[srcsOffset]; + int remaining = src.remaining(); + if (remaining == 0) { + // We must skip empty buffers as BIO_write will return 0 if asked to write something + // with length 0. + srcsOffset++; + continue; + } + int written = writeEncryptedData(src); + if (written > 0) { + bytesConsumed += written; + + if (written == remaining) { + srcsOffset++; + } else { + // We were not able to write everything into the BIO so break the write loop as otherwise + // we will produce an error on the next write attempt, which will trigger a SSL.clearError() + // later. + break; + } + } else { + // BIO_write returned a negative or zero number, this means we could not complete the write + // operation and should retry later. + // We ignore BIO_* errors here as we use in memory BIO anyway and will do another SSL_* call + // later on in which we will produce an exception in case of an error + SSL.clearError(); + break; + } + } while (srcsOffset < srcsEndOffset); + } + + // Number of produced bytes + int bytesProduced = 0; + + if (capacity > 0) { + // Write decrypted data to dsts buffers + int idx = dstsOffset; + while (idx < endOffset) { + ByteBuffer dst = dsts[idx]; + if (!dst.hasRemaining()) { + idx++; + continue; + } + + int bytesRead = readPlaintextData(dst); + + // TODO: We may want to consider if we move this check and only do it in a less often called place + // at the price of not being 100% accurate, like for example when calling SSL.getError(...). + rejectRemoteInitiatedRenegation(); + + if (bytesRead > 0) { + bytesProduced += bytesRead; + + if (!dst.hasRemaining()) { + idx++; + } else { + // We read everything return now. + return newResult(bytesConsumed, bytesProduced, status); + } + } else { + int sslError = SSL.getError(ssl, bytesRead); + switch (sslError) { + case SSL.SSL_ERROR_ZERO_RETURN: + // This means the connection was shutdown correctly, close inbound and outbound + if (!receivedShutdown) { + closeAll(); + } + // fall-trough! + case SSL.SSL_ERROR_WANT_READ: + case SSL.SSL_ERROR_WANT_WRITE: + // break to the outer loop + return newResult(bytesConsumed, bytesProduced, status); + default: + return sslReadErrorResult(SSL.getLastErrorNumber(), bytesConsumed, bytesProduced); + } + } + } + } else { + // If the capacity of all destination buffers is 0 we need to trigger a SSL_read anyway to ensure + // everything is flushed in the BIO pair and so we can detect it in the pendingAppData() call. + if (SSL.readFromSSL(ssl, EMPTY_ADDR, 0) <= 0) { + // We do not check SSL_get_error as we are not interested in any error that is not fatal. + int err = SSL.getLastErrorNumber(); + if (OpenSsl.isError(err)) { + return sslReadErrorResult(err, bytesConsumed, bytesProduced); + } + } + } + if (pendingAppData() > 0) { + // We filled all buffers but there is still some data pending in the BIO buffer, return BUFFER_OVERFLOW. + return new SSLEngineResult( + BUFFER_OVERFLOW, mayFinishHandshake(status != FINISHED ? getHandshakeStatus() : status), + bytesConsumed, bytesProduced); + } + + // Check to see if we received a close_notify message from the peer. + if (!receivedShutdown && (SSL.getShutdown(ssl) & SSL.SSL_RECEIVED_SHUTDOWN) == SSL.SSL_RECEIVED_SHUTDOWN) { + closeAll(); + } + + return newResult(bytesConsumed, bytesProduced, status); + } + } + + private SSLEngineResult sslReadErrorResult(int err, int bytesConsumed, int bytesProduced) throws SSLException { + String errStr = SSL.getErrorString(err); + + // Check if we have a pending handshakeException and if so see if we need to consume all pending data from the + // BIO first or can just shutdown and throw it now. + // This is needed so we ensure close_notify etc is correctly send to the remote peer. + // See https://github.com/netty/netty/issues/3900 + if (SSL.pendingWrittenBytesInBIO(networkBIO) > 0) { + if (handshakeException == null && handshakeState != HandshakeState.FINISHED) { + // we seems to have data left that needs to be transfered and so the user needs + // call wrap(...). Store the error so we can pick it up later. + handshakeException = new SSLHandshakeException(errStr); + } + return new SSLEngineResult(OK, NEED_WRAP, bytesConsumed, bytesProduced); + } + throw shutdownWithError("SSL_read", errStr); + } + + private int pendingAppData() { + // There won't be any application data until we're done handshaking. + // We first check handshakeFinished to eliminate the overhead of extra JNI call if possible. + return handshakeState == HandshakeState.FINISHED ? SSL.pendingReadableBytesInSSL(ssl) : 0; + } + + private SSLEngineResult newResult( + int bytesConsumed, int bytesProduced, SSLEngineResult.HandshakeStatus status) throws SSLException { + return new SSLEngineResult( + getEngineStatus(), mayFinishHandshake(status != FINISHED ? getHandshakeStatus() : status) + , bytesConsumed, bytesProduced); + } + + private void closeAll() throws SSLException { + receivedShutdown = true; + closeOutbound(); + closeInbound(); + } + + private void rejectRemoteInitiatedRenegation() throws SSLHandshakeException { + if (rejectRemoteInitiatedRenegation && SSL.getHandshakeCount(ssl) > 1) { + // TODO: In future versions me may also want to send a fatal_alert to the client and so notify it + // that the renegotiation failed. + shutdown(); + throw new SSLHandshakeException("remote-initiated renegotation not allowed"); + } + } + + public final SSLEngineResult unwrap(final ByteBuffer[] srcs, final ByteBuffer[] dsts) throws SSLException { + return unwrap(srcs, 0, srcs.length, dsts, 0, dsts.length); + } + + private ByteBuffer[] singleSrcBuffer(ByteBuffer src) { + singleSrcBuffer[0] = src; + return singleSrcBuffer; + } + + private void resetSingleSrcBuffer() { + singleSrcBuffer[0] = null; + } + + private ByteBuffer[] singleDstBuffer(ByteBuffer src) { + singleDstBuffer[0] = src; + return singleDstBuffer; + } + + private void resetSingleDstBuffer() { + singleDstBuffer[0] = null; + } + + @Override + public final synchronized SSLEngineResult unwrap( + final ByteBuffer src, final ByteBuffer[] dsts, final int offset, final int length) throws SSLException { + try { + return unwrap(singleSrcBuffer(src), 0, 1, dsts, offset, length); + } finally { + resetSingleSrcBuffer(); + } + } + + @Override + public final synchronized SSLEngineResult wrap(ByteBuffer src, ByteBuffer dst) throws SSLException { + try { + return wrap(singleSrcBuffer(src), dst); + } finally { + resetSingleSrcBuffer(); + } + } + + @Override + public final synchronized SSLEngineResult unwrap(ByteBuffer src, ByteBuffer dst) throws SSLException { + try { + return unwrap(singleSrcBuffer(src), singleDstBuffer(dst)); + } finally { + resetSingleSrcBuffer(); + resetSingleDstBuffer(); + } + } + + @Override + public final synchronized SSLEngineResult unwrap(ByteBuffer src, ByteBuffer[] dsts) throws SSLException { + try { + return unwrap(singleSrcBuffer(src), dsts); + } finally { + resetSingleSrcBuffer(); + } + } + + @Override + public final Runnable getDelegatedTask() { + // Currently, we do not delegate SSL computation tasks + // TODO: in the future, possibly create tasks to do encrypt / decrypt async + + return null; + } + + @Override + public final synchronized void closeInbound() throws SSLException { + if (isInboundDone) { + return; + } + + isInboundDone = true; + engineClosed = true; + + shutdown(); + + if (handshakeState != HandshakeState.NOT_STARTED && !receivedShutdown) { + throw new SSLException( + "Inbound closed before receiving peer's close_notify: possible truncation attack?"); + } + } + + @Override + public final synchronized boolean isInboundDone() { + return isInboundDone || engineClosed; + } + + @Override + public final synchronized void closeOutbound() { + if (isOutboundDone) { + return; + } + + isOutboundDone = true; + engineClosed = true; + + if (handshakeState != HandshakeState.NOT_STARTED && !isDestroyed()) { + int mode = SSL.getShutdown(ssl); + if ((mode & SSL.SSL_SENT_SHUTDOWN) != SSL.SSL_SENT_SHUTDOWN) { + int err = SSL.shutdownSSL(ssl); + if (err < 0) { + int sslErr = SSL.getError(ssl, err); + switch (sslErr) { + case SSL.SSL_ERROR_NONE: + case SSL.SSL_ERROR_WANT_ACCEPT: + case SSL.SSL_ERROR_WANT_CONNECT: + case SSL.SSL_ERROR_WANT_WRITE: + case SSL.SSL_ERROR_WANT_READ: + case SSL.SSL_ERROR_WANT_X509_LOOKUP: + case SSL.SSL_ERROR_ZERO_RETURN: + // Nothing to do here + break; + case SSL.SSL_ERROR_SYSCALL: + case SSL.SSL_ERROR_SSL: + if (logger.isDebugEnabled()) { + logger.debug("SSL_shutdown failed: OpenSSL error: {}", SSL.getLastError()); + } + // There was an internal error -- shutdown + shutdown(); + break; + default: + SSL.clearError(); + break; + } + } + } + } else { + // engine closing before initial handshake + shutdown(); + } + } + + @Override + public final synchronized boolean isOutboundDone() { + return isOutboundDone; + } + + @Override + public final String[] getSupportedCipherSuites() { + return OpenSsl.AVAILABLE_CIPHER_SUITES.toArray(new String[OpenSsl.AVAILABLE_CIPHER_SUITES.size()]); + } + + @Override + public final String[] getEnabledCipherSuites() { + final String[] enabled; + synchronized (this) { + if (!isDestroyed()) { + enabled = SSL.getCiphers(ssl); + } else { + return EmptyArrays.EMPTY_STRINGS; + } + } + if (enabled == null) { + return EmptyArrays.EMPTY_STRINGS; + } else { + synchronized (this) { + for (int i = 0; i < enabled.length; i++) { + String mapped = toJavaCipherSuite(enabled[i]); + if (mapped != null) { + enabled[i] = mapped; + } + } + } + return enabled; + } + } + + @Override + public final void setEnabledCipherSuites(String[] cipherSuites) { + checkNotNull(cipherSuites, "cipherSuites"); + + final StringBuilder buf = new StringBuilder(); + for (String c: cipherSuites) { + if (c == null) { + break; + } + + String converted = CipherSuiteConverter.toOpenSsl(c); + if (converted == null) { + converted = c; + } + + if (!OpenSsl.isCipherSuiteAvailable(converted)) { + throw new IllegalArgumentException("unsupported cipher suite: " + c + '(' + converted + ')'); + } + + buf.append(converted); + buf.append(':'); + } + + if (buf.length() == 0) { + throw new IllegalArgumentException("empty cipher suites"); + } + buf.setLength(buf.length() - 1); + + final String cipherSuiteSpec = buf.toString(); + + synchronized (this) { + if (!isDestroyed()) { + try { + SSL.setCipherSuites(ssl, cipherSuiteSpec); + } catch (Exception e) { + throw new IllegalStateException("failed to enable cipher suites: " + cipherSuiteSpec, e); + } + } else { + throw new IllegalStateException("failed to enable cipher suites: " + cipherSuiteSpec); + } + } + } + + @Override + public final String[] getSupportedProtocols() { + return OpenSsl.SUPPORTED_PROTOCOLS_SET.toArray(new String[OpenSsl.SUPPORTED_PROTOCOLS_SET.size()]); + } + + @Override + public final String[] getEnabledProtocols() { + List enabled = InternalThreadLocalMap.get().arrayList(); + // Seems like there is no way to explict disable SSLv2Hello in openssl so it is always enabled + enabled.add(OpenSsl.PROTOCOL_SSL_V2_HELLO); + + int opts; + synchronized (this) { + if (!isDestroyed()) { + opts = SSL.getOptions(ssl); + } else { + return enabled.toArray(new String[1]); + } + } + if ((opts & SSL.SSL_OP_NO_TLSv1) == 0) { + enabled.add(OpenSsl.PROTOCOL_TLS_V1); + } + if ((opts & SSL.SSL_OP_NO_TLSv1_1) == 0) { + enabled.add(OpenSsl.PROTOCOL_TLS_V1_1); + } + if ((opts & SSL.SSL_OP_NO_TLSv1_2) == 0) { + enabled.add(OpenSsl.PROTOCOL_TLS_V1_2); + } + if ((opts & SSL.SSL_OP_NO_SSLv2) == 0) { + enabled.add(OpenSsl.PROTOCOL_SSL_V2); + } + if ((opts & SSL.SSL_OP_NO_SSLv3) == 0) { + enabled.add(OpenSsl.PROTOCOL_SSL_V3); + } + return enabled.toArray(new String[enabled.size()]); + } + + @Override + public final void setEnabledProtocols(String[] protocols) { + if (protocols == null) { + // This is correct from the API docs + throw new IllegalArgumentException(); + } + boolean sslv2 = false; + boolean sslv3 = false; + boolean tlsv1 = false; + boolean tlsv1_1 = false; + boolean tlsv1_2 = false; + for (String p: protocols) { + if (!OpenSsl.SUPPORTED_PROTOCOLS_SET.contains(p)) { + throw new IllegalArgumentException("Protocol " + p + " is not supported."); + } + if (p.equals(OpenSsl.PROTOCOL_SSL_V2)) { + sslv2 = true; + } else if (p.equals(OpenSsl.PROTOCOL_SSL_V3)) { + sslv3 = true; + } else if (p.equals(OpenSsl.PROTOCOL_TLS_V1)) { + tlsv1 = true; + } else if (p.equals(OpenSsl.PROTOCOL_TLS_V1_1)) { + tlsv1_1 = true; + } else if (p.equals(OpenSsl.PROTOCOL_TLS_V1_2)) { + tlsv1_2 = true; + } + } + synchronized (this) { + if (!isDestroyed()) { + // Enable all and then disable what we not want + SSL.setOptions(ssl, SSL.SSL_OP_ALL); + + // Clear out options which disable protocols + SSL.clearOptions(ssl, SSL.SSL_OP_NO_SSLv2 | SSL.SSL_OP_NO_SSLv3 | SSL.SSL_OP_NO_TLSv1 | + SSL.SSL_OP_NO_TLSv1_1 | SSL.SSL_OP_NO_TLSv1_2); + + int opts = 0; + if (!sslv2) { + opts |= SSL.SSL_OP_NO_SSLv2; + } + if (!sslv3) { + opts |= SSL.SSL_OP_NO_SSLv3; + } + if (!tlsv1) { + opts |= SSL.SSL_OP_NO_TLSv1; + } + if (!tlsv1_1) { + opts |= SSL.SSL_OP_NO_TLSv1_1; + } + if (!tlsv1_2) { + opts |= SSL.SSL_OP_NO_TLSv1_2; + } + + // Disable protocols we do not want + SSL.setOptions(ssl, opts); + } else { + throw new IllegalStateException("failed to enable protocols: " + Arrays.asList(protocols)); + } + } + } + + @Override + public final SSLSession getSession() { + return session; + } + + @Override + public final synchronized void beginHandshake() throws SSLException { + switch (handshakeState) { + case STARTED_IMPLICITLY: + checkEngineClosed(BEGIN_HANDSHAKE_ENGINE_CLOSED); + + // A user did not start handshake by calling this method by him/herself, + // but handshake has been started already by wrap() or unwrap() implicitly. + // Because it's the user's first time to call this method, it is unfair to + // raise an exception. From the user's standpoint, he or she never asked + // for renegotiation. + + handshakeState = HandshakeState.STARTED_EXPLICITLY; // Next time this method is invoked by the user, + // we should raise an exception. + break; + case STARTED_EXPLICITLY: + // Nothing to do as the handshake is not done yet. + break; + case FINISHED: + if (clientMode) { + // Only supported for server mode at the moment. + throw RENEGOTIATION_UNSUPPORTED; + } + // For renegotiate on the server side we need to issue the following command sequence with openssl: + // + // SSL_renegotiate(ssl) + // SSL_do_handshake(ssl) + // ssl->state = SSL_ST_ACCEPT + // SSL_do_handshake(ssl) + // + // Bcause of this we fall-through to call handshake() after setting the state, as this will also take + // care of updating the internal OpenSslSession object. + // + // See also: + // https://github.com/apache/httpd/blob/2.4.16/modules/ssl/ssl_engine_kernel.c#L812 + // http://h71000.www7.hp.com/doc/83final/ba554_90007/ch04s03.html + if (SSL.renegotiate(ssl) != 1 || SSL.doHandshake(ssl) != 1) { + throw shutdownWithError("renegotiation failed"); + } + + SSL.setState(ssl, SSL.SSL_ST_ACCEPT); + + lastAccessed = System.currentTimeMillis(); + + // fall-through + case NOT_STARTED: + handshakeState = HandshakeState.STARTED_EXPLICITLY; + handshake(); + break; + default: + throw new Error(); + } + } + + private void checkEngineClosed(SSLException cause) throws SSLException { + if (engineClosed || isDestroyed()) { + throw cause; + } + } + + private static SSLEngineResult.HandshakeStatus pendingStatus(int pendingStatus) { + // Depending on if there is something left in the BIO we need to WRAP or UNWRAP + return pendingStatus > 0 ? NEED_WRAP : NEED_UNWRAP; + } + + private SSLEngineResult.HandshakeStatus handshake() throws SSLException { + if (handshakeState == HandshakeState.FINISHED) { + return FINISHED; + } + checkEngineClosed(HANDSHAKE_ENGINE_CLOSED); + + // Check if we have a pending handshakeException and if so see if we need to consume all pending data from the + // BIO first or can just shutdown and throw it now. + // This is needed so we ensure close_notify etc is correctly send to the remote peer. + // See https://github.com/netty/netty/issues/3900 + SSLHandshakeException exception = handshakeException; + if (exception != null) { + if (SSL.pendingWrittenBytesInBIO(networkBIO) > 0) { + // There is something pending, we need to consume it first via a WRAP so we not loose anything. + return NEED_WRAP; + } + // No more data left to send to the remote peer, so null out the exception field, shutdown and throw + // the exception. + handshakeException = null; + shutdown(); + throw exception; + } + + // Adding the OpenSslEngine to the OpenSslEngineMap so it can be used in the AbstractCertificateVerifier. + engineMap.add(this); + if (lastAccessed == -1) { + lastAccessed = System.currentTimeMillis(); + } + + if (!certificateSet && keyMaterialManager != null) { + certificateSet = true; + keyMaterialManager.setKeyMaterial(this); + } + + int code = SSL.doHandshake(ssl); + if (code <= 0) { + // Check if we have a pending exception that was created during the handshake and if so throw it after + // shutdown the connection. + if (handshakeException != null) { + exception = handshakeException; + handshakeException = null; + shutdown(); + throw exception; + } + + int sslError = SSL.getError(ssl, code); + + switch (sslError) { + case SSL.SSL_ERROR_WANT_READ: + case SSL.SSL_ERROR_WANT_WRITE: + return pendingStatus(SSL.pendingWrittenBytesInBIO(networkBIO)); + default: + // Everything else is considered as error + throw shutdownWithError("SSL_do_handshake"); + } + } + // if SSL_do_handshake returns > 0 or sslError == SSL.SSL_ERROR_NAME it means the handshake was finished. + session.handshakeFinished(); + engineMap.remove(ssl); + return FINISHED; + } + + private SSLEngineResult.Status getEngineStatus() { + return engineClosed? CLOSED : OK; + } + + private SSLEngineResult.HandshakeStatus mayFinishHandshake(SSLEngineResult.HandshakeStatus status) + throws SSLException { + if (status == NOT_HANDSHAKING && handshakeState != HandshakeState.FINISHED) { + // If the status was NOT_HANDSHAKING and we not finished the handshake we need to call + // SSL_do_handshake() again + return handshake(); + } + return status; + } + + @Override + public final synchronized SSLEngineResult.HandshakeStatus getHandshakeStatus() { + // Check if we are in the initial handshake phase or shutdown phase + return needPendingStatus() ? pendingStatus(SSL.pendingWrittenBytesInBIO(networkBIO)) : NOT_HANDSHAKING; + } + + private SSLEngineResult.HandshakeStatus getHandshakeStatus(int pending) { + // Check if we are in the initial handshake phase or shutdown phase + return needPendingStatus() ? pendingStatus(pending) : NOT_HANDSHAKING; + } + + private boolean needPendingStatus() { + return handshakeState != HandshakeState.NOT_STARTED && !isDestroyed() + && (handshakeState != HandshakeState.FINISHED || engineClosed); + } + + /** + * Converts the specified OpenSSL cipher suite to the Java cipher suite. + */ + private String toJavaCipherSuite(String openSslCipherSuite) { + if (openSslCipherSuite == null) { + return null; + } + + String prefix = toJavaCipherSuitePrefix(SSL.getVersion(ssl)); + return CipherSuiteConverter.toJava(openSslCipherSuite, prefix); + } + + /** + * Converts the protocol version string returned by {@link SSL#getVersion(long)} to protocol family string. + */ + private static String toJavaCipherSuitePrefix(String protocolVersion) { + final char c; + if (protocolVersion == null || protocolVersion.length() == 0) { + c = 0; + } else { + c = protocolVersion.charAt(0); + } + + switch (c) { + case 'T': + return "TLS"; + case 'S': + return "SSL"; + default: + return "UNKNOWN"; + } + } + + @Override + public final void setUseClientMode(boolean clientMode) { + if (clientMode != this.clientMode) { + throw new UnsupportedOperationException(); + } + } + + @Override + public final boolean getUseClientMode() { + return clientMode; + } + + @Override + public final void setNeedClientAuth(boolean b) { + setClientAuth(b ? ClientAuth.REQUIRE : ClientAuth.NONE); + } + + @Override + public final boolean getNeedClientAuth() { + return clientAuth == ClientAuth.REQUIRE; + } + + @Override + public final void setWantClientAuth(boolean b) { + setClientAuth(b ? ClientAuth.OPTIONAL : ClientAuth.NONE); + } + + @Override + public final boolean getWantClientAuth() { + return clientAuth == ClientAuth.OPTIONAL; + } + + private void setClientAuth(ClientAuth mode) { + if (clientMode) { + return; + } + synchronized (this) { + if (clientAuth == mode) { + // No need to issue any JNI calls if the mode is the same + return; + } + switch (mode) { + case NONE: + SSL.setVerify(ssl, SSL.SSL_CVERIFY_NONE, OpenSslContext.VERIFY_DEPTH); + break; + case REQUIRE: + SSL.setVerify(ssl, SSL.SSL_CVERIFY_REQUIRE, OpenSslContext.VERIFY_DEPTH); + break; + case OPTIONAL: + SSL.setVerify(ssl, SSL.SSL_CVERIFY_OPTIONAL, OpenSslContext.VERIFY_DEPTH); + break; + default: + throw new Error(mode.toString()); + } + clientAuth = mode; + } + } + + @Override + public final void setEnableSessionCreation(boolean b) { + if (b) { + throw new UnsupportedOperationException(); + } + } + + @Override + public final boolean getEnableSessionCreation() { + return false; + } + + @Override + public final synchronized SSLParameters getSSLParameters() { + SSLParameters sslParameters = super.getSSLParameters(); + + int version = PlatformDependent.javaVersion(); + if (version >= 7) { + sslParameters.setEndpointIdentificationAlgorithm(endPointIdentificationAlgorithm); + SslParametersUtils.setAlgorithmConstraints(sslParameters, algorithmConstraints); + if (version >= 8) { + if (SET_SERVER_NAMES_METHOD != null && sniHostNames != null) { + try { + SET_SERVER_NAMES_METHOD.invoke(sslParameters, sniHostNames); + } catch (IllegalAccessException e) { + throw new Error(e); + } catch (InvocationTargetException e) { + throw new Error(e); + } + } + if (SET_USE_CIPHER_SUITES_ORDER_METHOD != null && !isDestroyed()) { + try { + SET_USE_CIPHER_SUITES_ORDER_METHOD.invoke(sslParameters, + (SSL.getOptions(ssl) & SSL.SSL_OP_CIPHER_SERVER_PREFERENCE) != 0); + } catch (IllegalAccessException e) { + throw new Error(e); + } catch (InvocationTargetException e) { + throw new Error(e); + } + } + } + } + return sslParameters; + } + + @Override + public final synchronized void setSSLParameters(SSLParameters sslParameters) { + super.setSSLParameters(sslParameters); + + int version = PlatformDependent.javaVersion(); + if (version >= 7) { + endPointIdentificationAlgorithm = sslParameters.getEndpointIdentificationAlgorithm(); + algorithmConstraints = sslParameters.getAlgorithmConstraints(); + if (version >= 8) { + if (SNI_HOSTNAME_CLASS != null && clientMode && !isDestroyed()) { + assert GET_SERVER_NAMES_METHOD != null; + assert GET_ASCII_NAME_METHOD != null; + try { + List servernames = (List) GET_SERVER_NAMES_METHOD.invoke(sslParameters); + if (servernames != null) { + for (Object serverName : servernames) { + if (SNI_HOSTNAME_CLASS.isInstance(serverName)) { + SSL.setTlsExtHostName(ssl, (String) GET_ASCII_NAME_METHOD.invoke(serverName)); + } else { + throw new IllegalArgumentException("Only " + SNI_HOSTNAME_CLASS.getName() + + " instances are supported, but found: " + + serverName); + } + } + } + sniHostNames = servernames; + } catch (IllegalAccessException e) { + throw new Error(e); + } catch (InvocationTargetException e) { + throw new Error(e); + } + } + if (GET_USE_CIPHER_SUITES_ORDER_METHOD != null && !isDestroyed()) { + try { + if ((Boolean) GET_USE_CIPHER_SUITES_ORDER_METHOD.invoke(sslParameters)) { + SSL.setOptions(ssl, SSL.SSL_OP_CIPHER_SERVER_PREFERENCE); + } else { + SSL.clearOptions(ssl, SSL.SSL_OP_CIPHER_SERVER_PREFERENCE); + } + } catch (IllegalAccessException e) { + throw new Error(e); + } catch (InvocationTargetException e) { + throw new Error(e); + } + } + } + } + } + + private boolean isDestroyed() { + return destroyed != 0; + } + + private final class OpenSslSession implements SSLSession, ApplicationProtocolAccessor { + private final OpenSslSessionContext sessionContext; + + // These are guarded by synchronized(OpenSslEngine.this) as handshakeFinished() may be triggered by any + // thread. + private X509Certificate[] x509PeerCerts; + private String protocol; + private String applicationProtocol; + private Certificate[] peerCerts; + private String cipher; + private byte[] id; + private long creationTime; + + // lazy init for memory reasons + private Map values; + + OpenSslSession(OpenSslSessionContext sessionContext) { + this.sessionContext = sessionContext; + } + + @Override + public byte[] getId() { + synchronized (ReferenceCountedOpenSslEngine.this) { + if (id == null) { + return EmptyArrays.EMPTY_BYTES; + } + return id.clone(); + } + } + + @Override + public SSLSessionContext getSessionContext() { + return sessionContext; + } + + @Override + public long getCreationTime() { + synchronized (ReferenceCountedOpenSslEngine.this) { + if (creationTime == 0 && !isDestroyed()) { + creationTime = SSL.getTime(ssl) * 1000L; + } + } + return creationTime; + } + + @Override + public long getLastAccessedTime() { + long lastAccessed = ReferenceCountedOpenSslEngine.this.lastAccessed; + // if lastAccessed is -1 we will just return the creation time as the handshake was not started yet. + return lastAccessed == -1 ? getCreationTime() : lastAccessed; + } + + @Override + public void invalidate() { + synchronized (ReferenceCountedOpenSslEngine.this) { + if (!isDestroyed()) { + SSL.setTimeout(ssl, 0); + } + } + } + + @Override + public boolean isValid() { + synchronized (ReferenceCountedOpenSslEngine.this) { + if (!isDestroyed()) { + return System.currentTimeMillis() - (SSL.getTimeout(ssl) * 1000L) < (SSL.getTime(ssl) * 1000L); + } + } + return false; + } + + @Override + public void putValue(String name, Object value) { + if (name == null) { + throw new NullPointerException("name"); + } + if (value == null) { + throw new NullPointerException("value"); + } + Map values = this.values; + if (values == null) { + // Use size of 2 to keep the memory overhead small + values = this.values = new HashMap(2); + } + Object old = values.put(name, value); + if (value instanceof SSLSessionBindingListener) { + ((SSLSessionBindingListener) value).valueBound(new SSLSessionBindingEvent(this, name)); + } + notifyUnbound(old, name); + } + + @Override + public Object getValue(String name) { + if (name == null) { + throw new NullPointerException("name"); + } + if (values == null) { + return null; + } + return values.get(name); + } + + @Override + public void removeValue(String name) { + if (name == null) { + throw new NullPointerException("name"); + } + Map values = this.values; + if (values == null) { + return; + } + Object old = values.remove(name); + notifyUnbound(old, name); + } + + @Override + public String[] getValueNames() { + Map values = this.values; + if (values == null || values.isEmpty()) { + return EmptyArrays.EMPTY_STRINGS; + } + return values.keySet().toArray(new String[values.size()]); + } + + private void notifyUnbound(Object value, String name) { + if (value instanceof SSLSessionBindingListener) { + ((SSLSessionBindingListener) value).valueUnbound(new SSLSessionBindingEvent(this, name)); + } + } + + /** + * Finish the handshake and so init everything in the {@link OpenSslSession} that should be accessible by + * the user. + */ + void handshakeFinished() throws SSLException { + synchronized (ReferenceCountedOpenSslEngine.this) { + if (!isDestroyed()) { + id = SSL.getSessionId(ssl); + cipher = toJavaCipherSuite(SSL.getCipherForSSL(ssl)); + protocol = SSL.getVersion(ssl); + + initPeerCerts(); + selectApplicationProtocol(); + + handshakeState = HandshakeState.FINISHED; + } else { + throw new SSLException("Already closed"); + } + } + } + + /** + * Init peer certificates that can be obtained via {@link #getPeerCertificateChain()} + * and {@link #getPeerCertificates()}. + */ + private void initPeerCerts() { + // Return the full chain from the JNI layer. + byte[][] chain = SSL.getPeerCertChain(ssl); + final byte[] clientCert; + if (!clientMode) { + // if used on the server side SSL_get_peer_cert_chain(...) will not include the remote peer + // certificate. We use SSL_get_peer_certificate to get it in this case and add it to our + // array later. + // + // See https://www.openssl.org/docs/ssl/SSL_get_peer_cert_chain.html + clientCert = SSL.getPeerCertificate(ssl); + } else { + clientCert = null; + } + + if (chain == null && clientCert == null) { + peerCerts = EMPTY_CERTIFICATES; + x509PeerCerts = EMPTY_X509_CERTIFICATES; + } else { + int len = chain != null ? chain.length : 0; + + int i = 0; + Certificate[] peerCerts; + if (clientCert != null) { + len++; + peerCerts = new Certificate[len]; + peerCerts[i++] = new OpenSslX509Certificate(clientCert); + } else { + peerCerts = new Certificate[len]; + } + if (chain != null) { + X509Certificate[] pCerts = new X509Certificate[chain.length]; + + for (int a = 0; a < pCerts.length; ++i, ++a) { + byte[] bytes = chain[a]; + pCerts[a] = new OpenSslJavaxX509Certificate(bytes); + peerCerts[i] = new OpenSslX509Certificate(bytes); + } + x509PeerCerts = pCerts; + } else { + x509PeerCerts = EMPTY_X509_CERTIFICATES; + } + this.peerCerts = peerCerts; + } + } + + /** + * Select the application protocol used. + */ + private void selectApplicationProtocol() throws SSLException { + ApplicationProtocolConfig.SelectedListenerFailureBehavior behavior = apn.selectedListenerFailureBehavior(); + List protocols = apn.protocols(); + String applicationProtocol; + switch (apn.protocol()) { + case NONE: + break; + // We always need to check for applicationProtocol == null as the remote peer may not support + // the TLS extension or may have returned an empty selection. + case ALPN: + applicationProtocol = SSL.getAlpnSelected(ssl); + if (applicationProtocol != null) { + this.applicationProtocol = selectApplicationProtocol( + protocols, behavior, applicationProtocol); + } + break; + case NPN: + applicationProtocol = SSL.getNextProtoNegotiated(ssl); + if (applicationProtocol != null) { + this.applicationProtocol = selectApplicationProtocol( + protocols, behavior, applicationProtocol); + } + break; + case NPN_AND_ALPN: + applicationProtocol = SSL.getAlpnSelected(ssl); + if (applicationProtocol == null) { + applicationProtocol = SSL.getNextProtoNegotiated(ssl); + } + if (applicationProtocol != null) { + this.applicationProtocol = selectApplicationProtocol( + protocols, behavior, applicationProtocol); + } + break; + default: + throw new Error(); + } + } + + private String selectApplicationProtocol(List protocols, + ApplicationProtocolConfig.SelectedListenerFailureBehavior behavior, + String applicationProtocol) throws SSLException { + if (behavior == ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT) { + return applicationProtocol; + } else { + int size = protocols.size(); + assert size > 0; + if (protocols.contains(applicationProtocol)) { + return applicationProtocol; + } else { + if (behavior == ApplicationProtocolConfig.SelectedListenerFailureBehavior.CHOOSE_MY_LAST_PROTOCOL) { + return protocols.get(size - 1); + } else { + throw new SSLException("unknown protocol " + applicationProtocol); + } + } + } + } + + @Override + public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException { + synchronized (ReferenceCountedOpenSslEngine.this) { + if (peerCerts == null || peerCerts.length == 0) { + throw new SSLPeerUnverifiedException("peer not verified"); + } + return peerCerts; + } + } + + @Override + public Certificate[] getLocalCertificates() { + if (localCerts == null) { + return null; + } + return localCerts.clone(); + } + + @Override + public X509Certificate[] getPeerCertificateChain() throws SSLPeerUnverifiedException { + synchronized (ReferenceCountedOpenSslEngine.this) { + if (x509PeerCerts == null || x509PeerCerts.length == 0) { + throw new SSLPeerUnverifiedException("peer not verified"); + } + return x509PeerCerts; + } + } + + @Override + public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { + Certificate[] peer = getPeerCertificates(); + // No need for null or length > 0 is needed as this is done in getPeerCertificates() + // already. + return ((java.security.cert.X509Certificate) peer[0]).getSubjectX500Principal(); + } + + @Override + public Principal getLocalPrincipal() { + Certificate[] local = localCerts; + if (local == null || local.length == 0) { + return null; + } + return ((java.security.cert.X509Certificate) local[0]).getIssuerX500Principal(); + } + + @Override + public String getCipherSuite() { + synchronized (ReferenceCountedOpenSslEngine.this) { + if (cipher == null) { + return INVALID_CIPHER; + } + return cipher; + } + } + + @Override + public String getProtocol() { + String protocol = this.protocol; + if (protocol == null) { + synchronized (ReferenceCountedOpenSslEngine.this) { + if (!isDestroyed()) { + protocol = SSL.getVersion(ssl); + } else { + protocol = StringUtil.EMPTY_STRING; + } + } + } + return protocol; + } + + @Override + public String getApplicationProtocol() { + synchronized (ReferenceCountedOpenSslEngine.this) { + return applicationProtocol; + } + } + + @Override + public String getPeerHost() { + return ReferenceCountedOpenSslEngine.this.getPeerHost(); + } + + @Override + public int getPeerPort() { + return ReferenceCountedOpenSslEngine.this.getPeerPort(); + } + + @Override + public int getPacketBufferSize() { + return MAX_ENCRYPTED_PACKET_LENGTH; + } + + @Override + public int getApplicationBufferSize() { + return MAX_PLAINTEXT_LENGTH; + } + } +} + diff --git a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslServerContext.java b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslServerContext.java new file mode 100644 index 0000000000..c0740298e6 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslServerContext.java @@ -0,0 +1,191 @@ +/* + * Copyright 2016 The Netty Project + * + * The Netty Project licenses this file to you 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 io.netty.handler.ssl; + +import org.apache.tomcat.jni.SSL; +import org.apache.tomcat.jni.SSLContext; + +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLException; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509ExtendedTrustManager; +import javax.net.ssl.X509KeyManager; +import javax.net.ssl.X509TrustManager; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +/** + * A server-side {@link SslContext} which uses OpenSSL's SSL/TLS implementation. + *

Instances of this class must be {@link #release() released} or else native memory will leak! + */ +public final class ReferenceCountedOpenSslServerContext extends ReferenceCountedOpenSslContext { + private static final byte[] ID = new byte[] {'n', 'e', 't', 't', 'y'}; + private final OpenSslServerSessionContext sessionContext; + private final OpenSslKeyMaterialManager keyMaterialManager; + + ReferenceCountedOpenSslServerContext( + X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory, + X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, + Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, + long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth) throws SSLException { + this(trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, ciphers, + cipherFilter, toNegotiator(apn), sessionCacheSize, sessionTimeout, clientAuth); + } + + private ReferenceCountedOpenSslServerContext( + X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory, + X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, + Iterable ciphers, CipherSuiteFilter cipherFilter, OpenSslApplicationProtocolNegotiator apn, + long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth) throws SSLException { + super(ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, SSL.SSL_MODE_SERVER, keyCertChain, + clientAuth, true); + // Create a new SSL_CTX and configure it. + boolean success = false; + try { + ServerContext context = newSessionContext(this, ctx, engineMap, trustCertCollection, trustManagerFactory, + keyCertChain, key, keyPassword, keyManagerFactory); + sessionContext = context.sessionContext; + keyMaterialManager = context.keyMaterialManager; + success = true; + } finally { + if (!success) { + release(); + } + } + } + + @Override + public OpenSslServerSessionContext sessionContext() { + return sessionContext; + } + + @Override + OpenSslKeyMaterialManager keyMaterialManager() { + return keyMaterialManager; + } + + static final class ServerContext { + OpenSslServerSessionContext sessionContext; + OpenSslKeyMaterialManager keyMaterialManager; + } + + static ServerContext newSessionContext(ReferenceCountedOpenSslContext thiz, long ctx, OpenSslEngineMap engineMap, + X509Certificate[] trustCertCollection, + TrustManagerFactory trustManagerFactory, + X509Certificate[] keyCertChain, PrivateKey key, + String keyPassword, KeyManagerFactory keyManagerFactory) + throws SSLException { + ServerContext result = new ServerContext(); + synchronized (ReferenceCountedOpenSslContext.class) { + try { + SSLContext.setVerify(ctx, SSL.SSL_CVERIFY_NONE, VERIFY_DEPTH); + if (!OpenSsl.useKeyManagerFactory()) { + if (keyManagerFactory != null) { + throw new IllegalArgumentException( + "KeyManagerFactory not supported"); + } + checkNotNull(keyCertChain, "keyCertChain"); + + /* Set certificate verification policy. */ + SSLContext.setVerify(ctx, SSL.SSL_CVERIFY_NONE, VERIFY_DEPTH); + + setKeyMaterial(ctx, keyCertChain, key, keyPassword); + } else { + // javadocs state that keyManagerFactory has precedent over keyCertChain, and we must have a + // keyManagerFactory for the server so build one if it is not specified. + if (keyManagerFactory == null) { + keyManagerFactory = buildKeyManagerFactory( + keyCertChain, key, keyPassword, keyManagerFactory); + } + X509KeyManager keyManager = chooseX509KeyManager(keyManagerFactory.getKeyManagers()); + result.keyMaterialManager = useExtendedKeyManager(keyManager) ? + new OpenSslExtendedKeyMaterialManager( + (X509ExtendedKeyManager) keyManager, keyPassword) : + new OpenSslKeyMaterialManager(keyManager, keyPassword); + } + } catch (Exception e) { + throw new SSLException("failed to set certificate and key", e); + } + try { + if (trustCertCollection != null) { + trustManagerFactory = buildTrustManagerFactory(trustCertCollection, trustManagerFactory); + } else if (trustManagerFactory == null) { + // Mimic the way SSLContext.getInstance(KeyManager[], null, null) works + trustManagerFactory = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init((KeyStore) null); + } + + final X509TrustManager manager = chooseTrustManager(trustManagerFactory.getTrustManagers()); + + // IMPORTANT: The callbacks set for verification must be static to prevent memory leak as + // otherwise the context can never be collected. This is because the JNI code holds + // a global reference to the callbacks. + // + // See https://github.com/netty/netty/issues/5372 + + // Use this to prevent an error when running on java < 7 + if (useExtendedTrustManager(manager)) { + SSLContext.setCertVerifyCallback(ctx, + new ExtendedTrustManagerVerifyCallback(engineMap, (X509ExtendedTrustManager) manager)); + } else { + SSLContext.setCertVerifyCallback(ctx, new TrustManagerVerifyCallback(engineMap, manager)); + } + } catch (Exception e) { + throw new SSLException("unable to setup trustmanager", e); + } + } + + result.sessionContext = new OpenSslServerSessionContext(thiz); + result.sessionContext.setSessionIdContext(ID); + return result; + } + + private static final class TrustManagerVerifyCallback extends AbstractCertificateVerifier { + private final X509TrustManager manager; + + TrustManagerVerifyCallback(OpenSslEngineMap engineMap, X509TrustManager manager) { + super(engineMap); + this.manager = manager; + } + + @Override + void verify(ReferenceCountedOpenSslEngine engine, X509Certificate[] peerCerts, String auth) + throws Exception { + manager.checkClientTrusted(peerCerts, auth); + } + } + + private static final class ExtendedTrustManagerVerifyCallback extends AbstractCertificateVerifier { + private final X509ExtendedTrustManager manager; + + ExtendedTrustManagerVerifyCallback(OpenSslEngineMap engineMap, X509ExtendedTrustManager manager) { + super(engineMap); + this.manager = manager; + } + + @Override + void verify(ReferenceCountedOpenSslEngine engine, X509Certificate[] peerCerts, String auth) + throws Exception { + manager.checkClientTrusted(peerCerts, auth, engine); + } + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/SslContext.java b/handler/src/main/java/io/netty/handler/ssl/SslContext.java index fd7967d192..9e3986da9a 100644 --- a/handler/src/main/java/io/netty/handler/ssl/SslContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/SslContext.java @@ -396,8 +396,7 @@ public abstract class SslContext { X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory, X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, - long sessionCacheSize, long sessionTimeout, - ClientAuth clientAuth) throws SSLException { + long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth) throws SSLException { if (provider == null) { provider = defaultServerProvider(); @@ -414,6 +413,11 @@ public abstract class SslContext { trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, clientAuth); + case OPENSSL_REFCNT: + return new ReferenceCountedOpenSslServerContext( + trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword, + keyManagerFactory, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, + clientAuth); default: throw new Error(provider.toString()); } @@ -749,9 +753,13 @@ public abstract class SslContext { return new OpenSslClientContext( trustCert, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout); + case OPENSSL_REFCNT: + return new ReferenceCountedOpenSslClientContext( + trustCert, trustManagerFactory, keyCertChain, key, keyPassword, + keyManagerFactory, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout); + default: + throw new Error(provider.toString()); } - // Should never happen!! - throw new Error(); } static ApplicationProtocolConfig toApplicationProtocolConfig(Iterable nextProtocols) { @@ -813,14 +821,17 @@ public abstract class SslContext { /** * Creates a new {@link SSLEngine}. - * + *

If {@link SslProvider#OPENSSL_REFCNT} is used then the object must be released. One way to do this is to + * wrap in a {@link SslHandler} and insert it into a pipeline. See {@link #newHandler(ByteBufAllocator)}. * @return a new {@link SSLEngine} */ public abstract SSLEngine newEngine(ByteBufAllocator alloc); /** * Creates a new {@link SSLEngine} using advisory peer information. - * + *

If {@link SslProvider#OPENSSL_REFCNT} is used then the object must be released. One way to do this is to + * wrap in a {@link SslHandler} and insert it into a pipeline. + * See {@link #newHandler(ByteBufAllocator, String, int)}. * @param peerHost the non-authoritative name of the host * @param peerPort the non-authoritative port * @@ -835,7 +846,9 @@ public abstract class SslContext { /** * Creates a new {@link SslHandler}. - * + *

If {@link SslProvider#OPENSSL_REFCNT} is used then the returned {@link SslHandler} will release the engine + * that is wrapped. If the returned {@link SslHandler} is not inserted into a pipeline then you may leak native + * memory! * @return a new {@link SslHandler} */ public final SslHandler newHandler(ByteBufAllocator alloc) { @@ -844,7 +857,9 @@ public abstract class SslContext { /** * Creates a new {@link SslHandler} with advisory peer information. - * + *

If {@link SslProvider#OPENSSL_REFCNT} is used then the returned {@link SslHandler} will release the engine + * that is wrapped. If the returned {@link SslHandler} is not inserted into a pipeline then you may leak native + * memory! * @param peerHost the non-authoritative name of the host * @param peerPort the non-authoritative port * diff --git a/handler/src/main/java/io/netty/handler/ssl/SslContextBuilder.java b/handler/src/main/java/io/netty/handler/ssl/SslContextBuilder.java index 969bea199c..9f70b07682 100644 --- a/handler/src/main/java/io/netty/handler/ssl/SslContextBuilder.java +++ b/handler/src/main/java/io/netty/handler/ssl/SslContextBuilder.java @@ -385,6 +385,8 @@ public final class SslContextBuilder { /** * Create new {@code SslContext} instance with configured settings. + *

If {@link #sslProvider(SslProvider)} is set to {@link SslProvider#OPENSSL_REFCNT} then the caller is + * responsible for releasing this object, or else native memory may leak. */ public SslContext build() throws SSLException { if (forServer) { diff --git a/handler/src/main/java/io/netty/handler/ssl/SslHandler.java b/handler/src/main/java/io/netty/handler/ssl/SslHandler.java index 5eb4cd8bd3..7e815673f6 100644 --- a/handler/src/main/java/io/netty/handler/ssl/SslHandler.java +++ b/handler/src/main/java/io/netty/handler/ssl/SslHandler.java @@ -421,9 +421,8 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH // Check if queue is not empty first because create a new ChannelException is expensive pendingUnencryptedWrites.removeAndFailAll(new ChannelException("Pending write on removal of SslHandler")); } - if (engine instanceof OpenSslEngine) { - // Call shutdown so we ensure all the native memory is released asap - ((OpenSslEngine) engine).shutdown(); + if (engine instanceof ReferenceCountedOpenSslEngine) { + ((ReferenceCountedOpenSslEngine) engine).release(); } } diff --git a/handler/src/main/java/io/netty/handler/ssl/SslProvider.java b/handler/src/main/java/io/netty/handler/ssl/SslProvider.java index 3d4f08bfa9..00fc2aac93 100644 --- a/handler/src/main/java/io/netty/handler/ssl/SslProvider.java +++ b/handler/src/main/java/io/netty/handler/ssl/SslProvider.java @@ -16,6 +16,9 @@ package io.netty.handler.ssl; +import io.netty.util.ReferenceCounted; +import io.netty.util.internal.UnstableApi; + /** * An enumeration of SSL/TLS protocol providers. */ @@ -27,5 +30,10 @@ public enum SslProvider { /** * OpenSSL-based implementation. */ - OPENSSL + OPENSSL, + /** + * OpenSSL-based implementation which does not have finalizers and instead implements {@link ReferenceCounted}. + */ + @UnstableApi + OPENSSL_REFCNT } diff --git a/handler/src/test/java/io/netty/handler/ssl/OpenSslEngineTest.java b/handler/src/test/java/io/netty/handler/ssl/OpenSslEngineTest.java index d0620c67f3..4dab24bca3 100644 --- a/handler/src/test/java/io/netty/handler/ssl/OpenSslEngineTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/OpenSslEngineTest.java @@ -21,6 +21,7 @@ import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBeh import io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.netty.util.ReferenceCountUtil; import io.netty.util.internal.ThreadLocalRandom; import org.junit.BeforeClass; import org.junit.Test; @@ -79,26 +80,33 @@ public class OpenSslEngineTest extends SSLEngineTest { } @Test public void testWrapHeapBuffersNoWritePendingError() throws Exception { - final SslContext clientContext = SslContextBuilder.forClient() + clientSslCtx = SslContextBuilder.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE) .sslProvider(sslProvider()) .build(); SelfSignedCertificate ssc = new SelfSignedCertificate(); - SslContext serverContext = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) .sslProvider(sslProvider()) .build(); - SSLEngine clientEngine = clientContext.newEngine(UnpooledByteBufAllocator.DEFAULT); - SSLEngine serverEngine = serverContext.newEngine(UnpooledByteBufAllocator.DEFAULT); - handshake(clientEngine, serverEngine); + SSLEngine clientEngine = null; + SSLEngine serverEngine = null; + try { + clientEngine = clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT); + serverEngine = serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT); + handshake(clientEngine, serverEngine); - ByteBuffer src = ByteBuffer.allocate(1024 * 10); - ThreadLocalRandom.current().nextBytes(src.array()); - ByteBuffer dst = ByteBuffer.allocate(1); - // Try to wrap multiple times so we are more likely to hit the issue. - for (int i = 0; i < 100; i++) { - src.position(0); - dst.position(0); - assertSame(SSLEngineResult.Status.BUFFER_OVERFLOW, clientEngine.wrap(src, dst).getStatus()); + ByteBuffer src = ByteBuffer.allocate(1024 * 10); + ThreadLocalRandom.current().nextBytes(src.array()); + ByteBuffer dst = ByteBuffer.allocate(1); + // Try to wrap multiple times so we are more likely to hit the issue. + for (int i = 0; i < 100; i++) { + src.position(0); + dst.position(0); + assertSame(SSLEngineResult.Status.BUFFER_OVERFLOW, clientEngine.wrap(src, dst).getStatus()); + } + } finally { + cleanupSslEngine(clientEngine); + cleanupSslEngine(serverEngine); } } diff --git a/handler/src/test/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngineTest.java b/handler/src/test/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngineTest.java new file mode 100644 index 0000000000..6d2bae7a97 --- /dev/null +++ b/handler/src/test/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngineTest.java @@ -0,0 +1,37 @@ +/* + * Copyright 2016 The Netty Project + * + * The Netty Project licenses this file to you 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 io.netty.handler.ssl; + +import io.netty.util.ReferenceCountUtil; + +import javax.net.ssl.SSLEngine; + +public class ReferenceCountedOpenSslEngineTest extends OpenSslEngineTest { + @Override + protected SslProvider sslProvider() { + return SslProvider.OPENSSL_REFCNT; + } + + @Override + protected void cleanupSslContext(SslContext ctx) { + ReferenceCountUtil.release(ctx); + } + + @Override + protected void cleanupSslEngine(SSLEngine engine) { + ReferenceCountUtil.release(engine); + } +} diff --git a/handler/src/test/java/io/netty/handler/ssl/SSLEngineTest.java b/handler/src/test/java/io/netty/handler/ssl/SSLEngineTest.java index 2b1c9067d8..6a7bc0c497 100644 --- a/handler/src/test/java/io/netty/handler/ssl/SSLEngineTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/SSLEngineTest.java @@ -130,6 +130,14 @@ public abstract class SSLEngineTest { serverChannel.close().sync(); serverChannel = null; } + if (serverSslCtx != null) { + cleanupSslContext(serverSslCtx); + serverSslCtx = null; + } + if (clientSslCtx != null) { + cleanupSslContext(clientSslCtx); + clientSslCtx = null; + } Future serverGroupShutdownFuture = null; Future serverChildGroupShutdownFuture = null; Future clientGroupShutdownFuture = null; @@ -333,54 +341,73 @@ public abstract class SSLEngineTest { @Test public void testGetCreationTime() throws Exception { - SslContext context = SslContextBuilder.forClient().sslProvider(sslProvider()).build(); - SSLEngine engine = context.newEngine(UnpooledByteBufAllocator.DEFAULT); - assertTrue(engine.getSession().getCreationTime() <= System.currentTimeMillis()); + clientSslCtx = SslContextBuilder.forClient().sslProvider(sslProvider()).build(); + SSLEngine engine = null; + try { + engine = clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT); + assertTrue(engine.getSession().getCreationTime() <= System.currentTimeMillis()); + } finally { + cleanupSslEngine(engine); + } } @Test public void testSessionInvalidate() throws Exception { - final SslContext clientContext = SslContextBuilder.forClient() + clientSslCtx = SslContextBuilder.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE) .sslProvider(sslProvider()) .build(); SelfSignedCertificate ssc = new SelfSignedCertificate(); - SslContext serverContext = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) .sslProvider(sslProvider()) .build(); - SSLEngine clientEngine = clientContext.newEngine(UnpooledByteBufAllocator.DEFAULT); - SSLEngine serverEngine = serverContext.newEngine(UnpooledByteBufAllocator.DEFAULT); - handshake(clientEngine, serverEngine); + SSLEngine clientEngine = null; + SSLEngine serverEngine = null; + try { + clientEngine = clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT); + serverEngine = serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT); + handshake(clientEngine, serverEngine); - SSLSession session = serverEngine.getSession(); - assertTrue(session.isValid()); - session.invalidate(); - assertFalse(session.isValid()); + SSLSession session = serverEngine.getSession(); + assertTrue(session.isValid()); + session.invalidate(); + assertFalse(session.isValid()); + } finally { + cleanupSslEngine(clientEngine); + cleanupSslEngine(serverEngine); + } } @Test public void testSSLSessionId() throws Exception { - final SslContext clientContext = SslContextBuilder.forClient() + clientSslCtx = SslContextBuilder.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE) .sslProvider(sslProvider()) .build(); SelfSignedCertificate ssc = new SelfSignedCertificate(); - SslContext serverContext = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) .sslProvider(sslProvider()) .build(); - SSLEngine clientEngine = clientContext.newEngine(UnpooledByteBufAllocator.DEFAULT); - SSLEngine serverEngine = serverContext.newEngine(UnpooledByteBufAllocator.DEFAULT); + SSLEngine clientEngine = null; + SSLEngine serverEngine = null; + try { + clientEngine = clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT); + serverEngine = serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT); - // Before the handshake the id should have length == 0 - assertEquals(0, clientEngine.getSession().getId().length); - assertEquals(0, serverEngine.getSession().getId().length); + // Before the handshake the id should have length == 0 + assertEquals(0, clientEngine.getSession().getId().length); + assertEquals(0, serverEngine.getSession().getId().length); - handshake(clientEngine, serverEngine); + handshake(clientEngine, serverEngine); - // After the handshake the id should have length > 0 - assertNotEquals(0, clientEngine.getSession().getId().length); - assertNotEquals(0, serverEngine.getSession().getId().length); - assertArrayEquals(clientEngine.getSession().getId(), serverEngine.getSession().getId()); + // After the handshake the id should have length > 0 + assertNotEquals(0, clientEngine.getSession().getId().length); + assertNotEquals(0, serverEngine.getSession().getId().length); + assertArrayEquals(clientEngine.getSession().getId(), serverEngine.getSession().getId()); + } finally { + cleanupSslEngine(clientEngine); + cleanupSslEngine(serverEngine); + } } @Test(timeout = 3000) @@ -493,11 +520,11 @@ public abstract class SSLEngineTest { try { File serverKeyFile = new File(getClass().getResource("test_unencrypted.pem").getFile()); File serverCrtFile = new File(getClass().getResource("test.crt").getFile()); - SslContext sslContext = SslContextBuilder.forServer(serverCrtFile, serverKeyFile) + serverSslCtx = SslContextBuilder.forServer(serverCrtFile, serverKeyFile) .sslProvider(sslProvider()) .build(); - sslEngine = sslContext.newEngine(UnpooledByteBufAllocator.DEFAULT); + sslEngine = serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT); // Disable all protocols sslEngine.setEnabledProtocols(new String[]{}); @@ -518,6 +545,7 @@ public abstract class SSLEngineTest { if (sslEngine != null) { sslEngine.closeInbound(); sslEngine.closeOutbound(); + cleanupSslEngine(sslEngine); } } } @@ -575,6 +603,18 @@ public abstract class SSLEngineTest { protected abstract SslProvider sslProvider(); + /** + * Called from the test cleanup code and can be used to release the {@code ctx} if it must be done manually. + */ + protected void cleanupSslContext(SslContext ctx) { + } + + /** + * Called when ever an SSLEngine is not wrapped by a {@link SslHandler} and inserted into a pipeline. + */ + protected void cleanupSslEngine(SSLEngine engine) { + } + protected void setupHandlers(ApplicationProtocolConfig apn) throws InterruptedException, SSLException, CertificateException { setupHandlers(apn, apn);