Skip to content

Commit b515bc5

Browse files
committed
Add TlsVersion connection string option. Fixes #760
1 parent b933ce0 commit b515bc5

File tree

8 files changed

+174
-4
lines changed

8 files changed

+174
-4
lines changed

azure-pipelines.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ jobs:
140140
unsupportedFeatures: 'Ed25519,CachingSha2Password,Tls13,UuidToBin'
141141
'MySQL 8.0':
142142
image: 'mysql:8.0'
143-
unsupportedFeatures: 'Ed25519'
143+
unsupportedFeatures: 'Ed25519,Tls11'
144144
'Percona 5.7':
145145
image: 'percona:5.7.22'
146146
unsupportedFeatures: 'CachingSha2Password,Ed25519,Tls13,UuidToBin'

src/MySqlConnector/Core/ConnectionPool.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ private static IReadOnlyList<ConnectionPool> GetAllPools()
444444
private ConnectionPool(ConnectionSettings cs)
445445
{
446446
ConnectionSettings = cs;
447-
SslProtocols = Utility.GetDefaultSslProtocols();
447+
SslProtocols = cs.TlsVersions;
448448
m_generation = 0;
449449
m_cleanSemaphore = new SemaphoreSlim(1);
450450
m_sessionSemaphore = new SemaphoreSlim(cs.MaximumPoolSize);

src/MySqlConnector/Core/ConnectionSettings.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.IO;
4+
using System.Security.Authentication;
45
using MySql.Data.MySqlClient;
56
using MySqlConnector.Utilities;
67

@@ -56,6 +57,35 @@ public ConnectionSettings(MySqlConnectionStringBuilder csb)
5657
CertificateStoreLocation = csb.CertificateStoreLocation;
5758
CertificateThumbprint = csb.CertificateThumbprint;
5859

60+
if (csb.TlsVersion is null)
61+
{
62+
TlsVersions = Utility.GetDefaultSslProtocols();
63+
}
64+
else
65+
{
66+
TlsVersions = default;
67+
for (var i = 6; i < csb.TlsVersion.Length; i += 8)
68+
{
69+
char minorVersion = csb.TlsVersion[i];
70+
if (minorVersion == '0')
71+
TlsVersions |= SslProtocols.Tls;
72+
else if (minorVersion == '1')
73+
TlsVersions |= SslProtocols.Tls11;
74+
else if (minorVersion == '2')
75+
TlsVersions |= SslProtocols.Tls12;
76+
else if (minorVersion == '3')
77+
#if NET45 || NET461 || NET471 || NETSTANDARD1_3 || NETSTANDARD2_0 || NETSTANDARD2_1 || NETCOREAPP2_1
78+
TlsVersions |= 0;
79+
#else
80+
TlsVersions |= SslProtocols.Tls13;
81+
#endif
82+
else
83+
throw new InvalidOperationException("Unexpected character '{0}' for TLS minor version.".FormatInvariant(minorVersion));
84+
}
85+
if (TlsVersions == default)
86+
throw new NotSupportedException("All specified TLS versions are incompatible with this platform.");
87+
}
88+
5989
// Connection Pooling Options
6090
Pooling = csb.Pooling;
6191
ConnectionLifeTime = Math.Min(csb.ConnectionLifeTime, uint.MaxValue / 1000) * 1000;
@@ -141,6 +171,7 @@ private static MySqlGuidFormat GetEffectiveGuidFormat(MySqlGuidFormat guidFormat
141171
public string? SslKeyFile { get; }
142172
public MySqlCertificateStoreLocation CertificateStoreLocation { get; }
143173
public string? CertificateThumbprint { get; }
174+
public SslProtocols TlsVersions { get; }
144175

145176
// Connection Pooling Options
146177
public bool Pooling { get; }

src/MySqlConnector/Core/ServerSession.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Buffers.Text;
33
using System.Collections.Generic;
4+
using System.ComponentModel;
45
using System.Data;
56
using System.Diagnostics;
67
using System.Globalization;
@@ -326,7 +327,7 @@ public async Task ConnectAsync(ConnectionSettings cs, int startTickCount, ILoadB
326327
// (which is SslProtocols.None; see https://docs.microsoft.com/en-us/dotnet/framework/network-programming/tls),
327328
// then fall back to SslProtocols.Tls11 if that fails and it's possible that the cause is a yaSSL server.
328329
bool shouldRetrySsl;
329-
var sslProtocols = Pool?.SslProtocols ?? Utility.GetDefaultSslProtocols();
330+
var sslProtocols = Pool?.SslProtocols ?? cs.TlsVersions;
330331
PayloadData payload;
331332
InitialHandshakePayload initialHandshake;
332333
do
@@ -1296,6 +1297,8 @@ bool ValidateRemoteCertificate(object rcbSender, X509Certificate rcbCertificate,
12961297
throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "SSL Authentication Error", ex);
12971298
if (ex is IOException && clientCertificates is object)
12981299
throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "MySQL Server rejected client certificate", ex);
1300+
if (ex is Win32Exception win32 && win32.NativeErrorCode == -2146893007) // SEC_E_ALGORITHM_MISMATCH (0x80090331)
1301+
throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "The server doesn't support the client's specified TLS versions.", ex);
12991302
throw;
13001303
}
13011304
finally

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.Data.Common;
44
using System.Globalization;
5+
using System.Text.RegularExpressions;
56
using MySqlConnector.Utilities;
67

