Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml
Original file line number Diff line number Diff line change
Expand Up @@ -989,7 +989,7 @@ The following table lists the valid names for keyword values within the <xref:Mi
|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+)|
|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 />When specified, this comparison is always performed, including when the certificate already passes the usual chain-and-name validation. If the presented certificate does not match, or the file cannot be loaded or parsed, the TLS handshake fails; a configured `ServerCertificate` is never silently ignored.<br /><br />Certificate validation itself can be disabled by `TrustServerCertificate=true` (except with `Encrypt=strict`, where validation is always performed). When validation is disabled, `ServerCertificate` is not consulted.<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 @@ -1307,6 +1307,13 @@ Database = AdventureWorks
<![CDATA[
This property corresponds to the "Server Certificate" and "ServerCertificate" keys within the connection string.

> [!NOTE]
> When `ServerCertificate` is specified, the certificate presented by the server is always compared against the certificate loaded from this path, including when the presented certificate already passes chain-and-name validation. An exact match satisfies certificate validation.
>
> If the presented certificate does not match, or the file cannot be loaded or parsed, the TLS handshake fails; a configured `ServerCertificate` is never silently ignored.
>
> Certificate validation itself can be disabled by `TrustServerCertificate=true` (except with `Encrypt=strict`, where validation is always performed). When validation is disabled, `ServerCertificate` is not consulted.

]]>
</format>
</remarks>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,20 @@ internal class SniCommon
/// 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>
/// <remarks>
/// When <paramref name="validationCertFileName"/> is supplied, the presented server
/// certificate is always compared against it, even when <paramref name="policyErrors"/> is
/// <see cref="SslPolicyErrors.None"/>. An exact match satisfies certificate validation. A
/// configured certificate that cannot be loaded or parsed, or that does not match, fails
/// the connection; it is never silently ignored.
/// <para>
/// The one condition a match does not satisfy is a missing server certificate. SslStream
/// reports that as <see cref="SslPolicyErrors.RemoteCertificateNotAvailable"/> and passes a
/// null <paramref name="serverCert"/>; the two are expected to agree, but both are checked
/// so that neither an absent certificate nor the contradictory combination can be accepted
/// on the basis of a comparison.
/// </para>
/// </remarks>
/// <param name="connectionId">Connection ID/GUID for tracing</param>
/// <param name="targetServerName">Server that client is expecting to connect to</param>
/// <param name="hostNameInCertificate">Optional hostname to use for server certificate validation</param>
Expand All @@ -58,14 +72,14 @@ internal static bool ValidateSslServerCertificate(Guid connectionId, string targ
{
using (TrySNIEventScope.Create(nameof(SniCommon)))
{
if (policyErrors == SslPolicyErrors.None)
// When a pin is configured, require it to match even if the platform trusts the cert.
if (string.IsNullOrEmpty(validationCertFileName) && policyErrors == SslPolicyErrors.None)
{
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SniCommon), EventType.INFO, "Connection Id {0}, targetServerName {1}, SSL Server certificate not validated as PolicyErrors set to None.", args0: connectionId, args1: targetServerName);
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SniCommon), EventType.INFO, "Connection Id {0}, targetServerName {1}, SSL Server certificate has no policy errors and no ServerCertificate was supplied; skipping certificate comparison.", args0: connectionId, args1: targetServerName);
return true;
}

string serverNameToValidate;
X509Certificate validationCertificate = null;
if (!string.IsNullOrEmpty(hostNameInCertificate))
{
serverNameToValidate = hostNameInCertificate;
Expand All @@ -77,89 +91,114 @@ internal static bool ValidateSslServerCertificate(Guid connectionId, string targ

if (!string.IsNullOrEmpty(validationCertFileName))
{
try
// There is no trustworthy certificate to compare against when the server
// presented none. SslStream reports that as RemoteCertificateNotAvailable, and
// serverCert is null in that case; both are checked because a matching
// ServerCertificate must never satisfy validation when the presented
// certificate is absent or cannot be relied upon.
if (serverCert is null || policyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNotAvailable))
{
#if NET9_0_OR_GREATER
validationCertificate = X509CertificateLoader.LoadCertificateFromFile(validationCertFileName);
#else
validationCertificate = new X509Certificate(validationCertFileName);
#endif
}
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}, Exception occurred loading specified ServerCertificate: {1}, treating it as if ServerCertificate has not been specified.", args0: connectionId, args1: e.Message);
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SniCommon), EventType.ERR, "Connection Id {0}, ServerCertificate was specified but the server presented no certificate. Certificate validation failed.", args0: connectionId);
throw ADP.SSLCertificateAuthenticationException(Strings.SQL_ServerCertificateNotAvailable);
}
}

