8000 Merge pull request #458 from bgrainger/tls · mysql-net/MySqlConnector@0f50925 · GitHub
[go: up one dir, main page]

Skip to content

Commit 0f50925

Browse files
authored
Merge pull request #458 from bgrainger/tls
Use the best TLS version by default.
2 parents 14d90bb + e4c8aec commit 0f50925

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();
@@ -415,6 +418,7 @@ private static IReadOnlyList<ConnectionPool> GetAllPools()
415418
private ConnectionPool(ConnectionSettings cs)
416419
{
417420
ConnectionSettings = cs;
421+
SslProtocols = Utility.GetDefaultSslProtocols();
418422
m_generation = 0;
419423
m_cleanSemaphore = new SemaphoreSlim(1);
420424
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)
0