The C# code below, which logs in to a website,
- Works when called on computer A from Excel 2010 VBA via com-interop
- Works when called on computer B from a C# console application, but
- Fails when called on computer B from Excel 2010 VBA via com-interop
The main difference between computer A and computer B is that computer A has windows 10 version 1803, whereas computer B has windows 10 version 1809. Both computers have Studio 2017, and in all cases the target .Net Framework is 4.6.2.
using System; using System.Runtime.InteropServices; using System.IO; [Guid("97E1D9DB-8478-4E56-9D6D-26D8EF13B100")] [ComVisible(true)] public interface IToExcel { string Do(); } [Guid("BBF87E31-77E2-46B6-8093-1689A144BFC6")] [ClassInterface(ClassInterfaceType.None)] [ComVisible(true)] public class Main : IToExcel { private const string XAPP_ID = "..."; private const string USERNAME = "..."; private const string PASSWORD = "..."; private const string CERT_FILE = @"..."; private const string CERT_PASSWORD = "..."; private const string WEBSITE = "https:// ..."; public string Do() { System.Net.HttpWebRequest request = (System.Net.HttpWebRequest)System.Net.WebRequest.Create(new Uri(WEBSITE)); request.AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate; request.Method = "POST"; request.Accept = "application/json"; request.Timeout = request.ReadWriteTimeout = 20000; request.ContentType = "application/x-www-form-urlencoded"; request.UseDefaultCredentials = true; request.Proxy = null; // setup headers System.Net.WebHeaderCollection whc = new System.Net.WebHeaderCollection { { "X-Application", XAPP_ID }, { System.Net.HttpRequestHeader.AcceptCharset, "utf-8" }, { System.Net.HttpRequestHeader.AcceptEncoding, "gzip,deflate" } }; request.Headers.Add(whc); // setup certificate System.Security.Cryptography.X509Certificates.X509Certificate2 m_x509certificate = new System.Security.Cryptography.X509Certificates.X509Certificate2(CERT_FILE, CERT_PASSWORD); request.ClientCertificates.Add(m_x509certificate); // do call using (Stream stream = request.GetRequestStream()) { using (StreamWriter writer = new StreamWriter(stream, System.Text.Encoding.Default)) { writer.Write("username=" + USERNAME + "&password=" + PASSWORD); } } string responseData = string.Empty; using (System.Net.WebResponse response = request.GetResponse()) { using (Stream responseStream = response.GetResponseStream()) { using (StreamReader reader = new StreamReader(responseStream, System.Text.Encoding.UTF8)) { responseData = reader.ReadToEnd(); } } } return responseData; } } In all cases a little JSON object is returned, where the JSON object has a field called "loginStatus". When it works, "loginStatus"="SUCCESS", but when it fails "loginStatus"="CERT_AUTH_REQUIRED".
I tried looking at all the settings in System.Net.ServicePointManager but in all cases the settings were the same:
- ReusePort: False
- ServerCertificateValidationCallback:
- DnsRefreshTimeout: 120000
- EnableDnsRoundRobin: False
- Expect100Continue: True
- UseNagleAlgorithm: True
- MaxServicePointIdleTime: 100000
- DefaultConnectionLimit: 2
- MaxServicePoints: 0
- SecurityProtocol: Tls, Tls11, Tls12
- CheckCertificateRevocationList: False
- EncryptionPolicy: RequireEncryption
Beyond that, I don't know what else to check. Upgrading from .Net 4.6.2 to 4.7.1 had no effect, the results were the same.
I had wondered whether this is a bug in Windows 1809, but since it works when called directly in a .Net console application I assume it's some subtle configuration issue. Can anyone help me get this working from Excel 2010 on computer B ?
Update 8-Feb-2019
As suggested in the comments, I used Fiddler to review the structure of the https calls that are being made to the website. The two that work look identical, and the one that fails looks slightly different:
Calls that work OK
- TLS extension ec_point_formats = uncompressed [0x0]
- TLS extension encrypt_then_mac (RFC7366) not specified
- TLS extension renegotiation_info = 0
- Cipher TLS_EMPTY_RENEGOTIATION_INFO_SCSV not specified
Failed call
- TLS extension ec_point_formats = uncompressed [0x0], ansiX962_compressed_prime [0x1], ansiX962_compressed_char2 [0x2]
- TLS extension encrypt_then_mac (RFC7366) = empty
- TLS extension renegotiation_info not specified
- Cipher TLS_EMPTY_RENEGOTIATION_INFO_SCSV specified
But now I have that information, I'm not sure if it helps. Perhaps the calls that work are both generated by the same low level code (in spite of the fact that they're on different versions of Windows 10), and the call that fails is generated by different low level code.
Update 10-Feb-2019
When calling from Excel via com-interop, I made the code execute the EXE in a new AppDomain instead of calling the login code directly. And when I did that, the EXE failed to work and produced the same output as if I'd called the login code directly.
Below is some of the Visual Studio Output window which shows the order in which DLLs are loaded when the EXE file runs, just before the login code executes. The biggest difference between both the scenarios which succeed and the scenario which fails is that the scenario which fails never loads C:\Windows\System32\ncryptprov.dll. Does anyone know what causes that DLL to be loaded?
…
(Win32): Loaded 'C:\Windows\System32\msisip.dll'
(Win32): Loaded 'C:\Windows\System32\coml2.dll' --- LOADED EARLIER FROM EXCEL
(Win32): Loaded 'C:\Windows\System32\wshext.dll'
(Win32): Loaded 'C:\Windows\System32\AppxSip.dll'
(Win32): Loaded 'C:\Windows\System32\tdh.dll'
(Win32): Loaded 'C:\Windows\System32\xmllite.dll'
(Win32): Loaded 'C:\Windows\System32\OpcServices.dll'
(Win32): Loaded 'C:\Windows\System32\mintdh.dll'
(Win32): Loaded 'C:\Windows\System32\urlmon.dll' --- LOADED EARLIER FROM EXCEL
(Win32): Loaded 'C:\Windows\System32\mintdh.dll'
(Win32): Unloaded 'C:\Windows\System32\mintdh.dll'
(Win32): Loaded 'C:\Windows\System32\iertutil.dll' --- LOADED EARLIER FROM EXCEL
(Win32): Loaded 'C:\Windows\System32\WindowsPowerShell\v1.0\pwrshsip.dll'
(Win32): Loaded 'C:\Windows\System32\EsdSip.dll'
(Win32): Loaded 'C:\Windows\System32\userenv.dll' --- LOADED EARLIER FROM EXCEL
(Win32): Loaded 'C:\Windows\System32\dpapi.dll'
(Win32): Loaded 'C:\Windows\System32\dnsapi.dll'
(Win32): Loaded 'C:\Windows\System32\rasadhlp.dll'
(Win32): Loaded 'C:\Windows\System32\FWPUCLNT.DLL'
(Win32): Loaded 'C:\Windows\System32\secur32.dll'
(Win32): Loaded 'C:\Windows\System32\sspicli.dll' --- LOADED EARLIER FROM EXCEL
(Win32): Loaded 'C:\Windows\System32\schannel.dll'
(Win32): Loaded 'C:\Windows\System32\mskeyprotect.dll'
(Win32): Loaded 'C:\Windows\System32\ncrypt.dll'
(Win32): Loaded 'C:\Windows\System32\ntasn1.dll'
(Win32): Loaded 'C:\Windows\System32\ncryptprov.dll' --- NOT EVER LOADED FROM EXCEL
(Win32): Loaded 'C:\Windows\System32\ncryptsslp.dll'
At this point the C# code executes
Update 12-Feb-2019
Many many thanks to Simon Mourier for telling me how to setup the System.Net diagnostics. Running the diagnostics on computer B, the "System.Net information" rows that get output for the two cases start off the same, but eventually there is a difference. This is the output from the Console EXE file on Computer B (i.e. the case that works):
System.Net Information: 0 : [35268] Current OS installation type is 'Client'. System.Net Information: 0 : [35268] RAS supported: True System.Net Information: 0 : [35268] Associating HttpWebRequest#21454193 with ServicePoint#34640832 System.Net Information: 0 : [35268] Associating Connection#43332040 with HttpWebRequest#21454193 System.Net Information: 0 : [35268] Connection#43332040 - Created connection from XXX.XXX.XXX.XXX:53002 to YYY.YYY.YYY.YYY:443. System.Net Information: 0 : [35268] TlsStream#54444047::.ctor(host=<TargetWebSite>, #certs=1, checkCertificateRevocationList=False, sslProtocols=Tls12) System.Net Information: 0 : [35268] Associating HttpWebRequest#21454193 with ConnectStream#20234383 System.Net Information: 0 : [35268] HttpWebRequest#21454193 - Request: POST /api/certlogin HTTP/1.1 System.Net Information: 0 : [35268] ConnectStream#20234383 - Sending headers System.Net Information: 0 : [35268] SecureChannel#47891719::.ctor(hostname=<TargetWebSite>, #clientCertificates=1, encryptionPolicy=RequireEncryption) System.Net Information: 0 : [35268] Enumerating security packages: System.Net Information: 0 : [35268] Negotiate System.Net Information: 0 : [35268] NegoExtender System.Net Information: 0 : [35268] Kerberos System.Net Information: 0 : [35268] NTLM System.Net Information: 0 : [35268] TSSSP System.Net Information: 0 : [35268] pku2u System.Net Information: 0 : [35268] CloudAP System.Net Information: 0 : [35268] WDigest System.Net Information: 0 : [35268] Schannel System.Net Information: 0 : [35268] Microsoft Unified Security Protocol Provider System.Net Information: 0 : [35268] Default TLS SSP System.Net Information: 0 : [35268] CREDSSP System.Net Information: 0 : [35268] SecureChannel#47891719 - Attempting to restart the session using the user-provided certificate: [Version] System.Net Information: 0 : [35268] SecureChannel#47891719 - Left with 1 client certificates to choose from. System.Net Information: 0 : [35268] SecureChannel#47891719 - Trying to find a matching certificate in the certificate store. System.Net Information: 0 : [35268] SecureChannel#47891719 - Locating the private key for the certificate: [Version] System.Net Information: 0 : [35268] SecureChannel#47891719 - Certificate is of type X509Certificate2 and contains the private key. System.Net Information: 0 : [35268] SecureChannel#47891719::.AcquireClientCredentials, new SecureCredential() (flags=(ValidateManual, NoDefaultCred, SendAuxRecord, UseStrongCrypto), m_ProtocolFlags=(Tls12Client), m_EncryptionPolicy=RequireEncryption) System.Net Information: 0 : [35268] AcquireCredentialsHandle(package = Microsoft Unified Security Protocol Provider, intent = Outbound, scc = System.Net.SecureCredential) System.Net Information: 0 : [35268] InitializeSecurityContext(credential = System.Net.SafeFreeCredential_SECURITY, context = (null), targetName = <TargetWebSite>, inFlags = ReplayDetect, SequenceDetect, Confidentiality, AllocateMemory, InitManualCredValidation) System.Net Information: 0 : [35268] InitializeSecurityContext(In-Buffer length=0, Out-Buffer length=184, returned code=ContinueNeeded). System.Net Information: 0 : [35268] InitializeSecurityContext(credential = System.Net.SafeFreeCredential_SECURITY, context = 227c85a89b0:2449d0deff0, targetName = <TargetWebSite>, inFlags = ReplayDetect, SequenceDetect, Confidentiality, AllocateMemory, InitManualCredValidation) System.Net Information: 0 : [35268] InitializeSecurityContext(In-Buffers count=2, Out-Buffer length=0, returned code=CredentialsNeeded). However, when running from Excel 2010 via com-interop, instead of those last 4 InitializeSecurityContext lines, there are 6 InitializeSecurityContext lines as follows:
System.Net Information: 0 : [39988] InitializeSecurityContext(credential = System.Net.SafeFreeCredential_SECURITY, context = (null), targetName = <TargetWebSite>, inFlags = ReplayDetect, SequenceDetect, Confidentiality, AllocateMemory, InitManualCredValidation) System.Net Information: 0 : [39988] InitializeSecurityContext(In-Buffer length=0, Out-Buffer length=184, returned code=ContinueNeeded). System.Net Information: 0 : [39988] InitializeSecurityContext(credential = System.Net.SafeFreeCredential_SECURITY, context = 8a8e2f0:2449d0def90, targetName = <TargetWebSite>, inFlags = ReplayDetect, SequenceDetect, Confidentiality, AllocateMemory, InitManualCredValidation) System.Net Information: 0 : [39988] InitializeSecurityContext(In-Buffers count=2, Out-Buffer length=0, returned code=ContinueNeeded). System.Net Information: 0 : [39988] InitializeSecurityContext(credential = System.Net.SafeFreeCredential_SECURITY, context = 8a8e2f0:2449d0def90, targetName = <TargetWebSite>, inFlags = ReplayDetect, SequenceDetect, Confidentiality, AllocateMemory, InitManualCredValidation) System.Net Information: 0 : [39988] InitializeSecurityContext(In-Buffers count=2, Out-Buffer length=0, returned code=ContinueNeeded). The first two InitializeSecurityContext lines are the identical, so presumably crucial difference is on the third InitializeSecurityContext line, where the console EXE has
context = 227c85a89b0:2449d0deff0
but the failed run via com-interop executino has
context = 8a8e2f0:2449d0def90
After that, things don't look the same, as one would expect. Does anyone know what that difference means, and how to make the difference go away so that the com-interop execution behaves in the same way as the com-interop execution?
Update 13-Feb-2019
I have posted more of the diagnostic output on an MSDN forum.