Add client certificate loopback authentication - #4556
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds client-certificate loopback authentication support (managed SNI) by introducing new connection-string options, wiring them through TDS prelogin/TLS enablement, and updating validation/redaction, documentation, and test coverage.
Changes:
- Adds
Client Certificate/ClientCertificate,Client Key/ClientKey, andClient Key Password/ClientKeyPasswordoptions (plus builder properties + ref docs). - Implements managed-SNI client certificate loading and passes the certificate context into TLS authentication; updates PRELOGIN encryption flags and LOGIN7 credential suppression.
- Adds unit + functional + simulated-server tests, plus documentation snippet updates and a sample.
Reviewed changes
Copilot reviewed 31 out of 32 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ClientCertificateAuthenticationTests.cs | New simulated-server coverage for PRELOGIN flag + TLS client-cert presentation + empty LOGIN7 credentials. |
| src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlConnectionOptionsTest.cs | Validates parsing/aliases/conflicts and verifies redaction behavior for client-key passwords. |
| src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/ManagedSni/SqlClientCertificateLoaderTests.cs | New unit coverage for certificate/key loading paths (PFX, PEM/DER, chains, algorithm handling, error normalization). |
| src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/ManagedSni/SniPacketTests.cs | Updates test SNI handle stub to match new EnableSsl signature. |
| src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionTest.cs | Adds functional validation for conflicts with SqlCredential / AccessToken / callback; updates invalid keyword list. |
| src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionStringBuilderTest.cs | Adds keyword parsing coverage and round-trip verification for new builder properties. |
| src/Microsoft.Data.SqlClient/src/Resources/Strings.resx | Adds new user-facing strings for conflicts, platform gating, and certificate-load failures. |
| src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs | Updates strongly-typed resource accessors for the new strings. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs | Adds client-cert parameters to EnableSsl and explicitly rejects client cert auth on native SNI. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectManaged.netcore.cs | Plumbs new client-cert parameters through to managed SNI EnableSsl. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs | Extends abstract EnableSsl contract to include client-certificate inputs. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs | Ensures CLIENT_CERT prelogin flag is available across TFMs (while runtime-gated elsewhere). |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs | Sets/propagates client-cert handshake state and updates encryption negotiation handling. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs | Adds a helper exception factory for client-certificate auth conflicts. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionStringBuilder.cs | Adds new connection-string builder properties and keyword/synonym wiring. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionOptions.cs | Adds option parsing/validation (including conflicts + redaction support) for client cert + key + password. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.cs | Enforces conflict rules when mixing client cert auth with credential/token/SSPI mechanisms. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SqlClientCertificateLoader.netcore.cs | Adds managed-SNI certificate/key loading implementation with chain support and error normalization. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniTcpHandle.netcore.cs | Passes client-certificate context into TLS client authentication and disposes cert context on handle dispose. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniNpHandle.netcore.cs | Same as TCP handle changes, for NP transport. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniMarsHandle.netcore.cs | Updates MARS wrapper to forward new EnableSsl signature. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniMarsConnection.netcore.cs | Updates MARS connection to forward new EnableSsl signature. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniHandle.netcore.cs | Adds client cert context caching and new TLS authentication helpers used by ManagedSNI. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs | Omits username/password and credential object from LOGIN7 when using client certificate auth. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/ConnectionString/DbConnectionStringSynonyms.cs | Adds no-space synonyms for the new client certificate keywords. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/ConnectionString/DbConnectionStringKeywords.cs | Adds canonical keyword strings for the new connection-string options. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/ConnectionString/DbConnectionStringDefaults.cs | Adds default values for the new connection-string options. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/ConnectionString/DbConnectionString.netfx.cs | Extends sensitive-value masking to cover ClientKeyPassword in redacted strings. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs | Adds new exception helpers for mixed-usage client certificate authentication scenarios. |
| src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.cs | Updates reference surface for new SqlConnectionStringBuilder properties. |
| doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml | Documents new builder properties and their behavioral constraints. |
| doc/samples/SqlConnection_ClientCertificateAuthentication.cs | Adds an environment-variable-based sample showing new connection-string usage. |
Files not reviewed (1)
- src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs: Generated file
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- Replace sync-over-async TLS handshake with the synchronous AuthenticateAsClient(SslClientAuthenticationOptions) overload. - Fail the connection when a server declines encryption instead of silently sending an empty, anonymous LOGIN7 record. - Reject ChangePassword for certificate-authenticated connections. - Detect the certificate container format from file contents rather than the file extension. - Base credential conflict detection on values rather than keyword presence so empty keywords no longer conflict. - Gate the keywords, builder properties, and reference assembly surface behind #if NET; the keywords are unknown on .NET Framework. - Select the end-entity certificate from a PKCS#12 bundle by content. - Reject the ODBC 'file:' path syntax and report encrypted PKCS#1 private keys with an actionable error. - Revert the unrelated ServerCertificate copy-constructor fix. - Expand tests and documentation to cover the above. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Follow-up commit Behavior changes since the initial commit
Validation
Still gating merge A successful login against a real SQL Server on Linux loopback instance has not yet been demonstrated. The simulated TDS/TLS server tests cover the PRELOGIN flag, the presented certificate, the empty LOGIN7 record, and the encryption-degradation failure, but they are not a substitute for an end-to-end run. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 31 out of 32 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs: Generated file
Suppressed comments (2)
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionOptions.cs:180
- PR description says the connection-string API is available across target frameworks and that .NET Framework should fail explicitly with
PlatformNotSupportedExceptionwhen certificate authentication is used. However, these keywords are only added to the keyword map under#if NET, which means on .NET Framework the connection string fails earlier withKeywordNotSupported/ArgumentExceptionand the API surface is not available.
#if NET
// Client certificate authentication is implemented by managed SNI only, so the keywords
// are not recognized on .NET Framework where managed networking is unavailable.
AddKeywordToMap(DbConnectionStringKeywords.ClientCertificate,
DbConnectionStringSynonyms.ClientCertificate);
AddKeywordToMap(DbConnectionStringKeywords.ClientKey,
DbConnectionStringSynonyms.ClientKey);
AddKeywordToMap(DbConnectionStringKeywords.ClientKeyPassword,
DbConnectionStringSynonyms.ClientKeyPassword);
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SqlClientCertificateLoader.netcore.cs:164
LoadPkcs12throws aCryptographicExceptionwhen the PKCS#12 bundle contains no private key, butLoad(...)catchesCryptographicExceptionand wraps it asAuthenticationException(SQL_ClientCertificateLoadFailed). That loses the more actionableSQL_ClientCertificateMissingPrivateKeymessage at the top level (it only survives as an inner exception).
int leafCertificateIndex = FindLeafCertificateIndex(certificates);
if (leafCertificateIndex < 0)
{
throw new CryptographicException(StringsHelper.GetString(Strings.SQL_ClientCertificateMissingPrivateKey));
}
|
Hey @JustinMDotNet - looks like there are some conflicts that need resolution. |
A PKCS#12 bundle already contains its private key, so pairing it with Client Key is a misconfiguration. The detached-key path previously reported this as an unspecified certificate-load failure. Detect the container format from the file contents and report the conflict explicitly instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 31 out of 32 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs: Generated file
Suppressed comments (2)
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionOptions.cs:180
- The client-certificate keywords are only added to the parser keyword map under
#if NET, so onnet462these keywords are rejected during connection-string parsing (ArgumentException) rather than being accepted and failing at open-time withPlatformNotSupportedException. This contradicts the PR description’s stated scope for .NET Framework behavior; either update the PR description to match the current implementation, or remove the conditional keyword gating and instead throwPlatformNotSupportedExceptionwhen attempting to use client-certificate auth on unsupported platforms.
#if NET
// Client certificate authentication is implemented by managed SNI only, so the keywords
// are not recognized on .NET Framework where managed networking is unavailable.
AddKeywordToMap(DbConnectionStringKeywords.ClientCertificate,
DbConnectionStringSynonyms.ClientCertificate);
AddKeywordToMap(DbConnectionStringKeywords.ClientKey,
DbConnectionStringSynonyms.ClientKey);
AddKeywordToMap(DbConnectionStringKeywords.ClientKeyPassword,
DbConnectionStringSynonyms.ClientKeyPassword);
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SqlClientCertificateLoader.netcore.cs:164
LoadPkcs12throws aCryptographicExceptionwhen no certificate in the PKCS#12 bundle has a private key. That exception is immediately wrapped byLoad(...)into a genericAuthenticationException(SQL_ClientCertificateLoadFailed), which hides the more specificSQL_ClientCertificateMissingPrivateKeymessage that the rest of the loader uses. ThrowAuthenticationException(SQL_ClientCertificateMissingPrivateKey)directly here so callers get the intended actionable error.
int leafCertificateIndex = FindLeafCertificateIndex(certificates);
if (leafCertificateIndex < 0)
{
throw new CryptographicException(StringsHelper.GetString(Strings.SQL_ClientCertificateMissingPrivateKey));
}
…-certificate-auth # Conflicts: # src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlConnectionOptionsTest.cs
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 31 out of 32 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs: Generated file
Suppressed comments (2)
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SqlClientCertificateLoader.netcore.cs:164
- When a PKCS#12 bundle contains no private key, LoadPkcs12 throws a CryptographicException which is caught by Load(...) and rethrown as SQL_ClientCertificateLoadFailed. This prevents callers from seeing the specific SQL_ClientCertificateMissingPrivateKey message that you already use for other missing-key cases.
int leafCertificateIndex = FindLeafCertificateIndex(certificates);
if (leafCertificateIndex < 0)
{
throw new CryptographicException(StringsHelper.GetString(Strings.SQL_ClientCertificateMissingPrivateKey));
}
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionStringBuilder.cs:55
- The PR description states the connection-string API is available across target frameworks and that .NET Framework fails explicitly with PlatformNotSupportedException. However these new keywords/properties are compiled under
#if NET, which makes them unavailable on net462 (and related tests assert ArgumentException for netfx). Either update the PR description/scope, or remove the compile-time gating and allow parsing on .NET Framework with a runtime PlatformNotSupportedException during Open/EnableSsl.
Encrypt,
HostNameInCertificate,
ServerCertificate,
#if NET
ClientCertificate,
ClientKey,
ClientKeyPassword,
#endif
- Guard the documentation sample with #if false so the Samples project, which builds against the released package, still compiles. Follow the sample conventions: file-scoped namespace, Snippet1 markers, no license header, and a reference from the ClientCertificate snippet. - Throw AuthenticationException through ADP.SSLCertificateAuthenticationException so certificate failures are traced like other managed SNI failures, and add the inner-exception overload that requires. - Order the client certificate snippet entries alphabetically, matching the rest of SqlConnectionStringBuilder.xml. - Document the certificate loader types and members, matching the other ManagedSni files. - Order [PasswordPropertyText] on ClientKeyPassword as on Password, and place ChangePasswordConflictsWithClientCertificate next to its sibling. - Cover both the synchronous and asynchronous open paths in the certificate failure and encryption degradation tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 31 out of 32 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs: Generated file
Suppressed comments (2)
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SqlClientCertificateLoader.netcore.cs:193
LoadPkcs12throws aCryptographicExceptionfor the “missing private key” case, butLoadcatchesCryptographicExceptionand rethrows a genericSQL_ClientCertificateLoadFailedAuthenticationException. This prevents the more actionableSQL_ClientCertificateMissingPrivateKeymessage from ever surfacing for PKCS#12 bundles that lack a private key.
int leafCertificateIndex = FindLeafCertificateIndex(certificates);
if (leafCertificateIndex < 0)
{
throw new CryptographicException(StringsHelper.GetString(Strings.SQL_ClientCertificateMissingPrivateKey));
}
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionOptions.cs:181
- The PR description says the client-certificate connection-string API is available across target frameworks and that .NET Framework fails explicitly with
PlatformNotSupportedException. In the current implementation the keywords are not recognized on .NET Framework (#if NETaroundAddKeywordToMap), so a netfx caller will instead get anArgumentException(unsupported keyword) at parse time. Please align the PR description with this behavior, or remove the#if NETgating and ensure netfx reaches the intendedPlatformNotSupportedExceptionpath.
#if NET
// Client certificate authentication is implemented by managed SNI only, so the keywords
// are not recognized on .NET Framework where managed networking is unavailable.
AddKeywordToMap(DbConnectionStringKeywords.ClientCertificate,
DbConnectionStringSynonyms.ClientCertificate);
AddKeywordToMap(DbConnectionStringKeywords.ClientKey,
DbConnectionStringSynonyms.ClientKey);
AddKeywordToMap(DbConnectionStringKeywords.ClientKeyPassword,
DbConnectionStringSynonyms.ClientKeyPassword);
#endif
Per team guidance, native SNI already ships client certificate support, so the feature no longer requires managed networking. SNI exposes three inputs through SNIAuthProviderInfo: a Windows certificate store subject lookup, a store SHA1 lookup, and a caller supplied CERT_CONTEXT. The declared client certificate callback is not invoked by the shipping provider, so the certificate is handed over as a CERT_CONTEXT, which preserves the JDBC style file path contract. - Correct AuthProviderInfo.certContext and clientCertificateCallbackContext to IntPtr, matching the native CERT_CONTEXT* and PVOID fields. - Move the certificate loader out of ManagedSni so both SNI implementations share it, and build it for every target framework. - Load the certificate in TdsParserStateObjectNative.EnableSsl and pass X509Certificate2.Handle as certContext, releasing it on dispose. - Drop the PlatformNotSupportedException and un-gate the connection string keywords, builder properties, and reference assembly surface. - On .NET Framework support PKCS#12 containers, which carry their own private key, and report a specific error for a detached Client Key because RSA.ImportFromPem and ImportPkcs8PrivateKey do not exist there. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Root the certificate across the SNIAddProvider call. X509Certificate2.Handle does not keep its certificate alive, so the managed object could be collected while native SNI still held the PCCERT_CONTEXT. - Dispose any certificate left by a previous attempt before loading another. - Convert a certificate load failure on the native path into a SqlError that carries the original AuthenticationException, matching how managed SNI reports the same failure. It previously escaped as a raw AuthenticationException and bypassed the connection retry loop. - Document that native SNI presents only the end-entity certificate, since SNIAuthProviderInfo has no field for the issuer chain. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Description
Adds client certificate authentication for SQL Server on Linux loopback connections, matching the documented behavior of the ODBC and JDBC drivers. Implemented in managed SNI.
Client Certificate/ClientCertificate,Client Key/ClientKey, andClient Key Password/ClientKeyPasswordconnection-string keywords and the matchingSqlConnectionStringBuilderproperties. Paths are plain file paths, as in the JDBC driver.CLIENT_CERT(0x80) bit and sends LOGIN7 with an empty user name and password. Authentication completes during the TLS handshake, so no newSqlAuthenticationMethodvalue is introduced.Client Key, which accepts unencrypted RSA PKCS#1 and RSA PKCS#8 keys, or encrypted PKCS#8 whenClient Key Passwordis set.Encrypt=Optional,Mandatory, andStrictare all supported when the server negotiates encryption, on both the sync and async open paths.Client Key Passwordas sensitive connection information and always redacts it from tracing, regardless ofPersist Security Info.SqlCredential, a custom SSPI context provider, andSqlConnection.ChangePassword. Conflict detection is value-based, so an emptyUser ID=orIntegrated Security=falsedoes not conflict.file:path prefix, password-protected PKCS#1 keys, non-RSA detached keys, and aClient Keycombined with a PKCS#12 certificate.Scope and limitations
#if NETgated. On .NET Framework the keywords are unknown and the connection string is rejected by the parser, because managed networking is unavailable there.PlatformNotSupportedExceptionrather than being silently ignored. Native SNI ships as an external NuGet package with no source in this repository, so native support has to be a separate cross-repo change.subject:andsha1:forms are not accepted; that scaffolding was removed in Removed CTAIP, certificate authentication #2831.SqlConnection.ClearPoolorPooling=false.doc/samples/SqlConnection_ClientCertificateAuthentication.csis guarded with#if falsebecause the Samples project compiles against the releasedMicrosoft.Data.SqlClientpackage, which does not yet expose these properties. The guard should be removed once a package containing them ships.Release note: Added
Client Certificate,Client Key, andClient Key Passwordconnection-string support for SQL Server on Linux loopback certificate authentication on .NET when using managed networking.Issues
Fixes #4551
Testing
Connection-string and API surface
Client Key Passwordredaction from both the public connection string and the trace string.SqlConnectionStringBuilderround-trip andClear.SqlCredential,AccessToken,AccessTokenCallback, and bothChangePasswordoverloads.SqlConnectionOptionsand the builder indexer.Certificate loader
.pem,.cer, and extensionless paths, and PEM content loaded from a.pfxpath, confirming content-based detection.Client Keypaired with a PKCS#12 certificate, the ODBCfile:prefix, expired certificates, incorrect passwords, missing keys, and unsupported PEM key labels.Simulated TDS/TLS server
0x80, client certificate presentation, and empty LOGIN7 user name and password acrossOptional,Mandatory, andStrict, sync and async.SqlExceptionwrappingAuthenticationException, sync and async.Builds and runs
net8.0andnet9.0and omits them fornet462andnetstandard2.0.net462,net8.0,net9.0) and Unix (net8.0,net9.0), plus the reference assemblies.net8.0, 1145 onnet9.0, and 1148 onnet462; 0 failed on all three.origin/mainworktree on the same machine: identical pre-existing failure counts (2 onnet9.0, 86 onnet462, all Always Encrypted certificate-store andSqlDataRecordTest.GetUdt_ReturnsValueenvironmental failures). No regressions; this branch passes six more tests than the baseline.Not yet validated
Review
Reviewed with Claude Sonnet 5, then independently with GPT-5.6 Sol and Claude Opus 5, then GPT-5.6 Luna on the remediation, then a codebase-pattern conformance pass. All findings were addressed or answered in the review threads.
Tests added or updated
Public API changes documented
Verified against customer repro (requires a SQL Server on Linux loopback certificate environment)
Ensure no breaking changes introduced
Guidelines