if (validationCertificate != null)
{
using X509Certificate validationCertificate = LoadValidationCertificate(connectionId, validationCertFileName);

if (serverCert.GetRawCertData().AsSpan().SequenceEqual(validationCertificate.GetRawCertData().AsSpan()))
{
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SniCommon), EventType.INFO, "Connection Id {0}, ServerCertificate matches the certificate provided by the server. Certificate validation passed.", args0: connectionId);
return true;
}
else
{
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SniCommon), EventType.INFO, "Connection Id {0}, ServerCertificate doesn't match the certificate provided by the server. Certificate validation failed.", args0: connectionId);
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SniCommon), EventType.ERR, "Connection Id {0}, ServerCertificate doesn't match the certificate provided by the server. Certificate validation failed.", args0: connectionId);
throw ADP.SSLCertificateAuthenticationException(Strings.SQL_RemoteCertificateDoesNotMatchServerCertificate);
}
}
else

// If we get to this point then there is a ssl policy flag.
StringBuilder messageBuilder = new();
if (policyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNotAvailable))
{
// If we get to this point then there is a ssl policy flag.
StringBuilder messageBuilder = new();
if (policyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNotAvailable))
{
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SniCommon), EventType.ERR, "Connection Id {0}, targetServerName {1}, SSL Server certificate not validated as PolicyErrors set to RemoteCertificateNotAvailable.", args0: connectionId, args1: targetServerName);
messageBuilder.AppendLine(Strings.SQL_RemoteCertificateNotAvailable);
}
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SniCommon), EventType.ERR, "Connection Id {0}, targetServerName {1}, SSL Server certificate not validated as PolicyErrors set to RemoteCertificateNotAvailable.", args0: connectionId, args1: targetServerName);
messageBuilder.AppendLine(Strings.SQL_RemoteCertificateNotAvailable);
}

if (policyErrors.HasFlag(SslPolicyErrors.RemoteCertificateChainErrors))
{
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SniCommon), EventType.ERR, "Connection Id {0}, targetServerName {0}, SslPolicyError {1}, SSL Policy certificate chain has errors.", args0: connectionId, args1: targetServerName, args2: policyErrors);

// get the chain status from the certificate
X509Certificate2 cert2 = serverCert as X509Certificate2;
X509Chain chain = new();
chain.ChainPolicy.RevocationMode = X509RevocationMode.Offline;
StringBuilder chainStatusInformation = new();
bool chainIsValid = chain.Build(cert2);
Debug.Assert(!chainIsValid, "RemoteCertificateChainError flag is detected, but certificate chain is valid.");
if (!chainIsValid)
{
foreach (X509ChainStatus chainStatus in chain.ChainStatus)
{
chainStatusInformation.Append($"{chainStatus.StatusInformation}, [Status: {chainStatus.Status}]");
chainStatusInformation.AppendLine();
}
}
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SniCommon), EventType.ERR, "Connection Id {0}, targetServerName {1}, SslPolicyError {2}, SSL Policy certificate chain has errors. ChainStatus {3}", args0: connectionId, args1: targetServerName, args2: policyErrors, args3: chainStatusInformation);
messageBuilder.AppendFormat(Strings.SQL_RemoteCertificateChainErrors, chainStatusInformation);
messageBuilder.AppendLine();
}
if (policyErrors.HasFlag(SslPolicyErrors.RemoteCertificateChainErrors))
{
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SniCommon), EventType.ERR, "Connection Id {0}, targetServerName {0}, SslPolicyError {1}, SSL Policy certificate chain has errors.", args0: connectionId, args1: targetServerName, args2: policyErrors);

