Skip to content

Commit 0441595

Browse files
authored
Update | Update SSL certificate error messages (#2060)
1 parent c17240d commit 0441595

File tree

8 files changed

+626
-20
lines changed

8 files changed

+626
-20
lines changed

src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNICommon.cs

Lines changed: 102 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
using System.Net;
88
using System.Net.Security;
99
using System.Security.Cryptography.X509Certificates;
10+
using System.Text;
11+
using Microsoft.Data.Common;
1012
using System.Threading;
1113
using System.Threading.Tasks;
1214
using Microsoft.Data.ProviderBase;
@@ -150,31 +152,72 @@ internal static bool ValidateSslServerCertificate(string targetServerName, X509C
150152
return true;
151153
}
152154

153-
if ((policyErrors & SslPolicyErrors.RemoteCertificateNameMismatch) != 0)
155+
// If we get to this point then there is a ssl policy flag.
156+
StringBuilder messageBuilder = new();
157+
if (policyErrors.HasFlag(SslPolicyErrors.RemoteCertificateChainErrors))
154158
{
159+
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNICommon), EventType.ERR, "targetServerName {0}, SslPolicyError {1}, SSL Policy certificate chain has errors.", args0: targetServerName, args1: policyErrors);
160+
161+
// get the chain status from the certificate
162+
X509Certificate2 cert2 = cert as X509Certificate2;
163+
X509Chain chain = new();
164+
chain.ChainPolicy.RevocationMode = X509RevocationMode.Offline;
165+
StringBuilder chainStatusInformation = new();
166+
bool chainIsValid = chain.Build(cert2);
167+
Debug.Assert(!chainIsValid, "RemoteCertificateChainError flag is detected, but certificate chain is valid.");
168+
if (!chainIsValid)
169+
{
170+
foreach (X509ChainStatus chainStatus in chain.ChainStatus)
171+
{
172+
chainStatusInformation.Append($"{chainStatus.StatusInformation}, [Status: {chainStatus.Status}]");
173+
chainStatusInformation.AppendLine();
174+
}
175+
}
176+
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNICommon), EventType.ERR, "targetServerName {0}, SslPolicyError {1}, SSL Policy certificate chain has errors. ChainStatus {2}", args0: targetServerName, args1: policyErrors, args2: chainStatusInformation);
177+
messageBuilder.AppendFormat(Strings.SQL_RemoteCertificateChainErrors, chainStatusInformation);
178+
messageBuilder.AppendLine();
179+
}
180+
181+
if (policyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNotAvailable))
182+
{
183+
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNICommon), EventType.ERR, "targetServerName {0}, SSL Policy invalidated certificate.", args0: targetServerName);
184+
messageBuilder.AppendLine(Strings.SQL_RemoteCertificateNotAvailable);
185+
}
186+
187+
if (policyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch))
188+
{
189+
#if NET7_0_OR_GREATER
190+
X509Certificate2 cert2 = cert as X509Certificate2;
191+
if (!cert2.MatchesHostname(targetServerName))
192+
{
193+
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNICommon), EventType.ERR, "targetServerName {0}, Target Server name or HNIC does not match the Subject/SAN in Certificate.", args0: targetServerName);
194+
messageBuilder.AppendLine(Strings.SQL_RemoteCertificateNameMismatch);
195+
}
196+
#else
197+
// To Do: include certificate SAN (Subject Alternative Name) check.
155198
string certServerName = cert.Subject.Substring(cert.Subject.IndexOf('=') + 1);
156199

157200
// Verify that target server name matches subject in the certificate
158201
if (targetServerName.Length > certServerName.Length)
159202
{
160203
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNICommon), EventType.ERR, "targetServerName {0}, Target Server name is of greater length than Subject in Certificate.", args0: targetServerName);
161-
return false;
204+
messageBuilder.AppendLine(Strings.SQL_RemoteCertificateNameMismatch);
162205
}
163206
else if (targetServerName.Length == certServerName.Length)
164207
{
165208
// Both strings have the same length, so targetServerName must be a FQDN
166209
if (!targetServerName.Equals(certServerName, StringComparison.OrdinalIgnoreCase))
167210
{
168211
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNICommon), EventType.ERR, "targetServerName {0}, Target Server name does not match Subject in Certificate.", args0: targetServerName);
169-
return false;
212+
messageBuilder.AppendLine(Strings.SQL_RemoteCertificateNameMismatch);
170213
}
171214
}
172215
else
173216
{
174217
if (string.Compare(targetServerName, 0, certServerName, 0, targetServerName.Length, StringComparison.OrdinalIgnoreCase) != 0)
175218
{
176219
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNICommon), EventType.ERR, "targetServerName {0}, Target Server name does not match Subject in Certificate.", args0: targetServerName);
177-
return false;
220+
messageBuilder.AppendLine(Strings.SQL_RemoteCertificateNameMismatch);
178221
}
179222

