Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TDS8 Add Server Certificate Support #1822

Merged
merged 25 commits into from
Nov 10, 2022
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c6edb95
Add ServerCertificate as a keyword and reference it in the api calls
lcheunglci Sep 27, 2022
a35356b
Update functional test for the keyword support for server certificate
lcheunglci Sep 27, 2022
d968a22
Update the string resource file for HostNameInCertificate and ServerC…
lcheunglci Sep 27, 2022
f3e9703
Remove unreferenced tlsFirst param and set the server certificate in …
lcheunglci Sep 27, 2022
2dc03ef
Increment the SynonymCount by 1 for the new keyword
lcheunglci Sep 28, 2022
1d4aaea
Merge branch 'main' into TDS8-Add-ServerCertificate-Support
lcheunglci Oct 31, 2022
54080fa
Fix compiler errors
lcheunglci Oct 31, 2022
9072bbf
Fix the SNIAuthProviderInfo struct due to missing certificate context…
lcheunglci Oct 31, 2022
41da383
Add server certificate validation for netcore
lcheunglci Nov 1, 2022
1f5b57e
Add fix to netcore due to mismatch struct
lcheunglci Nov 1, 2022
c4512d3
Revert netfx compiler fix caused by local project targetframework
lcheunglci Nov 1, 2022
5e18eb1
Address comments and renamed serverCert to serverCertificateFilename
lcheunglci Nov 1, 2022
fd4f27b
Fix typo
lcheunglci Nov 1, 2022
e1b6a91
Fix typo
lcheunglci Nov 1, 2022
de5ef6d
Resolve merge conflict
lcheunglci Nov 2, 2022
bf536c4
Add ServerCertificate in the netfx ref project
lcheunglci Nov 2, 2022
191f2b3
Add documentation to ServerCertificate and HNIC in SqlConnectionBuild…
lcheunglci Nov 2, 2022
6e7a0d8
Apply suggestions from code review
lcheunglci Nov 3, 2022
b08ccd9
Update documentation re: encrypt mode for HNIC and ServerCertificate
lcheunglci Nov 4, 2022
a54bfc3
Merge branch 'main' into TDS8-Add-ServerCertificate-Support
lcheunglci Nov 7, 2022
01a6f58
Merge branch 'main' into TDS8-Add-ServerCertificate-Support
lcheunglci Nov 8, 2022
c364903
Apply suggestions from code review
lcheunglci Nov 9, 2022
b89fa5e
Address comments
lcheunglci Nov 10, 2022
02dd347
Merge branch 'TDS8-Add-ServerCertificate-Support' of https://github.c…
lcheunglci Nov 10, 2022
1c26fff
Merge branch 'main' into TDS8-Add-ServerCertificate-Support
lcheunglci Nov 10, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,7 @@ End Module
|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.|
|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+)|
|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+)|
|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+)|
|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.|
|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`).|
|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`|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,25 @@ This property corresponds to the "FailoverPartnerSPN" and "Failover Partner SPN"
</format>
</remarks>
</FailoverPartnerSPN>
<HostNameInCertificate>
<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>
<value>
The value of the <see cref="P:Microsoft.Data.SqlClient.SqlConnectionStringBuilder.HostNameInCertificate" /> property, or <see langword="String.Empty" /> if none has been supplied.
</value>
<remarks>
<format type="text/markdown">
<![CDATA[

## Remarks
This property corresponds to the "HostNameInCertificiate" and "Host Name in Certificiate" keys within the connection string.

> [!NOTE]
> 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.

]]>
</format>
</remarks>
</HostNameInCertificate>
<GetProperties>
<param name="propertyDescriptors">To be added.</param>
<summary>To be added.</summary>
Expand Down Expand Up @@ -787,7 +806,7 @@ Connections are considered the same if they have the same connection string. Dif
|Context Connection(Obsolete)|False|
|Current Language|Empty string|
|Data Source|Empty string|
|Encrypt|False|
|Encrypt|False in versions prior to 4.0, True in versions 4.0 and up|
|Enlist|True|
|Failover Partner|Empty string|
|Initial Catalog|Empty string|
Expand Down Expand Up @@ -842,6 +861,25 @@ Database = AdventureWorks
]]></format>
</remarks>
</Replication>
<ServerCertificate>
<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>
<value>
The value of the <see cref="P:Microsoft.Data.SqlClient.SqlConnectionStringBuilder.ServerCertificate" /> property, or <see langword="String.Empty" /> if none has been supplied.
</value>
<remarks>
<format type="text/markdown">
<![CDATA[

## Remarks
This property corresponds to the "ServerCertificate" and "Server Certificate" keys within the connection string.

> [!NOTE]
> 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.

]]>
</format>
</remarks>
</ServerCertificate>
<ServerSPN>
<summary>Gets or sets the service principal name (SPN) of the data source.</summary>
<value>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1033,6 +1033,11 @@ public SqlConnectionStringBuilder(string connectionString) { }
[System.ComponentModel.DisplayNameAttribute("Host Name In Certificate")]
[System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)]
public string HostNameInCertificate { get { throw null; } set { } }
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml' path='docs/members[@name="SqlConnectionStringBuilder"]/ServerCertificate/*'/>
David-Engel marked this conversation as resolved.
Show resolved Hide resolved
[System.ComponentModel.DisplayNameAttribute("Server Certificate")]
[System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)]
public string ServerCertificate { get { throw null; } set { } }

/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml' path='docs/members[@name="SqlConnectionStringBuilder"]/Enlist/*'/>
[System.ComponentModel.DisplayNameAttribute("Enlist")]
[System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ internal struct AuthProviderInfo
public uint flags;
[MarshalAs(UnmanagedType.Bool)]
public bool tlsFirst;
public object certContext;
[MarshalAs(UnmanagedType.LPWStr)]
public string certId;
[MarshalAs(UnmanagedType.Bool)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,50 @@ internal static bool ValidateSslServerCertificate(string targetServerName, X509C
return true;
}
}

/// <summary>
/// We validate the provided certificate provided by the client with the one from the server to see if it matches.
/// Certificate validation and chain trust validations are done by SSLStream class [System.Net.Security.SecureChannel.VerifyRemoteCertificate method]
/// This method is called as a result of callback for SSL Stream Certificate validation.
/// </summary>
/// <param name="clientCert">X.509 certificate provided by the client</param>
/// <param name="serverCert">X.509 certificate provided by the server</param>
/// <param name="policyErrors">Policy errors</param>
/// <returns>True if certificate is valid</returns>
internal static bool ValidateSslServerCertificate(X509Certificate clientCert, X509Certificate serverCert, SslPolicyErrors policyErrors)
{
using (TrySNIEventScope.Create("SNICommon.ValidateSslServerCertificate | SNI | SCOPE | INFO | Entering Scope {0} "))
{
if (policyErrors == SslPolicyErrors.None)
{
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNICommon), EventType.INFO, "serverCert {0}, SSL Server certificate not validated as PolicyErrors set to None.", args0: clientCert.Subject);
return true;
}

if ((policyErrors & SslPolicyErrors.RemoteCertificateNameMismatch) != 0)
{
// Verify that subject name matches
if (serverCert.Subject != clientCert.Subject)
{
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);
return false;
}
if (!serverCert.Equals(clientCert))
{
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNICommon), EventType.ERR, "certificate from server does not match with the certificate provided client.", args0: serverCert.Subject);
return false;
}
}
else
{
// Fail all other SslPolicy cases besides RemoteCertificateNameMismatch
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNICommon), EventType.ERR, "certificate subject: {0}, SslPolicyError {1}, SSL Policy invalidated certificate.", args0: clientCert.Subject, args1: policyErrors);
return false;
}
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNICommon), EventType.INFO, "certificate subject {0}, Client certificate validated successfully.", args0: clientCert.Subject);
return true;
}
}

internal static IPAddress[] GetDnsIpAddresses(string serverName)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,9 @@ private static bool IsErrorStatus(SecurityStatusPalErrorCode errorCode)
/// <param name="ipPreference">IP address preference</param>
/// <param name="cachedFQDN">Used for DNS Cache</param>
/// <param name="pendingDNSInfo">Used for DNS Cache</param>
/// <param name="tlsFirst"></param>
/// <param name="hostNameInCertificate"></param>
/// <param name="tlsFirst">Support TDS8.0</param>
/// <param name="hostNameInCertificate">Used for the HostName in certificate</param>
/// <param name="serverCertificateFilename">Used for the path to the Server Certificate</param>
/// <returns>SNI handle</returns>
internal static SNIHandle CreateConnectionHandle(
string fullServerName,
Expand All @@ -160,7 +161,8 @@ internal static SNIHandle CreateConnectionHandle(
string cachedFQDN,
ref SQLDNSInfo pendingDNSInfo,
bool tlsFirst,
string hostNameInCertificate)
string hostNameInCertificate,
string serverCertificateFilename)
{
instanceName = new byte[1];

Expand All @@ -187,7 +189,7 @@ internal static SNIHandle CreateConnectionHandle(
case DataSource.Protocol.None: // default to using tcp if no protocol is provided
case DataSource.Protocol.TCP:
sniHandle = CreateTcpHandle(details, timerExpire, parallel, ipPreference, cachedFQDN, ref pendingDNSInfo,
tlsFirst, hostNameInCertificate);
tlsFirst, hostNameInCertificate, serverCertificateFilename);
break;
case DataSource.Protocol.NP:
sniHandle = CreateNpHandle(details, timerExpire, parallel, tlsFirst);
Expand Down Expand Up @@ -284,8 +286,9 @@ private static byte[][] GetSqlServerSPNs(string hostNameOrAddress, string portOr
/// <param name="ipPreference">IP address preference</param>
/// <param name="cachedFQDN">Key for DNS Cache</param>
/// <param name="pendingDNSInfo">Used for DNS Cache</param>
/// <param name="tlsFirst"></param>
/// <param name="hostNameInCertificate"></param>
/// <param name="tlsFirst">Support TDS8.0</param>
/// <param name="hostNameInCertificate">Host name in certificate</param>
/// <param name="serverCertificateFilename">Used for the path to the Server Certificate</param>
/// <returns>SNITCPHandle</returns>
private static SNITCPHandle CreateTcpHandle(
DataSource details,
Expand All @@ -295,7 +298,8 @@ private static SNITCPHandle CreateTcpHandle(
string cachedFQDN,
ref SQLDNSInfo pendingDNSInfo,
bool tlsFirst,
string hostNameInCertificate)
string hostNameInCertificate,
string serverCertificateFilename)
{
// TCP Format:
// tcp:<host name>\<instance name>
Expand Down Expand Up @@ -334,7 +338,7 @@ private static SNITCPHandle CreateTcpHandle(
}

return new SNITCPHandle(hostName, port, timerExpire, parallel, ipPreference, cachedFQDN, ref pendingDNSInfo,
tlsFirst, hostNameInCertificate);
tlsFirst, hostNameInCertificate, serverCertificateFilename);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ internal sealed class SNITCPHandle : SNIPhysicalHandle
private readonly Socket _socket;
private NetworkStream _tcpStream;
private readonly string _hostNameInCertificate;
private readonly string _serverCertificateFilename;
private readonly bool _tlsFirst;

private Stream _stream;
Expand Down Expand Up @@ -121,7 +122,8 @@ public override int ProtocolVersion
/// <param name="cachedFQDN">Key for DNS Cache</param>
/// <param name="pendingDNSInfo">Used for DNS Cache</param>
/// <param name="tlsFirst">Support TDS8.0</param>
/// <param name="hostNameInCertificate">Host Name in Certoficate</param>
/// <param name="hostNameInCertificate">Host Name in Certificate</param>
/// <param name="serverCertificateFilename">Used for the path to the Server Certificate</param>
public SNITCPHandle(
string serverName,
int port,
Expand All @@ -131,7 +133,8 @@ public SNITCPHandle(
string cachedFQDN,
ref SQLDNSInfo pendingDNSInfo,
bool tlsFirst,
string hostNameInCertificate)
string hostNameInCertificate,
string serverCertificateFilename)
{
using (TrySNIEventScope.Create(nameof(SNITCPHandle)))
{
Expand All @@ -140,6 +143,7 @@ public SNITCPHandle(
_targetServer = serverName;
_tlsFirst = tlsFirst;
_hostNameInCertificate = hostNameInCertificate;
_serverCertificateFilename = serverCertificateFilename;
_sendSync = new object();

SQLDNSInfo cachedDNSInfo;
Expand Down Expand Up @@ -649,17 +653,18 @@ public override void DisableSsl()
/// Validate server certificate callback
/// </summary>
/// <param name="sender">Sender object</param>
/// <param name="cert">X.509 certificate</param>
/// <param name="serverCertificate">X.509 certificate provided from the server</param>
/// <param name="chain">X.509 chain</param>
/// <param name="policyErrors">Policy errors</param>
/// <returns>True if certificate is valid</returns>
private bool ValidateServerCertificate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors policyErrors)
private bool ValidateServerCertificate(object sender, X509Certificate serverCertificate, X509Chain chain, SslPolicyErrors policyErrors)
{
if (!_validateCert)
{
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNITCPHandle), EventType.INFO, "Connection Id {0}, Certificate will not be validated.", args0: _connectionId);
return true;
}

string serverNameToValidate;
if (!string.IsNullOrEmpty(_hostNameInCertificate))
{
Expand All @@ -670,8 +675,23 @@ private bool ValidateServerCertificate(object sender, X509Certificate cert, X509
serverNameToValidate = _targetServer;
}

if (!string.IsNullOrEmpty(_serverCertificateFilename))
{
X509Certificate clientCertificate = null;
try
{
clientCertificate = new X509Certificate(_serverCertificateFilename);
return SNICommon.ValidateSslServerCertificate(clientCertificate, serverCertificate, policyErrors);
}
catch (Exception e)
{
// if this fails, then fall back to the HostNameInCertificate or TargetServer validation.
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNITCPHandle), EventType.INFO, "Connection Id {0}, IOException occurred: {1}", args0: _connectionId, args1: e.Message);
}
}

SqlClientEventSource.Log.TrySNITraceEvent(nameof(SNITCPHandle), EventType.INFO, "Connection Id {0}, Certificate will be validated for Target Server name", args0: _connectionId);
return SNICommon.ValidateSslServerCertificate(serverNameToValidate, cert, policyErrors);
return SNICommon.ValidateSslServerCertificate(serverNameToValidate, serverCertificate, policyErrors);
}

/// <summary>
Expand Down
Loading