78
namespace MySql.Data.MySqlClient
@@ -122,6 +123,12 @@ public string? CertificateThumbprint
122123
set => MySqlConnectionStringOption.CertificateThumbprint.SetValue(this, value);
123124
}
124125

126+
public string? TlsVersion
127+
{
128+
get => MySqlConnectionStringOption.TlsVersion.GetValue(this);
129+
set => MySqlConnectionStringOption.TlsVersion.SetValue(this, value);
130+
}
131+
125132
// Connection Pooling Options
126133
public bool Pooling
127134
{
@@ -399,6 +406,7 @@ internal abstract class MySqlConnectionStringOption
399406
public static readonly MySqlConnectionStringReferenceOption<string> SslCa;
400407
public static readonly MySqlConnectionStringReferenceOption<string> SslCert;
401408
public static readonly MySqlConnectionStringReferenceOption<string> SslKey;
409+
public static readonly MySqlConnectionStringReferenceOption<string> TlsVersion;
402410

403411
// Connection Pooling Options
404412
public static readonly MySqlConnectionStringValueOption<bool> Pooling;
@@ -530,6 +538,44 @@ static MySqlConnectionStringOption()
530538
keys: new[] { "CertificateThumbprint", "Certificate Thumbprint", "Certificate Thumb Print" },
531539
defaultValue: null));
532540

541+
AddOption(TlsVersion = new MySqlConnectionStringReferenceOption<string>(
542+
keys: new[] { "TlsVersion", "Tls Version", "Tls-Version" },
543+
defaultValue: null,
544+
coerce: value =>
545+
{
546+
if (string.IsNullOrWhiteSpace(value))
547+
return null;
548+
549+
Span<bool> versions = stackalloc bool[4];
550+
foreach (var part in value!.TrimStart('[', '(').TrimEnd(')', ']').Split(','))
551+
{
552+
var match = Regex.Match(part, @"\s*TLS( ?v?(1|1\.?0|1\.?1|1\.?2|1\.?3))?$", RegexOptions.IgnoreCase);
553+
if (!match.Success)
554+
throw new ArgumentException($"Unrecognized TlsVersion protocol version '{part}'; permitted versions are: TLS 1.0, TLS 1.1, TLS 1.2, TLS 1.3.");
555+
var version = match.Groups[2].Value;
556+
if (version == "" || version == "1" || version == "10" || version == "1.0")
557+
versions[0] = true;
558+
else if (version == "11" || version == "1.1")
559+
versions[1] = true;
560+
else if (version == "12" || version == "1.2")
561+
versions[2] = true;
562+
else if (version == "13" || version == "1.3")
563+
versions[3] = true;
564+
}
565+
566+
var coercedValue = "";
567+
for (var i = 0; i < versions.Length; i++)
568+
{
569+
if (versions[i])
570+
{
571+
if (coercedValue.Length != 0)
572+
coercedValue += ", ";
573+
coercedValue += "TLS 1.{0}".FormatInvariant(i);
574+
}
575+
}
576+
return coercedValue;
577+
}));
578+
533579
// Connection Pooling Options
534580
AddOption(Pooling = new MySqlConnectionStringValueOption<bool>(
535581
keys: new[] { "Pooling" },

tests/MySqlConnector.Tests/MySqlConnectionStringBuilderTests.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Linq;
23
using MySql.Data.MySqlClient;
34
using Xunit;
45

@@ -66,6 +67,7 @@ public void Defaults()
6667
Assert.Null(csb.SslCert);
6768
Assert.Null(csb.SslKey);
6869
Assert.Equal(MySqlSslMode.Preferred, csb.SslMode);
70+
Assert.Null(csb.TlsVersion);
6971
Assert.True(csb.TreatTinyAsBoolean);
7072
Assert.False(csb.UseCompression);
7173
Assert.Equal("", csb.UserID);
@@ -355,5 +357,51 @@ public void SetServerSPNToNull()
355357
Assert.Equal("", csb.ConnectionString);
356358
}
357359
#endif
360+
361+
[Theory]
362+
[InlineData("Tls", "0")]
363+
[InlineData("Tls1", "0")]
364+
[InlineData("Tlsv1", "0")]
365+
[InlineData("Tlsv1.0", "0")]
366+
[InlineData("TLS 1.0", "0")]
367+
[InlineData("TLS v1.0", "0")]
368+
[InlineData("Tls11", "1")]
369+
[InlineData("Tlsv11", "1")]
370+
[InlineData("Tlsv1.1", "1")]
371+
[InlineData("TLS 1.1", "1")]
372+
[InlineData("TLS v1.1", "1")]
373+
[InlineData("Tls12", "2")]
374+
[InlineData("Tlsv12", "2")]
375+
[InlineData("Tlsv1.2", "2")]
376+
[InlineData("TLS 1.2", "2")]
377+
[InlineData("TLS v1.2", "2")]
378+
[InlineData("Tls13", "3")]
379+
[InlineData("Tlsv13", "3")]
380+
[InlineData("Tlsv1.3", "3")]
381+
[InlineData("TLS 1.3", "3")]
382+
[InlineData("TLS v1.3", "3")]
383+
[InlineData("Tls,Tls", "0")]
384+
[InlineData("Tls1.1,Tls v1.1, TLS 1.1", "1")]
385+
[InlineData("Tls12,Tls10", "0,2")]
386+
[InlineData("TLS v1.3, TLS12, Tls 1.1", "1,2,3")]
387+
public void ParseTlsVersion(string input, string expected)
388+
{
389+
var csb = new MySqlConnectionStringBuilder { TlsVersion = input };
390+
#if !BASELINE
391+
string[] normalizedVersions = new[] { "TLS 1.0", "TLS 1.1", "TLS 1.2", "TLS 1.3" };
392+
#else
393+
string[] normalizedVersions = new[] { "Tls", "Tls11", "Tls12", "Tls13" };
394+
#endif
395+
var expectedTlsVersion = string.Join(", ", expected.Split(',').Select(int.Parse).Select(x => normalizedVersions[x]));
396+
Assert.Equal(expectedTlsVersion, csb.TlsVersion);
397+
}
398+
399+
[Fact]
400+
public void ParseInvalidTlsVersion()
401+
{
402+
var csb = new MySqlConnectionStringBuilder();
403+
Assert.Throws<ArgumentException>(() => csb.TlsVersion = "Tls14");
404+
Assert.Throws<ArgumentException>(() => new MySqlConnectionStringBuilder("TlsVersion=Tls14"));
405+
}
358406
}
359407
}