180223
// Server name matches cert name for its whole length, so ensure that the
@@ -184,17 +227,18 @@ internal static bool ValidateSslServerCertificate(string targetServerName, X509C
184227
if (certServerName[targetServerName.Length] != '.')
185228
{
186229
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNICommon), EventType.ERR, "targetServerName {0}, Target Server name does not match Subject in Certificate.", args0: targetServerName);
187-
return false;
230+
messageBuilder.AppendLine(Strings.SQL_RemoteCertificateNameMismatch);
188231
}
189232
}
233+
#endif
190234
}
191-
else
235+
236+
if (messageBuilder.Length > 0)
192237
{
193-
// Fail all other SslPolicy cases besides RemoteCertificateNameMismatch
194-
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNICommon), EventType.ERR, "targetServerName {0}, SslPolicyError {1}, SSL Policy invalidated certificate.", args0: targetServerName, args1: policyErrors);
195-
return false;
238+
throw ADP.SSLCertificateAuthenticationException(messageBuilder.ToString());
196239
}
197-
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNICommon), EventType.INFO, "targetServerName {0}, Client certificate validated successfully.", args0: targetServerName);
240+
241+
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNICommon), EventType.INFO, " Remote certificate with subject: {0}, validated successfully.", args0: cert.Subject);
198242
return true;
199243
}
200244
}
@@ -218,26 +262,67 @@ internal static bool ValidateSslServerCertificate(X509Certificate clientCert, X5
218262
return true;
219263
}
220264

221-
if ((policyErrors & SslPolicyErrors.RemoteCertificateNameMismatch) != 0)
265+
StringBuilder messageBuilder = new();
266+
if (policyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNotAvailable))
222267
{
268+
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNICommon), EventType.ERR, "serverCert {0}, SSL Server certificate not validated as PolicyErrors set to RemoteCertificateNotAvailable.", args0: clientCert.Subject);
269+
messageBuilder.AppendLine(Strings.SQL_RemoteCertificateNotAvailable);
270+
}
271+
272+
if (policyErrors.HasFlag(SslPolicyErrors.RemoteCertificateChainErrors))
273+
{
274+
// get the chain status from the server certificate
275+
X509Certificate2 cert2 = serverCert as X509Certificate2;
276+
X509Chain chain = new();
277+
chain.ChainPolicy.RevocationMode = X509RevocationMode.Offline;
278+
StringBuilder chainStatusInformation = new();
279+
bool chainIsValid = chain.Build(cert2);
280+
Debug.Assert(!chainIsValid, "RemoteCertificateChainError flag is detected, but certificate chain is valid.");
281+
if (!chainIsValid)
282+
{
283+
foreach (X509ChainStatus chainStatus in chain.ChainStatus)
284+
{
285+
chainStatusInformation.Append($"{chainStatus.StatusInformation}, [Status: {chainStatus.Status}]");
286+
chainStatusInformation.AppendLine();
287+
}
288+
}
289+
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNICommon), EventType.ERR, "certificate subject from server is {0}, and does not match with the certificate provided client.", args0: cert2.SubjectName.Name);
290+
messageBuilder.AppendFormat(Strings.SQL_RemoteCertificateChainErrors, chainStatusInformation);
291+
messageBuilder.AppendLine();
292+
}
293+
294+
if (policyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch))
295+
{
296+
#if NET7_0_OR_GREATER
297+
X509Certificate2 s_cert = serverCert as X509Certificate2;
298+
X509Certificate2 c_cert = clientCert as X509Certificate2;
299+
300+
if (!s_cert.MatchesHostname(c_cert.SubjectName.Name))
301+
{
302+
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNICommon), EventType.ERR, "certificate from server does not match with the certificate provided client.", args0: s_cert.Subject);
303+
messageBuilder.AppendLine(Strings.SQL_RemoteCertificateNameMismatch);
304+
}
305+
#else
223306
// Verify that subject name matches
224307
if (serverCert.Subject != clientCert.Subject)
225308
{
226309
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNICommon), EventType.ERR, "certificate subject from server is {0}, and does not match with the certificate provided client.", args0: serverCert.Subject);
227-
return false;
310+
messageBuilder.AppendLine(Strings.SQL_RemoteCertificateNameMismatch);
228311
}
312+
229313
if (!serverCert.Equals(clientCert))
230314
{
231315
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNICommon), EventType.ERR, "certificate from server does not match with the certificate provided client.", args0: serverCert.Subject);
232-
return false;
316+
messageBuilder.AppendLine(Strings.SQL_RemoteCertificateNameMismatch);
233317
}
318+
#endif
234319
}
235-
else
320+
321+
if (messageBuilder.Length > 0)
236322
{
237-
// Fail all other SslPolicy cases besides RemoteCertificateNameMismatch
238-
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNICommon), EventType.ERR, "certificate subject: {0}, SslPolicyError {1}, SSL Policy invalidated certificate.", args0: clientCert.Subject, args1: policyErrors);
239-
return false;
323+
throw ADP.SSLCertificateAuthenticationException(messageBuilder.ToString());
240324
}
325+
241326
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNICommon), EventType.INFO, "certificate subject {0}, Client certificate validated successfully.", args0: clientCert.Subject);
242327
return true;
243328
}

