Correctly filter out TLSv1.3 ciphers if TLSv1.3 is not enabled (#10919)

Motivation:

We didnt correctly filter out TLSv1.3 ciphers when TLSv1.3 is not enabled.

Modifications:

- Filter out ciphers that are not supported due the selected TLS version
- Add unit test

Result:

Fixes https://github.com/netty/netty/issues/10911

Co-authored-by: Bryce Anderson <banderson@twitter.com>
This commit is contained in:
Norman Maurer 2021-01-28 11:00:51 +01:00 committed by GitHub
parent c932e1d652
commit df10dfbb49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 121 additions and 6 deletions

View File

@ -46,6 +46,7 @@ import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -160,6 +161,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
private volatile boolean destroyed; private volatile boolean destroyed;
private volatile String applicationProtocol; private volatile String applicationProtocol;
private volatile boolean needTask; private volatile boolean needTask;
private String[] explicitlyEnabledProtocols;
// Reference Counting // Reference Counting
private final ResourceLeakTracker<ReferenceCountedOpenSslEngine> leak; private final ResourceLeakTracker<ReferenceCountedOpenSslEngine> leak;
@ -339,7 +341,9 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
setClientAuth(clientMode ? ClientAuth.NONE : context.clientAuth); setClientAuth(clientMode ? ClientAuth.NONE : context.clientAuth);
if (context.protocols != null) { if (context.protocols != null) {
setEnabledProtocols(context.protocols); setEnabledProtocols0(context.protocols, true);
} else {
this.explicitlyEnabledProtocols = getEnabledProtocols();
} }
// Use SNI if peerHost was specified and a valid hostname // Use SNI if peerHost was specified and a valid hostname
@ -1531,14 +1535,17 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
public final String[] getEnabledCipherSuites() { public final String[] getEnabledCipherSuites() {
final String[] extraCiphers; final String[] extraCiphers;
final String[] enabled; final String[] enabled;
final boolean tls13Enabled;
synchronized (this) { synchronized (this) {
if (!isDestroyed()) { if (!isDestroyed()) {
enabled = SSL.getCiphers(ssl); enabled = SSL.getCiphers(ssl);
int opts = SSL.getOptions(ssl); int opts = SSL.getOptions(ssl);
if (isProtocolEnabled(opts, SSL.SSL_OP_NO_TLSv1_3, PROTOCOL_TLS_V1_3)) { if (isProtocolEnabled(opts, SSL.SSL_OP_NO_TLSv1_3, PROTOCOL_TLS_V1_3)) {
extraCiphers = OpenSsl.EXTRA_SUPPORTED_TLS_1_3_CIPHERS; extraCiphers = OpenSsl.EXTRA_SUPPORTED_TLS_1_3_CIPHERS;
tls13Enabled = true;
} else { } else {
extraCiphers = EmptyArrays.EMPTY_STRINGS; extraCiphers = EmptyArrays.EMPTY_STRINGS;
tls13Enabled = false;
} }
} else { } else {
return EmptyArrays.EMPTY_STRINGS; return EmptyArrays.EMPTY_STRINGS;
@ -1552,7 +1559,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
for (int i = 0; i < enabled.length; i++) { for (int i = 0; i < enabled.length; i++) {
String mapped = toJavaCipherSuite(enabled[i]); String mapped = toJavaCipherSuite(enabled[i]);
final String cipher = mapped == null ? enabled[i] : mapped; final String cipher = mapped == null ? enabled[i] : mapped;
if (!OpenSsl.isTlsv13Supported() && SslUtils.isTLSv13Cipher(cipher)) { if ((!tls13Enabled || !OpenSsl.isTlsv13Supported()) && SslUtils.isTLSv13Cipher(cipher)) {
continue; continue;
} }
enabledSet.add(cipher); enabledSet.add(cipher);
@ -1579,17 +1586,36 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
} }
synchronized (this) { synchronized (this) {
if (!isDestroyed()) { if (!isDestroyed()) {
// TODO: Should we also adjust the protocols based on if there are any ciphers left that can be used
// for TLSv1.3 or for previor SSL/TLS versions ?
try { try {
// Set non TLSv1.3 ciphers. // Set non TLSv1.3 ciphers.
SSL.setCipherSuites(ssl, cipherSuiteSpec, false); SSL.setCipherSuites(ssl, cipherSuiteSpec, false);
if (OpenSsl.isTlsv13Supported()) { if (OpenSsl.isTlsv13Supported()) {
// Set TLSv1.3 ciphers. // Set TLSv1.3 ciphers.
SSL.setCipherSuites(ssl, cipherSuiteSpecTLSv13, true); SSL.setCipherSuites(ssl, cipherSuiteSpecTLSv13, true);
} }
// We also need to update the enabled protocols to ensure we disable the protocol if there are
// no compatible ciphers left.
Set<String> protocols = new HashSet<String>(explicitlyEnabledProtocols.length);
Collections.addAll(protocols, explicitlyEnabledProtocols);
// We have no ciphers that are compatible with none-TLSv1.3, let us explicit disable all other
// protocols.
if (cipherSuiteSpec.isEmpty()) {
protocols.remove(PROTOCOL_TLS_V1);
protocols.remove(PROTOCOL_TLS_V1_1);
protocols.remove(PROTOCOL_TLS_V1_2);
protocols.remove(PROTOCOL_SSL_V3);
protocols.remove(PROTOCOL_SSL_V2);
protocols.remove(PROTOCOL_SSL_V2_HELLO);
}
// We have no ciphers that are compatible with TLSv1.3, let us explicit disable it.
if (cipherSuiteSpecTLSv13.isEmpty()) {
protocols.remove(PROTOCOL_TLS_V1_3);
}
// Update the protocols but not cache the value. We only cache when we call it from the user
// code or when we construct the engine.
setEnabledProtocols0(protocols.toArray(EmptyArrays.EMPTY_STRINGS), false);
} catch (Exception e) { } catch (Exception e) {
throw new IllegalStateException("failed to enable cipher suites: " + cipherSuiteSpec, e); throw new IllegalStateException("failed to enable cipher suites: " + cipherSuiteSpec, e);
} }
@ -1656,6 +1682,10 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
*/ */
@Override @Override
public final void setEnabledProtocols(String[] protocols) { public final void setEnabledProtocols(String[] protocols) {
setEnabledProtocols0(protocols, true);
}
private void setEnabledProtocols0(String[] protocols, boolean cache) {
if (protocols == null) { if (protocols == null) {
// This is correct from the API docs // This is correct from the API docs
throw new IllegalArgumentException(); throw new IllegalArgumentException();
@ -1711,6 +1741,9 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
} }
} }
synchronized (this) { synchronized (this) {
if (cache) {
this.explicitlyEnabledProtocols = protocols;
}
if (!isDestroyed()) { if (!isDestroyed()) {
// Clear out options which disable protocols // Clear out options which disable protocols
SSL.clearOptions(ssl, SSL.SSL_OP_NO_SSLv2 | SSL.SSL_OP_NO_SSLv3 | SSL.SSL_OP_NO_TLSv1 | SSL.clearOptions(ssl, SSL.SSL_OP_NO_SSLv2 | SSL.SSL_OP_NO_SSLv3 | SSL.SSL_OP_NO_TLSv1 |

View File

@ -129,6 +129,7 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame; import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
public abstract class SSLEngineTest { public abstract class SSLEngineTest {
@ -484,6 +485,87 @@ public abstract class SSLEngineTest {
} }
} }
@Test
public void testSetSupportedCiphers() throws Exception {
if (protocolCipherCombo != ProtocolCipherCombo.tlsv12()) {
return;
}
SelfSignedCertificate cert = new SelfSignedCertificate();
serverSslCtx = wrapContext(SslContextBuilder.forServer(cert.key(), cert.cert())
.protocols(protocols())
.ciphers(ciphers())
.sslProvider(sslServerProvider()).build());
final SSLEngine serverEngine =
wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
clientSslCtx = wrapContext(SslContextBuilder.forClient()
.trustManager(cert.certificate())
.protocols(protocols())
.ciphers(ciphers())
.sslProvider(sslClientProvider()).build());
final SSLEngine clientEngine =
wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
final String[] enabledCiphers = new String[]{ ciphers().get(0) };
try {
clientEngine.setEnabledCipherSuites(enabledCiphers);
serverEngine.setEnabledCipherSuites(enabledCiphers);
assertArrayEquals(enabledCiphers, clientEngine.getEnabledCipherSuites());
assertArrayEquals(enabledCiphers, serverEngine.getEnabledCipherSuites());
} finally {
cleanupClientSslEngine(clientEngine);
cleanupServerSslEngine(serverEngine);
cert.delete();
}
}
@Test(expected = SSLHandshakeException.class)
public void testIncompatibleCiphers() throws Exception {
assumeTrue(SslProvider.isTlsv13Supported(sslClientProvider()));
SelfSignedCertificate ssc = new SelfSignedCertificate();
// Select a mandatory cipher from the TLSv1.2 RFC https://www.ietf.org/rfc/rfc5246.txt so handshakes won't fail
// due to no shared/supported cipher.
clientSslCtx = wrapContext(SslContextBuilder.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.protocols(PROTOCOL_TLS_V1_3, PROTOCOL_TLS_V1_2, PROTOCOL_TLS_V1)
.sslContextProvider(clientSslContextProvider())
.sslProvider(sslClientProvider())
.build());
serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
.protocols(PROTOCOL_TLS_V1_3, PROTOCOL_TLS_V1_2, PROTOCOL_TLS_V1)
.sslContextProvider(serverSslContextProvider())
.sslProvider(sslServerProvider())
.build());
SSLEngine clientEngine = null;
SSLEngine serverEngine = null;
try {
clientEngine = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
serverEngine = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
// Set the server to only support a single TLSv1.2 cipher
final String serverCipher = "TLS_RSA_WITH_AES_128_CBC_SHA";
serverEngine.setEnabledCipherSuites(new String[] { serverCipher });
// Set the client to only support a single TLSv1.3 cipher
final String clientCipher = "TLS_AES_256_GCM_SHA384";
clientEngine.setEnabledCipherSuites(new String[] { clientCipher });
handshake(clientEngine, serverEngine);
// We shouldn't get here as the handshake should throw a SSLHandshakeException. If
// we do, throw our own exception that is more informative as to what has happened.
fail("Unexpected handshake success. Negotiated TLS version: " +
clientEngine.getSession().getProtocol() +
", cipher suite negotiated: " + clientEngine.getSession().getCipherSuite());
} finally {
cleanupClientSslEngine(clientEngine);
cleanupServerSslEngine(serverEngine);
ssc.delete();
}
}
@Test @Test
public void testMutualAuthDiffCerts() throws Exception { public void testMutualAuthDiffCerts() throws Exception {
File serverKeyFile = ResourcesUtil.getFile(getClass(), "test_encrypted.pem"); File serverKeyFile = ResourcesUtil.getFile(getClass(), "test_encrypted.pem");
@ -1238,7 +1320,7 @@ public abstract class SSLEngineTest {
@Test(timeout = 30000) @Test(timeout = 30000)
public void clientInitiatedRenegotiationWithFatalAlertDoesNotInfiniteLoopServer() public void clientInitiatedRenegotiationWithFatalAlertDoesNotInfiniteLoopServer()
throws CertificateException, SSLException, InterruptedException, ExecutionException { throws CertificateException, SSLException, InterruptedException, ExecutionException {
Assume.assumeTrue(PlatformDependent.javaVersion() >= 11); assumeTrue(PlatformDependent.javaVersion() >= 11);
final SelfSignedCertificate ssc = new SelfSignedCertificate(); final SelfSignedCertificate ssc = new SelfSignedCertificate();
serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
.sslProvider(sslServerProvider()) .sslProvider(sslServerProvider())