Skip to content

Fix | Perform ServerCertificate pin validation to verify with server provided cert#4445

Open
cheenamalhotra wants to merge 2 commits into
mainfrom
dev/cheena/server-cert-fix
Open

Fix | Perform ServerCertificate pin validation to verify with server provided cert#4445
cheenamalhotra wants to merge 2 commits into
mainfrom
dev/cheena/server-cert-fix

Conversation

@cheenamalhotra

@cheenamalhotra cheenamalhotra commented Jul 14, 2026

Copy link
Copy Markdown
Member

Description

Refines the managed SNI certificate-validation helper so that the ServerCertificate connection-string keyword behaves consistently with its documented "exact match" semantic.

Address internal item AB#46371

Two small refinements in SniCommon.ValidateSslServerCertificate:

  1. Short-circuit narrowed to the pin-less path. The early-return on policyErrors == SslPolicyErrors.None now also requires validationCertFileName to be null/empty. When the caller has supplied a ServerCertificate pin, the helper always compares it against the presented server certificate. The no-pin fast path is unchanged.
  2. Pin load failure fails closed. If the pin file can't be loaded/parsed, the helper now throws SSLCertificateAuthenticationException instead of continuing through chain/name checks — matching the caller's explicit intent to pin.

The TrustServerCertificate=true short-circuit on the transport handles is unchanged — when validation is disabled at that layer, the pin remains unconsulted, matching the existing semantic.

Backwards compatibility

  • No pin supplied → identical behavior.
  • Pin supplied, well-formed, and matches → identical behavior.
  • Pin supplied and file is missing/unreadable, or pin doesn't match the server cert → now throws SSLCertificateAuthenticationException at handshake, aligning runtime with the documented "exact match" contract.

Testing

Added SniCommonValidateSslServerCertificateTest covering the helper directly (both TCP and NP transports flow through it):

Scenario Expected
No pin + PolicyErrors.None returns true
Matching pin + PolicyErrors.None returns true
Mismatched pin + PolicyErrors.None throws AuthenticationException
Unreadable pin file + PolicyErrors.None throws AuthenticationException

Each runs against both "TCP" and "NP" transport labels via [Theory]/MemberData. Local run on net9.0: 8 passed / 0 failed.

Guidelines

Reviewed:

@cheenamalhotra cheenamalhotra requested a review from a team as a code owner July 14, 2026 00:43
Copilot AI review requested due to automatic review settings July 14, 2026 00:43
@github-project-automation github-project-automation Bot moved this to To triage in SqlClient Board Jul 14, 2026
@cheenamalhotra cheenamalhotra added Hotfix 7.0.3 PRs targeting main that should be backported to release/7.0 branch for next release. Hotfix 6.1.7 PRs targeting main that should be backported to release/6.1 branch for future hotfix labels Jul 14, 2026
@cheenamalhotra cheenamalhotra added this to the 7.1.0-preview3 milestone Jul 14, 2026

Copilot AI left a comment

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.

Pull request overview

This PR refines Managed SNI’s TLS server-certificate validation so the ServerCertificate connection-string keyword behaves as an explicit certificate pin (and updates docs/tests to reflect the intended semantics).

Changes:

  • Tightens the “policyErrors == None” fast path so it only applies when no ServerCertificate pin is provided.
  • Changes pin-load failures to fail closed by throwing an authentication exception.
  • Adds unit tests for pin vs. no-pin behavior and updates documentation to clarify “additive” pinning semantics.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniCommon.netcore.cs Adjusts certificate-validation control flow for pin handling and fail-closed behavior.
src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/ManagedSni/SniCommonValidateSslServerCertificateTest.cs Adds unit coverage for SniCommon.ValidateSslServerCertificate pin/no-pin scenarios.
doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml Documents ServerCertificate as an additive check (pin + standard validation).
doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml Expands connection-string keyword docs to clarify additive pinning and fail-closed behavior.

@cheenamalhotra cheenamalhotra force-pushed the dev/cheena/server-cert-fix branch from 3a048c7 to 59524a6 Compare July 14, 2026 00:58
Copilot AI review requested due to automatic review settings July 14, 2026 01:04
@cheenamalhotra cheenamalhotra force-pushed the dev/cheena/server-cert-fix branch from 59524a6 to e5a2542 Compare July 14, 2026 01:04

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs: Generated file

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs: Generated file

Comment on lines +74 to 77
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;

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

Comment on lines +18 to +26
/// <summary>
/// Tests for <see cref="SniCommon.ValidateSslServerCertificate"/> to ensure
/// that when a caller supplies a <c>ServerCertificate</c> pin the driver enforces the
/// pin as an *additive* check on top of normal TLS validation, and never accepts a
/// server certificate on the basis of platform trust alone.
///
/// The tests exercise the shared helper directly.
/// </summary>
public class SniCommonValidateSslServerCertificateTest

@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.

I think the PR description is inaccurate. My take is that ValidateSslServerCertificate is called by both of those transport flows, so we don't need to add any tests further up the call chain specific to TCP or NP.

|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.

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

/// 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?

Comment on lines +74 to 77
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;

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

}

string serverNameToValidate;
X509Certificate validationCertificate = null;

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.

We can narrow the scope of this variable to inside the try block on line 95:

try
{
    using X509Certificate validationCertificate = ...;

    // Do span checks...
}
catch (Exception e)
{
    // Load failed...
    throw ...;
}

I think this also eliminates the try-finally on line 91.

Comment on lines +18 to +26
/// <summary>
/// Tests for <see cref="SniCommon.ValidateSslServerCertificate"/> to ensure
/// that when a caller supplies a <c>ServerCertificate</c> pin the driver enforces the
/// pin as an *additive* check on top of normal TLS validation, and never accepts a
/// server certificate on the basis of platform trust alone.
///
/// The tests exercise the shared helper directly.
/// </summary>
public class SniCommonValidateSslServerCertificateTest

@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.

I think the PR description is inaccurate. My take is that ValidateSslServerCertificate is called by both of those transport flows, so we don't need to add any tests further up the call chain specific to TCP or NP.

return request.CreateSelfSigned(notBefore, notAfter);
}

private static string WriteCertToTempFile(X509Certificate2 cert)

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.

You could avoid the try/finally in each test if this became a disposable class:

private class TempCertFile : IDisposable
{
  public string Path { get; }

  public TempCertFile(X509Certificate2 cert)
  {
    string tempPath = ...
    File.WriteAllBytes(tempPath, cert...);
    Path = tempPath;
  }

  public void Dispose()
  {
    File.Delete(Path);
  }
}

Then in the tests:

using TempCertFile pinFile = new(pinCert);

ValidateSslServerCertificate(..., pinFile.Path, ...);

connectionId: Guid.NewGuid(),
targetServerName: "server.contoso.com",
hostNameInCertificate: null,
serverCert: null,

@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 should happen if serverCert isn't null, but policyErrors is RemoveCertificateNotAvailable? Logically I think we're saying that we don't expect a serverCert for certain policy error values, but the method docs don't talk about that relationship, and I'm not sure the method enforces it.

}

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).

@github-project-automation github-project-automation Bot moved this from To triage to Waiting for customer in SqlClient Board Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Hotfix 6.1.7 PRs targeting main that should be backported to release/6.1 branch for future hotfix Hotfix 7.0.3 PRs targeting main that should be backported to release/7.0 branch for next release.

Projects

Status: Waiting for customer

Development

Successfully merging this pull request may close these issues.

4 participants