src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
using IsolationLevel = System.Data.IsolationLevel;
2323
using Microsoft.Identity.Client;
2424
using Microsoft.SqlServer.Server;
25+
using System.Security.Authentication;
2526

2627
#if NETFRAMEWORK
2728
using Microsoft.Win32;
@@ -326,9 +327,16 @@ internal static ArgumentOutOfRangeException ArgumentOutOfRange(string message, s
326327
TraceExceptionAsReturnValue(e);
327328
return e;
328329
}
329-
#endregion
330330

331-
#region Helper Functions
331+
internal static AuthenticationException SSLCertificateAuthenticationException(string message)
332+
{
333+
AuthenticationException e = new(message);
334+
TraceExceptionAsReturnValue(e);
335+
return e;
336+
}
337+
#endregion
338+
339+
#region Helper Functions
332340
internal static ArgumentOutOfRangeException NotSupportedEnumerationValue(Type type, string value, string method)
333341
=> ArgumentOutOfRange(StringsHelper.GetString(Strings.ADP_NotSupportedEnumerationValue, type.Name, value, method), type.Name);
334342

src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Microsoft.Data.SqlClient/src/Resources/Strings.resx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4731,4 +4731,13 @@
47314731
<data name="SqlParameter_SourceColumnNullMapping" xml:space="preserve">
47324732
<value>When used by DataAdapter.Update, the parameter value is changed from DBNull.Value into (Int32)1 or (Int32)0 if non-null.</value>
47334733
</data>
4734+
<data name="SQL_RemoteCertificateChainErrors" xml:space="preserve">
4735+
<value>Certificate failed chain validation. Error(s): '{0}'.</value>
4736+
</data>
4737+
<data name="SQL_RemoteCertificateNameMismatch" xml:space="preserve">
4738+
<value>Certificate name mismatch. The provided 'DataSource' or 'HostNameInCertificate' does not match the name in the certificate.</value>
4739+
</data>
4740+
<data name="SQL_RemoteCertificateNotAvailable" xml:space="preserve">
4741+
<value>Certificate not available while validating the certificate.</value>
4742+
</data>
47344743
</root>

src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
using Xunit;
2121
using System.Net.NetworkInformation;
2222
using System.Text;
23+
using System.Security.Principal;
2324

