Skip to content

X509Chain.Build fails with NRE when ExtraStore certificates are reused within SslStream RemoteCertificateValidationCallback #123058

@brantburnett

Description

@brantburnett

Description

Within the SslStream RemoteCertificateValidationCallback, you can mutate the X509Chain and revalidate it. Starting in .NET 10, if you do so and add a global, shared certificate to the ExtraStore, this certificate is disposed even though it isn't owned by the RemoteCertificateValidationCallback. This results in subsequent requests to the validation callback failing due to trying to use a disposed certificate. The confusion is compounded by the fact that the exception isn't an ObjectDisposedException, but instead is a NullReferenceException because X509Certificate.Pal is null after dispose but assumed to be non-null in X509Chain.Build.

Reproduction Steps

using System.Security.Cryptography.X509Certificates;

using var httpClient = new HttpClient(
    new SocketsHttpHandler
    {
        SslOptions =
        {
            RemoteCertificateValidationCallback = (sender, cert, chain, errors) =>
            {
                chain!.ChainPolicy.ExtraStore.Add(Certificates.CapellaCaCert);
                chain.Reset();
                var built = chain.Build((X509Certificate2)cert!);

                return built && errors == System.Net.Security.SslPolicyErrors.None;
            }
        },
        PooledConnectionLifetime = TimeSpan.Zero,
    });

await httpClient.GetAsync("https://microsoft.com/");

static class Certificates
{
    public static ReadOnlySpan<byte> CapellaCaCertPem =>
        @"-----BEGIN CERTIFICATE-----
        MIIDFTCCAf2gAwIBAgIRANLVkgOvtaXiQJi0V6qeNtswDQYJKoZIhvcNAQELBQAw
        JDESMBAGA1UECgwJQ291Y2hiYXNlMQ4wDAYDVQQLDAVDbG91ZDAeFw0xOTEyMDYy
        MjEyNTlaFw0yOTEyMDYyMzEyNTlaMCQxEjAQBgNVBAoMCUNvdWNoYmFzZTEOMAwG
        A1UECwwFQ2xvdWQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCfvOIi
        enG4Dp+hJu9asdxEMRmH70hDyMXv5ZjBhbo39a42QwR59y/rC/sahLLQuNwqif85
        Fod1DkqgO6Ng3vecSAwyYVkj5NKdycQu5tzsZkghlpSDAyI0xlIPSQjoORA/pCOU
        WOpymA9dOjC1bo6rDyw0yWP2nFAI/KA4Z806XeqLREuB7292UnSsgFs4/5lqeil6
        rL3ooAw/i0uxr/TQSaxi1l8t4iMt4/gU+W52+8Yol0JbXBTFX6itg62ppb/Eugmn
        mQRMgL67ccZs7cJ9/A0wlXencX2ohZQOR3mtknfol3FH4+glQFn27Q4xBCzVkY9j
        KQ20T1LgmGSngBInAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE
        FJQOBPvrkU2In1Sjoxt97Xy8+cKNMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0B
        AQsFAAOCAQEARgM6XwcXPLSpFdSf0w8PtpNGehmdWijPM3wHb7WZiS47iNen3oq8
        m2mm6V3Z57wbboPpfI+VEzbhiDcFfVnK1CXMC0tkF3fnOG1BDDvwt4jU95vBiNjY
        xdzlTP/Z+qr0cnVbGBSZ+fbXstSiRaaAVcqQyv3BRvBadKBkCyPwo+7svQnScQ5P
        Js7HEHKVms5tZTgKIw1fbmgR2XHleah1AcANB+MAPBCcTgqurqr5G7W2aPSBLLGA
        fRIiVzm7VFLc7kWbp7ENH39HVG6TZzKnfl9zJYeiklo5vQQhGSMhzBsO70z4RRzi
        DPFAN/4qZAgD5q3AFNIq2WWADFQGSwVJhg==
        -----END CERTIFICATE-----"u8;

    public static X509Certificate2 CapellaCaCert { get; } =
        X509CertificateLoader.LoadCertificate(CapellaCaCertPem);
}

Expected behavior

The certificate added to ExtraStore is not disposed by the SslStream when added by the callback. Additionally, if the ExtraStore does receive a disposed certificate, an ObjectDisposedException should be thrown rather than a NullReferenceException.

Actual behavior

There is a NullReferenceException thrown, on both Windows and Linux, internally when building the certificate chain. The stack trace below is on Windows, it is different but similar on Linux.

