-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Description
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.
Configuration
.NET 10 Windows or Linux, possibly other OSes
Other information
No response