2425
namespace Microsoft.Data.SqlClient.ManualTesting.Tests
2526
{
@@ -85,6 +86,35 @@ public static class DataTestUtility
8586
public static readonly string KerberosDomainUser = null;
8687
internal static readonly string KerberosDomainPassword = null;
8788

89+
// SQL server Version
90+
private static string s_sQLServerVersion = string.Empty;
91+
private static bool s_isTDS8Supported;
92+
93+
public static string SQLServerVersion
94+
{
95+
get
96+
{
97+
if (!string.IsNullOrEmpty(TCPConnectionString))
98+
{
99+
s_sQLServerVersion ??= GetSqlServerVersion(TCPConnectionString);
100+
}
101+
return s_sQLServerVersion;
102+
}
103+
}
104+
105+
// Is TDS8 supported
106+
public static bool IsTDS8Supported
107+
{
108+
get
109+
{
110+
if (!string.IsNullOrEmpty(TCPConnectionString))
111+
{
112+
s_isTDS8Supported = GetSQLServerStatusOnTDS8(TCPConnectionString);
113+
}
114+
return s_isTDS8Supported;
115+
}
116+
}
117+
88118
static DataTestUtility()
89119
{
90120
Config c = Config.Load();
@@ -237,6 +267,41 @@ private static Task<string> AcquireTokenAsync(string authorityURL, string userID
237267

238268
public static bool IsKerberosTest => !string.IsNullOrEmpty(KerberosDomainUser) && !string.IsNullOrEmpty(KerberosDomainPassword);
239269

270+
public static string GetSqlServerVersion(string connectionString)
271+
{
272+
string version = string.Empty;
273+
using SqlConnection conn = new(connectionString);
274+
conn.Open();
275+
SqlCommand command = conn.CreateCommand();
276+
command.CommandText = "SELECT SERVERProperty('ProductMajorVersion')";
277+
SqlDataReader reader = command.ExecuteReader();
278+
if (reader.Read())
279+
{
280+
version = reader.GetString(0);
281+
}
282+
return version;
283+
}
284+
285+
public static bool GetSQLServerStatusOnTDS8(string connectionString)
286+
{
287+
bool isTDS8Supported = false;
288+
SqlConnectionStringBuilder builder = new(connectionString)
289+
{
290+
[nameof(SqlConnectionStringBuilder.Encrypt)] = SqlConnectionEncryptOption.Strict
291+
};
292+
try
293+
{
294+
SqlConnection conn = new(builder.ConnectionString);
295+
conn.Open();
296+
isTDS8Supported = true;
297+
}
298+
catch (SqlException)
299+
{
300+
301+
}
302+
return isTDS8Supported;
303+
}
304+
240305
public static bool IsDatabasePresent(string name)
241306
{
242307
AvailableDatabases = AvailableDatabases ?? new Dictionary<string, bool>();
@@ -257,6 +322,17 @@ public static bool IsDatabasePresent(string name)
257322
return present;
258323
}
259324

325+
public static bool IsAdmin
326+
{
327+
get
328+
{
329+
#if NET6_0_OR_GREATER
330+
System.Diagnostics.Debug.Assert(OperatingSystem.IsWindows());
331+
#endif
332+
return new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
333+
}
334+
}
335+
260336
/// <summary>
261337
/// Checks if object SYS.SENSITIVITY_CLASSIFICATIONS exists in SQL Server
262338
/// </summary>
@@ -302,6 +378,12 @@ public static bool AreConnStringsSetup()
302378
return !string.IsNullOrEmpty(NPConnectionString) && !string.IsNullOrEmpty(TCPConnectionString);
303379
}
304380

381+
public static bool IsSQL2022() => string.Equals("16", SQLServerVersion.Trim());
382+
383+
public static bool IsSQL2019() => string.Equals("15", SQLServerVersion.Trim());
384+
385+
public static bool IsSQL2016() => string.Equals("14", s_sQLServerVersion.Trim());
386+
305387
public static bool IsSQLAliasSetup()
306388
{
307389
return !string.IsNullOrEmpty(AliasName);
@@ -885,7 +967,7 @@ public static bool ParseDataSource(string dataSource, out string hostname, out i
885967

886968
if (dataSource.Contains(","))
887969
{
888-
if (!Int32.TryParse(dataSource.Substring(dataSource.LastIndexOf(",",StringComparison.Ordinal) + 1), out port))
970+
if (!Int32.TryParse(dataSource.Substring(dataSource.LastIndexOf(",", StringComparison.Ordinal) + 1), out port))
889971
{
890972
return false;
891973
}

0 commit comments

Comments
 (0)