Skip to content

Commit e4c8aec

Browse files
committed
Use the best TLS version by default.
Specifying SslProtocols.None allows the .NET Framework to choose the best TLS version supported by the OS (on .NET 4.7 and later). However, negotiating TLS 1.2 with a Windows Schannel client against a yaSSL-based MySQL Server will fail; automatically try with a lower TLS version if it's possible we're in this scenario.
1 parent 59c450c commit e4c8aec

File tree

11 files changed

+164
-56
lines changed

11 files changed

+164
-56
lines changed

.ci/config/config.compression+ssl.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"ConnectionString": "server=127.0.0.1;user id=ssltest;password=test;port=3306;database=mysqltest;ssl mode=required;use compression=true;DefaultCommandTimeout=3600",
44
"PasswordlessUser": "no_password",
55
"SecondaryDatabase": "testdb2",
6-
"UnsupportedFeatures": "RsaEncryption,CachingSha2Password",
6+
"UnsupportedFeatures": "RsaEncryption,CachingSha2Password,Tls12",
77
"MySqlBulkLoaderLocalCsvFile": "../../../../TestData/LoadData_UTF8_BOM_Unix.CSV",
88
"MySqlBulkLoaderLocalTsvFile": "../../../../TestData/LoadData_UTF8_BOM_Unix.TSV",
99
"CertificatesPath": "../../../../../.ci/server/certs"

.ci/config/config.compression.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"ConnectionString": "server=127.0.0.1;user id=mysqltest;password='test;key=\"val';port=3306;database=mysqltest;ssl mode=none;UseCompression=true;DefaultCommandTimeout=3600",
44
"PasswordlessUser": "no_password",
55
"SecondaryDatabase": "testdb2",
6-
"UnsupportedFeatures": "RsaEncryption,CachingSha2Password",
6+
"UnsupportedFeatures": "RsaEncryption,CachingSha2Password,Tls12",
77
"MySqlBulkLoaderLocalCsvFile": "../../../../TestData/LoadData_UTF8_BOM_Unix.CSV",
88
"MySqlBulkLoaderLocalTsvFile": "../../../../TestData/LoadData_UTF8_BOM_Unix.TSV"
99
}

.ci/config/config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"ConnectionString": "server=127.0.0.1;user id=mysqltest;password='test;key=\"val';port=3306;database=mysqltest;ssl mode=none;Use Affected Rows=true;DefaultCommandTimeout=3600",
44
"PasswordlessUser": "no_password",
55
"SecondaryDatabase": "testdb2",
6-
"UnsupportedFeatures": "RsaEncryption,CachingSha2Password",
6+
"UnsupportedFeatures": "RsaEncryption,CachingSha2Password,Tls12",
77
"MySqlBulkLoaderLocalCsvFile": "../../../../TestData/LoadData_UTF8_BOM_Unix.CSV",
88
"MySqlBulkLoaderLocalTsvFile": "../../../../TestData/LoadData_UTF8_BOM_Unix.TSV"
99
}

.ci/config/config.ssl.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"ConnectionString": "server=127.0.0.1;user id=ssltest;password=test;port=3306;database=mysqltest;ssl mode=required;certificate file=../../../../../.ci/server/certs/ssl-client.pfx;DefaultCommandTimeout=3600",
44
"PasswordlessUser": "no_password",
55
"SecondaryDatabase": "testdb2",
6-
"UnsupportedFeatures": "RsaEncryption,CachingSha2Password",
6+
"UnsupportedFeatures": "RsaEncryption,CachingSha2Password,Tls12",
77
"MySqlBulkLoaderLocalCsvFile": "../../../../TestData/LoadData_UTF8_BOM_Unix.CSV",
88
"MySqlBulkLoaderLocalTsvFile": "../../../../TestData/LoadData_UTF8_BOM_Unix.TSV",
99
"CertificatesPath": "../../../../../.ci/server/certs"

