Skip to content

Commit

Permalink
TDS8 Add Server Certificate Support (#1822)
Browse files Browse the repository at this point in the history
  • Loading branch information
lcheunglci authored Nov 10, 2022
1 parent 2a54bc5 commit f0f36f2
Show file tree
Hide file tree
Showing 25 changed files with 369 additions and 85 deletions.
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/*'/>
[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

0 comments on commit f0f36f2

Please sign in to comment.