Anatomy of the .NET Core: how we configured NTLM under Linux

We continue to talk about the migration of mobile services in ASP.NET Core and Docker. This article will talk about the WCF client module mentioned in the previous article , NTLM-authorization and other problems during its migration. Now let's talk about why we had to study the anatomy a bit and feel the .NET Core from the inside.
Anatomy of the .NET Core: how we configured NTLM under Linux configured debug in the docker-image and locally launched the service in the windows-container.
When I tried to send a request to the WCF service, I received a very florid error:
System.ServiceModel.Security.MessageSecurityException: The HTTP request is unauthorized with client authentication scheme 'Negotiate'. The authentication header received from the server was' Negotiate TlRMTVNTUAACAAAAEAAQADgAA


By the method of samples came to the fact that in the credentials of the service it is required to specify Domain. It's funny that you can specify any value, if only not null - then it works.

    static partial void ConfigureEndpoint (ServiceEndpoint serviceEndpoint, ClientCredentials clientCredentials)
clientCredentials.Windows.ClientCredential.Domain = "";


Everything, now requests go, now in Windows-container of business ok. We go further.


We try. NET Core under Linux


Switching to an assembly in a Linux container, for the sake of interest, the value of Domain was removed - and it works.


The first problem when sending requests to WCF is related to SSL. Swears like this:


System.Net.Http.CurlException SSL peer certificate or SSH remote key was not OK


Which means: there is no trust in the certificate. If WCF-service sent not only the final certificate, but all the intermediate certificates, there would be no problems.


As decided:


1. We pump out intermediate certificates.


For example, in Chrome, open the link and go to F12 in the Security tab. Next View Certificate → Certification Path. For each certificate, open the View Certificate and on the Details tab of the Copy To File button, save the Base-64 encoded certificates to the project directory. You must change the file extension to .crt.


2. Add a new layer to the Dockerfile.

    FROM microsoft /aspnetcore: latest AS base