.travis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ services: docker
55
env:
66
- IMAGE=mysql:5.6
77
NAME=mysql
8-
OMIT_FEATURES=Json,Sha256Password,RsaEncryption,LargePackets,CachingSha2Password,SessionTrack
8+
OMIT_FEATURES=Json,Sha256Password,RsaEncryption,LargePackets,CachingSha2Password,SessionTrack,Tls11,Tls12
99
- IMAGE=mysql:5.7
1010
NAME=mysql
11-
OMIT_FEATURES=RsaEncryption,CachingSha2Password
11+
OMIT_FEATURES=RsaEncryption,CachingSha2Password,Tls12
1212
- IMAGE=mysql:8.0
1313
NAME=mysql
1414
OMIT_FEATURES=None

src/MySqlConnector/Core/ConnectionPool.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Concurrent;
33
using System.Collections.Generic;
44
using System.Linq;
5+
using System.Security.Authentication;
56
using System.Threading;
67
using System.Threading.Tasks;
78
using MySql.Data.MySqlClient;
@@ -17,6 +18,8 @@ internal sealed class ConnectionPool
1718

1819
public ConnectionSettings ConnectionSettings { get; }
1920

21+
public SslProtocols SslProtocols { get; set; }
22+
2023
public async ValueTask<ServerSession> GetSessionAsync(MySqlConnection connection, IOBehavior ioBehavior, CancellationToken cancellationToken)
2124
{
2225
cancellationToken.ThrowIfCancellationRequested();
@@ -411,6 +414,7 @@ private static IReadOnlyList<ConnectionPool> GetAllPools()
411414
private ConnectionPool(ConnectionSettings cs)
412415
{
413416
ConnectionSettings = cs;
417+
SslProtocols = Utility.GetDefaultSslProtocols();
414418
m_generation = 0;
415419
m_cleanSemaphore = new SemaphoreSlim(1);
416420
m_sessionSemaphore = new SemaphoreSlim(cs.MaximumPoolSize);

src/MySqlConnector/Core/ServerSession.cs

Lines changed: 82 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -225,60 +225,93 @@ public async Task ConnectAsync(ConnectionSettings cs, ILoadBalancer loadBalancer
225225
VerifyState(State.Created);
226226
m_state = State.Connecting;
227227
}
228-
var connected = false;
229-
if (cs.ConnectionType == ConnectionType.Tcp)
230-
connected = await OpenTcpSocketAsync(cs, loadBalancer, ioBehavior, cancellationToken).ConfigureAwait(false);
231-
else if (cs.ConnectionType == ConnectionType.Unix)
232-
connected = await OpenUnixSocketAsync(cs, ioBehavior, cancellationToken).ConfigureAwait(false);
233-
if (!connected)
228+
229+
// TLS negotiation should automatically fall back to the best version supported by client and server. However,
230+
// Windows Schannel clients will fail to connect to a yaSSL-based MySQL Server if TLS 1.2 is requested and
231+
// have to use only TLS 1.1: https://github.com/mysql-net/MySqlConnector/pull/101
232+
// In order to use the best protocol possible (i.e., not always default to TLS 1.1), we try the OS-default protocol
233+
// (which is SslProtocols.None; see https://docs.microsoft.com/en-us/dotnet/framework/network-programming/tls),
234+
// then fall back to SslProtocols.Tls11 if that fails and it's possible that the cause is a yaSSL server.
235+
bool shouldRetrySsl;
236+
var sslProtocols = Pool?.SslProtocols ?? Utility.GetDefaultSslProtocols();
237+
PayloadData payload;
238+
InitialHandshakePayload initialHandshake;
239+
do
234240
{
235-
lock (m_lock)
236-
m_state = State.Failed;
237-
Log.Error("{0} connecting failed", m_logArguments);
238-
throw new MySqlException("Unable to connect to any of the specified MySQL hosts.");
239-
}
241+
shouldRetrySsl = (sslProtocols == SslProtocols.None || (sslProtocols & SslProtocols.Tls12) == SslProtocols.Tls12) && Utility.IsWindows();
242+
243+
var connected = false;
244+
if (cs.ConnectionType == ConnectionType.Tcp)
245+
connected = await OpenTcpSocketAsync(cs, loadBalancer, ioBehavior, cancellationToken).ConfigureAwait(false);
246+
else if (cs.ConnectionType == ConnectionType.Unix)
247+
connected = await OpenUnixSocketAsync(cs, ioBehavior, cancellationToken).ConfigureAwait(false);
248+
if (!connected)
249+
{
250+
lock (m_lock)
251+
m_state = State.Failed;
252+
Log.Error("{0} connecting failed", m_logArguments);
253+
throw new MySqlException("Unable to connect to any of the specified MySQL hosts.");
254+
}
240255

241-
var byteHandler = new SocketByteHandler(m_socket);
242-
m_payloadHandler = new StandardPayloadHandler(byteHandler);
256+
var byteHandler = new SocketByteHandler(m_socket);
257+
m_payloadHandler = new StandardPayloadHandler(byteHandler);
243258

244-
var payload = await ReceiveAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
245-
var initialHandshake = InitialHandshakePayload.Create(payload);
259+
payload = await ReceiveAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
260+
initialHandshake = InitialHandshakePayload.Create(payload);
246261

247-
// if PluginAuth is supported, then use the specified auth plugin; else, fall back to protocol capabilities to determine the auth type to use
248-
string authPluginName;
249-
if ((initialHandshake.ProtocolCapabilities & ProtocolCapabilities.PluginAuth) != 0)
250-
authPluginName = initialHandshake.AuthPluginName;
251-
else
252-
authPluginName = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.SecureConnection) == 0 ? "mysql_old_password" : "mysql_native_password";
253-
m_logArguments[1] = authPluginName;
254-
Log.Debug("{0} server sent auth_plugin_name '{1}'", m_logArguments);
255-
if (authPluginName != "mysql_native_password" && authPluginName != "sha256_password" && authPluginName != "caching_sha2_password")
256-
{
257-
Log.Error("{0} unsupported authentication method '{1}'", m_logArguments);
258-
throw new NotSupportedException("Authentication method '{0}' is not supported.".FormatInvariant(initialHandshake.AuthPluginName));
259-
}
262+
// if PluginAuth is supported, then use the specified auth plugin; else, fall back to protocol capabilities to determine the auth type to use
263+
string authPluginName;
264+
if ((initialHandshake.ProtocolCapabilities & ProtocolCapabilities.PluginAuth) != 0)
265+
authPluginName = initialHandshake.AuthPluginName;
266+
else
267+
authPluginName = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.SecureConnection) == 0 ? "mysql_old_password" : "mysql_native_password";
268+
m_logArguments[1] = authPluginName;
269+
Log.Debug("{0} server sent auth_plugin_name '{1}'", m_logArguments);
270+
if (authPluginName != "mysql_native_password" && authPluginName != "sha256_password" && authPluginName != "caching_sha2_password")
271+
{
272+
Log.Error("{0} unsupported authentication method '{1}'", m_logArguments);
273+
throw new NotSupportedException("Authentication method '{0}' is not supported.".FormatInvariant(initialHandshake.AuthPluginName));
274+
}
260275

