Skip to content

Commit e09c61f

Browse files
hajekjbgrainger
authored andcommitted
Add support for reading certificates from the Certificate Store (#537)
Fixes #536. Signed-off-by: Jan Hajek <[email protected]>
1 parent 0eb9ddb commit e09c61f

File tree

7 files changed

+145
-1
lines changed

7 files changed

+145
-1
lines changed

docs/content/connection-options.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,16 @@ These are the options that need to be used in order to configure a connection to
105105
<td></td>
106106
<td>This option specifies the path to a CA certificate file in a PEM Encoded (.pem) format. This should be used in with <code>SslMode=VerifyCA</code> or <code>SslMode=VerifyFull</code> to enable verification of a CA certificate that is not trusted by the Operating System's certificate store.</td>
107107
</tr>
108+
<tr>
109+
<td>Certificate Store Location, CertificateStoreLocation</td>
110+
<td>None</td>
111+
<td>Specifies whether the connection should be encrypted with a certificate from the Certificate Store on the machine, you can specify either <code>CurrentUser</code> or <code>LocalMachine</code> as valid locations, defaults to None which results in the certificate store not being used.</td>
112+
</tr>
113+
<tr>
114+
<td>Certificate Thumbprint, CertificateThumbprint</td>
115+
<td></td>
116+
<td>Specifies which specific certificate should be used from the Certificate Store specified in the setting above. If none is specified and Certificate Store is chosen, all certificates from the store will be used to attempt to make the connection.</td>
117+
</tr>
108118
</table>
109119

110120
Connection Pooling Options

src/MySqlConnector/Core/ConnectionSettings.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ public ConnectionSettings(MySqlConnectionStringBuilder csb)
4747
CertificateFile = csb.CertificateFile;
4848
CertificatePassword = csb.CertificatePassword;
4949
CACertificateFile = csb.CACertificateFile;
50+
CertificateStoreLocation = csb.CertificateStoreLocation;
51+
CertificateThumbprint = csb.CertificateThumbprint;
5052

5153
// Connection Pooling Options
5254
Pooling = csb.Pooling;
@@ -124,6 +126,8 @@ private static MySqlGuidFormat GetEffectiveGuidFormat(MySqlGuidFormat guidFormat
124126
public string CertificateFile { get; }
125127
public string CertificatePassword { get; }
126128
public string CACertificateFile { get; }
129+
public MySqlCertificateStoreLocation CertificateStoreLocation { get; }
130+
public string CertificateThumbprint { get; }
127131

128132
// Connection Pooling Options
129133
public bool Pooling { get; }

src/MySqlConnector/Core/ServerSession.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,47 @@ private async Task InitSslAsync(ProtocolCapabilities serverCapabilities, Connect
876876
{
877877
Log.Info("Session{0} initializing TLS connection", m_logArguments);
878878
X509CertificateCollection clientCertificates = null;
879+
880+
if (cs.CertificateStoreLocation != MySqlCertificateStoreLocation.None)
881+
{
882+
try
883+
{
884+
var storeLocation = (cs.CertificateStoreLocation == MySqlCertificateStoreLocation.CurrentUser) ? StoreLocation.CurrentUser : StoreLocation.LocalMachine;
885+
var store = new X509Store(StoreName.My, storeLocation);
886+
store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
887+
888+
if (cs.CertificateThumbprint == null)
889+
{
890+
if (store.Certificates.Count == 0)
891+
{
892+
Log.Error("Session{0} no certificates were found in the certificate store", m_logArguments);
893+
throw new MySqlException("No certificates were found in the certifcate store");
894+
}
895+
896+
clientCertificates = new X509CertificateCollection(store.Certificates);
897+
}
898+
else
899+
{
900+
var requireValid = (cs.SslMode == MySqlSslMode.VerifyCA || cs.SslMode == MySqlSslMode.VerifyFull) ? true : false;
901+
var foundCertificates = store.Certificates.Find(X509FindType.FindByThumbprint, cs.CertificateThumbprint, requireValid);
902+
if (foundCertificates.Count == 0)
903+
{
904+
m_logArguments[1] = cs.CertificateThumbprint;
905+
Log.Error("Session{0} certificate with specified thumbprint not found in store '{1}'", m_logArguments);
906+
throw new MySqlException("Certificate with Thumbprint " + cs.CertificateThumbprint + " not found");
907+
}
908+
909+
clientCertificates = new X509CertificateCollection(foundCertificates);
910+
}
911+
}
912+
catch (CryptographicException ex)
913+
{
914+
m_logArguments[1] = cs.CertificateStoreLocation;
915+
Log.Error(ex, "Session{0} couldn't load certificate from CertificateStoreLocation '{1}'", m_logArguments);
916+
throw new MySqlException("Certificate couldn't be loaded from the CertificateStoreLocation", ex);
917+
}
918+
}
919+
879920
if (cs.CertificateFile != null)
880921
{
881922
try
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
7+
namespace MySql.Data.MySqlClient
8+
{
9+
public enum MySqlCertificateStoreLocation
10+
{
11+
/// <summary>
12+
/// Do not use certificate store
13+
/// </summary>
14+
None,
15+
/// <summary>
16+
/// Use certificate store for the current user
17+
/// </summary>
18+
CurrentUser,
19+
/// <summary>
20+
/// User certificate store for the machine
21+
/// </summary>
22+
LocalMachine
23+
}
24+
}

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

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,18 @@ public string CACertificateFile
9191
set => MySqlConnectionStringOption.CACertificateFile.SetValue(this, value);
9292
}
9393

94+
public MySqlCertificateStoreLocation CertificateStoreLocation
95+
{
96+
get => MySqlConnectionStringOption.CertificateStoreLocation.GetValue(this);
97+
set => MySqlConnectionStringOption.CertificateStoreLocation.SetValue(this, value);
98+
}
99+
100+
public string CertificateThumbprint
101+
{
102+
get => MySqlConnectionStringOption.CertificateThumbprint.GetValue(this);
103+
set => MySqlConnectionStringOption.CertificateThumbprint.SetValue(this, value);
104+
}
105+
94106
// Connection Pooling Options
95107
public bool Pooling
96108
{
@@ -320,6 +332,8 @@ internal abstract class MySqlConnectionStringOption
320332
public static readonly MySqlConnectionStringOption<MySqlSslMode> SslMode;
321333
public static readonly MySqlConnectionStringOption<string> CertificateFile;
322334
public static readonly MySqlConnectionStringOption<string> CertificatePassword;
335+
public static readonly MySqlConnectionStringOption<MySqlCertificateStoreLocation> CertificateStoreLocation;
336+
public static readonly MySqlConnectionStringOption<string> CertificateThumbprint;
323337
public static readonly MySqlConnectionStringOption<string> CACertificateFile;
324338

325339
// Connection Pooling Options
@@ -430,6 +444,14 @@ static MySqlConnectionStringOption()
430444
keys: new[] { "CACertificateFile", "CA Certificate File" },
431445
defaultValue: null));
432446

447+
AddOption(CertificateStoreLocation = new MySqlConnectionStringOption<MySqlCertificateStoreLocation>(
448+
keys: new[] { "CertificateStoreLocation", "Certificate Store Location" },
449+
defaultValue: MySqlCertificateStoreLocation.None));
450+
451+
AddOption(CertificateThumbprint = new MySqlConnectionStringOption<string>(
452+
keys: new[] { "CertificateThumbprint", "Certificate Thumbprint", "Certificate Thumb Print" },
453+
defaultValue: null));
454+
433455
// Connection Pooling Options
434456
AddOption(Pooling = new MySqlConnectionStringOption<bool>(
435457
keys: new[] { "Pooling" },
@@ -582,7 +604,7 @@ private static T ChangeType(object objectValue)
582604
return (T) (object) false;
583605
}
584606

585-
if ((typeof(T) == typeof(MySqlLoadBalance) || typeof(T) == typeof(MySqlSslMode) || typeof(T) == typeof(MySqlDateTimeKind) || typeof(T) == typeof(MySqlGuidFormat) || typeof(T) == typeof(MySqlConnectionProtocol)) && objectValue is string enumString)
607+
if ((typeof(T) == typeof(MySqlLoadBalance) || typeof(T) == typeof(MySqlSslMode) || typeof(T) == typeof(MySqlDateTimeKind) || typeof(T) == typeof(MySqlGuidFormat) || typeof(T) == typeof(MySqlConnectionProtocol) || typeof(T) == typeof(MySqlCertificateStoreLocation)) && objectValue is string enumString)
586608
{
587609
try
588610
{

tests/MySqlConnector.Tests/MySqlConnectionStringBuilderTests.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ public void Defaults()
1616
Assert.True(csb.AutoEnlist);
1717
Assert.Null(csb.CertificateFile);
1818
Assert.Null(csb.CertificatePassword);
19+
Assert.Equal(MySqlCertificateStoreLocation.None, csb.CertificateStoreLocation);
20+
Assert.Null(csb.CertificateThumbprint);
1921
Assert.Equal("", csb.CharacterSet);
2022
Assert.Equal(0u, csb.ConnectionLifeTime);
2123
Assert.Equal(MySqlConnectionProtocol.Sockets, csb.ConnectionProtocol);
@@ -85,6 +87,8 @@ public void ParseConnectionString()
8587
"auto enlist=False;" +
8688
"certificate file=file.pfx;" +
8789
"certificate password=Pass1234;" +
90+
"certificate store location=CurrentUser;" +
91+
"certificate thumb print=thumbprint123;" +
8892
"Character Set=latin1;" +
8993
"Compress=true;" +
9094
"connect timeout=30;" +
@@ -128,6 +132,8 @@ public void ParseConnectionString()
128132
Assert.False(csb.AutoEnlist);
129133
Assert.Equal("file.pfx", csb.CertificateFile);
130134
Assert.Equal("Pass1234", csb.CertificatePassword);
135+
Assert.Equal(MySqlCertificateStoreLocation.CurrentUser, csb.CertificateStoreLocation);
136+
Assert.Equal("thumbprint123", csb.CertificateThumbprint);
131137
Assert.Equal("latin1", csb.CharacterSet);
132138
Assert.Equal(15u, csb.ConnectionLifeTime);
133139
Assert.Equal(MySqlConnectionProtocol.NamedPipe, csb.ConnectionProtocol);

tests/SideBySide/SslTests.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.IO;
22
using System.Security.Authentication;
3+
using System.Security.Cryptography.X509Certificates;
34
using System.Threading.Tasks;
45
using MySql.Data.MySqlClient;
56
using Xunit;
@@ -75,6 +76,42 @@ public async Task ConnectSslClientCertificate(string certFile, string certFilePa
7576
}
7677
}
7778

79+
[SkippableTheory(ConfigSettings.RequiresSsl | ConfigSettings.KnownClientCertificate)]
80+
[InlineData("ssl-client.pfx", MySqlCertificateStoreLocation.CurrentUser, null)]
81+
public async Task ConnectSslClientCertificateFromCertificateStore(string certFile, MySqlCertificateStoreLocation storeLocation, string thumbprint)
82+
{
83+
// Create a mock of certificate store
84+
X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
85+
store.Open(OpenFlags.ReadWrite);
86+
var certificate = new X509Certificate2(Path.Combine(AppConfig.CertsPath, certFile));
87+
store.Add(certificate);
88+
89+
var csb = AppConfig.CreateConnectionStringBuilder();
90+
91+
csb.CertificateStoreLocation = storeLocation;
92+
csb.CertificateThumbprint = thumbprint;
93+
94+
using (var connection = new MySqlConnection(csb.ConnectionString))
95+
{
96+
using (var cmd = connection.CreateCommand())
97+
{
98+
await connection.OpenAsync();
99+
#if !BASELINE
100+
Assert.True(connection.SslIsEncrypted);
101+
Assert.True(connection.SslIsSigned);
102+
Assert.True(connection.SslIsAuthenticated);
103+
Assert.True(connection.SslIsMutuallyAuthenticated);
104+
#endif
105+
cmd.CommandText = "SHOW SESSION STATUS LIKE 'Ssl_version'";
106+
var sslVersion = (string) await cmd.ExecuteScalarAsync();
107+
Assert.False(string.IsNullOrWhiteSpace(sslVersion));
108+
}
109+
}
110+
111+
// Remove the certificate from store
112+
store.Remove(certificate);
113+
}
114+
78115
[SkippableFact(ConfigSettings.RequiresSsl, Baseline = "MySql.Data does not check for a private key")]
79116
public async Task ConnectSslClientCertificateNoPrivateKey()
80117
{

0 commit comments

Comments
 (0)