This post was most recently updated on June 30th, 2021.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 70s, if not before. But in the cloud, with dependencies to other services, 7 years is a long time!
So – I was brought onboard to solve the issue. The service was supposed to be connecting to SharePoint Online. It was originally developed by a 3rd party who were not involved anymore, and even if they were they might not have been useful, as the lead developer had passed away anyway.
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 a 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()
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 backwards compatibility reasons).
So… Out comes the wizardry.
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 olden 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 caled “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>
Don’t 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 cheat your 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.
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>
Some of these links helped, some didn’t really, but I’m documenting them here anyway for any later usage.
- 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:
- HttpContext.WebSockets.IsWebSocketRequest always null in .NET Core? - July 27, 2021
- Docker-compose fails on Windows with “Error while fetching server API version: (2, ‘CreateFile’, ‘The system cannot find the file specified.’)” - July 20, 2021
- Azure DevOps suddenly 404s for your project? Easy fix! - July 13, 2021