-
Notifications
You must be signed in to change notification settings - Fork 331
Fix | Perform ServerCertificate pin validation to verify with server provided cert #4445
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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** | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why must |
||
| /// 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> | ||
|
|
@@ -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. | ||
|
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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: |
||
|
|
@@ -76,39 +88,61 @@ internal static bool ValidateSslServerCertificate(Guid connectionId, string targ | |
| serverNameToValidate = targetServerName; | ||
| } | ||
|
|
||
| if (!string.IsNullOrEmpty(validationCertFileName)) | ||
| try | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)) | ||
| { | ||
|
|
@@ -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(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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 inValidateSslServerCertificate?There was a problem hiding this comment.
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.