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 @@ -1003,7 +1003,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 />This check is **additive** to normal certificate validation, not a replacement for it. When the driver validates the server certificate, the presented certificate must both pass chain-and-name validation **and** match the certificate loaded from this path exactly. If the file cannot be loaded or parsed, the TLS handshake fails; the connection is never silently accepted on the basis of platform trust alone.<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+)|

@paulmedynski paulmedynski Jul 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is platform trust? Is it a broadly understood industry term? How is it different than the checks we perform in ValidateSslServerCertificate?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we check for PolicyErrors = None, that confirms Platform/Runtime on OS trusts the certificate.

|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 @@ -1332,6 +1332,16 @@ Database = AdventureWorks
<![CDATA[
This property corresponds to the "Server Certificate" and "ServerCertificate" keys within the connection string.

> [!NOTE]
> `ServerCertificate` is **additive** to normal certificate validation, not a replacement for it. When the driver validates the server certificate, the presented certificate must:
>
> 1. Pass chain-and-name validation, **and**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, so it isn't possible to supply a self-signed server cert pin and use it to validate? I guess you could also load the server cert into the store and then the chain-and-name would validate as well, but that seems like an extra hoop to jump through. This is all existing behaviour though, so we're stuck with it I guess.

> 2. Be an exact byte-for-byte match of the certificate loaded from the file at this path.
>
> Both checks are enforced independently. If the file cannot be loaded or parsed, the TLS handshake fails; the connection is never silently accepted on the basis of platform trust alone.
>
> 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 @@ -44,8 +44,15 @@ internal class SniCommon
internal const int LocalDBBadRuntime = 57;

/// <summary>
/// We either validate that the provided 'validationCert' matches the 'serverCert', or we validate that the server name in the 'serverCert' matches 'targetServerName'.
/// Certificate validation and chain trust validations are done by SSLStream class [System.Net.Security.SecureChannel.VerifyRemoteCertificate method]
/// Validates the server's TLS certificate. When a <paramref name="validationCertFileName"/>
/// pin is supplied, the presented server certificate must be a byte-for-byte match of the
/// pinned certificate. The pin is <b>additive</b>: it does not replace chain / name
/// validation. The presented certificate must both match the pin (when supplied) and
/// pass the platform's chain / name checks (i.e. <paramref name="policyErrors"/> must

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why must policyErrors be None? Do we mean that chain/name checks are only performed when policyErrors is None? What do we do for other values of policyErrors?

/// be <see cref="SslPolicyErrors.None"/>). If the pin file cannot be loaded or parsed,
/// validation fails closed.
/// Certificate chain / name validation is performed by SslStream
/// [System.Net.Security.SecureChannel.VerifyRemoteCertificate].
/// This method is called as a result of callback for SSL Stream Certificate validation.
/// </summary>
/// <param name="connectionId">Connection ID/GUID for tracing</param>
Expand All @@ -59,7 +66,12 @@ internal static bool ValidateSslServerCertificate(Guid connectionId, string targ
{
using (SqlClientSNIEventScope.Create(nameof(SniCommon)))
{
if (policyErrors == SslPolicyErrors.None)
// When the caller supplies a ServerCertificate pin we always compare the presented
// server certificate against the pin, even if the platform's chain / name validation
// already succeeded (policyErrors == None). The pin is additive: it must match in
// addition to any platform trust decision. Only skip further work when no pin was
// supplied AND the platform reported no policy errors.
Comment thread
cheenamalhotra marked this conversation as resolved.
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);
return true;
Comment on lines +74 to 77

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. In this case, this method has explicitly validated the server cert, so the wording is actually the opposite. Maybe:

Server certificate has no policy errors and no pin file was supplied; skipping pin checks

Expand All @@ -76,39 +88,61 @@ internal static bool ValidateSslServerCertificate(Guid connectionId, string targ
serverNameToValidate = targetServerName;
}

if (!string.IsNullOrEmpty(validationCertFileName))
try

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this new pin checking is additive, then why are we performing it before all of the existing policy error checks? I think it would make more sense to perform this after the existing checks (i.e. below line 190).

{
try
if (!string.IsNullOrEmpty(validationCertFileName))
{
try
{
#if NET9_0_OR_GREATER
validationCertificate = X509CertificateLoader.LoadCertificateFromFile(validationCertFileName);
validationCertificate = X509CertificateLoader.LoadCertificateFromFile(validationCertFileName);
#else
validationCertificate = new X509Certificate(validationCertFileName);
validationCertificate = new X509Certificate(validationCertFileName);
#endif
}
catch (Exception e)
{
// Fail closed: the caller explicitly asked us to pin against a specific
// certificate, so if we cannot load / parse that pin we must not accept the
// server certificate on the basis of platform trust alone. The exception
// details are traced separately; the user-facing message identifies the
// configured pin 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));
}

// If the server did not present a certificate (e.g. SslPolicyErrors
// .RemoteCertificateNotAvailable), there is nothing to compare the pin
// against. Skip the byte comparison and fall through to the policy-error
// handler below, which will report the missing-certificate condition with
// the appropriate AuthenticationException instead of a NullReferenceException.
if (serverCert != null)
{
// Enforce the pin: the presented server certificate must be a byte-for-byte
// match of the pinned certificate. On mismatch, fail immediately.
if (!serverCert.GetRawCertData().AsSpan().SequenceEqual(validationCertificate.GetRawCertData().AsSpan()))
{
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);
throw ADP.SSLCertificateAuthenticationException(Strings.SQL_RemoteCertificateDoesNotMatchServerCertificate);
}

SqlClientEventSource.Log.TrySNITraceEvent(nameof(SniCommon), EventType.INFO, "Connection Id {0}, ServerCertificate matches the certificate provided by the server. Continuing with chain / name validation.", args0: connectionId);
// Pin matched. Fall through so that chain / name validation is still
// enforced additively; a matching pin must never mask a policy error.
}
}
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);
}
}

if (validationCertificate != null)
{
if (serverCert.GetRawCertData().AsSpan().SequenceEqual(validationCertificate.GetRawCertData().AsSpan()))
// If the platform reported no policy errors, validation succeeds. When a pin
// was supplied we only reach this point after the pin matched, so this covers
// both the pin and no-pin success paths additively.
if (policyErrors == SslPolicyErrors.None)
{
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SniCommon), EventType.INFO, "Connection Id {0}, ServerCertificate matches the certificate provided by the server. Certificate validation passed.", args0: connectionId);
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SniCommon), EventType.INFO, "Connection Id {0}, certificate with subject: {1}, validated successfully.", args0: connectionId, args1: serverCert.Subject);
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);
throw ADP.SSLCertificateAuthenticationException(Strings.SQL_RemoteCertificateDoesNotMatchServerCertificate);
}
}
else
{
// If we get to this point then there is a ssl policy flag.

// At this point at least one platform policy error is set. Build a descriptive
// message and fail — even if a ServerCertificate pin matched above.
StringBuilder messageBuilder = new();
if (policyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNotAvailable))
{
Expand Down Expand Up @@ -154,10 +188,16 @@ internal static bool ValidateSslServerCertificate(Guid connectionId, string targ
{
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;
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SniCommon), EventType.INFO, "Connection Id {0}, certificate with subject: {1}, validated successfully.", args0: connectionId, args1: serverCert.Subject);
return true;
}
finally
{
// X509Certificate owns unmanaged handles; dispose the pin certificate we
// loaded above so it is not left to the finalizer.
validationCertificate?.Dispose();
}
}
}

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Microsoft.Data.SqlClient/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -2127,6 +2127,9 @@
<data name="SQL_RemoteCertificateDoesNotMatchServerCertificate" xml:space="preserve">
<value>The certificate provided by the server does not match the certificate provided by the ServerCertificate option.</value>
</data>
<data name="SQL_ServerCertificateFileLoadFailed" xml:space="preserve">
<value>The certificate file specified by the 'ServerCertificate' option could not be loaded or parsed: '{0}'.</value>
</data>
<data name="SQL_JsonDocumentNotSupportedOnColumnType" xml:space="preserve">
<value>Invalid attempt to get JsonDocument on column '{0}'. JsonDocument is only supported for columns of type json.</value>
</data>
Expand Down
Loading
Loading