System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception.
 ---> System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Security.Cryptography.X509Certificates.StorePal.LinkFromCertificateCollection(X509Certificate2Collection certificates)
   at System.Security.Cryptography.X509Certificates.ChainPal.BuildChain(Boolean useMachineContext, ICertificatePal cert, X509Certificate2Collection extraStore, OidCollection applicationPolicy, OidCollection certificatePolicy, X509RevocationMode revocationMode, X509RevocationFlag revocationFlag, X509Certificate2Collection customTrustStore, X509ChainTrustMode trustMode, DateTime verificationTime, TimeSpan timeout, Boolean disableAia)
   at System.Security.Cryptography.X509Certificates.X509Chain.Build(X509Certificate2 certificate, Boolean throwOnException)
   at Couchbase.Core.IO.Authentication.X509.CertificateFactory.<>c__DisplayClass7_0.<GetValidatorWithPredefinedCertificates>b__0(Object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) in E:\repos\couchbase-net-client\src\Couchbase\Core\IO\Authentication\X509\CertificateFactory.cs:line 147
   at Couchbase.Core.IO.Connections.CallbackCreator.Callback(Object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) in E:\repos\couchbase-net-client\src\Couchbase\Core\IO\Connections\CallbackCreator.cs:line 74
   at Couchbase.Core.IO.HTTP.CouchbaseHttpClientFactory.<>c__DisplayClass11_1.<CreateClientHandler>b__1(Object __sender, X509Certificate __certificate, X509Chain __chain, SslPolicyErrors __sslPolicyErrors) in E:\repos\couchbase-net-client\src\Couchbase\Core\IO\HTTP\CouchbaseHttpClientFactory.cs:line 171
   at System.Net.Security.SslStream.VerifyRemoteCertificate(RemoteCertificateValidationCallback remoteCertValidationCallback, SslCertificateTrust trust, ProtocolToken& alertToken, SslPolicyErrors& sslPolicyErrors, X509ChainStatusFlags& chainStatus)
   at System.Net.Security.SslStream.CompleteHandshake(ProtocolToken& alertToken, SslPolicyErrors& sslPolicyErrors, X509ChainStatusFlags& chainStatus)
   at System.Net.Security.SslStream.CompleteHandshake(SslAuthenticationOptions sslAuthenticationOptions)
   at System.Net.Security.SslStream.ForceAuthenticationAsync[TIOAdapter](Boolean receiveFirst, Byte[] reAuthenticationData, CancellationToken cancellationToken)
   at System.Net.Security.SslStream.ProcessAuthenticationWithTelemetryAsync(Boolean isAsync, CancellationToken cancellationToken)
   at System.Net.Http.ConnectHelper.EstablishSslConnectionAsync(SslClientAuthenticationOptions sslOptions, HttpRequestMessage request, Boolean async, Stream stream, CancellationToken cancellationToken)
   --- End of inner exception stack trace ---
   at System.Net.Http.ConnectHelper.EstablishSslConnectionAsync(SslClientAuthenticationOptions sslOptions, HttpRequestMessage request, Boolean async, Stream stream, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.ConnectAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.CreateHttp11ConnectionAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.InjectNewHttp11ConnectionAsync(QueueItem queueItem)
   at System.Threading.Tasks.TaskCompletionSourceWithCancellation`1.WaitWithCancellationAsync(CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionWaiter`1.WaitForConnectionWithTelemetryAsync(HttpRequestMessage request, HttpConnectionPool pool, Boolean async, CancellationToken requestCancellationToken)
   at System.Net.Http.HttpConnectionPool.SendWithVersionDetectionAndRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken)
   at System.Net.Http.DiagnosticsHandler.SendAsyncCore(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
   at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
   at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
   at Couchbase.Search.SearchClient.QueryAsync(String indexName, FtsSearchRequest ftsSearchRequest, VectorSearch vectorSearch, IScope scope, CancellationToken cancellationToken) in E:\repos\couchbase-net-client\src\Couchbase\Search\SearchClient.cs:line 161

Regression?

This worked on .NET 9 and other previous versions of .NET. It regressed in .NET 10, and appears to have been caused by this change: 901395a#diff-af1f1cbeb01ef6f5fab8d41b75d3f9ac05e31fe12e424c3e07f0740294d292e8R1167-R1172

Known Workarounds

Clear the certificates added to the ExtraStore before returning from the callback.

https://review.couchbase.org/c/couchbase-net-client/+/238451

Configuration

.NET 10 Windows or Linux, possibly other OSes

Other information

No response

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions