Report the outcome of every physical connection attempt, identifying the client certificate used - #3257
Open
mgravell wants to merge 4 commits into
Open
Report the outcome of every physical connection attempt, identifying the client certificate used#3257mgravell wants to merge 4 commits into
mgravell wants to merge 4 commits into
Conversation
…the client certificate used Adds ConfigurationOptions.ConnectionAttemptCompleted, raised exactly once per physical connection attempt (initial connect or reconnect, interactive or subscription): success once the Redis handshake completes, or failure if the attempt ends first. Unlike ConnectionFailed, failed reconnects are not suppressed while a server stays unreachable. The args identify the client certificate by subject, issuer and SHA-1/SHA-256 thumbprint, snapshotted rather than retained: the certificate actually sent after a completed handshake, or the one the selection callback returned when the handshake itself failed. See #3252.
…rationOptions field in ExpectedFields TlsHostName is what the library asked TLS to authenticate (and sent as SNI): the resolved host, or the caller's TargetHost when SslClientAuthenticationOptions is used; null when the library did not perform TLS.
…ion attempt ConnectionAttemptStage (Connect, Tunnel, Tls, Handshake, Established; spaced by 100 to leave room) says how far an attempt got. ServerCertificatePolicyErrors/ChainStatus/Accepted capture the server certificate validation, observed via a wrapping callback (platform-default semantics when none is configured); when that injected callback rejects, the AuthenticationException is re-described so observing does not cost the platform's error detail. The TLS block now resolves SslClientAuthenticationOptions before constructing the SslStream, and only wraps callbacks the options do not supply: SslStream rejects a constructor callback that differs from one in the options.
… failures correctly - Guard the certificate snapshot per value, and the whole reporting path: reading a certificate disposed mid-attempt throws (measured: CryptographicException), and this runs on the connection's own path, including establishing a healthy connection. - Rename ConnectionAttemptEventArgs to ConnectionAttemptCompletedEventArgs, matching the event. - Add SequenceNumber and CompletedTimeUtc, since handlers on worker threads can observe outcomes out of order. - Document the classification: a wrong password is Handshake + AuthenticationFailure (the server replied), a TLS 1.3 client-certificate rejection is Handshake + SocketClosed (the server hung up); plus guidance on corroboration, retrying the primary, fallback expiry and ordering; SHA-1 and PII notes. - Tests: wrong password (plaintext, and TLS with an accepted certificate), the documented classifier, a disposed certificate, and outcome sequencing.
|
Thanks for this Marc, this change covers my need well. I don't have any concerns with this PR, outside of the one potential hashing perf concern I left a comment on. Thanks again! |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Proposed resolution for #3252: report the outcome of every physical connection attempt, including how far it got, which client certificate it used, and what happened to the server certificate.
Rather than new
Tunnelhooks, this adds an observation-only event onConfigurationOptions, alongside the existingCertificateSelection/CertificateValidationevents. It is subscribed before connecting (so initial attempts are seen as well as reconnects), copied byClone(), and raised on a worker viaCompleteAsWorker, the same asConnectionFailed, so handlers never run on the connection's own thread. Keeping it offTunnelavoids every chaining tunnel (e.g.LoggingTunnel) having to remember to forward new members, and the library continues to own TLS.What is reported
ConfigurationOptions.ConnectionAttemptCompleted(EventHandler<ConnectionAttemptCompletedEventArgs>) is raised exactly once per physical connection attempt: initial connect or reconnect, interactive or subscription.ConnectionFailed, failed reconnects keep being reported while a server stays unreachable;ConnectionFailed/ConnectionRestoredbehaviour is unchanged.ConnectionAttemptCompletedEventArgs:EndPoint,ConnectionTypeIsSuccess,FailureType,ExceptionStageConnect(100),Tunnel(200),Tls(300),Handshake(400), orEstablished(500) on success; values are spaced so finer stages can be added laterTlsHostNameEndPoint(explicitSslHost, or a host inferred for an address endpoint)ClientCertificateSubject,ClientCertificateIssuer,ClientCertificateThumbprint(SHA-1),ClientCertificateThumbprintSha256ServerCertificatePolicyErrors,ServerCertificateChainStatus,ServerCertificateAcceptedSslPolicyErrors, plus the combinedX509ChainStatusFlagsbehind any chain error), and whether it was acceptedSequenceNumber,CompletedTimeUtcThe client certificate itself is not exposed: descriptive values are snapshotted when the outcome is recorded, and the library does not retain the certificate (or its key) beyond that. Each value is read defensively: a certificate disposed while the attempt is in flight throws when read (measured:
CryptographicException), and that value is then reported as null. The reporting path as a whole is also guarded, so observing can never affect the connection, including one being established. Correlation is per physical connection, so concurrent interactive/subscription attempts completing out of order cannot be misattributed.Which client certificate is reported:
SslStream.LocalCertificate). This is null if the server did not ask for one, even though the selection callback still runs in that case.CertificateSelection.Server certificate validation is observed by wrapping the validation callback, because
AuthenticationExceptiondoes not carrySslPolicyErrorsas data (modern .NET describes them in the message text; netfx does not). With no callback configured, the wrapper applies the platform default (accept only a certificate with no errors). In that case the platform's exception message would otherwise become "rejected by the provided callback", so it is re-described with the errors, and the original is kept as the inner exception. Observing therefore does not cost diagnostic detail.Measured (in-process TLS server, net10.0)
StageFailureTypeConnectUnableToConnect(or similar)TlsAuthenticationFailureRemoteCertificateChainErrors/UntrustedRoot, accepted:falseHandshakeSocketClosedtrueHandshakeAuthenticationFailuretruewith TLS)EstablishedNoneThe TLS 1.3 row is the one #3252 cares about. The client's handshake completes before the server rejects the certificate, so the failure is observed during the Redis handshake, as
SocketClosed. The wrong-password row happens at the same stage; the difference is that the server replies with an error (AuthenticationFailure) rather than hanging up (SocketClosed). A separate "Redis authentication" stage would not help here: with TLS 1.3, the certificate is rejected whileHELLO/AUTHis in flight, so both happen at the same point.Capturing what #3252 asks for
Classifying an outcome for a certificate-health policy (the same classification is in
docs/Authentication.md, and pinned by a test):What
certsdoes with that is the consumer's policy, deliberately not prescribed here.SocketClosedis a weak signal, though: restarts, resets,maxclientsand network blips look the same. So the docs recommend that a policy should:SequenceNumberrather than arrival order.Not included / limitations
SslStreamdoes not expose alerts. The combination above is as close as the platform allows.AUTHas a stage distinct from the rest of the handshake: not needed to separate a wrong password from a certificate rejection (see above), and it would not separate them anyway. The stage values leave room for it if there is another reason.SslClientAuthenticationOptions:SslStreamthrows if a constructor callback differs from one supplied in the options, so the library only wraps callbacks the options leave to it. If the options supply their own validation callback, the server-certificate members are null. A client certificate supplied via the options is only identified after a completed handshake. A test covers the no-conflict behaviour.ConnectTransportAsync): outcomes are reported, but connect and TLS are the tunnel's, so they report asConnect, with no TLS details.Tests
ConnectionAttemptUnitTestscovers:Stage: Connect);SslClientAuthenticationOptionsvalidation callback not displaced;Handshake+AuthenticationFailure, with and without TLS, and not classified as a certificate rejection;TLS tests run on .NET only.
InProcessTestServergains:ClientCertificateValidator, which makes the server require a client certificate and accept or reject it;validateServerCertificateswitch onGetClientConfig.ConfigTests.ExpectedFieldslists the newConfigurationOptionsfield.docs/Authentication.mddocuments the event alongsideCertificateSelection, including the classification above, guidance for a fallback policy, and cautions on SHA-1 thumbprints and logging certificate subjects.The full repo builds clean on all TFMs with
/p:CI=true; the relevant tests pass on net8.0 and net10.0. net481 builds but was not run.Checklist