This post was most recently updated on July 26th, 2024.
5 min read.This article explains a kind of weird fix to a pretty annoying issue with a legacy service. And I’m not just calling it legacy because it’s WCF – I’m calling it legacy because it’s old. The particular piece of software was developed about 7 years ago (2014-ish)! It had worked for quite a while – but now it was down.
I know, I know – 2014 is not THAT old. There’s surely COBOL that’s been running since the 70s, if not before. But in the cloud, with dependencies on other services, 7 years is a long time!
So – I was brought on board to solve the issue. The service was supposed to be connecting to SharePoint Online. It was originally developed by a 3rd party who was not involved anymore, and even if they were they might not have been useful, as the lead developer had passed away.
Problem
The developer originally built the service using .NET Framework 4.5.1. The developer was not available – a rumor tells me he has in fact perished. The source code was not available, either. All of the exceptions were seemingly swallowed. Application Insights wasn’t in use.
Okay, I just… Yeah.
Where do you start with this?
Well, I started by enabling Application Insights and reproducing the issue. That gave me some small extra hints.
And these failed dependencies looked somewhat like this:
Okay. Simple enough. Connection from Azure App Service to SharePoint failed.
The next step was downloading the binaries, investigating them with dotPeek, and copy-pasting the super-simplified version of the reflected code to a new console application, developed in .NET Framework 4.5.1.
And sure enough, it didn’t work. Instead, I got the error below:
System.Net.WebException: The underlying connection was closed: An unexpected error occurred on a send. ---> System.IO.IOException: Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host. ---> System.Net.Sockets.SocketException: An existing connection was forcibly closed by the remote host at System.Net.Sockets.Socket.Receive(Byte[] buffer, Int32 offset, Int32 size, SocketFlags socketFlags) at System.Net.Sockets.NetworkStream.Read(Byte[] buffer, Int32 offset, Int32 size) --- End of inner exception stack trace --- at System.Net.Sockets.NetworkStream.Read(Byte[] buffer, Int32 offset, Int32 size) at System.Net.FixedSizeReader.ReadPacket(Byte[] buffer, Int32 offset, Int32 count) at System.Net.Security.SslState.StartReceiveBlob(Byte[] buffer, AsyncProtocolRequest asyncRequest) at System.Net.Security.SslState.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest) at System.Net.Security.SslState.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest) at System.Net.Security.SslState.ForceAuthentication(Boolean receiveFirst, Byte[] buffer, AsyncProtocolRequest asyncRequest) at System.Net.Security.SslState.ProcessAuthentication(LazyAsyncResult lazyResult) at System.Net.TlsStream.CallProcessAuthentication(Object state) at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx) at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx) at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state) at System.Net.TlsStream.ProcessAuthentication(LazyAsyncResult result) at System.Net.TlsStream.Write(Byte[] buffer, Int32 offset, Int32 size) at System.Net.PooledStream.Write(Byte[] buffer, Int32 offset, Int32 size) at System.Net.ConnectStream.WriteHeaders(Boolean async) --- End of inner exception stack trace --- at System.Net.HttpWebRequest.GetResponse() at Microsoft.SharePoint.Client.Idcrl.SharePointOnlineAuthenticationProvider.GetIdcrlHeader(Uri url, Boolean alwaysThrowOnFailure, EventHandler1 executingWebRequest) at Microsoft.SharePoint.Client.Idcrl.SharePointOnlineAuthenticationProvider.GetAuthenticationCookie(Uri url, String username, SecureString password, Boolean alwaysThrowOnFailure, EventHandler1 executingWebRequest) at Microsoft.SharePoint.Client.SharePointOnlineCredentials.GetAuthenticationCookie(Uri url, Boolean refresh, Boolean alwaysThrowOnFailure) at Microsoft.SharePoint.Client.ClientRuntimeContext.SetupRequestCredential(ClientRuntimeContext context, HttpWebRequest request) at Microsoft.SharePoint.Client.SPWebRequestExecutor.GetRequestStream() at Microsoft.SharePoint.Client.ClientContext.GetFormDigestInfoPrivate() at Microsoft.SharePoint.Client.ClientContext.EnsureFormDigest() at Microsoft.SharePoint.Client.ClientContext.ExecuteQuery()
What gives?
Reason
Starting late 2020 (15.10.), Microsoft started completely disallowing TLS 1.0 and 1.1 for clients connecting to SharePoint Online. See References for more details. This process was partially postponed due to COVID-19, but in March/April most of the last API endpoints stopped accepting connections using these protocols.
.NET Framework 4.5.1 uses TLS 1.0 by default, even if the platform (such as an Azure App Service or your IIS instance) supports newer protocol versions.
So without some wizardry, your .NET Framework 4.5.1 application won’t be able to connect to SharePoint Online (among other things).
But your app already knows TLS 1.2 if it’s built on .NET Framework 4.5+. It can use it. It just chooses not to (probably for backward compatibility reasons).
So… Out comes the wizardry.
Solution
Before you even ask: Microsoft won’t enable TLS 1.0 for you. So that’s not a solution. Instead, you’ve got a couple of options:
- Modernize your application to .NET Framework 4.7.2 or newer (it’ll use TLS 1.2 by default then)
- Add this line to your application: ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
- Convince your app to use TLS 1.2 using unholy and poorly documented workarounds
I didn’t have the source code available, so I couldn’t modernize the application. I couldn’t add the code for the second option either, so I pretty much had one option left: Convincing my app to use TLS 1.2 (using unholy and poorly documented workarounds).
But how do you convince an app it should just magically become secure all of a sudden? It’s so used to its old ways!
Well, as always, there are workarounds available :)
Solution 1: Sweet-talk your console app to behave well
For console apps, you can fix all kinds of things using the associated .config file. And especially for this matter, you have a plethora of switches that you can apply by supplying them as a ;-separated string of key-boolean -pairs, inside a tag called “AppContextSwitchOverrides”.
Kind of convoluted, right? And like I said, the switches are plentiful. See the whole list in the references -section.
Anyway, for this use case, Microsoft realized this beautifully named switch: DontEnableSchUseStrongCrypto.
<runtime>
<AppContextSwitchOverrides value="Switch.System.Net.DontEnableSchUseStrongCrypto=false" />
</runtime>
So the switch tells the server now to enable “something” … And now we’ll set it to “false”.
So essentially, “disable something” is set to “false”. So we actually want to enable, that something, instead of not NOT enabling it.
Confusing, right? Naming stuff is hard. But this way, we’ll actually kindly suggest our application uses the strongest possible cryptography available – and for .NET Framework 4.5+, TLS 1.2 is usable, just not used by default.
And incredibly – if your first option doesn’t work, there’s another one you can use! Just as confusing.
<runtime>
<AppContextSwitchOverrides value="Switch.System.Net.DontEnableSystemDefaultTlsVersions=false"/>
</runtime>
This will definitely also work on some non-console apps – but our little WCF Service would not have any of our sweet-talking, so we had to try something else!
Solution 2: Trick your web app to think it knows TLS 1.2
When sweet-talking doesn’t help, it’s time for some trickery!
Time needed: 10 minutes
How to try and force your ASP.NET web app into using TLS 1.2?
- Find your web.config file
If you’re editing a live application in Azure App Service, access Kudu and navigate to site -> wwwroot, and the web.config is either in the root or in the folder mapped to your virtual path.
- Change your Target Framework to be 4.7.2 or newer
Inside <configuration>, you should find <system.web> (and if you don’t, just create it). Add something like this inside it:
- TLS 1.2 for fun and profit!
Your application now thinks it should use the defaults for .NET Framework 4.7.2, and, like magic, it switches to TLS 1.2.
… and that should solve your problem. For now.
Hey, if it’s stupid and it works, it’s not stupid.
Oh – and here’s a copy-pasteable web.config file for your reference:
<configuration>
<system.web>
<compilation targetFramework="4.5.1" />
<httpRuntime targetFramework="4.7.2"/>
</system.web>
</configuration>
References
Some of these links helped, some didn’t really, but I’m documenting them here anyway
- TLS 1.0 & TLS 1.1 deprecation:
- How to force TLS 1.2:
- Legacy Auth against SharePoint:
- .NET Guidelines and other cool stuff:
- Background stuff and other useful links:
- https://stackoverflow.com/questions/45382254/update-net-web-service-to-use-tls-1-2
- https://docs.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/startup/supportedruntime-element
- https://docs.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/runtime/appcontextswitchoverrides-element
- https://docs.microsoft.com/en-us/dotnet/framework/migration-guide/retargeting/4.5.1-4.7
- https://stackoverflow.com/questions/34920429/is-tls-1-1-and-tls-1-2-enabled-by-default-for-net-4-5-and-net-4-5-1/45692875
- “Performing cleanup” – Excel is stuck with an old, conflicted file and will never recover. - November 12, 2024
- How to add multiple app URIs for your Entra app registration? - November 5, 2024
- How to access Environment Secrets with GitHub Actions? - October 29, 2024