-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Description
Description
This is a brief summary of a longer internal conversation I had with @bartonjs after I saw outbound SSL (and therefore all HTTP) connections suddenly fail with this exception in some customer machines, after our product upgraded from 2.2 to 3.1, as a result of which, this has now been documented as a breaking change here. But this doesn't solve the problem. A result of this change is, beyond the regression itself from 2.2, is that while all non .NET processes work fine with SSL, including curl, Python and GoLang, .NET fails, depending on the configuration mentioned below, and whether the process is running as root or otherwise.
Configuration
- .NET 3.0+
- SLES 12 SP2+. This could happen with any distro in similar situations as below.
- x64
- Necessary root certs only have the
BEGIN TRUSTED CERTIFICATEversion. Typically in SLES,SSL_CERT_DIR = /var/lib/ca-certificates/opensslhas this version. - Necessary root certs have a
BEGIN CERTIFICATEversion, but it's only copy is in/etc/ssl/certs, which is not seen by .NET as it's notSSL_CERT_DIRon SLES. GoLang has done a fix to explicitly look there for SLES. This could be the onlyBEGIN CERTIFICATEcopy since customers may not have run the distro specific sync script which updates the concatenatedSSL_CERT_FILE. At least in the case of SLES, there's not a strong case to mandate this, as the script puts this notice in the generated file:
# automatically created by /usr/lib/ca-certificates/update.d/99certbundle.run. Do not edit!
#
# Use of this file is deprecated and should only be used as last
# resort by applications that do not support p11-kit or reading /etc/ssl/certs.
# You should avoid hardcoding any paths in applications anyways though. Use
# functions that know the operating system defaults instead:
#
# - openssl: SSL_CTX_set_default_verify_paths()
# - gnutls: gnutls_certificate_set_x509_system_trust(cred)
#
- Necessary root certs have a
BEGIN CERTIFICATEversion in theSSL_CERT_FILE, but it's not accessible to non-root users, perhaps because customers have a umask with o-rwx as a matter of security hardening the root user before they run the distro specific sync script. In case of SLES,SSL_CERT_FILE = /var/lib/ca-certificates/ca-bundle.pemand the script isupdate-ca-certificates.
So effectively on SLES, we are dependent solely on the concatenated bundle file's BEGIN CERTIFICATE entries, and ignoring both /etc/ssl/certs and the BEGIN TRUSTED CERTIFICATE versions in the SSL_CERT_DIR.
Regression?
Yes, this is a problem starting .NET Core 3.0.
Other information
Multiple fixes are possible:
- Optimal: Generalize chain building. Chain will build from leaf to root cert by maximizing the purpose of the overall chain, first trying a general cert and then a more specific one, while keeping track of the intersection of purposes as the effective chain purpose. Users of the chain may use it or not based on purpose. IMHO, this should not break either the purpose behind this regression as well as the cases discussed in this issue.
- Spill: Another modification to above option is to allow users to specify a set of purposes for every chain building run with a default retaining current behavior. Http/Ssl layers can override it.
- Low-hanging: Explicitly add
/etc/ssl/certsto the lookup directory, on top ofSSL_CERT_DIR.
It appears the low-hanging fix is a reasonable one to take. It's also the least risk IMO, as the deprecation notice in ca-bundle.pem in the case of SLES is explicitly telling you to read that directory; as are other language runtimes.
Sample exception causing HTTP failures:
---> System.Security.Authentication.AuthenticationException: The remote certificate is invalid according to the validation procedure.
at System.Net.Security.SslStream.StartSendAuthResetSignal(ProtocolToken message, AsyncProtocolRequest asyncRequest, ExceptionDispatchInfo exception)
at System.Net.Security.SslStream.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.ProcessReceivedBlob(Byte[] buffer, Int32 count, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.StartReadFrame(Byte[] buffer, Int32 readBytes, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.StartReceiveBlob(Byte[] buffer, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.ProcessReceivedBlob(Byte[] buffer, Int32 count, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.StartReadFrame(Byte[] buffer, Int32 readBytes, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.PartialFrameCallback(AsyncProtocolRequest asyncRequest)
--- End of stack trace from previous location where exception was thrown ---
at System.Net.Security.SslStream.ThrowIfExceptional()
at System.Net.Security.SslStream.InternalEndProcessAuthentication(LazyAsyncResult lazyResult)
at System.Net.Security.SslStream.EndProcessAuthentication(IAsyncResult result)
at System.Net.Security.SslStream.EndAuthenticateAsClient(IAsyncResult asyncResult)
at System.Net.Security.SslStream.<>c.<AuthenticateAsClientAsync>b__65_1(IAsyncResult iar)
at System.Threading.Tasks.TaskFactory`1.FromAsyncCoreLogic(IAsyncResult iar, Func`2 endFunction, Action`1 endAction, Task`1 promise, Boolean requiresSynchronization)
--- End of stack trace from previous location where exception was thrown ---
at System.Net.Http.ConnectHelper.EstablishSslConnectionAsyncCore(Stream stream, SslClientAuthenticationOptions sslOptions, CancellationToken cancellationToken)
--- End of inner exception stack trace ---
at System.Net.Http.ConnectHelper.EstablishSslConnectionAsyncCore(Stream stream, SslClientAuthenticationOptions sslOptions, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.ConnectAsync(HttpRequestMessage request, Boolean allowHttp2, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.CreateHttp11ConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.GetHttpConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean doRequestAuth, CancellationToken cancellationToken)
at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
at System.Net.Http.HttpClient.FinishSendAsyncBuffered(Task`1 sendTask, HttpRequestMessage request, CancellationTokenSource cts, Boolean disposeCts)