261-
ServerVersion = new ServerVersion(Encoding.ASCII.GetString(initialHandshake.ServerVersion));
262-
ConnectionId = initialHandshake.ConnectionId;
263-
AuthPluginData = initialHandshake.AuthPluginData;
264-
m_useCompression = cs.UseCompression && (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.Compress) != 0;
276+
ServerVersion = new ServerVersion(Encoding.ASCII.GetString(initialHandshake.ServerVersion));
277+
ConnectionId = initialHandshake.ConnectionId;
278+
AuthPluginData = initialHandshake.AuthPluginData;
279+
m_useCompression = cs.UseCompression && (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.Compress) != 0;
265280

266-
m_supportsConnectionAttributes = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.ConnectionAttributes) != 0;
267-
m_supportsDeprecateEof = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.DeprecateEof) != 0;
268-
var serverSupportsSsl = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.Ssl) != 0;
281+
m_supportsConnectionAttributes = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.ConnectionAttributes) != 0;
282+
m_supportsDeprecateEof = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.DeprecateEof) != 0;
283+
var serverSupportsSsl = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.Ssl) != 0;
269284

270-
Log.Info("{0} made connection; ServerVersion={1}; ConnectionId={2}; Flags: {3}{4}{5}{6}", m_logArguments[0], ServerVersion.OriginalString, ConnectionId,
271-
m_useCompression ? "Cmp " :"", m_supportsConnectionAttributes ? "Attr " : "", m_supportsDeprecateEof ? "" : "Eof ", serverSupportsSsl ? "Ssl " : "");
285+
Log.Info("{0} made connection; ServerVersion={1}; ConnectionId={2}; Flags: {3}{4}{5}{6}", m_logArguments[0], ServerVersion.OriginalString, ConnectionId,
286+
m_useCompression ? "Cmp " : "", m_supportsConnectionAttributes ? "Attr " : "", m_supportsDeprecateEof ? "" : "Eof ", serverSupportsSsl ? "Ssl " : "");
272287

