Skip to content

Commit f0f36f2

Browse files
authored
TDS8 Add Server Certificate Support (#1822)
1 parent 2a54bc5 commit f0f36f2

25 files changed

+369
-85
lines changed

doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,7 @@ End Module
542542
|Failover Partner|N/A|The name of the failover partner server where database mirroring is configured.<br /><br /> If the value of this key is "", then **Initial Catalog** must be present, and its value must not be "".<br /><br /> The server name can be 128 characters or less.<br /><br /> If you specify a failover partner but the failover partner server is not configured for database mirroring and the primary server (specified with the Server keyword) is not available, then the connection will fail.<br /><br /> If you specify a failover partner and the primary server is not configured for database mirroring, the connection to the primary server (specified with the Server keyword) will succeed if the primary server is available.|
543543
|Failover Partner SPN<br /><br /> -or-<br /><br /> FailoverPartnerSPN|N/A|The SPN for the failover partner. The default value is an empty string, which causes SqlClient to use the default, driver-generated SPN.<br /><br /> (Only available in v5.0+)|
544544
|Host Name In Certificate<br /><br /> -or-<br /><br />HostNameInCertificate|N/A|The host name to use when validating the server certificate. When not specified, the server name from the Data Source is used for certificate validation.<br /><br /> (Only available in v5.0+)|
545+
|Server Certificate<br /><br /> -or-<br /><br />ServerCertificate|N/A|The path to a certificate file to match against the SQL Server TLS/SSL certificate. The accepted certificate formats are PEM, DER, and CER. If specified, the SQL Server certificate is checked by verifying if the ServerCertificate provided is an exact match.<br /><br /> (Only available in v5.1+)|
545546
|Initial Catalog<br /><br /> -or-<br /><br /> Database|N/A|The name of the database.<br /><br /> The database name can be 128 characters or less.|
546547
|Integrated Security<br /><br /> -or-<br /><br /> Trusted_Connection|'false'|When `false`, User ID and Password are specified in the connection. When `true`, the current Windows account credentials are used for authentication.<br /><br /> Recognized values are `true`, `false`, `yes`, `no`, and `sspi` (strongly recommended), which is equivalent to `true`.<br /><br /> If User ID and Password are specified and Integrated Security is set to true, the User ID and Password will be ignored and Integrated Security will be used.<br /><br /> <xref:Microsoft.Data.SqlClient.SqlCredential> is a more secure way to specify credentials for a connection that uses SQL Server Authentication (`Integrated Security=false`).|
547548
|IP Address Preference<br /><br /> -or-<br /><br /> IPAddressPreference|IPv4First|The IP address family preference when establishing TCP connections. If `Transparent Network IP Resolution` (in .NET Framework) or `Multi Subnet Failover` is set to true, this setting has no effect. Supported values include:<br /><br /> `IPAddressPreference=IPv4First`<br /><br />`IPAddressPreference=IPv6First`<br /><br />`IPAddressPreference=UsePlatformDefault`|

doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,25 @@ This property corresponds to the "FailoverPartnerSPN" and "Failover Partner SPN"
481481
</format>
482482
</remarks>
483483
</FailoverPartnerSPN>
484+
<HostNameInCertificate>
485+
<summary>Gets or sets the host name to use when validating the server certificate for the connection. When not specified, the server name from the `Data Source` is used for certificate validation. (Only available in v5.0+)</summary>
486+
<value>
487+
The value of the <see cref="P:Microsoft.Data.SqlClient.SqlConnectionStringBuilder.HostNameInCertificate" /> property, or <see langword="String.Empty" /> if none has been supplied.
488+
</value>
489+
<remarks>
490+
<format type="text/markdown">
491+
<![CDATA[
492+
493+
## Remarks
494+
This property corresponds to the "HostNameInCertificiate" and "Host Name in Certificiate" keys within the connection string.
495+
496+
> [!NOTE]
497+
> This property only applies when using `Encrypt` in <xref:Microsoft.Data.SqlClient.SqlConnectionEncryptOption.Mandatory%2A> or <xref:Microsoft.Data.SqlClient.SqlConnectionEncryptOption.Strict%2A> mode, otherwise it is ignored.
498+
499+
]]>
500+
</format>
501+
</remarks>
502+
</HostNameInCertificate>
484503
<GetProperties>
485504
<param name="propertyDescriptors">To be added.</param>
486505
<summary>To be added.</summary>
@@ -787,7 +806,7 @@ Connections are considered the same if they have the same connection string. Dif
787806
|Context Connection(Obsolete)|False|
788807
|Current Language|Empty string|
789808
|Data Source|Empty string|
790-
|Encrypt|False|
809+
|Encrypt|False in versions prior to 4.0, True in versions 4.0 and up|
791810
|Enlist|True|
792811
|Failover Partner|Empty string|
793812
|Initial Catalog|Empty string|
@@ -842,6 +861,25 @@ Database = AdventureWorks
842861
]]></format>
843862
</remarks>
844863
</Replication>
864+
<ServerCertificate>
865+
<summary>Gets or sets the path to a certificate file to match against the SQL Server TLS/SSL certificate for the connection. The accepted certificate formats are PEM, DER, and CER. If specified, the SQL Server certificate is checked by verifying if the `ServerCertificate` provided is an exact match. (Only available in v5.1+)</summary>
866+
<value>
867+
The value of the <see cref="P:Microsoft.Data.SqlClient.SqlConnectionStringBuilder.ServerCertificate" /> property, or <see langword="String.Empty" /> if none has been supplied.
868+
</value>
869+
<remarks>
870+
<format type="text/markdown">
871+
<![CDATA[
872+
873+
## Remarks
874+
This property corresponds to the "ServerCertificate" and "Server Certificate" keys within the connection string.
875+
876+
> [!NOTE]
877+
> This property only applies when using `Encrypt` in <xref:Microsoft.Data.SqlClient.SqlConnectionEncryptOption.Mandatory%2A> or <xref:Microsoft.Data.SqlClient.SqlConnectionEncryptOption.Strict%2A> mode, otherwise it is ignored.
878+
879+
]]>
880+
</format>
881+
</remarks>
882+
</ServerCertificate>
845883
<ServerSPN>
846884
<summary>Gets or sets the service principal name (SPN) of the data source.</summary>
847885
<value>