tests/MySqlConnector.Tests/MySqlConnector.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
</ItemGroup>
3333

3434
<ItemGroup Condition=" '$(Configuration)' == 'Baseline' ">
35-
<PackageReference Include="MySql.Data" Version="8.0.16" />
35+
<PackageReference Include="MySql.Data" Version="8.0.19" />
3636
<Compile Remove="ByteBufferWriterTests.cs;CachedProcedureTests.cs;ConnectionTests.cs;FakeMySqlServer.cs;FakeMySqlServerConnection.cs;LoadBalancerTests.cs;MySqlExceptionTests.cs;NormalizeTests.cs;ServerVersionTests.cs;StatementPreparerTests.cs;TypeMapperTests.cs;UtilityTests.cs" />
3737
</ItemGroup>
3838

tests/SideBySide/SslTests.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.IO;
23
using System.Runtime.InteropServices;
34
using System.Security.Authentication;
@@ -196,6 +197,47 @@ public async Task ConnectSslTlsVersion()
196197
Assert.Equal(expectedProtocolString, reader.GetString(1));
197198
}
198199

200+
[SkippableFact(ConfigSettings.RequiresSsl)]
201+
public async Task ForceTls11()
202+
{
203+
// require TLS 1.1 and TLS 1.2
204+
if (!AppConfig.SupportedFeatures.HasFlag(ServerFeatures.Tls11) || !AppConfig.SupportedFeatures.HasFlag(ServerFeatures.Tls12))
205+
return;
206+
207+
var csb = AppConfig.CreateConnectionStringBuilder();
208+
csb.TlsVersion = "TLS 1.1";
209+
210+
using var connection = new MySqlConnection(csb.ConnectionString);
211+
await connection.OpenAsync();
212+
213+
#if !BASELINE
214+
Assert.Equal(SslProtocols.Tls11, connection.SslProtocol);
215+
#endif
216+
using var cmd = new MySqlCommand("show status like 'Ssl_version';", connection);
217+
using var reader = await cmd.ExecuteReaderAsync();
218+
Assert.True(reader.Read());
219+
Assert.Equal("TLSv1.1", reader.GetString(1));
220+
}
221+
222+
#if NETCOREAPP3_0
223+
[SkippableFact(ConfigSettings.RequiresSsl)]
224+
public async Task RequireTls13()
225+
{
226+
if (AppConfig.SupportedFeatures.HasFlag(ServerFeatures.Tls13))
227+
return;
228+
229+
var csb = AppConfig.CreateConnectionStringBuilder();
230+
csb.TlsVersion = "TLS 1.3";
231+
232+
using var connection = new MySqlConnection(csb.ConnectionString);
233+
#if !BASELINE
234+
await Assert.ThrowsAsync<MySqlException>(async () => await connection.OpenAsync());
235+
#else
236+
await Assert.ThrowsAsync<Win32Exception>(async () => await connection.OpenAsync());
237+
#endif
238+
}
239+
#endif
240+
199241
readonly DatabaseFixture m_database;
200242
}
201243
}

0 commit comments

Comments
 (0)