if (policyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch))
// get the chain status from the certificate
X509Certificate2 cert2 = serverCert as X509Certificate2;
X509Chain chain = new();
chain.ChainPolicy.RevocationMode = X509RevocationMode.Offline;
StringBuilder chainStatusInformation = new();
bool chainIsValid = chain.Build(cert2);
Debug.Assert(!chainIsValid, "RemoteCertificateChainError flag is detected, but certificate chain is valid.");
if (!chainIsValid)
{
X509Certificate2 cert2 = serverCert as X509Certificate2;
if (!cert2.MatchesHostname(serverNameToValidate))
foreach (X509ChainStatus chainStatus in chain.ChainStatus)
{
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SniCommon), EventType.ERR, "Connection Id {0}, serverNameToValidate {1}, Target Server name or HNIC does not match the Subject/SAN in Certificate.", args0: connectionId, args1: serverNameToValidate);
messageBuilder.AppendLine(Strings.SQL_RemoteCertificateNameMismatch);
chainStatusInformation.Append($"{chainStatus.StatusInformation}, [Status: {chainStatus.Status}]");
chainStatusInformation.AppendLine();
}
}
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SniCommon), EventType.ERR, "Connection Id {0}, targetServerName {1}, SslPolicyError {2}, SSL Policy certificate chain has errors. ChainStatus {3}", args0: connectionId, args1: targetServerName, args2: policyErrors, args3: chainStatusInformation);
messageBuilder.AppendFormat(Strings.SQL_RemoteCertificateChainErrors, chainStatusInformation);
messageBuilder.AppendLine();
}

if (messageBuilder.Length > 0)
if (policyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch))
{
X509Certificate2 cert2 = serverCert as X509Certificate2;
if (!cert2.MatchesHostname(serverNameToValidate))
{
throw ADP.SSLCertificateAuthenticationException(messageBuilder.ToString());
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SniCommon), EventType.ERR, "Connection Id {0}, serverNameToValidate {1}, Target Server name or HNIC does not match the Subject/SAN in Certificate.", args0: connectionId, args1: serverNameToValidate);
messageBuilder.AppendLine(Strings.SQL_RemoteCertificateNameMismatch);
}
}

if (messageBuilder.Length > 0)
{
throw ADP.SSLCertificateAuthenticationException(messageBuilder.ToString());
}

SqlClientEventSource.Log.TrySNITraceEvent(nameof(SniCommon), EventType.INFO, "Connection Id {0}, certificate with subject: {1}, validated successfully.", args0: connectionId, args1: serverCert.Subject);
return true;
}
}

/// <summary>
/// Loads the certificate specified by the <c>ServerCertificate</c> option, failing closed
/// when it cannot be loaded or parsed.
/// </summary>
/// <param name="connectionId">Connection ID/GUID for tracing</param>
/// <param name="validationCertFileName">Path to the X.509 certificate file</param>
/// <exception cref="System.Security.Authentication.AuthenticationException">
/// Thrown when the file cannot be loaded or parsed.
/// </exception>
private static X509Certificate LoadValidationCertificate(Guid connectionId, string validationCertFileName)
{
try
{
#if NET9_0_OR_GREATER
return X509CertificateLoader.LoadCertificateFromFile(validationCertFileName);
#else
return new X509Certificate(validationCertFileName);
#endif
}
catch (Exception e)
{
// Fail closed: the caller explicitly asked us to compare against a specific
// certificate, so if we cannot load / parse it we must not fall back to validating
// by host name alone. The exception details are traced separately; the user-facing
// message identifies the configured file path so operators can locate the
// misconfiguration.
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SniCommon), EventType.ERR, "Connection Id {0}, Exception occurred loading specified ServerCertificate '{1}': {2}. Failing certificate validation.", args0: connectionId, args1: validationCertFileName, args2: e.Message);
throw ADP.SSLCertificateAuthenticationException(StringsHelper.GetString(Strings.SQL_ServerCertificateFileLoadFailed, validationCertFileName));
}
}

internal static IPAddress[] GetDnsIpAddresses(string serverName, TimeoutTimer timeout)
{
IPAddress[] ipAddresses = GetDnsIpAddresses(serverName);
Expand Down
Loading
Loading