src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,6 +1033,11 @@ public SqlConnectionStringBuilder(string connectionString) { }
10331033
[System.ComponentModel.DisplayNameAttribute("Host Name In Certificate")]
10341034
[System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)]
10351035
public string HostNameInCertificate { get { throw null; } set { } }
1036+
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml' path='docs/members[@name="SqlConnectionStringBuilder"]/ServerCertificate/*'/>
1037+
[System.ComponentModel.DisplayNameAttribute("Server Certificate")]
1038+
[System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)]
1039+
public string ServerCertificate { get { throw null; } set { } }
1040+
10361041
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml' path='docs/members[@name="SqlConnectionStringBuilder"]/Enlist/*'/>
10371042
[System.ComponentModel.DisplayNameAttribute("Enlist")]
10381043
[System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)]

src/Microsoft.Data.SqlClient/netcore/src/Interop/SNINativeMethodWrapper.Windows.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ internal struct AuthProviderInfo
5151
public uint flags;
5252
[MarshalAs(UnmanagedType.Bool)]
5353
public bool tlsFirst;
54+
public object certContext;
5455
[MarshalAs(UnmanagedType.LPWStr)]
5556
public string certId;
5657
[MarshalAs(UnmanagedType.Bool)]

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,50 @@ internal static bool ValidateSslServerCertificate(string targetServerName, X509C
194194
return true;
195195
}
196196
}
197+
198+
/// <summary>
199+
/// We validate the provided certificate provided by the client with the one from the server to see if it matches.
200+
/// Certificate validation and chain trust validations are done by SSLStream class [System.Net.Security.SecureChannel.VerifyRemoteCertificate method]
201+
/// This method is called as a result of callback for SSL Stream Certificate validation.
202+
/// </summary>
203+
/// <param name="clientCert">X.509 certificate provided by the client</param>
204+
/// <param name="serverCert">X.509 certificate provided by the server</param>
205+
/// <param name="policyErrors">Policy errors</param>
206+
/// <returns>True if certificate is valid</returns>
207+
internal static bool ValidateSslServerCertificate(X509Certificate clientCert, X509Certificate serverCert, SslPolicyErrors policyErrors)
208+
{
209+
using (TrySNIEventScope.Create("SNICommon.ValidateSslServerCertificate | SNI | SCOPE | INFO | Entering Scope {0} "))
210+
{
211+
if (policyErrors == SslPolicyErrors.None)
212+
{
213+
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNICommon), EventType.INFO, "serverCert {0}, SSL Server certificate not validated as PolicyErrors set to None.", args0: clientCert.Subject);
214+
return true;
215+
}
216+
217+
if ((policyErrors & SslPolicyErrors.RemoteCertificateNameMismatch) != 0)
218+
{
219+
// Verify that subject name matches
220+
if (serverCert.Subject != clientCert.Subject)
221+
{
222+
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);
223+
return false;
224+
}
225+
if (!serverCert.Equals(clientCert))
226+
{
227+
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNICommon), EventType.ERR, "certificate from server does not match with the certificate provided client.", args0: serverCert.Subject);
228+
return false;
229+
}
230+
}
231+
else
232+
{
233+
// Fail all other SslPolicy cases besides RemoteCertificateNameMismatch
234+
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNICommon), EventType.ERR, "certificate subject: {0}, SslPolicyError {1}, SSL Policy invalidated certificate.", args0: clientCert.Subject, args1: policyErrors);
235+
return false;
236+
}
237+
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNICommon), EventType.INFO, "certificate subject {0}, Client certificate validated successfully.", args0: clientCert.Subject);
238+
return true;
239+
}
240+
}
197241

198242
internal static IPAddress[] GetDnsIpAddresses(string serverName)
199243
{

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

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,9 @@ private static bool IsErrorStatus(SecurityStatusPalErrorCode errorCode)
142142
/// <param name="ipPreference">IP address preference</param>
143143
/// <param name="cachedFQDN">Used for DNS Cache</param>
144144
/// <param name="pendingDNSInfo">Used for DNS Cache</param>
145-
/// <param name="tlsFirst"></param>
146-
/// <param name="hostNameInCertificate"></param>
145+
/// <param name="tlsFirst">Support TDS8.0</param>
146+
/// <param name="hostNameInCertificate">Used for the HostName in certificate</param>
147+
/// <param name="serverCertificateFilename">Used for the path to the Server Certificate</param>
147148
/// <returns>SNI handle</returns>
148149
internal static SNIHandle CreateConnectionHandle(
149150
string fullServerName,
@@ -160,7 +161,8 @@ internal static SNIHandle CreateConnectionHandle(
160161
string cachedFQDN,
161162
ref SQLDNSInfo pendingDNSInfo,
162163
bool tlsFirst,
163-
string hostNameInCertificate)
164+
string hostNameInCertificate,
165+
string serverCertificateFilename)
164166
{
165167
instanceName = new byte[1];
166168

@@ -187,7 +189,7 @@ internal static SNIHandle CreateConnectionHandle(
187189
case DataSource.Protocol.None: // default to using tcp if no protocol is provided
188190
case DataSource.Protocol.TCP:
189191
sniHandle = CreateTcpHandle(details, timerExpire, parallel, ipPreference, cachedFQDN, ref pendingDNSInfo,
190-
tlsFirst, hostNameInCertificate);
192+
tlsFirst, hostNameInCertificate, serverCertificateFilename);
191193
break;
192194
case DataSource.Protocol.NP:
193195
sniHandle = CreateNpHandle(details, timerExpire, parallel, tlsFirst);
@@ -284,8 +286,9 @@ private static byte[][] GetSqlServerSPNs(string hostNameOrAddress, string portOr
284286
/// <param name="ipPreference">IP address preference</param>
285287
/// <param name="cachedFQDN">Key for DNS Cache</param>
286288
/// <param name="pendingDNSInfo">Used for DNS Cache</param>
287-
/// <param name="tlsFirst"></param>
288-
/// <param name="hostNameInCertificate"></param>
289+
/// <param name="tlsFirst">Support TDS8.0</param>
290+
/// <param name="hostNameInCertificate">Host name in certificate</param>
291+
/// <param name="serverCertificateFilename">Used for the path to the Server Certificate</param>
289292
/// <returns>SNITCPHandle</returns>
290293
private static SNITCPHandle CreateTcpHandle(
291294
DataSource details,
@@ -295,7 +298,8 @@ private static SNITCPHandle CreateTcpHandle(
295298
string cachedFQDN,
296299
ref SQLDNSInfo pendingDNSInfo,
297300
bool tlsFirst,
298-
string hostNameInCertificate)
301+
string hostNameInCertificate,
302+
string serverCertificateFilename)
299303
{
300304
// TCP Format:
301305
// tcp:<host name>\<instance name>
@@ -334,7 +338,7 @@ private static SNITCPHandle CreateTcpHandle(
334338
}
335339

336340
return new SNITCPHandle(hostName, port, timerExpire, parallel, ipPreference, cachedFQDN, ref pendingDNSInfo,
337-
tlsFirst, hostNameInCertificate);
341+
tlsFirst, hostNameInCertificate, serverCertificateFilename);
338342
}
339343

340344
/// <summary>

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

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ internal sealed class SNITCPHandle : SNIPhysicalHandle
2727
private readonly Socket _socket;
2828
private NetworkStream _tcpStream;
2929
private readonly string _hostNameInCertificate;
30+
private readonly string _serverCertificateFilename;
3031
private readonly bool _tlsFirst;
3132

3233
private Stream _stream;
@@ -121,7 +122,8 @@ public override int ProtocolVersion
121122
/// <param name="cachedFQDN">Key for DNS Cache</param>
122123
/// <param name="pendingDNSInfo">Used for DNS Cache</param>
123124
/// <param name="tlsFirst">Support TDS8.0</param>
124-
/// <param name="hostNameInCertificate">Host Name in Certoficate</param>
125+
/// <param name="hostNameInCertificate">Host Name in Certificate</param>
126+
/// <param name="serverCertificateFilename">Used for the path to the Server Certificate</param>
125127
public SNITCPHandle(
126128
string serverName,
127129
int port,
@@ -131,7 +133,8 @@ public SNITCPHandle(
131133
string cachedFQDN,
132134
ref SQLDNSInfo pendingDNSInfo,
133135
bool tlsFirst,
134-
string hostNameInCertificate)
136+
string hostNameInCertificate,
137+
string serverCertificateFilename)
135138
{
136139
using (TrySNIEventScope.Create(nameof(SNITCPHandle)))
137140
{
@@ -140,6 +143,7 @@ public SNITCPHandle(
140143
_targetServer = serverName;
141144
_tlsFirst = tlsFirst;
142145
_hostNameInCertificate = hostNameInCertificate;
146+
_serverCertificateFilename = serverCertificateFilename;
143147
_sendSync = new object();
144148

145149
SQLDNSInfo cachedDNSInfo;
@@ -649,17 +653,18 @@ public override void DisableSsl()
649653
/// Validate server certificate callback
650654
/// </summary>
651655
/// <param name="sender">Sender object</param>
652-
/// <param name="cert">X.509 certificate</param>
656+
/// <param name="serverCertificate">X.509 certificate provided from the server</param>
653657
/// <param name="chain">X.509 chain</param>
654658
/// <param name="policyErrors">Policy errors</param>
655659
/// <returns>True if certificate is valid</returns>
656-
private bool ValidateServerCertificate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors policyErrors)
660+
private bool ValidateServerCertificate(object sender, X509Certificate serverCertificate, X509Chain chain, SslPolicyErrors policyErrors)
657661
{
658662
if (!_validateCert)
659663
{
660664
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNITCPHandle), EventType.INFO, "Connection Id {0}, Certificate will not be validated.", args0: _connectionId);
661665
return true;
662666
}
667+
663668
string serverNameToValidate;
664669
if (!string.IsNullOrEmpty(_hostNameInCertificate))
665670
{
@@ -670,8 +675,23 @@ private bool ValidateServerCertificate(object sender, X509Certificate cert, X509
670675
serverNameToValidate = _targetServer;
671676
}
672677

678+
if (!string.IsNullOrEmpty(_serverCertificateFilename))
679+
{
680+
X509Certificate clientCertificate = null;
681+
try
682+
{
683+
clientCertificate = new X509Certificate(_serverCertificateFilename);
684+
return SNICommon.ValidateSslServerCertificate(clientCertificate, serverCertificate, policyErrors);
685+
}
686+
catch (Exception e)
687+
{
688+
// if this fails, then fall back to the HostNameInCertificate or TargetServer validation.
689+
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNITCPHandle), EventType.INFO, "Connection Id {0}, IOException occurred: {1}", args0: _connectionId, args1: e.Message);
690+
}
691+
}
692+
673693
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNITCPHandle), EventType.INFO, "Connection Id {0}, Certificate will be validated for Target Server name", args0: _connectionId);
674-
return SNICommon.ValidateSslServerCertificate(serverNameToValidate, cert, policyErrors);
694+
return SNICommon.ValidateSslServerCertificate(serverNameToValidate, serverCertificate, policyErrors);
675695
}
676696

677697
/// <summary>

0 commit comments

Comments
 (0)