FROM base AS cert
COPY Certificates /*. Crt /usr /local /share /ca-certificates /
RUN update-ca-certificates || exit 0


For debugging and experimentation, you can just temporarily disable SSL validation:

    clientCredentials.ServiceCertificate.SslCertificateAuthentication =
new X509ServiceCertificateAuthentication ()
CertificateValidationMode = X509CertificateValidationMode.None,
RevocationMode = X509RevocationMode.NoCheck


The most useful and important thing we learned when we got CurlException is that libcurl is used for network requests.


The delicious part was waiting for us ahead.


Linux + WCF + NTLM = love, but after supper


Now the road blocked this


MessageSecurityException The HTTP request is unauthorized with client authentication scheme 'Negotiate'. The authentication header received from the server was 'Negotiate, NTLM'.


We are changing Security.Transport.ClientCredentialType = HttpClientCredentialType.Windows on HttpClientCredentialType.Ntlm


The error has changed a little, but it did not become easier:


MessageSecurityException The HTTP request is unauthorized with client authentication scheme 'Ntlm'. The authentication header received from the server was 'Negotiate, NTLM'.


Let's make sure that we have not encountered yet another docker-related feature, such as the Domain value.


We start the service in the virtual Ubuntu LTS.

Lyrical digression: Docker for Windows loves Hyper-V and can refuse to work when installing other virtual machines. Therefore, this time I had to raise Ubuntu under Hyper-V, in which the copy between the host and guest machines does not work, which can not but rejoice.
By the way, Microsoft, how is the friendship with Apple?
Nearby lay the Mac with Visual Studio installed. His hands were combed. On startup with the SslCertificateAuthentication disabled, the error falls. The handler does not support any custom handling of certificates with this combination of libcurl (???) and its SSL backend ("SecureTransport") . If you reassert the validation of certificates, then there will be the same error with NTLM. And yet, the first error brings a suspicion that the differences from Linux can be significant.
What are the other ways to return?
Ubuntu on Windows - when the service started, they rested on the error System.DllNotFoundException: Unable to load DLL 'System.Net.Http.Native' .

And on the case: on a pure Linux the error is exactly the same as in the container, which means that the problem lies in the implementation of the WCF client.


We try to go on the other side. From the command line, run:


1. curl -v --negotiate - it ends with the error

    * gss_init_sec_context () failed: SPNEGO can not find mechanisms to negotiate.    


2. curl -v --ntlm - all is well, the query works


It's time to remember the list of officially supported feature in WCF . In the search line it is written that Core on Linux does not know how to use NTLM. But this does not stick with the fact that curl is able, and it would be strange not to realize such a popular option.


From on the Internet we learn that Negotiate Negotiate is different: in certain implementations there is support for fallback Kerberos → NTLM (everywhere on Windows), but in others it does not. Curl of the latter, and Negotiate becomes an obstacle.


All this suggests that HttpClient can not take into account this nuance, which means that there is hope of winning.


We look at the sources


And here you can not be happy about the new Microsoft for their decision to open the code to the world. In sorts find the key CURLHANDLER_DEBUG_VERBOSE = true , which tells us what libcurl does when WCF requests are executed.


In the logs we already see the familiar error gss_init_sec_context () failed and for HttpClientCredentialType.Windows, and for HttpClientCredentialType.Ntlm.


Now it's clear that the WCF client does not respond to switching from Windows-based authorization to NTLM and tries to use Negotiate in both cases. Most likely, this is due to the WWW-Authentication 'Negotiate, NTLM' double header, which sends the WCF service, and since Negotiate is a stronger authorization, it is used.


From the libcurl mank, it was appreciated that the authorization type is set via the option CURLOPT_HTTPAUTH . On this trail we went to authorization selection table :

    private static readonly KeyValuePair   []s_orderedAuthTypes = new KeyValuePair   []{
new KeyValuePair ("Negotiate", CURLAUTH.Negotiate),
new KeyValuePair ("NTLM", CURLAUTH.NTLM),
new KeyValuePair ("Digest", CURLAUTH.Digest),
new KeyValuePair ("Basic", CURLAUTH.Basic),


Attributes static readonly look especially tempting, as it means that it is enough to play with Reflection with the values ​​in the table at the start of the service, and with HTTP requests there will be no overhead.


Added to Program.cs the following code:

    public static void Main (string[].args)
//redirect Negotiate to NTLM (only for libcurl on Linux)
var curlHandlerType = typeof (HttpClient) .Assembly.GetTypes ()
.FirstOrDefault (type => type.Name == "CurlHandler");
if (curlHandlerType! = null)
var authTypesField = curlHandlerType.GetField ("s_orderedAuthTypes",
.BindingFlags.Static | BindingFlags.NonPublic);
var authTypes = authTypesField.GetValue (null);
var authTypesGetByIndex = authTypes.GetType (). GetMethod ("Get");
var ntlmKeyValuePair = authTypesGetByIndex.Invoke (authTypes, new object[]{1});
var ntlmValue = ntlmKeyValuePair.GetType (). GetProperty ("Value");
var CURLAUTH = ntlmValue.GetMethod.ReturnType;
var CURLAUTH_NTLM = ntlmValue.GetValue (ntlmKeyValuePair);
var authTypeKeyValuePairBuilder = typeof (KeyValuePairBuilder <,> )
.MakeGenericType (new[]{Typeof (string), CURLAUTH});
var builder = Activator.CreateInstance (authTypeKeyValuePairBuilder);
var negotiateToNtlmKeyValuePair = authTypeKeyValuePairBuilder
. GetMethod ("Build")
.Invoke (builder, new object[]{"", CURLAUTH_NTLM});
var authTypesSetByIndex = authTypes.GetType (). GetMethod ("Set");
authTypesSetByIndex.Invoke (authTypes,
new object[]{? negotiateToNtlmKeyValuePair});
//makes it possible to call Activator.CreateInstance on KeyValuePair struct
public class KeyValuePairBuilder
public KeyValuePair Build (K k, V v)
return new KeyValuePair (k, v);


Here we nail the correspondence between "Negotiate" and CURLAUTH.NTLM.


Voila, now the queries are working successfully.


Bonus track


We did not stop there. If you look closely at the logs, you can see that one WCF request-response includes several HTTP requests and responses, and one of the answers stably returns from Bad Request . What's the matter?


For an erroneous query, the method is used. HEAD . In fact, the same behavior is easily emulated with curl-I . In libcurl, this corresponds to the option CURLOPTION_NOBODY . In corefx this option is used when sending HttpMethod.Head Requests.
We go up the stack in WCF. We see that in the method SendPreauthenticationHeadRequestIfNeeded is sent. HEAD request for authorization, and all errorsbki just ignored:

//There is a possibility that a HEAD pre-auth request might fail when the actual request
//will succeed. For example, when the web service refuses. HEAD requests. We do not want
//to fail the actual request because of some subtlety which causes the HEAD request.
await SendPreauthenticationHeadRequestIfNeeded ();
catch {/* ignored * /}


Here the flag similar to obviously suggests itself. HttpClientHandler.PreAuthenticate , in order not to run a request, doomed in advance to 400.


Since they have not taken care of us, we will cut them.


Method SendPreauthenticationHeadRequestIfNeeded asynchronous, so his patching can lead to a red-eye at an extremely early age. If you look around, you can see a simple and unpretentious method AuthenticationSchemeMayRequireResend . Obviously, if it always returns false, then will not be started. SendPreauthenticationHeadRequestIfNeeded .


We proceed to the operation.


We add a new project WcfPreauthPatch to the solution. Now we put Cecil, by means of which we will get into IL-code. You need a beta version to work under .NET Core.


Install-Package Mono.Cecil -Version ???-beta7-ProjectName WcfPreauthPatch


The code is:

    static void Main (string[].args)
var curlHandlerType = typeof (HttpClient) .Assembly.GetTypes ()
.FirstOrDefault (type => type.Name == "CurlHandler");
if (curlHandlerType == null) return; //continue only when libcurl is used
var wcfDllPath = typeof (System.ServiceModel.ClientBase <>)
var wcfAssembly = AssemblyDefinition.ReadAssembly (wcfDllPath);.
var requestType = wcfAssembly.MainModule.GetAllTypes ()
.FirstOrDefault (type =. > type.Name.Contains ("HttpClientChannelAsyncRequest"));
var authRequiredMethod = requestType.Methods
.FirstOrDefault (method => method.Name.Contains ("AuthenticationSchemeMayRequireResend"));
.authRequiredMethod.Body.Instructions .Insert (? Instruction.Create (OpCodes.Ldc_I4_0)); //put false on stack
.authRequiredMethod.Body.Instructions.Insert (? Instruction.Create (OpCodes.Ret));
.wcfAssembly.Write (wcfDllPath + ".patched");
.File.Delete (wcfDllPath);
.File.Move (wcfDllPath + ".patched", wcfDllPath);

In the Dockerfile, add

    # for build image
FROM build AS build-wcf-patch
WORKDIR /src /WcfPreauthPatch /
RUN dotnet build -c Debug -o /app
# for release image
FROM base AS base-wcf-preauth-fixed
COPY --from = publish /app.
RUN dotnet WcfPreauthPatch.dll

Run the service and make sure that the logs with one query is less.
WCF-client in .NET Core delivered us a lot of trouble.
On github already there is a discussion of the issues raised in the article and questions:
1. Negotiate /NTLM
2. Preauthentication request
However, as we saw, these problems are not completely solved. We hope that our 5 kopecks in the discussion will add a new turn to the process.

For a snack a few ideas and facts

When integration with the docker is implemented, patching can be started as a postbuild target.
There are NTLM-proxy, for example, CNTLM. An alternative path with NTLM proxy configuration inside the container also has perspectives and is more versatile, and the ready customized image will be worthy of computation on the Docker Hub.
Hypothetically, you can try to edit the authorization helper WWW-Authentication, which comes from the WCF-service. It is necessary to redefine the behavior of the WCF client via IEndpointBehavior and the AfterReceiveReply method. However, this will only work if the preauthentication request is disabled, because AfterReceiveReply will not catch him.
If you use /have access to HttpClient, here is reference on a workaround for a similar problem with NTLM.
It is not possible to patch CurlHandler with Cecil: System.Net.Http.dll is a mixed mode assembly, and such an option is not yet supported in Cecil.
Substitution of a pointer to a method in runtime, described in article , does not work, do not try.
Under Linux in. NET Core there is no support for function breakpoints.
+ 0 -

Add comment