273-
if (cs.SslMode != MySqlSslMode.None && (cs.SslMode != MySqlSslMode.Preferred || serverSupportsSsl))
274-
{
275-
if (!serverSupportsSsl)
288+
if (cs.SslMode != MySqlSslMode.None && (cs.SslMode != MySqlSslMode.Preferred || serverSupportsSsl))
276289
{
277-
Log.Error("{0} requires SSL but server doesn't support it", m_logArguments);
278-
throw new MySqlException("Server does not support SSL");
290+
if (!serverSupportsSsl)
291+
{
292+
Log.Error("{0} requires SSL but server doesn't support it", m_logArguments);
293+
throw new MySqlException("Server does not support SSL");
294+
}
295+
296+
try
297+
{
298+
await InitSslAsync(initialHandshake.ProtocolCapabilities, cs, sslProtocols, ioBehavior, cancellationToken).ConfigureAwait(false);
299+
shouldRetrySsl = false;
300+
}
301+
catch (Exception ex) when (shouldRetrySsl && ((ex is MySqlException && ex.InnerException is IOException) || ex is IOException))
302+
{
303+
// negotiating TLS 1.2 with a yaSSL-based server throws an exception on Windows, see comment at top of method
304+
Log.Warn(ex, "{0} failed negotiating TLS; falling back to TLS 1.1", m_logArguments);
305+
sslProtocols = SslProtocols.Tls | SslProtocols.Tls11;
306+
if (Pool != null)
307+
Pool.SslProtocols = sslProtocols;
308+
}
279309
}
280-
await InitSslAsync(initialHandshake.ProtocolCapabilities, cs, ioBehavior, cancellationToken).ConfigureAwait(false);
281-
}
310+
else
311+
{
312+
shouldRetrySsl = false;
313+
}
314+
} while (shouldRetrySsl);
282315

283316
if (m_supportsConnectionAttributes && s_connectionAttributes == null)
284317
s_connectionAttributes = CreateConnectionAttributes();
@@ -772,7 +805,7 @@ private async Task<bool> OpenUnixSocketAsync(ConnectionSettings cs, IOBehavior i
772805
return false;
773806
}
774807

