From 7d4aaa268b8a536f61fbc7711365147c58238745 Mon Sep 17 00:00:00 2001 From: Norman Maurer Date: Sun, 7 Mar 2021 19:15:39 +0100 Subject: [PATCH] Support session cache for client and server when using native SSLEngine implementation (#10994) Motivation: At the moment we don't support session caching on the client side at all when using the native SSL implementation. We should at least allow to enable it. Modification: Allow to enable session cache for client side but disable ti by default due a JDK bug atm. Result: Be able to cache sessions on the client side when using native SSL implementation . --- .../handler/ssl/ExtendedOpenSslSession.java | 72 ++- .../io/netty/handler/ssl/JdkSslContext.java | 10 - .../handler/ssl/OpenSslClientContext.java | 5 +- .../ssl/OpenSslClientSessionCache.java | 138 +++++ .../io/netty/handler/ssl/OpenSslContext.java | 18 +- .../handler/ssl/OpenSslServerContext.java | 5 +- .../ssl/OpenSslServerSessionContext.java | 76 +-- .../io/netty/handler/ssl/OpenSslSession.java | 32 +- .../handler/ssl/OpenSslSessionCache.java | 492 ++++++++++++++++++ .../handler/ssl/OpenSslSessionContext.java | 111 +++- .../netty/handler/ssl/OpenSslSessionId.java | 66 +++ .../ReferenceCountedOpenSslClientContext.java | 56 +- .../ssl/ReferenceCountedOpenSslContext.java | 37 +- .../ssl/ReferenceCountedOpenSslEngine.java | 320 +++++++----- .../ReferenceCountedOpenSslServerContext.java | 16 +- .../java/io/netty/handler/ssl/SslContext.java | 8 +- .../java/io/netty/handler/ssl/SslUtils.java | 4 + .../ssl/util/LazyJavaxX509Certificate.java | 8 + .../ssl/ConscryptJdkSslEngineInteropTest.java | 6 + .../ConscryptOpenSslEngineInteropTest.java | 8 + .../handler/ssl/ConscryptSslEngineTest.java | 12 + .../ssl/JdkConscryptSslEngineInteropTest.java | 12 + .../ssl/JdkOpenSslEngineInteroptTest.java | 2 + .../OpenSslConscryptSslEngineInteropTest.java | 8 + .../netty/handler/ssl/OpenSslEngineTest.java | 34 ++ .../ssl/OpenSslJdkSslEngineInteroptTest.java | 3 +- .../ReferenceCountedOpenSslEngineTest.java | 2 + .../io/netty/handler/ssl/SSLEngineTest.java | 331 +++++++++++- .../io/netty/handler/ssl/SslHandlerTest.java | 75 ++- 29 files changed, 1593 insertions(+), 374 deletions(-) create mode 100644 handler/src/main/java/io/netty/handler/ssl/OpenSslClientSessionCache.java create mode 100644 handler/src/main/java/io/netty/handler/ssl/OpenSslSessionCache.java create mode 100644 handler/src/main/java/io/netty/handler/ssl/OpenSslSessionId.java diff --git a/handler/src/main/java/io/netty/handler/ssl/ExtendedOpenSslSession.java b/handler/src/main/java/io/netty/handler/ssl/ExtendedOpenSslSession.java index d6a4ce4955..963a9b616a 100644 --- a/handler/src/main/java/io/netty/handler/ssl/ExtendedOpenSslSession.java +++ b/handler/src/main/java/io/netty/handler/ssl/ExtendedOpenSslSession.java @@ -15,12 +15,14 @@ */ package io.netty.handler.ssl; +import io.netty.util.internal.EmptyArrays; import io.netty.util.internal.SuppressJava6Requirement; import javax.net.ssl.ExtendedSSLSession; import javax.net.ssl.SSLException; import javax.net.ssl.SSLPeerUnverifiedException; -import javax.net.ssl.SSLSessionContext; +import javax.net.ssl.SSLSessionBindingEvent; +import javax.net.ssl.SSLSessionBindingListener; import javax.security.cert.X509Certificate; import java.security.Principal; import java.security.cert.Certificate; @@ -61,8 +63,23 @@ abstract class ExtendedOpenSslSession extends ExtendedSSLSession implements Open } @Override - public final void handshakeFinished() throws SSLException { - wrapped.handshakeFinished(); + public OpenSslSessionId sessionId() { + return wrapped.sessionId(); + } + + @Override + public void setSessionId(OpenSslSessionId id) { + wrapped.setSessionId(id); + } + + @Override + public final void setLocalCertificate(Certificate[] localCertificate) { + wrapped.setLocalCertificate(localCertificate); + } + + @Override + public String[] getPeerSupportedSignatureAlgorithms() { + return EmptyArrays.EMPTY_STRINGS; } @Override @@ -81,7 +98,7 @@ abstract class ExtendedOpenSslSession extends ExtendedSSLSession implements Open } @Override - public final SSLSessionContext getSessionContext() { + public final OpenSslSessionContext getSessionContext() { return wrapped.getSessionContext(); } @@ -106,13 +123,22 @@ abstract class ExtendedOpenSslSession extends ExtendedSSLSession implements Open } @Override - public final void putValue(String s, Object o) { - wrapped.putValue(s, o); + public final void putValue(String name, Object value) { + if (value instanceof SSLSessionBindingListener) { + // Decorate the value if needed so we submit the correct SSLSession instance + value = new SSLSessionBindingListenerDecorator((SSLSessionBindingListener) value); + } + wrapped.putValue(name, value); } @Override public final Object getValue(String s) { - return wrapped.getValue(s); + Object value = wrapped.getValue(s); + if (value instanceof SSLSessionBindingListenerDecorator) { + // Unwrap as needed so we return the original value + return ((SSLSessionBindingListenerDecorator) value).delegate; + } + return value; } @Override @@ -179,4 +205,36 @@ abstract class ExtendedOpenSslSession extends ExtendedSSLSession implements Open public final int getApplicationBufferSize() { return wrapped.getApplicationBufferSize(); } + + private final class SSLSessionBindingListenerDecorator implements SSLSessionBindingListener { + + final SSLSessionBindingListener delegate; + + SSLSessionBindingListenerDecorator(SSLSessionBindingListener delegate) { + this.delegate = delegate; + } + + @Override + public void valueBound(SSLSessionBindingEvent event) { + delegate.valueBound(new SSLSessionBindingEvent(ExtendedOpenSslSession.this, event.getName())); + } + + @Override + public void valueUnbound(SSLSessionBindingEvent event) { + delegate.valueUnbound(new SSLSessionBindingEvent(ExtendedOpenSslSession.this, event.getName())); + } + } + + @Override + public void handshakeFinished(byte[] id, String cipher, String protocol, byte[] peerCertificate, + byte[][] peerCertificateChain, long creationTime, long timeout) throws SSLException { + wrapped.handshakeFinished(id, cipher, protocol, peerCertificate, peerCertificateChain, creationTime, timeout); + } + + @Override + public String toString() { + return "ExtendedOpenSslSession{" + + "wrapped=" + wrapped + + '}'; + } } diff --git a/handler/src/main/java/io/netty/handler/ssl/JdkSslContext.java b/handler/src/main/java/io/netty/handler/ssl/JdkSslContext.java index 9a1b40b1bc..b5d5e3431b 100644 --- a/handler/src/main/java/io/netty/handler/ssl/JdkSslContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/JdkSslContext.java @@ -316,16 +316,6 @@ public class JdkSslContext extends SslContext { return unmodifiableCipherSuites; } - @Override - public final long sessionCacheSize() { - return sessionContext().getSessionCacheSize(); - } - - @Override - public final long sessionTimeout() { - return sessionContext().getSessionTimeout(); - } - @Override public final SSLEngine newEngine(ByteBufAllocator alloc) { return configureAndWrapEngine(context().createSSLEngine(), alloc); 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 1b19dc653d..3648b71af9 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslClientContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslClientContext.java @@ -188,13 +188,14 @@ public final class OpenSslClientContext extends OpenSslContext { long sessionCacheSize, long sessionTimeout, boolean enableOcsp, String keyStore, Map.Entry, Object>... options) throws SSLException { - super(ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, SSL.SSL_MODE_CLIENT, keyCertChain, + super(ciphers, cipherFilter, apn, SSL.SSL_MODE_CLIENT, keyCertChain, ClientAuth.NONE, protocols, false, enableOcsp, options); boolean success = false; try { OpenSslKeyMaterialProvider.validateKeyMaterialSupported(keyCertChain, key, keyPassword); sessionContext = newSessionContext(this, ctx, engineMap, trustCertCollection, trustManagerFactory, - keyCertChain, key, keyPassword, keyManagerFactory, keyStore); + keyCertChain, key, keyPassword, keyManagerFactory, keyStore, + sessionCacheSize, sessionTimeout); success = true; } finally { if (!success) { diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslClientSessionCache.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslClientSessionCache.java new file mode 100644 index 0000000000..f0c6daf4f6 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslClientSessionCache.java @@ -0,0 +1,138 @@ +/* + * Copyright 2021 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: + * + * https://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.internal.tcnative.SSL; +import io.netty.util.AsciiString; + +import java.util.HashMap; +import java.util.Map; + +/** + * {@link OpenSslSessionCache} that is used by the client-side. + */ +final class OpenSslClientSessionCache extends OpenSslSessionCache { + // TODO: Should we support to have a List of OpenSslSessions for a Host/Port key and so be able to + // support sessions for different protocols / ciphers to the same remote peer ? + private final Map sessions = new HashMap(); + + OpenSslClientSessionCache(OpenSslEngineMap engineMap) { + super(engineMap); + } + + @Override + protected boolean sessionCreated(NativeSslSession session) { + assert Thread.holdsLock(this); + HostPort hostPort = keyFor(session.getPeerHost(), session.getPeerPort()); + if (hostPort == null || sessions.containsKey(hostPort)) { + return false; + } + sessions.put(hostPort, session); + return true; + } + + @Override + protected void sessionRemoved(NativeSslSession session) { + assert Thread.holdsLock(this); + HostPort hostPort = keyFor(session.getPeerHost(), session.getPeerPort()); + if (hostPort == null) { + return; + } + sessions.remove(hostPort); + } + + @Override + void setSession(long ssl, String host, int port) { + HostPort hostPort = keyFor(host, port); + if (hostPort == null) { + return; + } + final NativeSslSession session; + final boolean reused; + synchronized (this) { + session = sessions.get(hostPort); + if (session == null) { + return; + } + if (!session.isValid()) { + removeSessionWithId(session.sessionId()); + return; + } + // Try to set the session, if true is returned OpenSSL incremented the reference count + // of the underlying SSL_SESSION*. + reused = SSL.setSession(ssl, session.session()); + } + + if (reused) { + if (session.shouldBeSingleUse()) { + // Should only be used once + session.invalidate(); + } + session.updateLastAccessedTime(); + } + } + + private static HostPort keyFor(String host, int port) { + if (host == null && port < 1) { + return null; + } + return new HostPort(host, port); + } + + @Override + synchronized void clear() { + super.clear(); + sessions.clear(); + } + + /** + * Host / Port tuple used to find a {@link OpenSslSession} in the cache. + */ + private static final class HostPort { + private final int hash; + private final String host; + private final int port; + + HostPort(String host, int port) { + this.host = host; + this.port = port; + // Calculate a hashCode that does ignore case. + this.hash = 31 * AsciiString.hashCode(host) + port; + } + + @Override + public int hashCode() { + return hash; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof HostPort)) { + return false; + } + HostPort other = (HostPort) obj; + return port == other.port && host.equalsIgnoreCase(other.host); + } + + @Override + public String toString() { + return "HostPort{" + + "host='" + host + '\'' + + ", port=" + port + + '}'; + } + } +} 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 c3273fdcb2..e50108cf35 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslContext.java @@ -29,21 +29,21 @@ import javax.net.ssl.SSLException; */ public abstract class OpenSslContext extends ReferenceCountedOpenSslContext { OpenSslContext(Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apnCfg, - long sessionCacheSize, long sessionTimeout, int mode, Certificate[] keyCertChain, + int mode, Certificate[] keyCertChain, ClientAuth clientAuth, String[] protocols, boolean startTls, boolean enableOcsp, Map.Entry, Object>... options) throws SSLException { - super(ciphers, cipherFilter, toNegotiator(apnCfg), sessionCacheSize, sessionTimeout, mode, keyCertChain, + super(ciphers, cipherFilter, toNegotiator(apnCfg), mode, keyCertChain, clientAuth, protocols, startTls, enableOcsp, false, options); } - OpenSslContext(Iterable ciphers, CipherSuiteFilter cipherFilter, - OpenSslApplicationProtocolNegotiator apn, long sessionCacheSize, - long sessionTimeout, int mode, Certificate[] keyCertChain, - ClientAuth clientAuth, String[] protocols, boolean startTls, - boolean enableOcsp, Map.Entry, Object>... options) throws SSLException { - super(ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, mode, keyCertChain, clientAuth, protocols, - startTls, enableOcsp, false, options); + OpenSslContext(Iterable ciphers, CipherSuiteFilter cipherFilter, OpenSslApplicationProtocolNegotiator apn, + int mode, Certificate[] keyCertChain, + ClientAuth clientAuth, String[] protocols, boolean startTls, boolean enableOcsp, + Map.Entry, Object>... options) + throws SSLException { + super(ciphers, cipherFilter, apn, mode, keyCertChain, + clientAuth, protocols, startTls, enableOcsp, false, options); } @Override 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 2ab132b9e4..381c846009 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java @@ -346,7 +346,7 @@ public final class OpenSslServerContext extends OpenSslContext { long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls, boolean enableOcsp, String keyStore, Map.Entry, Object>... options) throws SSLException { - super(ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, SSL.SSL_MODE_SERVER, keyCertChain, + super(ciphers, cipherFilter, apn, SSL.SSL_MODE_SERVER, keyCertChain, clientAuth, protocols, startTls, enableOcsp, options); // Create a new SSL_CTX and configure it. @@ -354,7 +354,8 @@ public final class OpenSslServerContext extends OpenSslContext { try { OpenSslKeyMaterialProvider.validateKeyMaterialSupported(keyCertChain, key, keyPassword); sessionContext = newSessionContext(this, ctx, engineMap, trustCertCollection, trustManagerFactory, - keyCertChain, key, keyPassword, keyManagerFactory, keyStore); + keyCertChain, key, keyPassword, keyManagerFactory, keyStore, + sessionCacheSize, sessionTimeout); success = true; } finally { if (!success) { 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 bf33b9faf3..eba161f361 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslServerSessionContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslServerSessionContext.java @@ -26,81 +26,7 @@ import java.util.concurrent.locks.Lock; */ public final class OpenSslServerSessionContext extends OpenSslSessionContext { OpenSslServerSessionContext(ReferenceCountedOpenSslContext context, OpenSslKeyMaterialProvider provider) { - super(context, provider); - } - - @Override - public void setSessionTimeout(int seconds) { - if (seconds < 0) { - throw new IllegalArgumentException(); - } - Lock writerLock = context.ctxLock.writeLock(); - writerLock.lock(); - try { - SSLContext.setSessionCacheTimeout(context.ctx, seconds); - } finally { - writerLock.unlock(); - } - } - - @Override - public int getSessionTimeout() { - Lock readerLock = context.ctxLock.readLock(); - readerLock.lock(); - try { - return (int) SSLContext.getSessionCacheTimeout(context.ctx); - } finally { - readerLock.unlock(); - } - } - - @Override - public void setSessionCacheSize(int size) { - if (size < 0) { - throw new IllegalArgumentException(); - } - Lock writerLock = context.ctxLock.writeLock(); - writerLock.lock(); - try { - SSLContext.setSessionCacheSize(context.ctx, size); - } finally { - writerLock.unlock(); - } - } - - @Override - public int getSessionCacheSize() { - Lock readerLock = context.ctxLock.readLock(); - readerLock.lock(); - try { - return (int) SSLContext.getSessionCacheSize(context.ctx); - } finally { - readerLock.unlock(); - } - } - - @Override - public void setSessionCacheEnabled(boolean enabled) { - long mode = enabled ? SSL.SSL_SESS_CACHE_SERVER : SSL.SSL_SESS_CACHE_OFF; - - Lock writerLock = context.ctxLock.writeLock(); - writerLock.lock(); - try { - SSLContext.setSessionCacheMode(context.ctx, mode); - } finally { - writerLock.unlock(); - } - } - - @Override - public boolean isSessionCacheEnabled() { - Lock readerLock = context.ctxLock.readLock(); - readerLock.lock(); - try { - return SSLContext.getSessionCacheMode(context.ctx) == SSL.SSL_SESS_CACHE_SERVER; - } finally { - readerLock.unlock(); - } + super(context, provider, SSL.SSL_SESS_CACHE_SERVER, new OpenSslSessionCache(context.engineMap)); } /** diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslSession.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslSession.java index 056d55f743..4e6ef35d23 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslSession.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslSession.java @@ -15,16 +15,36 @@ */ package io.netty.handler.ssl; +import io.netty.util.ReferenceCounted; + import javax.net.ssl.SSLException; import javax.net.ssl.SSLSession; +import java.security.cert.Certificate; +/** + * {@link SSLSession} that is specific to our native implementation and {@link ReferenceCounted} to track native + * resources. + */ interface OpenSslSession extends SSLSession { /** - * Finish the handshake and so init everything in the {@link OpenSslSession} that should be accessible by - * the user. + * Return the {@link OpenSslSessionId} that can be used to identify this session. */ - void handshakeFinished() throws SSLException; + OpenSslSessionId sessionId(); + + /** + * Set the local certificate chain that is used. It is not expected that this array will be changed at all + * and so its ok to not copy the array. + */ + void setLocalCertificate(Certificate[] localCertificate); + + /** + * Set the {@link OpenSslSessionId} for the {@link OpenSslSession}. + */ + void setSessionId(OpenSslSessionId id); + + @Override + OpenSslSessionContext getSessionContext(); /** * Expand (or increase) the value returned by {@link #getApplicationBufferSize()} if necessary. @@ -33,4 +53,10 @@ interface OpenSslSession extends SSLSession { * @param packetLengthDataOnly The packet size which exceeds the current {@link #getApplicationBufferSize()}. */ void tryExpandApplicationBufferSize(int packetLengthDataOnly); + + /** + * Called once the handshake has completed. + */ + void handshakeFinished(byte[] id, String cipher, String protocol, byte[] peerCertificate, + byte[][] peerCertificateChain, long creationTime, long timeout) throws SSLException; } diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionCache.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionCache.java new file mode 100644 index 0000000000..10d02577cd --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionCache.java @@ -0,0 +1,492 @@ +/* + * Copyright 2021 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: + * + * https://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.internal.tcnative.SSLSession; +import io.netty.internal.tcnative.SSLSessionCache; +import io.netty.util.ResourceLeakDetector; +import io.netty.util.ResourceLeakDetectorFactory; +import io.netty.util.ResourceLeakTracker; +import io.netty.util.internal.EmptyArrays; +import io.netty.util.internal.SystemPropertyUtil; + +import javax.security.cert.X509Certificate; +import java.security.Principal; +import java.security.cert.Certificate; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * {@link SSLSessionCache} implementation for our native SSL implementation. + */ +class OpenSslSessionCache implements SSLSessionCache { + private static final OpenSslSession[] EMPTY_SESSIONS = new OpenSslSession[0]; + + private static final int DEFAULT_CACHE_SIZE; + static { + // Respect the same system property as the JDK implementation to make it easy to switch between implementations. + int cacheSize = SystemPropertyUtil.getInt("javax.net.ssl.sessionCacheSize", 20480); + if (cacheSize >= 0) { + DEFAULT_CACHE_SIZE = cacheSize; + } else { + DEFAULT_CACHE_SIZE = 20480; + } + } + private final OpenSslEngineMap engineMap; + + private final Map sessions = + new LinkedHashMap() { + + private static final long serialVersionUID = -7773696788135734448L; + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + int maxSize = maximumCacheSize.get(); + if (maxSize >= 0 && size() > maxSize) { + removeSessionWithId(eldest.getKey()); + } + // We always need to return false as we modify the map directly. + return false; + } + }; + + private final AtomicInteger maximumCacheSize = new AtomicInteger(DEFAULT_CACHE_SIZE); + + // Let's use the same default value as OpenSSL does. + // See https://www.openssl.org/docs/man1.1.1/man3/SSL_get_default_timeout.html + private final AtomicInteger sessionTimeout = new AtomicInteger(300); + private int sessionCounter; + + OpenSslSessionCache(OpenSslEngineMap engineMap) { + this.engineMap = engineMap; + } + + final void setSessionTimeout(int seconds) { + int oldTimeout = sessionTimeout.getAndSet(seconds); + if (oldTimeout > seconds) { + // Drain the whole cache as this way we can use the ordering of the LinkedHashMap to detect early + // if there are any other sessions left that are invalid. + clear(); + } + } + + final int getSessionTimeout() { + return sessionTimeout.get(); + } + + /** + * Called once a new {@link OpenSslSession} was created. + * + * @param session the new session. + * @return {@code true} if the session should be cached, {@code false} otherwise. + */ + protected boolean sessionCreated(NativeSslSession session) { + return true; + } + + /** + * Called once an {@link OpenSslSession} was removed from the cache. + * + * @param session the session to remove. + */ + protected void sessionRemoved(NativeSslSession session) { } + + final void setSessionCacheSize(int size) { + long oldSize = maximumCacheSize.getAndSet(size); + if (oldSize > size || size == 0) { + // Just keep it simple for now and drain the whole cache. + clear(); + } + } + + final int getSessionCacheSize() { + return maximumCacheSize.get(); + } + + private void expungeInvalidSessions() { + if (sessions.isEmpty()) { + return; + } + long now = System.currentTimeMillis(); + Iterator> iterator = sessions.entrySet().iterator(); + while (iterator.hasNext()) { + NativeSslSession session = iterator.next().getValue(); + // As we use a LinkedHashMap we can break the while loop as soon as we find a valid session. + // This is true as we always drain the cache as soon as we change the timeout to a smaller value as + // it was set before. This way its true that the insertation order matches the timeout order. + if (session.isValid(now)) { + break; + } + iterator.remove(); + + notifyRemovalAndFree(session); + } + } + + @Override + public final boolean sessionCreated(long ssl, long sslSession) { + ReferenceCountedOpenSslEngine engine = engineMap.get(ssl); + if (engine == null) { + // We couldn't find the engine itself. + return false; + } + NativeSslSession session = new NativeSslSession(sslSession, engine.getPeerHost(), engine.getPeerPort(), + getSessionTimeout() * 1000L); + engine.setSessionId(session.sessionId()); + synchronized (this) { + // Mimic what OpenSSL is doing and expunge every 255 new sessions + // See https://www.openssl.org/docs/man1.0.2/man3/SSL_CTX_flush_sessions.html + if (++sessionCounter == 255) { + sessionCounter = 0; + expungeInvalidSessions(); + } + + if (!sessionCreated(session)) { + // Should not be cached, return false. In this case we also need to call close() to ensure we + // close the ResourceLeakTracker. + session.close(); + return false; + } + + final NativeSslSession old = sessions.put(session.sessionId(), session); + if (old != null) { + notifyRemovalAndFree(old); + } + } + return true; + } + + @Override + public final long getSession(long ssl, byte[] sessionId) { + OpenSslSessionId id = new OpenSslSessionId(sessionId); + final NativeSslSession session; + synchronized (this) { + session = sessions.get(id); + if (session == null) { + return -1; + } + + // If the session is not valid anymore we should remove it from the cache and just signal back + // that we couldn't find a session that is re-usable. + if (!session.isValid() || + // This needs to happen in the synchronized block so we ensure we never destroy it before we + // incremented the reference count. If we cant increment the reference count there is something + // wrong. In this case just remove the session from the cache and signal back that we couldn't + // find a session for re-use. + !session.upRef()) { + // Remove the session from the cache. This will also take care of calling SSL_SESSION_free(...) + removeSessionWithId(session.sessionId()); + return -1; + } + + // At this point we already incremented the reference count via SSL_SESSION_up_ref(...). + if (session.shouldBeSingleUse()) { + // Should only be used once. In this case invalidate the session which will also ensure we remove it + // from the cache and call SSL_SESSION_free(...). + removeSessionWithId(session.sessionId()); + } + } + session.updateLastAccessedTime(); + return session.session(); + } + + void setSession(long ssl, String host, int port) { + // Do nothing by default as this needs special handling for the client side. + } + + /** + * Remove the session with the given id from the cache + */ + final synchronized void removeSessionWithId(OpenSslSessionId id) { + NativeSslSession sslSession = sessions.remove(id); + if (sslSession != null) { + notifyRemovalAndFree(sslSession); + } + } + + /** + * Returns {@code true} if there is a session for the given id in the cache. + */ + final synchronized boolean containsSessionWithId(OpenSslSessionId id) { + return sessions.containsKey(id); + } + + private void notifyRemovalAndFree(NativeSslSession session) { + sessionRemoved(session); + session.free(); + } + + /** + * Return the {@link OpenSslSession} which is cached for the given id. + */ + final synchronized OpenSslSession getSession(OpenSslSessionId id) { + NativeSslSession session = sessions.get(id); + if (session != null && !session.isValid()) { + // The session is not valid anymore, let's remove it and just signal back that there is no session + // with the given ID in the cache anymore. This also takes care of calling SSL_SESSION_free(...) + removeSessionWithId(session.sessionId()); + return null; + } + return session; + } + + /** + * Returns a snapshot of the session ids of the current valid sessions. + */ + final List getIds() { + final OpenSslSession[] sessionsArray; + synchronized (this) { + sessionsArray = sessions.values().toArray(EMPTY_SESSIONS); + } + List ids = new ArrayList(sessionsArray.length); + for (OpenSslSession session: sessionsArray) { + if (session.isValid()) { + ids.add(session.sessionId()); + } + } + return ids; + } + + /** + * Clear the cache and free all cached SSL_SESSION*. + */ + synchronized void clear() { + Iterator> iterator = sessions.entrySet().iterator(); + while (iterator.hasNext()) { + NativeSslSession session = iterator.next().getValue(); + iterator.remove(); + + // Notify about removal. This also takes care of calling SSL_SESSION_free(...). + notifyRemovalAndFree(session); + } + } + + /** + * {@link OpenSslSession} implementation which wraps the native SSL_SESSION* while in cache. + */ + static final class NativeSslSession implements OpenSslSession { + static final ResourceLeakDetector LEAK_DETECTOR = ResourceLeakDetectorFactory.instance() + .newResourceLeakDetector(NativeSslSession.class); + private final ResourceLeakTracker leakTracker; + private final long session; + private final String peerHost; + private final int peerPort; + private final OpenSslSessionId id; + private final long timeout; + private final long creationTime = System.currentTimeMillis(); + private volatile long lastAccessedTime = creationTime; + private volatile boolean valid = true; + private boolean freed; + + NativeSslSession(long session, String peerHost, int peerPort, long timeout) { + this.session = session; + this.peerHost = peerHost; + this.peerPort = peerPort; + this.timeout = timeout; + this.id = new OpenSslSessionId(io.netty.internal.tcnative.SSLSession.getSessionId(session)); + leakTracker = LEAK_DETECTOR.track(this); + } + + @Override + public void setSessionId(OpenSslSessionId id) { + throw new UnsupportedOperationException(); + } + + boolean shouldBeSingleUse() { + assert !freed; + return SSLSession.shouldBeSingleUse(session); + } + + long session() { + assert !freed; + return session; + } + + boolean upRef() { + assert !freed; + return SSLSession.upRef(session); + } + + synchronized void free() { + close(); + SSLSession.free(session); + } + + void close() { + assert !freed; + freed = true; + invalidate(); + if (leakTracker != null) { + leakTracker.close(this); + } + } + + @Override + public OpenSslSessionId sessionId() { + return id; + } + + boolean isValid(long now) { + return creationTime + timeout >= now && valid; + } + + @Override + public void setLocalCertificate(Certificate[] localCertificate) { + throw new UnsupportedOperationException(); + } + + @Override + public OpenSslSessionContext getSessionContext() { + return null; + } + + @Override + public void tryExpandApplicationBufferSize(int packetLengthDataOnly) { + throw new UnsupportedOperationException(); + } + + @Override + public void handshakeFinished(byte[] id, String cipher, String protocol, byte[] peerCertificate, + byte[][] peerCertificateChain, long creationTime, long timeout) { + throw new UnsupportedOperationException(); + } + + @Override + public byte[] getId() { + return id.cloneBytes(); + } + + @Override + public long getCreationTime() { + return creationTime; + } + + void updateLastAccessedTime() { + lastAccessedTime = System.currentTimeMillis(); + } + + @Override + public long getLastAccessedTime() { + return lastAccessedTime; + } + + @Override + public void invalidate() { + valid = false; + } + + @Override + public boolean isValid() { + return isValid(System.currentTimeMillis()); + } + + @Override + public void putValue(String name, Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public Object getValue(String name) { + return null; + } + + @Override + public void removeValue(String name) { + // NOOP + } + + @Override + public String[] getValueNames() { + return EmptyArrays.EMPTY_STRINGS; + } + + @Override + public Certificate[] getPeerCertificates() { + throw new UnsupportedOperationException(); + } + + @Override + public Certificate[] getLocalCertificates() { + throw new UnsupportedOperationException(); + } + + @Override + public X509Certificate[] getPeerCertificateChain() { + throw new UnsupportedOperationException(); + } + + @Override + public Principal getPeerPrincipal() { + throw new UnsupportedOperationException(); + } + + @Override + public Principal getLocalPrincipal() { + throw new UnsupportedOperationException(); + } + + @Override + public String getCipherSuite() { + return null; + } + + @Override + public String getProtocol() { + return null; + } + + @Override + public String getPeerHost() { + return peerHost; + } + + @Override + public int getPeerPort() { + return peerPort; + } + + @Override + public int getPacketBufferSize() { + return ReferenceCountedOpenSslEngine.MAX_RECORD_SIZE; + } + + @Override + public int getApplicationBufferSize() { + return ReferenceCountedOpenSslEngine.MAX_PLAINTEXT_LENGTH; + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof OpenSslSession)) { + return false; + } + OpenSslSession session1 = (OpenSslSession) o; + return id.equals(session1.sessionId()); + } + } +} 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 afabdaaaf8..0da26c9971 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionContext.java @@ -24,14 +24,13 @@ import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSessionContext; import java.util.Arrays; import java.util.Enumeration; -import java.util.NoSuchElementException; +import java.util.Iterator; import java.util.concurrent.locks.Lock; /** * OpenSSL specific {@link SSLSessionContext} implementation. */ public abstract class OpenSslSessionContext implements SSLSessionContext { - private static final Enumeration EMPTY = new EmptyEnumeration(); private final OpenSslSessionStats stats; @@ -42,29 +41,76 @@ public abstract class OpenSslSessionContext implements SSLSessionContext { final ReferenceCountedOpenSslContext context; + private final OpenSslSessionCache sessionCache; + private final long mask; + // 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(ReferenceCountedOpenSslContext context, OpenSslKeyMaterialProvider provider) { + OpenSslSessionContext(ReferenceCountedOpenSslContext context, OpenSslKeyMaterialProvider provider, long mask, + OpenSslSessionCache cache) { this.context = context; this.provider = provider; + this.mask = mask; stats = new OpenSslSessionStats(context); + sessionCache = cache; + SSLContext.setSSLSessionCache(context.ctx, cache); } final boolean useKeyManager() { return provider != null; } + @Override + public void setSessionCacheSize(int size) { + ObjectUtil.checkPositiveOrZero(size, "size"); + sessionCache.setSessionCacheSize(size); + } + + @Override + public int getSessionCacheSize() { + return sessionCache.getSessionCacheSize(); + } + + @Override + public void setSessionTimeout(int seconds) { + ObjectUtil.checkPositiveOrZero(seconds, "seconds"); + + Lock writerLock = context.ctxLock.writeLock(); + writerLock.lock(); + try { + SSLContext.setSessionCacheTimeout(context.ctx, seconds); + sessionCache.setSessionTimeout(seconds); + } finally { + writerLock.unlock(); + } + } + + @Override + public int getSessionTimeout() { + return sessionCache.getSessionTimeout(); + } + @Override public SSLSession getSession(byte[] bytes) { - ObjectUtil.checkNotNull(bytes, "bytes"); - return null; + return sessionCache.getSession(new OpenSslSessionId(bytes)); } @Override public Enumeration getIds() { - return EMPTY; + return new Enumeration() { + private final Iterator ids = sessionCache.getIds().iterator(); + @Override + public boolean hasMoreElements() { + return ids.hasNext(); + } + + @Override + public byte[] nextElement() { + return ids.next().cloneBytes(); + } + }; } /** @@ -124,12 +170,33 @@ public abstract class OpenSslSessionContext implements SSLSessionContext { /** * Enable or disable caching of SSL sessions. */ - public abstract void setSessionCacheEnabled(boolean enabled); + public void setSessionCacheEnabled(boolean enabled) { + long mode = enabled ? mask | SSL.SSL_SESS_CACHE_NO_INTERNAL_LOOKUP | + SSL.SSL_SESS_CACHE_NO_INTERNAL_STORE : SSL.SSL_SESS_CACHE_OFF; + Lock writerLock = context.ctxLock.writeLock(); + writerLock.lock(); + try { + SSLContext.setSessionCacheMode(context.ctx, mode); + if (!enabled) { + sessionCache.clear(); + } + } finally { + writerLock.unlock(); + } + } /** * Return {@code true} if caching of SSL sessions is enabled, {@code false} otherwise. */ - public abstract boolean isSessionCacheEnabled(); + public boolean isSessionCacheEnabled() { + Lock readerLock = context.ctxLock.readLock(); + readerLock.lock(); + try { + return (SSLContext.getSessionCacheMode(context.ctx) & mask) != 0; + } finally { + readerLock.unlock(); + } + } /** * Returns the stats of this context. @@ -138,21 +205,25 @@ public abstract class OpenSslSessionContext implements SSLSessionContext { return stats; } + /** + * Remove the given {@link OpenSslSession} from the cache, and so not re-use it for new connections. + */ + final void removeFromCache(OpenSslSessionId id) { + sessionCache.removeSessionWithId(id); + } + + final boolean isInCache(OpenSslSessionId id) { + return sessionCache.containsSessionWithId(id); + } + + void setSessionFromCache(String host, int port, long ssl) { + sessionCache.setSession(ssl, host, port); + } + final void destroy() { if (provider != null) { provider.destroy(); } - } - - private static final class EmptyEnumeration implements Enumeration { - @Override - public boolean hasMoreElements() { - return false; - } - - @Override - public byte[] nextElement() { - throw new NoSuchElementException(); - } + sessionCache.clear(); } } diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionId.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionId.java new file mode 100644 index 0000000000..76941f74ae --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionId.java @@ -0,0 +1,66 @@ +/* + * Copyright 2021 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: + * + * https://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.EmptyArrays; + +import java.util.Arrays; + +/** + * Represent the session ID used by an {@link OpenSslSession}. + */ +final class OpenSslSessionId { + + private final byte[] id; + private final int hashCode; + + static final OpenSslSessionId NULL_ID = new OpenSslSessionId(EmptyArrays.EMPTY_BYTES); + + OpenSslSessionId(byte[] id) { + // We take ownership if the byte[] and so there is no need to clone it. + this.id = id; + // cache the hashCode as the byte[] array will never change + this.hashCode = Arrays.hashCode(id); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof OpenSslSessionId)) { + return false; + } + + return Arrays.equals(id, ((OpenSslSessionId) o).id); + } + + @Override + public String toString() { + return "OpenSslSessionId{" + + "id=" + Arrays.toString(id) + + '}'; + } + + @Override + public int hashCode() { + return hashCode; + } + + byte[] cloneBytes() { + return id.clone(); + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslClientContext.java b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslClientContext.java index cb36da0c9c..317c558639 100644 --- a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslClientContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslClientContext.java @@ -64,12 +64,13 @@ public final class ReferenceCountedOpenSslClientContext extends ReferenceCounted String[] protocols, long sessionCacheSize, long sessionTimeout, boolean enableOcsp, String keyStore, Map.Entry, Object>... options) throws SSLException { - super(ciphers, cipherFilter, toNegotiator(apn), sessionCacheSize, sessionTimeout, SSL.SSL_MODE_CLIENT, - keyCertChain, ClientAuth.NONE, protocols, false, enableOcsp, true, options); + super(ciphers, cipherFilter, toNegotiator(apn), SSL.SSL_MODE_CLIENT, keyCertChain, + ClientAuth.NONE, protocols, false, enableOcsp, true, options); boolean success = false; try { sessionContext = newSessionContext(this, ctx, engineMap, trustCertCollection, trustManagerFactory, - keyCertChain, key, keyPassword, keyManagerFactory, keyStore); + keyCertChain, key, keyPassword, keyManagerFactory, keyStore, + sessionCacheSize, sessionTimeout); success = true; } finally { if (!success) { @@ -89,7 +90,8 @@ public final class ReferenceCountedOpenSslClientContext extends ReferenceCounted TrustManagerFactory trustManagerFactory, X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, - String keyStore) throws SSLException { + String keyStore, long sessionCacheSize, long sessionTimeout) + 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"); @@ -164,9 +166,18 @@ public final class ReferenceCountedOpenSslClientContext extends ReferenceCounted throw new SSLException("unable to setup trustmanager", e); } OpenSslClientSessionContext context = new OpenSslClientSessionContext(thiz, keyMaterialProvider); + context.setSessionCacheEnabled(CLIENT_ENABLE_SESSION_CACHE); + if (sessionCacheSize > 0) { + context.setSessionCacheSize((int) Math.min(sessionCacheSize, Integer.MAX_VALUE)); + } + if (sessionTimeout > 0) { + context.setSessionTimeout((int) Math.min(sessionTimeout, Integer.MAX_VALUE)); + } + if (CLIENT_ENABLE_SESSION_TICKET) { context.setTicketKeys(); } + keyMaterialProvider = null; return context; } finally { @@ -187,44 +198,9 @@ public final class ReferenceCountedOpenSslClientContext extends ReferenceCounted } } - // No cache is currently supported for client side mode. static final class OpenSslClientSessionContext extends OpenSslSessionContext { OpenSslClientSessionContext(ReferenceCountedOpenSslContext context, OpenSslKeyMaterialProvider provider) { - super(context, provider); - } - - @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; + super(context, provider, SSL.SSL_SESS_CACHE_CLIENT, new OpenSslClientSessionCache(context.engineMap)); } } diff --git a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslContext.java b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslContext.java index 98d63f99a5..0546d1a99a 100644 --- a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslContext.java @@ -105,6 +105,12 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen static final boolean SERVER_ENABLE_SESSION_TICKET_TLSV13 = SystemPropertyUtil.getBoolean("jdk.tls.server.enableSessionTicketExtension", true); + static final boolean SERVER_ENABLE_SESSION_CACHE = + SystemPropertyUtil.getBoolean("io.netty.handler.ssl.openssl.sessionCacheServer", true); + // session caching is disabled by default on the client side due a JDK bug: + // https://mail.openjdk.java.net/pipermail/security-dev/2021-March/024758.html + static final boolean CLIENT_ENABLE_SESSION_CACHE = + SystemPropertyUtil.getBoolean("io.netty.handler.ssl.openssl.sessionCacheClient", false); /** * The OpenSSL SSL_CTX object. * @@ -112,8 +118,6 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen */ protected long ctx; private final List unmodifiableCiphers; - private final long sessionCacheSize; - private final long sessionTimeout; private final OpenSslApplicationProtocolNegotiator apn; private final int mode; @@ -194,8 +198,7 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen final boolean tlsFalseStart; ReferenceCountedOpenSslContext(Iterable ciphers, CipherSuiteFilter cipherFilter, - OpenSslApplicationProtocolNegotiator apn, long sessionCacheSize, - long sessionTimeout, int mode, Certificate[] keyCertChain, + OpenSslApplicationProtocolNegotiator apn, int mode, Certificate[] keyCertChain, ClientAuth clientAuth, String[] protocols, boolean startTls, boolean enableOcsp, boolean leakDetection, Map.Entry, Object>... ctxOptions) throws SSLException { @@ -345,22 +348,6 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen } } - /* Set session cache size, if specified */ - if (sessionCacheSize <= 0) { - // Get the default session cache size using SSLContext.setSessionCacheSize() - sessionCacheSize = SSLContext.setSessionCacheSize(ctx, 20480); - } - this.sessionCacheSize = sessionCacheSize; - SSLContext.setSessionCacheSize(ctx, sessionCacheSize); - - /* Set session timeout, if specified */ - if (sessionTimeout <= 0) { - // Get the default session timeout using SSLContext.setSessionCacheTimeout() - sessionTimeout = SSLContext.setSessionCacheTimeout(ctx, 300); - } - this.sessionTimeout = sessionTimeout; - SSLContext.setSessionCacheTimeout(ctx, sessionTimeout); - if (enableOcsp) { SSLContext.enableOcsp(ctx, isClient()); } @@ -393,16 +380,6 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen return unmodifiableCiphers; } - @Override - public final long sessionCacheSize() { - return sessionCacheSize; - } - - @Override - public final long sessionTimeout() { - return sessionTimeout; - } - @Override public ApplicationProtocolNegotiator applicationProtocolNegotiator() { return apn; diff --git a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngine.java b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngine.java index 219c97fbbe..b1f5222085 100644 --- a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngine.java +++ b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngine.java @@ -63,7 +63,6 @@ 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; @@ -125,7 +124,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc /** * Depends upon tcnative ... only use if tcnative is available! */ - private static final int MAX_RECORD_SIZE = SSL.SSL_MAX_RECORD_LENGTH; + static final int MAX_RECORD_SIZE = SSL.SSL_MAX_RECORD_LENGTH; 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); @@ -162,6 +161,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc private volatile String applicationProtocol; private volatile boolean needTask; private String[] explicitlyEnabledProtocols; + private boolean sessionSet; // Reference Counting private final ResourceLeakTracker leak; @@ -187,7 +187,6 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc }; private volatile ClientAuth clientAuth = ClientAuth.NONE; - private volatile Certificate[] localCertificateChain; // Updated once a new handshake is started and so the SSLSession reused. private volatile long lastAccessed = -1; @@ -238,6 +237,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc this.alloc = checkNotNull(alloc, "alloc"); apn = (OpenSslApplicationProtocolNegotiator) context.applicationProtocolNegotiator(); clientMode = context.isClient(); + if (PlatformDependent.javaVersion() >= 7) { session = new ExtendedOpenSslSession(new DefaultOpenSslSession(context.sessionContext())) { private String[] peerSupportedSignatureAlgorithms; @@ -317,9 +317,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc engineMap = context.engineMap; enableOcsp = context.enableOcsp; if (!context.sessionContext().useKeyManager()) { - // If we do not use the KeyManagerFactory we need to set localCertificateChain now. - // When we use a KeyManagerFactory it will be set during setKeyMaterial(...). - localCertificateChain = context.keyCertChain; + session.setLocalCertificate(context.keyCertChain); } this.jdkCompatibilityMode = jdkCompatibilityMode; @@ -357,15 +355,9 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc SSL.enableOcsp(ssl); } - int mode = SSL.getMode(ssl); - if (context.tlsFalseStart) { - mode |= SSL.SSL_MODE_ENABLE_FALSE_START; - } - if (!jdkCompatibilityMode) { - mode |= SSL.SSL_MODE_ENABLE_PARTIAL_WRITE; + SSL.setMode(ssl, SSL.getMode(ssl) | SSL.SSL_MODE_ENABLE_PARTIAL_WRITE); } - SSL.setMode(ssl, mode); if (isProtocolEnabled(SSL.getOptions(ssl), SSL.SSL_OP_NO_TLSv1_3, PROTOCOL_TLS_V1_3)) { final boolean enableTickets = clientMode ? @@ -418,7 +410,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc } SSL.setKeyMaterial(ssl, keyMaterial.certificateChainAddress(), keyMaterial.privateKeyAddress()); } - localCertificateChain = keyMaterial.certificateChain(); + session.setLocalCertificate(keyMaterial.certificateChain()); return true; } @@ -429,6 +421,13 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc return new SecretKeySpec(SSL.getMasterKey(ssl), "AES"); } + synchronized boolean isSessionReused() { + if (isDestroyed()) { + return false; + } + return SSL.isSessionReused(ssl); + } + /** * Sets the OCSP response. */ @@ -1897,6 +1896,12 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc // Adding the OpenSslEngine to the OpenSslEngineMap so it can be used in the AbstractCertificateVerifier. engineMap.add(this); + + if (!sessionSet) { + parentContext.sessionContext().setSessionFromCache(getPeerHost(), getPeerPort(), ssl); + sessionSet = true; + } + if (lastAccessed == -1) { lastAccessed = System.currentTimeMillis(); } @@ -1928,7 +1933,10 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc return NEED_WRAP; } // if SSL_do_handshake returns > 0 or sslError == SSL.SSL_ERROR_NAME it means the handshake was finished. - session.handshakeFinished(); + session.handshakeFinished(SSL.getSessionId(ssl), SSL.getCipherForSSL(ssl), SSL.getVersion(ssl), + SSL.getPeerCertificate(ssl), SSL.getPeerCertChain(ssl), + SSL.getTime(ssl) * 1000L, parentContext.sessionTimeout() * 1000L); + selectApplicationProtocol(); return FINISHED; } @@ -2180,6 +2188,71 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc return Buffer.address(b); } + /** + * 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) { + ReferenceCountedOpenSslEngine.this.applicationProtocol = selectApplicationProtocol( + protocols, behavior, applicationProtocol); + } + break; + case NPN: + applicationProtocol = SSL.getNextProtoNegotiated(ssl); + if (applicationProtocol != null) { + ReferenceCountedOpenSslEngine.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) { + ReferenceCountedOpenSslEngine.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); + } + } + } + } + + final void setSessionId(OpenSslSessionId id) { + session.setSessionId(id); + } + private final class DefaultOpenSslSession implements OpenSslSession { private final OpenSslSessionContext sessionContext; @@ -2188,12 +2261,13 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc private X509Certificate[] x509PeerCerts; private Certificate[] peerCerts; + private boolean valid = true; private String protocol; private String cipher; - private byte[] id; - private long creationTime; + private OpenSslSessionId id = OpenSslSessionId.NULL_ID; + private volatile long creationTime; private volatile int applicationBufferSize = MAX_PLAINTEXT_LENGTH; - + private volatile Certificate[] localCertificateChain; // lazy init for memory reasons private Map values; @@ -2206,28 +2280,49 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc } @Override - public byte[] getId() { + public void setSessionId(OpenSslSessionId sessionId) { synchronized (ReferenceCountedOpenSslEngine.this) { - if (id == null) { - return EmptyArrays.EMPTY_BYTES; + if (this.id == OpenSslSessionId.NULL_ID) { + this.id = sessionId; + creationTime = System.currentTimeMillis(); } - return id.clone(); } } @Override - public SSLSessionContext getSessionContext() { + public OpenSslSessionId sessionId() { + synchronized (ReferenceCountedOpenSslEngine.this) { + if (this.id == OpenSslSessionId.NULL_ID && !isDestroyed()) { + byte[] sessionId = SSL.getSessionId(ssl); + if (sessionId != null) { + id = new OpenSslSessionId(sessionId); + } + } + + return id; + } + } + + @Override + public void setLocalCertificate(Certificate[] localCertificate) { + this.localCertificateChain = localCertificate; + } + + @Override + public byte[] getId() { + return sessionId().cloneBytes(); + } + + @Override + public OpenSslSessionContext getSessionContext() { return sessionContext; } @Override public long getCreationTime() { synchronized (ReferenceCountedOpenSslEngine.this) { - if (creationTime == 0 && !isDestroyed()) { - creationTime = SSL.getTime(ssl) * 1000L; - } + return creationTime; } - return creationTime; } @Override @@ -2240,20 +2335,16 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc @Override public void invalidate() { synchronized (ReferenceCountedOpenSslEngine.this) { - if (!isDestroyed()) { - SSL.setTimeout(ssl, 0); - } + valid = false; + sessionContext.removeFromCache(id); } } @Override public boolean isValid() { synchronized (ReferenceCountedOpenSslEngine.this) { - if (!isDestroyed()) { - return System.currentTimeMillis() - (SSL.getTimeout(ssl) * 1000L) < (SSL.getTime(ssl) * 1000L); - } + return valid || sessionContext.isInCache(id); } - return false; } @Override @@ -2328,15 +2419,50 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc * the user. */ @Override - public void handshakeFinished() throws SSLException { + public void handshakeFinished(byte[] id, String cipher, String protocol, byte[] peerCertificate, + byte[][] peerCertificateChain, long creationTime, long timeout) + throws SSLException { synchronized (ReferenceCountedOpenSslEngine.this) { if (!isDestroyed()) { - id = SSL.getSessionId(ssl); - cipher = toJavaCipherSuite(SSL.getCipherForSSL(ssl)); - protocol = SSL.getVersion(ssl); + this.creationTime = creationTime; + if (this.id == OpenSslSessionId.NULL_ID) { + this.id = id == null ? OpenSslSessionId.NULL_ID : new OpenSslSessionId(id); + } + this.cipher = toJavaCipherSuite(cipher); + this.protocol = protocol; + + if (clientMode) { + if (isEmpty(peerCertificateChain)) { + peerCerts = EmptyArrays.EMPTY_CERTIFICATES; + x509PeerCerts = EmptyArrays.EMPTY_JAVAX_X509_CERTIFICATES; + } else { + peerCerts = new Certificate[peerCertificateChain.length]; + x509PeerCerts = new X509Certificate[peerCertificateChain.length]; + initCerts(peerCertificateChain, 0); + } + } else { + // 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 + if (isEmpty(peerCertificate)) { + peerCerts = EmptyArrays.EMPTY_CERTIFICATES; + x509PeerCerts = EmptyArrays.EMPTY_JAVAX_X509_CERTIFICATES; + } else { + if (isEmpty(peerCertificateChain)) { + peerCerts = new Certificate[] {new LazyX509Certificate(peerCertificate)}; + x509PeerCerts = new X509Certificate[] {new LazyJavaxX509Certificate(peerCertificate)}; + } else { + peerCerts = new Certificate[peerCertificateChain.length + 1]; + x509PeerCerts = new X509Certificate[peerCertificateChain.length + 1]; + peerCerts[0] = new LazyX509Certificate(peerCertificate); + x509PeerCerts[0] = new LazyJavaxX509Certificate(peerCertificate); + initCerts(peerCertificateChain, 1); + } + } + } - initPeerCerts(); - selectApplicationProtocol(); calculateMaxWrapOverhead(); handshakeState = HandshakeState.FINISHED; @@ -2346,47 +2472,6 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc } } - /** - * 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); - if (clientMode) { - if (isEmpty(chain)) { - peerCerts = EmptyArrays.EMPTY_CERTIFICATES; - x509PeerCerts = EmptyArrays.EMPTY_JAVAX_X509_CERTIFICATES; - } else { - peerCerts = new Certificate[chain.length]; - x509PeerCerts = new X509Certificate[chain.length]; - initCerts(chain, 0); - } - } else { - // 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 - byte[] clientCert = SSL.getPeerCertificate(ssl); - if (isEmpty(clientCert)) { - peerCerts = EmptyArrays.EMPTY_CERTIFICATES; - x509PeerCerts = EmptyArrays.EMPTY_JAVAX_X509_CERTIFICATES; - } else { - if (isEmpty(chain)) { - peerCerts = new Certificate[] {new LazyX509Certificate(clientCert)}; - x509PeerCerts = new X509Certificate[] {new LazyJavaxX509Certificate(clientCert)}; - } else { - peerCerts = new Certificate[chain.length + 1]; - x509PeerCerts = new X509Certificate[chain.length + 1]; - peerCerts[0] = new LazyX509Certificate(clientCert); - x509PeerCerts[0] = new LazyJavaxX509Certificate(clientCert); - initCerts(chain, 1); - } - } - } - } - private void initCerts(byte[][] chain, int startPos) { for (int i = 0; i < chain.length; i++) { int certPos = startPos + i; @@ -2395,67 +2480,6 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc } } - /** - * 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) { - ReferenceCountedOpenSslEngine.this.applicationProtocol = selectApplicationProtocol( - protocols, behavior, applicationProtocol); - } - break; - case NPN: - applicationProtocol = SSL.getNextProtoNegotiated(ssl); - if (applicationProtocol != null) { - ReferenceCountedOpenSslEngine.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) { - ReferenceCountedOpenSslEngine.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) { @@ -2468,7 +2492,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc @Override public Certificate[] getLocalCertificates() { - Certificate[] localCerts = ReferenceCountedOpenSslEngine.this.localCertificateChain; + Certificate[] localCerts = this.localCertificateChain; if (localCerts == null) { return null; } @@ -2495,7 +2519,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc @Override public Principal getLocalPrincipal() { - Certificate[] local = ReferenceCountedOpenSslEngine.this.localCertificateChain; + Certificate[] local = this.localCertificateChain; if (local == null || local.length == 0) { return null; } @@ -2553,5 +2577,13 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc applicationBufferSize = MAX_RECORD_SIZE; } } + + @Override + public String toString() { + return "DefaultOpenSslSession{" + + "sessionContext=" + sessionContext + + ", id=" + id + + '}'; + } } } diff --git a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslServerContext.java b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslServerContext.java index ab5b3d352b..f143d8a4c0 100644 --- a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslServerContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslServerContext.java @@ -23,7 +23,6 @@ import io.netty.internal.tcnative.SniHostNameMatcher; import io.netty.util.CharsetUtil; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.SuppressJava6Requirement; -import io.netty.util.internal.SystemPropertyUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -72,13 +71,14 @@ public final class ReferenceCountedOpenSslServerContext extends ReferenceCounted long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls, boolean enableOcsp, String keyStore, Map.Entry, Object>... options) throws SSLException { - super(ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, SSL.SSL_MODE_SERVER, keyCertChain, + super(ciphers, cipherFilter, apn, SSL.SSL_MODE_SERVER, keyCertChain, clientAuth, protocols, startTls, enableOcsp, true, options); // Create a new SSL_CTX and configure it. boolean success = false; try { sessionContext = newSessionContext(this, ctx, engineMap, trustCertCollection, trustManagerFactory, - keyCertChain, key, keyPassword, keyManagerFactory, keyStore); + keyCertChain, key, keyPassword, keyManagerFactory, keyStore, + sessionCacheSize, sessionTimeout); if (SERVER_ENABLE_SESSION_TICKET) { sessionContext.setTicketKeys(); } @@ -101,7 +101,7 @@ public final class ReferenceCountedOpenSslServerContext extends ReferenceCounted TrustManagerFactory trustManagerFactory, X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, - String keyStore) + String keyStore, long sessionCacheSize, long sessionTimeout) throws SSLException { OpenSslKeyMaterialProvider keyMaterialProvider = null; try { @@ -185,6 +185,14 @@ public final class ReferenceCountedOpenSslServerContext extends ReferenceCounted OpenSslServerSessionContext sessionContext = new OpenSslServerSessionContext(thiz, keyMaterialProvider); sessionContext.setSessionIdContext(ID); + // Enable session caching by default + sessionContext.setSessionCacheEnabled(SERVER_ENABLE_SESSION_CACHE); + if (sessionCacheSize > 0) { + sessionContext.setSessionCacheSize((int) Math.min(sessionCacheSize, Integer.MAX_VALUE)); + } + if (sessionTimeout > 0) { + sessionContext.setSessionTimeout((int) Math.min(sessionTimeout, Integer.MAX_VALUE)); + } keyMaterialProvider = null; 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 ab03a91fef..2ff8c4f1b5 100644 --- a/handler/src/main/java/io/netty/handler/ssl/SslContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/SslContext.java @@ -895,12 +895,16 @@ public abstract class SslContext { /** * Returns the size of the cache used for storing SSL session objects. */ - public abstract long sessionCacheSize(); + public long sessionCacheSize() { + return sessionContext().getSessionCacheSize(); + } /** * Returns the timeout for the cached SSL session objects, in seconds. */ - public abstract long sessionTimeout(); + public long sessionTimeout() { + return sessionContext().getSessionTimeout(); + } /** * @deprecated Use {@link #applicationProtocolNegotiator()} instead. diff --git a/handler/src/main/java/io/netty/handler/ssl/SslUtils.java b/handler/src/main/java/io/netty/handler/ssl/SslUtils.java index 6bd6525954..e57de49183 100644 --- a/handler/src/main/java/io/netty/handler/ssl/SslUtils.java +++ b/handler/src/main/java/io/netty/handler/ssl/SslUtils.java @@ -473,6 +473,10 @@ final class SslUtils { return TLSV13_CIPHERS.contains(cipher); } + static boolean isEmpty(Object[] arr) { + return arr == null || arr.length == 0; + } + private SslUtils() { } } diff --git a/handler/src/main/java/io/netty/handler/ssl/util/LazyJavaxX509Certificate.java b/handler/src/main/java/io/netty/handler/ssl/util/LazyJavaxX509Certificate.java index 65831c0628..66a769d14a 100644 --- a/handler/src/main/java/io/netty/handler/ssl/util/LazyJavaxX509Certificate.java +++ b/handler/src/main/java/io/netty/handler/ssl/util/LazyJavaxX509Certificate.java @@ -101,6 +101,14 @@ public final class LazyJavaxX509Certificate extends X509Certificate { return bytes.clone(); } + /** + * Return the underyling {@code byte[]} without cloning it first. This {@code byte[]} must never + * be mutated. + */ + byte[] getBytes() { + return bytes; + } + @Override public void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, diff --git a/handler/src/test/java/io/netty/handler/ssl/ConscryptJdkSslEngineInteropTest.java b/handler/src/test/java/io/netty/handler/ssl/ConscryptJdkSslEngineInteropTest.java index ca3a8bd6c6..b151ab4003 100644 --- a/handler/src/test/java/io/netty/handler/ssl/ConscryptJdkSslEngineInteropTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/ConscryptJdkSslEngineInteropTest.java @@ -22,6 +22,7 @@ import org.junit.Ignore; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import javax.net.ssl.SSLSessionContext; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -80,4 +81,9 @@ public class ConscryptJdkSslEngineInteropTest extends SSLEngineTest { // TODO(scott): work around for a JDK issue. The exception should be SSLHandshakeException. return super.mySetupMutualAuthServerIsValidServerException(cause) || causedBySSLException(cause); } + + @Override + protected void invalidateSessionsAndAssert(SSLSessionContext context) { + // Not supported by conscrypt + } } diff --git a/handler/src/test/java/io/netty/handler/ssl/ConscryptOpenSslEngineInteropTest.java b/handler/src/test/java/io/netty/handler/ssl/ConscryptOpenSslEngineInteropTest.java index 3fbaf9d53c..893a61a4fa 100644 --- a/handler/src/test/java/io/netty/handler/ssl/ConscryptOpenSslEngineInteropTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/ConscryptOpenSslEngineInteropTest.java @@ -22,6 +22,7 @@ import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLSessionContext; import java.security.Provider; import java.util.ArrayList; @@ -150,6 +151,11 @@ public class ConscryptOpenSslEngineInteropTest extends ConscryptSslEngineTest { super.testSessionLocalWhenNonMutualWithKeyManager(); } + @Override + protected void invalidateSessionsAndAssert(SSLSessionContext context) { + // Not supported by conscrypt + } + @Override protected SSLEngine wrapEngine(SSLEngine engine) { return Java8SslTestUtils.wrapSSLEngineForTesting(engine); @@ -160,6 +166,8 @@ public class ConscryptOpenSslEngineInteropTest extends ConscryptSslEngineTest { protected SslContext wrapContext(SslContext context) { if (context instanceof OpenSslContext) { ((OpenSslContext) context).setUseTasks(useTasks); + // Explicit enable the session cache as its disabled by default on the client side. + ((OpenSslContext) context).sessionContext().setSessionCacheEnabled(true); } return context; } diff --git a/handler/src/test/java/io/netty/handler/ssl/ConscryptSslEngineTest.java b/handler/src/test/java/io/netty/handler/ssl/ConscryptSslEngineTest.java index 1fb60455e0..66726af6ab 100644 --- a/handler/src/test/java/io/netty/handler/ssl/ConscryptSslEngineTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/ConscryptSslEngineTest.java @@ -20,6 +20,7 @@ import org.junit.Ignore; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import javax.net.ssl.SSLSessionContext; import java.security.Provider; import java.util.ArrayList; import java.util.Collection; @@ -78,4 +79,15 @@ public class ConscryptSslEngineTest extends SSLEngineTest { @Override public void testMutualAuthValidClientCertChainTooLongFailRequireClientAuth() { } + + @Override + protected void invalidateSessionsAndAssert(SSLSessionContext context) { + // Not supported by conscrypt + } + + @Ignore("Possible Conscrypt bug") + public void testSessionCacheTimeout() throws Exception { + // Skip + // https://github.com/google/conscrypt/issues/851 + } } diff --git a/handler/src/test/java/io/netty/handler/ssl/JdkConscryptSslEngineInteropTest.java b/handler/src/test/java/io/netty/handler/ssl/JdkConscryptSslEngineInteropTest.java index b6dc0c377d..fb6c877693 100644 --- a/handler/src/test/java/io/netty/handler/ssl/JdkConscryptSslEngineInteropTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/JdkConscryptSslEngineInteropTest.java @@ -23,6 +23,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import javax.net.ssl.SSLSessionContext; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -92,4 +93,15 @@ public class JdkConscryptSslEngineInteropTest extends SSLEngineTest { // Ignore as Conscrypt does not correctly return the local certificates while the TrustManager is invoked. // See https://github.com/google/conscrypt/issues/634 } + + @Override + protected void invalidateSessionsAndAssert(SSLSessionContext context) { + // Not supported by conscrypt + } + + @Ignore("Possible Conscrypt bug") + public void testSessionCacheTimeout() { + // Skip + // https://github.com/google/conscrypt/issues/851 + } } diff --git a/handler/src/test/java/io/netty/handler/ssl/JdkOpenSslEngineInteroptTest.java b/handler/src/test/java/io/netty/handler/ssl/JdkOpenSslEngineInteroptTest.java index f32df94370..c19314aa32 100644 --- a/handler/src/test/java/io/netty/handler/ssl/JdkOpenSslEngineInteroptTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/JdkOpenSslEngineInteroptTest.java @@ -167,6 +167,8 @@ public class JdkOpenSslEngineInteroptTest extends SSLEngineTest { protected SslContext wrapContext(SslContext context) { if (context instanceof OpenSslContext) { ((OpenSslContext) context).setUseTasks(useTasks); + // Explicit enable the session cache as its disabled by default on the client side. + ((OpenSslContext) context).sessionContext().setSessionCacheEnabled(true); } return context; } diff --git a/handler/src/test/java/io/netty/handler/ssl/OpenSslConscryptSslEngineInteropTest.java b/handler/src/test/java/io/netty/handler/ssl/OpenSslConscryptSslEngineInteropTest.java index 9588b6b90c..9690765622 100644 --- a/handler/src/test/java/io/netty/handler/ssl/OpenSslConscryptSslEngineInteropTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/OpenSslConscryptSslEngineInteropTest.java @@ -22,6 +22,7 @@ import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLSessionContext; import java.security.Provider; import java.util.ArrayList; import java.util.Collection; @@ -142,6 +143,11 @@ public class OpenSslConscryptSslEngineInteropTest extends ConscryptSslEngineTest super.testSessionLocalWhenNonMutualWithKeyManager(); } + @Override + protected void invalidateSessionsAndAssert(SSLSessionContext context) { + // Not supported by conscrypt + } + @Override protected SSLEngine wrapEngine(SSLEngine engine) { return Java8SslTestUtils.wrapSSLEngineForTesting(engine); @@ -152,6 +158,8 @@ public class OpenSslConscryptSslEngineInteropTest extends ConscryptSslEngineTest protected SslContext wrapContext(SslContext context) { if (context instanceof OpenSslContext) { ((OpenSslContext) context).setUseTasks(useTasks); + // Explicit enable the session cache as its disabled by default on the client side. + ((OpenSslContext) context).sessionContext().setSessionCacheEnabled(true); } return context; } 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 9207c044d5..7525405f07 100644 --- a/handler/src/test/java/io/netty/handler/ssl/OpenSslEngineTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/OpenSslEngineTest.java @@ -1419,7 +1419,41 @@ public class OpenSslEngineTest extends SSLEngineTest { protected SslContext wrapContext(SslContext context) { if (context instanceof OpenSslContext) { ((OpenSslContext) context).setUseTasks(useTasks); + // Explicit enable the session cache as its disabled by default on the client side. + ((OpenSslContext) context).sessionContext().setSessionCacheEnabled(true); } return context; } + + @Test + @Override + public void testSessionCache() throws Exception { + super.testSessionCache(); + assertSessionContext(clientSslCtx); + assertSessionContext(serverSslCtx); + } + + private static void assertSessionContext(SslContext context) { + if (context == null) { + return; + } + OpenSslSessionContext serverSessionCtx = (OpenSslSessionContext) context.sessionContext(); + assertTrue(serverSessionCtx.isSessionCacheEnabled()); + if (serverSessionCtx.getIds().hasMoreElements()) { + serverSessionCtx.setSessionCacheEnabled(false); + assertFalse(serverSessionCtx.getIds().hasMoreElements()); + assertFalse(serverSessionCtx.isSessionCacheEnabled()); + } + } + + @Override + protected void assertSessionReusedForEngine(SSLEngine clientEngine, SSLEngine serverEngine, boolean reuse) { + assertEquals(reuse, unwrapEngine(clientEngine).isSessionReused()); + assertEquals(reuse, unwrapEngine(serverEngine).isSessionReused()); + } + + @Override + protected boolean isSessionMaybeReused(SSLEngine engine) { + return unwrapEngine(engine).isSessionReused(); + } } diff --git a/handler/src/test/java/io/netty/handler/ssl/OpenSslJdkSslEngineInteroptTest.java b/handler/src/test/java/io/netty/handler/ssl/OpenSslJdkSslEngineInteroptTest.java index 261b95ee88..f0090d83dd 100644 --- a/handler/src/test/java/io/netty/handler/ssl/OpenSslJdkSslEngineInteroptTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/OpenSslJdkSslEngineInteroptTest.java @@ -15,7 +15,6 @@ */ package io.netty.handler.ssl; -import io.netty.util.internal.PlatformDependent; import org.junit.BeforeClass; import org.junit.Ignore; import org.junit.Test; @@ -153,6 +152,8 @@ public class OpenSslJdkSslEngineInteroptTest extends SSLEngineTest { protected SslContext wrapContext(SslContext context) { if (context instanceof OpenSslContext) { ((OpenSslContext) context).setUseTasks(useTasks); + // Explicit enable the session cache as its disabled by default on the client side. + ((OpenSslContext) context).sessionContext().setSessionCacheEnabled(true); } return context; } diff --git a/handler/src/test/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngineTest.java b/handler/src/test/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngineTest.java index b7a51b2ffe..f795ed2395 100644 --- a/handler/src/test/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngineTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngineTest.java @@ -78,6 +78,8 @@ public class ReferenceCountedOpenSslEngineTest extends OpenSslEngineTest { protected SslContext wrapContext(SslContext context) { if (context instanceof ReferenceCountedOpenSslContext) { ((ReferenceCountedOpenSslContext) context).setUseTasks(useTasks); + // Explicit enable the session cache as its disabled by default on the client side. + ((ReferenceCountedOpenSslContext) context).sessionContext().setSessionCacheEnabled(true); } return context; } 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 e9388833d1..1f46d79215 100644 --- a/handler/src/test/java/io/netty/handler/ssl/SSLEngineTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/SSLEngineTest.java @@ -53,6 +53,7 @@ import org.conscrypt.OpenSSLProvider; import org.junit.After; import org.junit.Assume; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mock; @@ -83,6 +84,7 @@ import java.security.cert.CertificateException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Enumeration; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -110,6 +112,7 @@ 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.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; @@ -1284,14 +1287,14 @@ public abstract class SSLEngineTest { .trustManager(InsecureTrustManagerFactory.INSTANCE) .sslProvider(sslClientProvider()) // This test only works for non TLSv1.3 for now - .protocols(PROTOCOL_TLS_V1_2) + .protocols(protocols()) .sslContextProvider(clientSslContextProvider()) .build()); SelfSignedCertificate ssc = new SelfSignedCertificate(); serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) .sslProvider(sslServerProvider()) // This test only works for non TLSv1.3 for now - .protocols(PROTOCOL_TLS_V1_2) + .protocols(protocols()) .sslContextProvider(serverSslContextProvider()) .build()); SSLEngine clientEngine = null; @@ -1306,10 +1309,54 @@ public abstract class SSLEngineTest { 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()); + if (protocolCipherCombo == ProtocolCipherCombo.TLSV13) { + // Allocate something which is big enough for sure + ByteBuffer packetBuffer = allocateBuffer(32 * 1024); + ByteBuffer appBuffer = allocateBuffer(32 * 1024); + + appBuffer.clear().position(4).flip(); + packetBuffer.clear(); + + do { + SSLEngineResult result; + + do { + result = serverEngine.wrap(appBuffer, packetBuffer); + } while (appBuffer.hasRemaining() || result.bytesProduced() > 0); + + appBuffer.clear(); + packetBuffer.flip(); + do { + result = clientEngine.unwrap(packetBuffer, appBuffer); + } while (packetBuffer.hasRemaining() || result.bytesProduced() > 0); + + packetBuffer.clear(); + appBuffer.clear().position(4).flip(); + + do { + result = clientEngine.wrap(appBuffer, packetBuffer); + } while (appBuffer.hasRemaining() || result.bytesProduced() > 0); + + appBuffer.clear(); + packetBuffer.flip(); + + do { + result = serverEngine.unwrap(packetBuffer, appBuffer); + } while (packetBuffer.hasRemaining() || result.bytesProduced() > 0); + + packetBuffer.clear(); + appBuffer.clear().position(4).flip(); + } while (clientEngine.getSession().getId().length == 0); + + // With TLS1.3 we should see pseudo IDs and so these should never match. + assertFalse(Arrays.equals(clientEngine.getSession().getId(), serverEngine.getSession().getId())); + } else { + // 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 { cleanupClientSslEngine(clientEngine); cleanupServerSslEngine(serverEngine); @@ -1486,8 +1533,7 @@ public abstract class SSLEngineTest { ByteBuffer cTOs = allocateBuffer(clientEngine.getSession().getPacketBufferSize()); ByteBuffer sTOc = allocateBuffer(serverEngine.getSession().getPacketBufferSize()); - ByteBuffer serverAppReadBuffer = allocateBuffer( - serverEngine.getSession().getApplicationBufferSize()); + ByteBuffer serverAppReadBuffer = allocateBuffer(serverEngine.getSession().getApplicationBufferSize()); ByteBuffer clientAppReadBuffer = allocateBuffer( clientEngine.getSession().getApplicationBufferSize()); @@ -1501,7 +1547,6 @@ public abstract class SSLEngineTest { boolean clientHandshakeFinished = false; boolean serverHandshakeFinished = false; - boolean cTOsHasRemaining; boolean sTOcHasRemaining; @@ -2925,6 +2970,270 @@ public abstract class SSLEngineTest { } } + @Test + public void testSessionCache() throws Exception { + clientSslCtx = wrapContext(SslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .sslProvider(sslClientProvider()) + .sslContextProvider(clientSslContextProvider()) + .protocols(protocols()) + .ciphers(ciphers()) + .build()); + SelfSignedCertificate ssc = new SelfSignedCertificate(); + serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + .sslProvider(sslServerProvider()) + .sslContextProvider(serverSslContextProvider()) + .protocols(protocols()) + .ciphers(ciphers()) + .build()); + + try { + doHandshakeVerifyReusedAndClose("a.netty.io", 9999, false); + doHandshakeVerifyReusedAndClose("a.netty.io", 9999, true); + doHandshakeVerifyReusedAndClose("b.netty.io", 9999, false); + invalidateSessionsAndAssert(serverSslCtx.sessionContext()); + invalidateSessionsAndAssert(clientSslCtx.sessionContext()); + } finally { + ssc.delete(); + } + } + + protected void invalidateSessionsAndAssert(SSLSessionContext context) { + Enumeration ids = context.getIds(); + while (ids.hasMoreElements()) { + byte[] id = ids.nextElement(); + SSLSession session = context.getSession(id); + if (session != null) { + session.invalidate(); + assertFalse(session.isValid()); + assertNull(context.getSession(id)); + } + } + } + + private static void assertSessionCache(SSLSessionContext sessionContext, int numSessions) { + Enumeration ids = sessionContext.getIds(); + int numIds = 0; + while (ids.hasMoreElements()) { + numIds++; + byte[] id = ids.nextElement(); + assertNotEquals(0, id.length); + SSLSession session = sessionContext.getSession(id); + assertArrayEquals(id, session.getId()); + } + assertEquals(numSessions, numIds); + } + + private void doHandshakeVerifyReusedAndClose(String host, int port, boolean reuse) + throws Exception { + SSLEngine clientEngine = null; + SSLEngine serverEngine = null; + try { + clientEngine = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT, host, port)); + serverEngine = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT)); + handshake(clientEngine, serverEngine); + int clientSessions = currentSessionCacheSize(clientSslCtx.sessionContext()); + int serverSessions = currentSessionCacheSize(serverSslCtx.sessionContext()); + int nCSessions = clientSessions; + int nSSessions = serverSessions; + boolean clientSessionReused = false; + boolean serverSessionReused = false; + if (protocolCipherCombo == ProtocolCipherCombo.TLSV13) { + // Allocate something which is big enough for sure + ByteBuffer packetBuffer = allocateBuffer(32 * 1024); + ByteBuffer appBuffer = allocateBuffer(32 * 1024); + + appBuffer.clear().position(4).flip(); + packetBuffer.clear(); + + do { + SSLEngineResult result; + + do { + result = serverEngine.wrap(appBuffer, packetBuffer); + } while (appBuffer.hasRemaining() || result.bytesProduced() > 0); + + appBuffer.clear(); + packetBuffer.flip(); + do { + result = clientEngine.unwrap(packetBuffer, appBuffer); + } while (packetBuffer.hasRemaining() || result.bytesProduced() > 0); + + packetBuffer.clear(); + appBuffer.clear().position(4).flip(); + + do { + result = clientEngine.wrap(appBuffer, packetBuffer); + } while (appBuffer.hasRemaining() || result.bytesProduced() > 0); + + appBuffer.clear(); + packetBuffer.flip(); + + do { + result = serverEngine.unwrap(packetBuffer, appBuffer); + } while (packetBuffer.hasRemaining() || result.bytesProduced() > 0); + + packetBuffer.clear(); + appBuffer.clear().position(4).flip(); + nCSessions = currentSessionCacheSize(clientSslCtx.sessionContext()); + nSSessions = currentSessionCacheSize(serverSslCtx.sessionContext()); + clientSessionReused = isSessionMaybeReused(clientEngine); + serverSessionReused = isSessionMaybeReused(serverEngine); + } while ((reuse && (!clientSessionReused || !serverSessionReused)) + || (!reuse && (nCSessions < clientSessions || + // server may use multiple sessions + nSSessions < serverSessions))); + } + + assertSessionReusedForEngine(clientEngine, serverEngine, reuse); + + closeOutboundAndInbound(clientEngine, serverEngine); + } finally { + cleanupClientSslEngine(clientEngine); + cleanupServerSslEngine(serverEngine); + } + } + + protected boolean isSessionMaybeReused(SSLEngine engine) { + return true; + } + + private static int currentSessionCacheSize(SSLSessionContext ctx) { + Enumeration ids = ctx.getIds(); + int i = 0; + while (ids.hasMoreElements()) { + i++; + ids.nextElement(); + } + return i; + } + + private void closeOutboundAndInbound(SSLEngine clientEngine, SSLEngine serverEngine) throws SSLException { + assertFalse(clientEngine.isInboundDone()); + assertFalse(clientEngine.isOutboundDone()); + assertFalse(serverEngine.isInboundDone()); + assertFalse(serverEngine.isOutboundDone()); + + ByteBuffer empty = allocateBuffer(0); + + // Ensure we allocate a bit more so we can fit in multiple packets. This is needed as we may call multiple + // time wrap / unwrap in a for loop before we drain the buffer we are writing in. + ByteBuffer cTOs = allocateBuffer(clientEngine.getSession().getPacketBufferSize() * 4); + ByteBuffer sTOs = allocateBuffer(serverEngine.getSession().getPacketBufferSize() * 4); + ByteBuffer cApps = allocateBuffer(clientEngine.getSession().getApplicationBufferSize() * 4); + ByteBuffer sApps = allocateBuffer(serverEngine.getSession().getApplicationBufferSize() * 4); + + clientEngine.closeOutbound(); + for (;;) { + // call wrap till we produced all data + SSLEngineResult result = clientEngine.wrap(empty, cTOs); + if (result.getStatus() == Status.CLOSED && result.bytesProduced() == 0) { + break; + } + assertTrue(cTOs.hasRemaining()); + } + cTOs.flip(); + + for (;;) { + // call unwrap till we consumed all data + SSLEngineResult result = serverEngine.unwrap(cTOs, sApps); + if (result.getStatus() == Status.CLOSED && result.bytesProduced() == 0) { + break; + } + assertTrue(sApps.hasRemaining()); + } + + serverEngine.closeOutbound(); + for (;;) { + // call wrap till we produced all data + SSLEngineResult result = serverEngine.wrap(empty, sTOs); + if (result.getStatus() == Status.CLOSED && result.bytesProduced() == 0) { + break; + } + assertTrue(sTOs.hasRemaining()); + } + sTOs.flip(); + + for (;;) { + // call unwrap till we consumed all data + SSLEngineResult result = clientEngine.unwrap(sTOs, cApps); + if (result.getStatus() == Status.CLOSED && result.bytesProduced() == 0) { + break; + } + assertTrue(cApps.hasRemaining()); + } + + // Now close the inbound as well + clientEngine.closeInbound(); + serverEngine.closeInbound(); + } + + protected void assertSessionReusedForEngine(SSLEngine clientEngine, SSLEngine serverEngine, boolean reuse) { + // NOOP + } + + @Test + public void testSessionCacheTimeout() throws Exception { + clientSslCtx = wrapContext(SslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .sslProvider(sslClientProvider()) + .sslContextProvider(clientSslContextProvider()) + .protocols(protocols()) + .ciphers(ciphers()) + .sessionTimeout(1) + .build()); + SelfSignedCertificate ssc = new SelfSignedCertificate(); + serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + .sslProvider(sslServerProvider()) + .sslContextProvider(serverSslContextProvider()) + .protocols(protocols()) + .ciphers(ciphers()) + .sessionTimeout(1) + .build()); + + try { + doHandshakeVerifyReusedAndClose("a.netty.io", 9999, false); + + // Let's sleep for a bit more then 1 second so the cache should timeout the sessions. + Thread.sleep(1500); + + assertSessionCache(serverSslCtx.sessionContext(), 0); + assertSessionCache(clientSslCtx.sessionContext(), 0); + } finally { + ssc.delete(); + } + } + + @Test + public void testSessionCacheSize() throws Exception { + clientSslCtx = wrapContext(SslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .sslProvider(sslClientProvider()) + .sslContextProvider(clientSslContextProvider()) + .protocols(protocols()) + .ciphers(ciphers()) + .sessionCacheSize(1) + .build()); + SelfSignedCertificate ssc = new SelfSignedCertificate(); + serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + .sslProvider(sslServerProvider()) + .sslContextProvider(serverSslContextProvider()) + .protocols(protocols()) + .ciphers(ciphers()) + .build()); + + try { + doHandshakeVerifyReusedAndClose("a.netty.io", 9999, false); + // As we have a cache size of 1 we should never have more then one session in the cache + doHandshakeVerifyReusedAndClose("b.netty.io", 9999, false); + + // We should at least reuse b.netty.io + doHandshakeVerifyReusedAndClose("b.netty.io", 9999, true); + } finally { + ssc.delete(); + } + } + @Test public void testSessionBindingEvent() throws Exception { clientSslCtx = wrapContext(SslContextBuilder.forClient() @@ -3537,7 +3846,7 @@ public abstract class SSLEngineTest { final Promise promise = sb.config().group().next().newPromise(); serverChannel = sb.childHandler(new ChannelInitializer() { @Override - protected void initChannel(Channel ch) throws Exception { + protected void initChannel(Channel ch) { ch.config().setAllocator(new TestByteBufAllocator(ch.config().getAllocator(), type)); SslHandler sslHandler = delegatingExecutor == null ? @@ -3595,7 +3904,7 @@ public abstract class SSLEngineTest { new java.security.cert.X509Certificate[] { ssc.cert() }, null, ssc.key(), null, null, null); } - private final class TestTrustManagerFactory extends X509ExtendedTrustManager { + private static final class TestTrustManagerFactory extends X509ExtendedTrustManager { private final Certificate localCert; private volatile boolean verified; diff --git a/handler/src/test/java/io/netty/handler/ssl/SslHandlerTest.java b/handler/src/test/java/io/netty/handler/ssl/SslHandlerTest.java index d4cba7dd23..1945d1e260 100644 --- a/handler/src/test/java/io/netty/handler/ssl/SslHandlerTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/SslHandlerTest.java @@ -1143,6 +1143,10 @@ public class SslHandlerTest { .protocols(protocol) .build(); + // Explicit enable session cache as it's disabled by default atm. + ((OpenSslContext) sslClientCtx).sessionContext() + .setSessionCacheEnabled(true); + final SelfSignedCertificate cert = new SelfSignedCertificate(); final SslContext sslServerCtx = SslContextBuilder.forServer(cert.key(), cert.cert()) .sslProvider(provider) @@ -1161,25 +1165,41 @@ public class SslHandlerTest { EventLoopGroup group = new NioEventLoopGroup(); Channel sc = null; - Channel cc = null; - final SslHandler clientSslHandler = sslClientCtx.newHandler(UnpooledByteBufAllocator.DEFAULT); - final SslHandler serverSslHandler = sslServerCtx.newHandler(UnpooledByteBufAllocator.DEFAULT); - - final BlockingQueue queue = new LinkedBlockingQueue(); final byte[] bytes = new byte[96]; PlatformDependent.threadLocalRandom().nextBytes(bytes); try { + final AtomicReference assertErrorRef = new AtomicReference(); sc = new ServerBootstrap() .group(group) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer() { @Override protected void initChannel(Channel ch) { - ch.pipeline().addLast(serverSslHandler); + final SslHandler sslHandler = sslServerCtx.newHandler(ch.alloc()); + ch.pipeline().addLast(sslServerCtx.newHandler(UnpooledByteBufAllocator.DEFAULT)); ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { + + private int handshakeCount; + @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { if (evt instanceof SslHandshakeCompletionEvent) { + handshakeCount++; + ReferenceCountedOpenSslEngine engine = + (ReferenceCountedOpenSslEngine) sslHandler.engine(); + // This test only works for non TLSv1.3 as TLSv1.3 will establish sessions after + // the handshake is done. + // See https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_sess_set_get_cb.html + if (!SslUtils.PROTOCOL_TLS_V1_3.equals(engine.getSession().getProtocol())) { + // First should not re-use the session + try { + assertEquals(handshakeCount > 1, engine.isSessionReused()); + } catch (AssertionError error) { + assertErrorRef.set(error); + return; + } + } + ctx.writeAndFlush(Unpooled.wrappedBuffer(bytes)); } } @@ -1188,6 +1208,31 @@ public class SslHandlerTest { }) .bind(new InetSocketAddress(0)).syncUninterruptibly().channel(); + InetSocketAddress serverAddr = (InetSocketAddress) sc.localAddress(); + testSessionTickets(serverAddr, group, sslClientCtx, bytes, false); + testSessionTickets(serverAddr, group, sslClientCtx, bytes, true); + AssertionError error = assertErrorRef.get(); + if (error != null) { + throw error; + } + } finally { + if (sc != null) { + sc.close().syncUninterruptibly(); + } + group.shutdownGracefully(); + ReferenceCountUtil.release(sslClientCtx); + } + } + + private static void testSessionTickets(InetSocketAddress serverAddress, EventLoopGroup group, + SslContext sslClientCtx, final byte[] bytes, boolean isReused) + throws Throwable { + Channel cc = null; + final BlockingQueue queue = new LinkedBlockingQueue(); + try { + final SslHandler clientSslHandler = sslClientCtx.newHandler(UnpooledByteBufAllocator.DEFAULT, + serverAddress.getAddress().getHostAddress(), serverAddress.getPort()); + ChannelFuture future = new Bootstrap() .group(group) .channel(NioSocketChannel.class) @@ -1210,11 +1255,18 @@ public class SslHandlerTest { } }); } - }).connect(sc.localAddress()); + }).connect(serverAddress); cc = future.syncUninterruptibly().channel(); - assertTrue(clientSslHandler.handshakeFuture().await().isSuccess()); - assertTrue(serverSslHandler.handshakeFuture().await().isSuccess()); + assertTrue(clientSslHandler.handshakeFuture().sync().isSuccess()); + + ReferenceCountedOpenSslEngine engine = (ReferenceCountedOpenSslEngine) clientSslHandler.engine(); + // This test only works for non TLSv1.3 as TLSv1.3 will establish sessions after + // the handshake is done. + // See https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_sess_set_get_cb.html + if (!SslUtils.PROTOCOL_TLS_V1_3.equals(engine.getSession().getProtocol())) { + assertEquals(isReused, engine.isSessionReused()); + } Object obj = queue.take(); if (obj instanceof ByteBuf) { ByteBuf buffer = (ByteBuf) obj; @@ -1232,11 +1284,6 @@ public class SslHandlerTest { if (cc != null) { cc.close().syncUninterruptibly(); } - if (sc != null) { - sc.close().syncUninterruptibly(); - } - group.shutdownGracefully(); - ReferenceCountUtil.release(sslClientCtx); } }