Repository navigation
[DO NOT MERGE] [Draft] Connection encryption info – Slice C: Managed SNI certificate capture - #4835
Conversation
There was a problem hiding this comment.
🔵 Needs a closer look
TLS transport changes still lack effective same-attempt isolation coverage and require the listed real-server validation.
4 open findings
What changed in this PR
Adds Slice C of connection encryption diagnostics, capturing Managed SNI server certificate details for successful and failed connections.
Changes:
- Captures certificate hashes, validation verdicts, and chain status.
- Propagates captured data through TDS and MARS layers.
- Adds unit coverage and design documentation.
| File | Description |
|---|---|
ManagedSniCertificateCaptureTests.cs |
Adds simulated TLS integration tests. |
ConnectionEncryptionInfoTests.cs |
Updates managed-SNI hash expectations. |
SniCommonServerCertificateCaptureTest.cs |
Tests validation and capture logic. |
TdsParserStateObjectManaged.netcore.cs |
Exposes managed transport capture. |
TdsParserStateObject.cs |
Defines the transport capture hook. |
TdsParser.cs |
Records capture after TLS negotiation. |
SniTcpHandle.netcore.cs |
Captures TCP TLS certificate details. |
SniNpHandle.netcore.cs |
Captures Named Pipes TLS certificate details. |
SniMarsHandle.netcore.cs |
Forwards capture through MARS handles. |
SniMarsConnection.netcore.cs |
Exposes the physical MARS capture. |
SniHandle.netcore.cs |
Adds the capture property contract. |
SniCommon.netcore.cs |
Produces validation verdicts, hashes, and chain flags. |
ServerCertificateCapture.cs |
Defines the immutable transport capture. |
EncryptionInfoCapture.cs |
Publishes certificate fields in snapshots. |
2353-connection-encryption-info.md |
Documents Managed SNI capture semantics. |
🧠 Review effort: Balanced
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| Exception? legacy = null; | ||
| try | ||
| { | ||
| SniCommon.ValidateSslServerCertificate(Guid.NewGuid(), targetServerName, hostNameInCertificate, cert, pinFile?.Path, policyErrors); |
| [Theory] | ||
| [InlineData(false)] | ||
| [InlineData(true)] | ||
| public async Task Open_FailureThenSuccess_DoesNotCarryPreviousAttemptCertificate(bool async) |
| [Theory] | ||
| [MemberData(nameof(VerdictCases))] | ||
| public void ValidateSslServerCertificate_RecordsEffectiveVerdict( |
| [Theory] | ||
| [InlineData(false)] | ||
| [InlineData(true)] | ||
| public async Task Open_TrustServerCertificate_PublishesHashWithoutVerdict(bool async) |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## dev/automation/encryption-info-slice-b-mode-protocol #4835 +/- ##
=======================================================================================
Coverage ? 66.33%
=======================================================================================
Files ? 308
Lines ? 68599
Branches ? 0
=======================================================================================
Hits ? 45506
Misses ? 23093
Partials ? 0
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
7d60501 to
549fd10
Compare
549fd10 to
502f3f3
Compare
| // The certificate validation callback runs synchronously within EnableSsl, so whatever the | ||
| // transport observed is complete here; record it before any failure is reported so that | ||
| // the login exception carries it. | ||
| _connHandler?.EncryptionInfoCapture?.RecordServerCertificate(_physicalStateObj.GetServerCertificateCapture()); |
e4e6869 to
e9fbf4b
Compare
e9fbf4b to
78f101e
Compare
78f101e to
1a6e5c6
Compare
There was a problem hiding this comment.
🟡 Changes recommended
The public XML documentation still promises a null protocol when no TLS handshake completes, but the callback now exposes an observed protocol even when certificate validation aborts the handshake.
6 open issues
Reset encryption info before retrying after PRELOGIN instance failure Test does not verify isolation between handshake attempts Legacy comparison reuses the changed implementation Clarify callback protocol docs after failed TLS handshake · New Missing XML summaries on theory methods and helpers Missing XML summaries on tests and helpers
Review effort: Balanced
| if (Protocol is null && capture.Protocol is SslProtocols protocol && protocol != SslProtocols.None) | ||
| { | ||
| Protocol = protocol; |
1a6e5c6 to
a0c686e
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
Real-SQL-Server validation remains pending for Windows/Linux Managed SNI, Strict/TDS 8, named-pipe TLS, and MARS, leaving certificate-capture and negotiation correctness unestablished across those paths.
6 open issues
Reset encryption info before retrying after PRELOGIN instance failure Test does not verify isolation between handshake attempts Legacy comparison reuses the changed implementation Clarify callback protocol docs after failed TLS handshake Missing XML summaries on theory methods and helpers Missing XML summaries on tests and helpers
Review effort: Balanced
…tails (Slice C) Capture SHA-256 of the presented leaf certificate, the effective SqlClient validation verdict, and the original callback chain status flags in Managed SNI, and flow them into the per-attempt encryption info capture so both SqlConnection.EncryptionInfo and SqlException.EncryptionInfo report them. No trust decisions, messages, or tracing are changed. Part of #2353 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Named pipes validate against the target '.', which X509Certificate2.MatchesHostname rejects with ArgumentException before the final verdict is recorded. Record the errors already established (e.g. ChainError and callback chain status) before the host name check so the capture survives. The exception is not caught or changed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Record (int)SslStream.NegotiatedCipherSuite after the handshake (authoritative) and inside the certificate validation callback (adopt-if-none), so failed certificate validation keeps the observed suite. TCP and named-pipe handles; MARS passes through. Reads are exception-safe; 0 is treated as unavailable. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
… failures Main's #4891 makes the TDS simulator rethrow connection faults on Stop. The untrusted, host-name-mismatch and pin-mismatch capture tests now set ExpectTlsHandshakeFailure so the server treats the client's rejection as expected. Test-only change. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Assert the public CLS-compliant NegotiatedCipherSuiteId property in the Managed SNI simulator tests and describe the two-property shape in the Managed capture section. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
… SNI) Managed SNI now captures a copy of the presented leaf certificate's DER encoding (GetRawCertData) instead of its SHA-256 hash, and passes it to the snapshot so SqlConnectionEncryptionInfo.GetServerCertificate() can return it. Certificates over 64 KiB are not captured, and the first certificate presented in an attempt is kept. Capture still happens in the validation callback's finally block, so it is present on TrustServerCertificate bypass and on validation failure. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
a0c686e to
4d35501
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
Platform-sensitive TLS diagnostics still require runtime validation on real SQL Server on Windows/Linux and for named-pipe TLS, Strict/TDS 8, and MARS, which the simulated TLS 1.2 results do not establish.
6 open issues
Reset encryption info before retrying after PRELOGIN instance failure Test does not verify isolation between handshake attempts Legacy comparison reuses the changed implementation Clarify callback protocol docs after failed TLS handshake Missing XML summaries on theory methods and helpers Missing XML summaries on tests and helpers
Review effort: Balanced



Summary
Managed SNI now captures server certificate details from the
RemoteCertificateValidationCallbackduring the TLS handshake and flows them throughTdsParserStateObjectinto the per-attemptEncryptionInfoCapturefrom Slice B. BothSqlConnection.EncryptionInfo(success) andSqlException.EncryptionInfo(failure) include them. Capture reuses the callback's inputs and the existing verdict. It doesn't run extra validation or network work, and it doesn't change trust decisions, exception messages, or tracing. Native SNI returns no certificate data yet (Slice D).Part of #2353
Stacked on #4831
Certificate instead of hash
Following the Threat Model Review, the SHA-256 hash is removed. Managed SNI now captures the presented certificate itself:
GetRawCertData()in the validation callback'sfinallyblock (so it is present on reject and onTrustServerCertificatebypass), skipped above 64 KiB, first certificate per attempt wins.ServerCertificateCapture.Hash256becameRawCertificateandEncryptionInfoCapture.CertificateHash256becameServerCertificateRawData; the bytes go to the snapshot constructor (presentedServerCertificate).Capture points
nullwhenPresentedServerCertificateRawData(internal) /GetServerCertificate()GetRawCertData()). The first certificate presented in the attempt is keptTrustServerCertificatebypass and on validation failureServerCertificateValidationErrorsServerCertificateMissing(null cert /RemoteCertificateNotAvailable),ChainError,HostNameMismatch(afterHostNameInCertificate),PinnedCertificateMismatch;Nonewhen passedTrustServerCertificate), or theServerCertificatefile couldn't be readServerCertificateChainStatusX509Chain.ChainStatusflags (no rebuild);NoErrorwhen cleanServerCertificatepin mode (chain not consulted, even on match), no certificate, null chain, orRemoteCertificateChainErrorswith no flagsNegotiatedEncryptionProtocol(Managed contribution)SslStream.SslProtocol, read inside the validation callback (try/catch-guarded;Nonemaps to null). Used only if the attempt has no protocol yet. net10.0 only (Managed SNI isn't compiled for net462)SslStream, the read throws, or the value isNoneNegotiatedCipherSuiteId/ .NETNegotiatedCipherSuite(Managed contribution)(int)SslStream.NegotiatedCipherSuite, read after the handshake completes (authoritative, recorded viaRecordCipherSuite) and inside the validation callback (adopted only if none recorded). TCP and named pipe; MARS passes through. try/catch-guarded;0maps to null. net10.0 onlySslStream, the read throws, or the value is0NegotiatedEncryptionModeRecordFailedTlsAttemptrecords the mode only when a protocol was observed (G2/D15)On Managed SNI, a certificate-validation failure now reports mode
Full, the observed protocol (for exampleTls12) and the observed cipher suite together with the presented certificate,ServerCertificateValidationErrorsandServerCertificateChainStatus. Slice B'sDiscardUnobservedNegotiationkeeps them because a protocol was observed.Named pipes over Managed SNI validate against the target
..X509Certificate2.MatchesHostname(".")throwsArgumentException, which was already the case on main. To make the capture survive that throw,ValidateSslServerCertificaterecords the errors it already knows (for exampleChainErrorand the callback chain status) before the host name check. The exception is not caught or changed. If no error is known before the check, the fields stay null rather than reportingNone.Values reset at the start of every TLS handshake / login attempt. They're stored through a volatile field written in the callback's
finallyand read afterAuthenticateAsClientreturns synchronously insideEnableSsl.Tests
SniCommonServerCertificateCaptureTest(net10.0): table-driven verdicts (null cert, eachSslPolicyErrorscombination, HNIC override/mismatch, pin match/mismatch/missing/unreadable), outcome and message unchanged vs the legacy overload, everyX509ChainStatusFlagsvalue and the full set preserved, captured bytes equal the DER of aCertificateRequestself-signed cert (and are a copy), over-64-KiB cert not captured, first certificate wins, snapshot round-trip viaGetServerCertificate().RawData, null vs zero, unbuilt chain not rebuilt, no leak intoMessage/ToString().ManagedSniCertificateCaptureTests(simulated TDS server, managed SNI forced on Windows, Open and OpenAsync): TSC bypass (certificate only), matching pin, untrusted self-signed (ChainError + chain flags + certificate), hostname mismatch, HNIC override, pin mismatch, attempt isolation.ManagedSniCertificateCaptureTestsfailure cases (untrusted, hostname mismatch, HNIC override mismatch, pin mismatch, failure-then-success) assert modeFulland protocolTls12alongside the certificate fields. A newSniCommontest covers a sender that isn't anSslStream, which leaves the protocol unavailable.Updated
ConnectionEncryptionInfoTests: bypass on managed SNI reports the certificate, and an untrusted failure on managed SNI reports a non-null protocol.ValidateSslServerCertificate_NamedPipeDotTarget_CapturesKnownErrorsAndKeepsException:ChainErrorand the callback chain flags are captured.ChainError, Actualnull.Head e4e6869, ManagedSni, SniCommon and EncryptionInfo filters on net10.0: 230/230 passed, with 0 warnings.
Results on head 502f3f3 (rebased on [DO NOT MERGE] [Draft] Connection encryption info – Slice B: negotiated mode and TLS protocol #4831 @ 43b09dd), using the filters ManagedSni, SniCommon, SimulatedServerTests, SqlException, SqlConnection and EncryptionInfo:
PSModulePath): 358/358 passed.Cipher suite (head e9fbf4b, rebased on [DO NOT MERGE] [Draft] Connection encryption info – Slice B: negotiated mode and TLS protocol #4831 @ e6c78ca):
NegotiatedCipherSuiteequalsSslStream.NegotiatedCipherSuiteread from the client handle.ConnectionEncryptionInfoTestsasserts a non-null suite with Managed TLS and null without TLS.SslStream(null, no throw) and adopt-if-none.Results on head e9fbf4b, with the filters ManagedSni, SniCommon, SimulatedServerTests, SqlException, SqlConnection and EncryptionInfo:
PSModulePath): 385/385 passed.Results on head a0c686e (rebased on [DO NOT MERGE] [Draft] Connection encryption info – Slice B: negotiated mode and TLS protocol #4831 @ a1d9ba6, full certificate instead of hash), same filters:
PSModulePath): 671/671 passed.Manual tests still needed
Encrypt=Strict(the simulated server only supports PRELOGIN TLS)