775-
private async Task InitSslAsync(ProtocolCapabilities serverCapabilities, ConnectionSettings cs, IOBehavior ioBehavior, CancellationToken cancellationToken)
808+
private async Task InitSslAsync(ProtocolCapabilities serverCapabilities, ConnectionSettings cs, SslProtocols sslProtocols, IOBehavior ioBehavior, CancellationToken cancellationToken)
776809
{
777810
Log.Info("{0} initializing TLS connection", m_logArguments);
778811
X509CertificateCollection clientCertificates = null;
@@ -861,11 +894,6 @@ bool ValidateRemoteCertificate(object rcbSender, X509Certificate rcbCertificate,
861894
else
862895
sslStream = new SslStream(m_networkStream, false, ValidateRemoteCertificate, ValidateLocalCertificate);
863896

864-
// SslProtocols.Tls1.2 throws an exception in Windows, see https://github.com/mysql-net/MySqlConnector/pull/101
865-
var sslProtocols = SslProtocols.Tls | SslProtocols.Tls11;
866-
if (!Utility.IsWindows())
867-
sslProtocols |= SslProtocols.Tls12;
868-
869897
var checkCertificateRevocation = cs.SslMode == MySqlSslMode.VerifyFull;
870898

871899
var initSsl = HandshakeResponse41Payload.CreateWithSsl(serverCapabilities, cs, m_useCompression);
@@ -889,6 +917,8 @@ bool ValidateRemoteCertificate(object rcbSender, X509Certificate rcbCertificate,
889917
m_payloadHandler.ByteHandler = sslByteHandler;
890918
m_isSecureConnection = true;
891919
m_sslStream = sslStream;
920+
m_logArguments[1] = sslStream.SslProtocol;
921+
Log.Info("{0} connected TLS with protocol {1}", m_logArguments);
892922
}
893923
catch (Exception ex)
894924
{
@@ -1082,6 +1112,8 @@ private void VerifyState(State state1, State state2, State state3)
10821112

10831113
internal bool SslIsMutuallyAuthenticated => m_sslStream?.IsMutuallyAuthenticated ?? false;
10841114

1115+
internal SslProtocols SslProtocol => m_sslStream?.SslProtocol ?? SslProtocols.None;
1116+
10851117
private byte[] CreateConnectionAttributes()
10861118
{
10871119
Log.Debug("{0} creating connection attributes", m_logArguments);

src/MySqlConnector/MySql.Data.MySqlClient/MySqlConnection.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Data;
44
using System.Data.Common;
55
using System.Net.Sockets;
6+
using System.Security.Authentication;
67
using System.Threading;
78
using System.Threading.Tasks;
89
using MySqlConnector.Core;
@@ -426,6 +427,8 @@ private async ValueTask<ServerSession> CreateSessionAsync(IOBehavior? ioBehavior
426427

427428
internal bool SslIsMutuallyAuthenticated => m_session.SslIsMutuallyAuthenticated;
428429

430+
internal SslProtocols SslProtocol => m_session.SslProtocol;
431+
429432
internal void SetState(ConnectionState newState)
430433
{
431434
if (m_connectionState != newState)

src/MySqlConnector/Utilities/Utility.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
using System.Globalization;
33
using System.IO;
44
using System.Linq;
5+
using System.Net.Security;
56
using System.Runtime.InteropServices;
7+
using System.Security.Authentication;
68
using System.Security.Cryptography;
79
using System.Text;
810
using System.Threading.Tasks;
@@ -254,5 +256,41 @@ public static void GetOSDetails(out string os, out string osDescription, out str
254256
architecture = RuntimeInformation.ProcessArchitecture.ToString();
255257
}
256258
#endif
259+
260+
#if NET45 || NET46
261+
public static SslProtocols GetDefaultSslProtocols()
262+
{
263+
if (!s_defaultSslProtocols.HasValue)
264+
{
265+
try
266+
{
267+
using (var memoryStream = new MemoryStream())
268+
using (var sslStream = new SslStream(memoryStream))
269+
{
270+
sslStream.AuthenticateAsClient("localhost", null, SslProtocols.None, false);
271+
}
272+
}
273+
catch (ArgumentException ex) when (ex.ParamName == "sslProtocolType")
274+
{
275+
// Prior to .NET Framework 4.7, SslProtocols.None is not a valid argument to AuthenticateAsClientAsync.
276+
// If the NET46 build is loaded by an application that targets. NET 4.7 (or later), or if app.config has set
277+
// Switch.System.Net.DontEnableSystemDefaultTlsVersions to false, then SslProtocols.None will work; otherwise,
278+
// if the application targets .NET 4.6.2 or earlier and hasn't changed the AppContext switch, then it will
279+
// fail at runtime; we catch the exception and explicitly specify the protocols to use.
280+
s_defaultSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12;
281+
}
282+
catch (Exception)
283+
{
284+
s_defaultSslProtocols = SslProtocols.None;
285+
}
286+
}
287+
288+
return s_defaultSslProtocols.Value;
289+
}
290+
291+
static SslProtocols? s_defaultSslProtocols;
292+
#else
293+
public static SslProtocols GetDefaultSslProtocols() => SslProtocols.None;
294+
#endif
257295
}
258296
}

tests/SideBySide/ServerFeatures.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,7 @@ public enum ServerFeatures
1616
Timeout = 128,
1717
ErrorCodes = 256,
1818
KnownCertificateAuthority = 512,
19+
Tls11 = 1024,
20+
Tls12 = 2048,
1921
}
2022
}

0 commit comments

Comments
 (0)