Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[.NET 9.0 Regression] "Credentials supplied to the package were not recognized" when using certificates with wcf client #110067

Open
julienGrd opened this issue Nov 21, 2024 · 18 comments
Labels
area-System.Net.Security os-windows untriaged New issue has not been triaged by the area owner

Comments

@julienGrd
Copy link

Description

Hello guys, i have a piece of code in my app which call a soap service using wcf client and certificate authentification.

This code work fine in .net8.0, but after upgrading to .net9.0 it stopped working and finish with this exception "Credentials supplied to the package were not recognized".

I was wondering which change made in the framework can explain that.

this is the part where i configure the service

  private void ManageEndpoint<T>(ClientBase<T> pClient) where T: class
  {
      pClient.Endpoint.Address = new EndpointAddress(_urlService);
      ServicePointManager.Expect100Continue = true;
      ServicePointManager.DefaultConnectionLimit = 9999;
      ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
      try
      {
         X509Certificate2 lAuthCertificate = new X509Certificate2(this._certif.CertifContent, this._certif.Password, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);



          if (lAuthCertificate != null && pClient.Endpoint.Address.Uri.AbsoluteUri.StartsWith("https"))
          {

              //Ajout du certificat d’authentification aux crédentials pour etablir la connexion TLS
              if (pClient.ClientCredentials != null) pClient.ClientCredentials.ClientCertificate.Certificate = lAuthCertificate;

              System.Net.ServicePointManager.ServerCertificateValidationCallback += CertificateValidationCallBack;

              CheckCertificates();
          }
      }
      catch (Exception ex)
      {
          throw new Exception("Certificat INS invalide : " + ex.Message);
      }
  }


  private bool CertificateValidationCallBack(
                                                  object sender,
                                                  System.Security.Cryptography.X509Certificates.X509Certificate certificate,
                                                  System.Security.Cryptography.X509Certificates.X509Chain chain,
                                                  System.Net.Security.SslPolicyErrors sslPolicyErrors)
  {
     return certificate.Subject.Contains("services-ps-tlsm.ameli.fr");
  }

Interesting things : since .net 9.0, all call to ServicePointManager are flag obsolete. But i don't know it the error can come frome here. The "new X509Certificate2" is also obsolete but if i change for "X509CertificateLoader.LoadPkcs12" i still have same error.

This error is really annoying for me, it would be really a pain to come back in .net 8.0 as i already upgraded many client.

Unfortunately i can't give a repro project as there is very sensitives informations, especially the certificate used for connexion (but if its really necessary, i will check that).

Do you have some tips on how i can debug that or if you know which changes can explain that ?

This issue is high priority for me.

thanks !

Reproduction Steps

calling wcf service with client certificate authentication

Expected behavior

Have the same behavior wetween .net8.0 and .net9.0

Actual behavior

the code throw exception in .net9.0

Regression?

yes, was working in .net8.0

Known Workarounds

no workaround for now

Configuration

.net9, win-x64

Other information

No response

@dotnet-issue-labeler dotnet-issue-labeler bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Nov 21, 2024
@dotnet-policy-service dotnet-policy-service bot added the untriaged New issue has not been triaged by the area owner label Nov 21, 2024
@vcsjones vcsjones added area-System.Net.Security and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Nov 22, 2024
@wfurt
Copy link
Member

wfurt commented Nov 22, 2024

this looks like dup of #109050. It seems like the something changed in certificate loading.
In general, I think this is sign that schannel cannot access the keys.

@julienGrd
Copy link
Author

this looks like dup of #109050. It seems like the something changed in certificate loading. In general, I think this is sign that schannel cannot access the keys.

I just solved my problem by removed the flag when i imported the certificate (i honestly don't know why i put these flags on the original code, but removing them seem make the job

=> code working in .net 8.0

X509Certificate2 lAuthCertificate = new X509Certificate2(this._certif.CertifContent, this._certif.Password, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);

=> code working in .net 9.0

X509Certificate2 lAuthCertificate = new X509Certificate2(this._certif.CertifContent);

but there is definitivly change in .net9 also in ServicePointManager
In .net 8 this event was fire System.Net.ServicePointManager.ServerCertificateValidationCallback, it's not in .net9.0

This is my final code which works in .net9, without warning and with tlsoption and callback

 private void ManageEndpoint<T>(ClientBase<T> pClient) where T: class
 {
     //porté par CustomHttpClientHander désormais
     //ServicePointManager.Expect100Continue = true;
     //ServicePointManager.DefaultConnectionLimit = 9999;
     ////plante si laisse SSL3
     //ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;// | SecurityProtocolType.Ssl3;
     try
     {
         //on charge le certif
         X509Certificate2 lAuthCertificate = null;
         if (IsCps)
         {
             Pkcs11 lPkcs11 = new Pkcs11();
             lAuthCertificate = lPkcs11.authentificationCertificate;
             //SignatureCertificate = pkcs11.signatureCertificate;
         }
         else
         {
             //ne pas mettre de X509KeyStorageFlags, depuis .net 9 ca fait planté l'appel avec "Les informations d’identification fournies au package n’ont pas été reconnues"
             lAuthCertificate = X509CertificateLoader.LoadPkcs12(this._certif.CertifContent, this._certif.Password);
         }

         pClient.Endpoint.Address = new EndpointAddress(_urlService);
         var httpMessageHandler = new CustomHttpClientHander(IsCps, lAuthCertificate);
         pClient.Endpoint.EndpointBehaviors.Add(new CustomEndpointBehavior(httpMessageHandler));

         if (lAuthCertificate != null && pClient.Endpoint.Address.Uri.AbsoluteUri.StartsWith("https"))
         { // protocole TLS

             //Ajout du certificat d’authentification aux crédentials pour etablir la connexion TLS
             if (pClient.ClientCredentials != null) 
                 pClient.ClientCredentials.ClientCertificate.Certificate = lAuthCertificate;

             //porté par CustomHttpClientHander
             //System.Net.ServicePointManager.ServerCertificateValidationCallback += CertificateValidationCallBack;

             CheckCertificates();
         }
     }
     catch (Exception ex)
     {
         throw new Exception("Certificat INS invalide : " + ex.Message);
     }
 }

 public class CustomEndpointBehavior : IEndpointBehavior
 {
     private readonly Func<HttpMessageHandler> _httpHandler;

     public CustomEndpointBehavior(CustomHttpClientHander factory)
     {
         _httpHandler = () => factory.CreateHandler();
     }

     public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
     {
         bindingParameters.Add(new Func<HttpClientHandler, HttpMessageHandler>(handler => _httpHandler()));
     }

     public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) { }
     public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { }
     public void Validate(ServiceEndpoint endpoint) { }
 }

 public class CustomHttpClientHander(bool IsCps, X509Certificate2 pAuthCertificate)
 {
     public HttpClientHandler CreateHandler()
     {

         var lHandler = new HttpClientHandler
         {
             ServerCertificateCustomValidationCallback = (HttpRequestMessage hrqMessage,
                 X509Certificate2 cert, X509Chain chain, SslPolicyErrors sslPolicyErrors) =>
             {
                 if (!IsCps)
                     return cert.Subject.Contains("services-ps-tlsm.ameli.fr");
                 else
                     return cert.Subject.Contains("services-ps.ameli.fr");
             },
             MaxConnectionsPerServer = 9999,
             SslProtocols = System.Security.Authentication.SslProtocols.Tls12 | System.Security.Authentication.SslProtocols.Tls13
         };

         lHandler.ClientCertificates.Add(pAuthCertificate);

         return lHandler;
     }
 }

@wfurt
Copy link
Member

wfurt commented Nov 22, 2024

For a while the ServicePointManager was only applicable if you use HttpWebRequest but normally ignored by HttpClient. It is certainly possible that WCF has some layer on top of it. If you can reproduce behavior difference without WCF you can open separate issue in this repo. AFAIK obsoletion was really meant only for visibility. (way too many customers drag old properties that do nothing on .NET (Core))

@julienGrd
Copy link
Author

Yeah i don't know exactly neither but there is definitively a strange behavior with the wcf client.

anyway my problem is solved, and i get rid of call at the service point manager so everything is good on my side,,

thanks for your help !

@dotnet-policy-service dotnet-policy-service bot removed the untriaged New issue has not been triaged by the area owner label Nov 22, 2024
@julienGrd julienGrd reopened this Nov 26, 2024
@dotnet-policy-service dotnet-policy-service bot added the untriaged New issue has not been triaged by the area owner label Nov 26, 2024
@julienGrd
Copy link
Author

I reopend this issue because actually my solution not works in production.

i tried the different workaround explain in other issues but nothing seem to work.

do you guys have others ideas or workaround i could try ?

Its kind of a disaster on my side, all my clients are already upgraded to .net 9.0, but without make this works i will have to make a regression to .net8.0.

Any helps will be appreciated

thanks !

@wfurt
Copy link
Member

wfurt commented Nov 26, 2024

There may be old key somewhere on the disk. I think @bartonjs had some instructions how to look in #109050.

You can alto try this little trick:

if (!ephemeralKey && PlatformDetection.IsWindows)
{
X509Certificate2 ephemeral = endEntity;
endEntity = X509CertificateLoader.LoadPkcs12(endEntity.Export(X509ContentType.Pfx), (string?)null, X509KeyStorageFlags.Exportable);
ephemeral.Dispose();
}

exporting and importing the cert may help.

@julienGrd
Copy link
Author

There may be old key somewhere on the disk. I think @bartonjs had some instructions how to look in #109050.

You can alto try this little trick:

runtime/src/libraries/Common/tests/System/Net/Configuration.Certificates.Dynamic.cs

Lines 163 to 168 in 0170d78

if (!ephemeralKey && PlatformDetection.IsWindows)
{
X509Certificate2 ephemeral = endEntity;
endEntity = X509CertificateLoader.LoadPkcs12(endEntity.Export(X509ContentType.Pfx), (string?)null, X509KeyStorageFlags.Exportable);
ephemeral.Dispose();
}
exporting and importing the cert may help.

actually the certificate is not supposed to be installed on the machine but loaded dynamically with the content of the certificate and his password.
I just make a test by taking production certificate of my client, connectiong to the production service on my development machine, and it works..., so its definitively machine related problem.

Maybe its related at the user associated in the pool in the production server ? (don't have total admin rights)

I will try investigate on the production server if i see this certificate is installed somewhere

any others suggestion is welcoming

@julienGrd
Copy link
Author

OK guys, its definitively associated to the user who execute the app.

In production, if i change the user associated with the pool in IIS for an administrator, it start working again.

But its not an acceptable workaround, the user is not supposed to have admin rights in my app.

Based on these new informations, i wait for your suggestions

@bartonjs
Copy link
Member

Private keys, when not loaded with EphemeralKeySet, are stored in either a machine central location (which you can force with MachineKeySet) or a user relative one (which you can force with UserKeySet). If neither User or Machine are specified, the contents of the PFX decide on a key-by-key basis. (EphemeralKeySet doesn't work with SslStream on Windows, because it doesn't work with the underlying Windows TLS provider).

There are two different machine central locations (because why not) and two different user locations; one for the legacy Windows CAPI system, another for the "new" (2005) Windows CNG system. The only difference between .NET 8 and .NET 9 certificate loading when using new X509Certificate2(pfx, ...) is that a PFX that doesn't have opinions across the two will now end up in CNG when it used to end up in CAPI. (If you change to the new loader and don't specify custom loader limits, then all keys end up in CNG).

If you can access the PFX that's failing, you can share metadata about it with something like

A work in progress program to show metadata of a PFX
using System.CodeDom.Compiler;
using System.Formats.Asn1;
using System.Security.Cryptography;
using System.Security.Cryptography.Pkcs;
using System.Security.Cryptography.X509Certificates;

namespace PfxInfo
{
    internal class Program
    {
        private static IndentedTextWriter s_writer = new IndentedTextWriter(Console.Out, "  ");

        private static void Main(string[] args)
        {
            if (args.Length == 0)
            {
                Console.WriteLine("Input one or more files to read.");
                return;
            }

            foreach (string path in args)
            {
                ShowPfx(path);
            }
        }

        private static void ShowPfx(string path)
        {
            WriteLine(ConsoleColor.Yellow, $"{path}");

            s_writer.Indent++;
            string? pwd = null;
            bool pwdSpecified = false;

            try
            {
                byte[] bytes;

                try
                {
                    bytes = File.ReadAllBytes(path);
                }
                catch (Exception e)
                {
                    WriteLine(ConsoleColor.Red, $"Could not read file: {e.GetType().Name}: {e.Message}");
                    return;
                }

                Pkcs12Info p12Info;

                try
                {
                    p12Info = Pkcs12Info.Decode(bytes, out int consumed, skipCopy: true);

                    if (consumed != bytes.Length)
                    {
                        WriteLine(ConsoleColor.DarkYellow, $"Warning: PKCS#12/PFX was only {consumed} of {bytes.Length} bytes.");
                    }
                }
                catch (Exception e)
                {
                    WriteLine(ConsoleColor.Red, $"File did not parse as a PKCS#12/PFX: {e.GetType().Name}: {e.Message}");
                    return;
                }

                if (p12Info.IntegrityMode != Pkcs12IntegrityMode.Password)
                {
                    s_writer.WriteLine($"Integrity mode: {p12Info.IntegrityMode}");
                }
                else
                {
                    if (p12Info.VerifyMac(pwd))
                    {
                        s_writer.WriteLine("MAC verified with the NULL password");
                    }
                    else if (p12Info.VerifyMac(""))
                    {
                        s_writer.WriteLine("MAC verified with the empty password");
                    }
                    else
                    {
                        MaybePasswordPrompt(ref pwd, ref pwdSpecified);

                        if (p12Info.VerifyMac(pwd))
                        {
                            s_writer.WriteLine("MAC verified with the supplied password");
                        }
                        else if (!pwdSpecified)
                        {
                            s_writer.WriteLine("Skipping MAC verification");
                        }
                        else
                        {
                            WriteLine(ConsoleColor.DarkRed, "Password did not verify the MAC");
                        }
                    }
                }

                int contentsId = 0;

                foreach (Pkcs12SafeContents contents in p12Info.AuthenticatedSafe)
                {
                    WriteLine(ConsoleColor.DarkGreen, $"AuthSafe Contents {contentsId}");
                    contentsId++;

                    s_writer.Indent++;

                    try
                    {
                        s_writer.WriteLine($"Confidentiality Mode: {contents.ConfidentialityMode}");

                        if (contents.ConfidentialityMode == Pkcs12ConfidentialityMode.Password)
                        {
                            if (!pwdSpecified)
                            {
                                try
                                {
                                    contents.Decrypt(pwd);
                                    s_writer.WriteLine("Contents decrypted with the NULL password");
                                }
                                catch (Exception)
                                {
                                    try
                                    {
                                        contents.Decrypt("");
                                        s_writer.WriteLine("Contents decrypted with the NULL password");
                                    }
                                    catch (Exception)
                                    {
                                        MaybePasswordPrompt(ref pwd, ref pwdSpecified);

                                        if (!pwdSpecified)
                                        {
                                            s_writer.WriteLine("Skipping decryption.");
                                            continue;
                                        }
                                    }
                                }
                            }

                            if (pwdSpecified)
                            {
                                try
                                {
                                    contents.Decrypt(pwd);
                                }
                                catch (Exception e)
                                {
                                    WriteLine(ConsoleColor.DarkRed, $"Contents did not decrypt: {e.GetType().Name}: {e.Message}");
                                    continue;
                                }
                            }
                            
                        }
                        else if (contents.ConfidentialityMode != Pkcs12ConfidentialityMode.None)
                        {
                            s_writer.WriteLine("Skipping decryption.");
                            continue;
                        }

                        int bagId = 0;

                        foreach (Pkcs12SafeBag bag in contents.GetBags())
                        {
                            WriteLine(ConsoleColor.DarkGreen, $"SafeBag {bagId}: {bag.GetType().Name}");
                            bagId++;

                            s_writer.Indent++;

                            try
                            {
                                if (bag is Pkcs12CertBag certBag)
                                {
                                    s_writer.WriteLine($"Certificate type: {certBag.GetCertificateType().Value}");

                                    if (certBag.IsX509Certificate)
                                    {
                                        X509Certificate2? cert = null;

                                        try
                                        {
                                            cert = certBag.GetCertificate();
                                            
                                            s_writer.WriteLine("Certificate:");
                                            s_writer.Indent++;

                                            try
                                            {
                                                s_writer.WriteLine($"Issuer: {cert.Issuer}");
                                                s_writer.WriteLine($"Serial Number (hex): {cert.SerialNumber}");
                                                s_writer.WriteLine($"Subject: {cert.Subject}");
                                                s_writer.WriteLine($"Not Before: {cert.NotBefore:yyyy-MM-dd HH:mm:ss}");
                                                s_writer.WriteLine($"Not After: {cert.NotAfter:yyyy-MM-dd HH:mm:ss}");
                                            }
                                            finally
                                            {
                                                s_writer.Indent--;
                                            }
                                        }
                                        catch (Exception)
                                        {
                                            WriteLine(ConsoleColor.DarkRed, $"Certificate did not load.");
                                        }
                                        finally
                                        {
                                            cert?.Dispose();
                                        }
                                    }
                                }

                                ShowAttributes(bag.Attributes);
                            }
                            finally
                            {
                                s_writer.Indent--;
                            }
                        }
                    }
                    finally
                    {
                        s_writer.Indent--;
                    }
                }
            }
            finally
            {
                s_writer.Indent--;
            }

            static void ShowAttributes(CryptographicAttributeObjectCollection? attributes)
            {
                if (attributes?.Count > 0)
                {
                    s_writer.WriteLine("Attributes:");
                    s_writer.Indent++;

                    try
                    {
                        int attrSetId = 0;

                        foreach (CryptographicAttributeObject attrSet in attributes)
                        {
                            s_writer.WriteLine($"Set {attrSetId}: {attrSet.Oid.Value} ({attrSet.Oid.FriendlyName})");
                            attrSetId++;

                            s_writer.Indent++;

                            try
                            {
                                int valueId = 0;

                                foreach (AsnEncodedData attr in attrSet.Values)
                                {
                                    s_writer.WriteLine(valueId);
                                    valueId++;

                                    s_writer.Indent++;

                                    try
                                    {
                                        byte[] encoded = attr.RawData;

                                        if (encoded.Length < 12)
                                        {
                                            s_writer.WriteLine(Convert.ToHexString(encoded));
                                        }
                                        else
                                        {
                                            s_writer.WriteLine(
                                                $"{encoded.Length} :: {Convert.ToHexString(encoded.AsSpan(0, 4))}...{Convert.ToHexString(encoded.AsSpan()[^4..])}");
                                        }

                                        if (encoded.Length > 0)
                                        {
                                            UniversalTagNumber kind =
                                                (UniversalTagNumber)encoded[0];

                                            switch (kind)
                                            {
                                                case UniversalTagNumber.BMPString:
                                                case UniversalTagNumber.IA5String:
                                                case UniversalTagNumber.NumericString:
                                                case UniversalTagNumber.PrintableString:
                                                case UniversalTagNumber.T61String:
                                                case UniversalTagNumber.UTF8String:
                                                {
                                                    try
                                                    {
                                                        string decoded =
                                                            AsnDecoder.
                                                                ReadCharacterString(
                                                                    encoded,
                                                                    AsnEncodingRules.BER,
                                                                    kind,
                                                                    out int consumed);

                                                        s_writer.WriteLine($"({kind}): {decoded}");

                                                        if (consumed != encoded.Length)
                                                        {
                                                            WriteLine(
                                                                ConsoleColor.DarkYellow,
                                                                $"Warning: String was only {consumed} of {encoded.Length} byte(s)");
                                                        }
                                                    }
                                                    catch (Exception e)
                                                    {
                                                        WriteLine(
                                                            ConsoleColor.DarkRed,
                                                            $"String did not decode as {kind}: {e.GetType().Name}: {e.Message}");
                                                    }

                                                    break;
                                                }
                                            }
                                        }
                                        s_writer.WriteLine();
                                    }
                                    finally
                                    {
                                        s_writer.Indent--;
                                    }
                                }
                            }
                            finally
                            {
                                s_writer.Indent--;
                            }
                        }
                    }
                    finally
                    {
                        s_writer.Indent--;
                    }
                }
                else
                {
                    s_writer.WriteLine("No attributes.");
                }
            }

            static void MaybePasswordPrompt(ref string? pwd, ref bool specified)
            {
                if (!specified)
                {
                    string tmp = PasswordPrompt(ref specified);

                    if (specified)
                    {
                        pwd = tmp;
                    }
                }
            }

            static string PasswordPrompt(ref bool specified)
            {
                Console.WriteLine("Enter password: ");
                Span<char> buf = stackalloc char[128];
                string? pwd = null;

                int i = 0;
                ConsoleKeyInfo c;

                while ((c = Console.ReadKey(true)).KeyChar is not ('\r' or '\n'))
                {
                    if (i < buf.Length)
                    {
                        buf[i] = c.KeyChar;
                    }
                    else 
                    {
                        if (pwd is null)
                        {
                            pwd = new string(buf);
                        }

                        pwd += c.KeyChar;
                    }

                    i++;
                }

                if (pwd is null)
                {
                    if (i == 0)
                    {
                        return "";
                    }

                    specified = true;
                    return new string(buf.Slice(0, i));
                }

                specified = true;
                return pwd;
            }
        }

        private static void WriteLine(ConsoleColor foregroundColor, string message)
        {
            try
            {
                Console.ForegroundColor = foregroundColor;
                s_writer.WriteLine(message);
            }
            finally
            {
                Console.ResetColor();
            }
        }
    }
}

If you can load the same PFX in both 8 and 9 and show some info like

private static void LoadedPfxInfo(string file, string pwd, X509KeyStorageFlags flags)
{
    using (X509Certificate2 cert = new X509Certificate2(file, pwd, flags))
    {
        LoadedPfxInfo(cert);
    }
}

private static void FullLoadedPfxInfo(string file, string pwd, X509KeyStorageFlags flags)
{
    X509Certificate2Collection coll = new X509Certificate2Collection();
    coll.Import(file, pwd, flags);

    foreach (X509Certificate2 cert in coll)
    {
        using (cert)
        {
            LoadedPfxInfo(cert);
        }
    }
}

private static void LoadedPfxInfo(X509Certificate2 cert)
{
    Console.WriteLine();
    Console.WriteLine(cert.SubjectName);
    Console.WriteLine($"  HasPrivateKey: {cert.HasPrivateKey}");

    if (cert.HasPrivateKey)
    {
        CngKey cngKey = null;

        using AsymmetricAlgorithm privateKey = 
            cert.GetRSAPublicKey() ??
            cert.GetDSAPrivateKey() ??
            (AsymmetricAlgorithm)cert.GetECDsaPrivateKey();

        if (privateKey is RSACng rsaCng)
        {
            cngKey = rsaCng.Key;
        }
        else if (privateKey is DSACng dsaCng)
        {
            cngKey = dsaCng.Key;
        }
        else if (privateKey is ECDsaCng ecdsaCng)
        {
            cngKey = ecdsaCng.Key;
        }
        else if (privateKey is ICspAsymmetricAlgorithm capiKey)
        {
            CspKeyContainerInfo cspInfo = capiKey.CspKeyContainerInfo;

            Console.WriteLine("  Private Key loaded via CAPI");
            Console.WriteLine($"  Machine Key: {cspInfo.MachineKeyStore}");
            Console.WriteLine($"  Provider Name: {cspInfo.ProviderName}");
        }

        if (cngKey is not null)
        {
            using (cngKey)
            {
                Console.WriteLine("  Private key loaded via CNG");
                Console.WriteLine($"  Machine Key: {cngKey.IsMachineKey}");
                Console.WriteLine($"  Provider Name: {cngKey.Provider?.Provider}");
            }
        }
    }
}

that would be useful in diagnosing things.

@julienGrd
Copy link
Author

I made some progress, i just discovered that when i put LoadUserProfile=True in my iis pool it start work again.

Actually, there is no specific reason this parameter would be different than the default value (the defaut value is true)

I think its because this pool is created by code (in c# with Microsoft.Web.Administration), and the default value of a booleean is false so lPool.ProcessModel.LoadUserProfile is init with false.

Does this make sense for you, that LoadUserProfile at false produce this error in .net9 ?

Anyway for me its an acceptable workaround, i will update my update procedure for correct the pool on my productions servers.

I let a bit the issue open in case i discover other problems related

@bartonjs
Copy link
Member

Does this make sense for you, that LoadUserProfile at false produce this error in .net9 ?

Yep. Since you're no longer specifying MachineKeySet, the key wants to go to the user key store. The user key store requires that a user profile be loaded. (That's a Windows thing, not a .NET thing)

@julienGrd
Copy link
Author

Does this make sense for you, that LoadUserProfile at false produce this error in .net9 ?

Yep. Since you're no longer specifying MachineKeySet, the key wants to go to the user key store. The user key store requires that a user profile be loaded. (That's a Windows thing, not a .NET thing)

OK i understand, but its strange the machineKeySet flag was working on .net 8 and not in .net9 (i have same error when i put this flag in production)

@bartonjs
Copy link
Member

Yeah, that part is surprising. My rough guess is that your PFX doesn't specify a key storage provider, so on .NET 8 it went into CAPI Machine Keys but on .NET 9 it went into CNG Machine Keys; but that'd require a reproing file to really investigate. (Or the info from the code snippets I shared)

@julienGrd
Copy link
Author

Yeah, that part is surprising. My rough guess is that your PFX doesn't specify a key storage provider, so on .NET 8 it went into CAPI Machine Keys but on .NET 9 it went into CNG Machine Keys; but that'd require a reproing file to really investigate. (Or the info from the code snippets I shared)

I will try to take a look at your code and execute with my certificatr to see a bit more whats happen

@julienGrd
Copy link
Author

Yeah, that part is surprising. My rough guess is that your PFX doesn't specify a key storage provider, so on .NET 8 it went into CAPI Machine Keys but on .NET 9 it went into CNG Machine Keys; but that'd require a reproing file to really investigate. (Or the info from the code snippets I shared)

I run your code with my certificat, it didn't give me so much informations
this is the result in .net8 (FullLoadedPfxInfo with flag machineKeySet or UserKeySet i have same results)

System.Security.Cryptography.X509Certificates.X500DistinguishedName
  HasPrivateKey: False

System.Security.Cryptography.X509Certificates.X500DistinguishedName
  HasPrivateKey: False

System.Security.Cryptography.X509Certificates.X500DistinguishedName
  HasPrivateKey: True

this is the result in .net9 give me extaly same results

System.Security.Cryptography.X509Certificates.X500DistinguishedName
  HasPrivateKey: False

System.Security.Cryptography.X509Certificates.X500DistinguishedName
  HasPrivateKey: False

System.Security.Cryptography.X509Certificates.X500DistinguishedName
  HasPrivateKey: True

How do you interpret these results ?

@Nness
Copy link

Nness commented Dec 4, 2024

@bartonjs @wfurt Just want to mention this issue also cause RavenDB.Client to not work. Exactly same error.

Reproduction Steps

Calling RavenDB database by providing X509Certificate2 certificate. I was using X509KeyStorageFlags.MachineKeySet in the past, at both my local development machine and server.

When create X509Certificate2 I use new X509Certificate2 for either .Net 8.0 or .Net 9.0. I have also tested X509CertificateLoader, it will result same.

To load the certificate, the code I have save pfx in local disk then use X509CertificateLoader.LoadPkcs12FromFile(path, password, flags) to load.

After upgrade to .net 9

  • Local machine works if I use: X509KeyStorageFlags.UserKeySet
  • IIS server on VM, with application pool identity: none of the flag works.
  • IIS server on VM, with local system identity: works for either UserKeySet flag or MachineKeySet flag I believe.

Expected behavior

Have the same behavior between .net8.0 and .net9.0

Actual behavior

the code throw exception in .net9.0

Regression?

yes, was working in .net8.0

Known Workarounds

Use X509KeyStorageFlags.UserKeySet under local development machine works, or on IIS server, change identity to LocalSystem.

Configuration

.net9, win-x64

Other information

In .Net 9.0 host under live IIS with ApplicationPoolIdentity.

  • Always produce certificate with SubjectName, doesn't mater which flags I have used. System.Security.Cryptography.X509Certificates.X500DistinguishedName.
  • HasPrivateKey is always true, doesn't matter which flags I have used, either UserKeySet or MachineKeySet
  • When calling GetRSAPrivateKey(), I got same error for either UserKeySet or MachineKeySet. Error is The system cannot find the file specified..

In .Net 9.0 host under live IIS with LocalSystem.

  • HasPrivateKey is always true for either UserKeySet or MachineKeySet.
  • Priviate Key is loaded via CNG for any flags.
  • Machine Key
    • UserKeySet: machine key is false
    • MachineKeySet: machine key is true
    • UserKeySet | MachineKeySet: machine key is false
    • Provider Name is same for any kind of flag combination.: Microsoft Software Key Storage Provider

In .Net 9.0 host under live IIS with LocalService

Similar to LocalSystem but only UserKeySet is working where MachineKeySet is not. The error when calling GetRSAPrivateKey() will result error: Keyset does not exist

In .Net 9.0 host under live IIS with NetworkService

Similar to LocalSystem but only UserKeySet is working where MachineKeySet is not. The error when calling GetRSAPrivateKey() will result error: Keyset does not exist

In .Net 9.0 host in local development IIS express by VS.

  • Always produce certificate with SubjectName, doesn't mater which flags I have used. System.Security.Cryptography.X509Certificates.X500DistinguishedName.
  • HasPrivateKey is always true, doesn't matter which flags I have used, either UserKeySet or MachineKeySet
  • When use MachineKeySet, the error I have received is Keyset does not exist.

In .Net 8.0 running similar code in LinqPad.

  • MachineKeySet
    • SubjectName is still System.Security.Cryptography.X509Certificates.X500DistinguishedName
    • HasPrivateKey: true
    • Type of private key is RSACng
    • Machine Key: true,
    • Provider Name: Microsoft Enhanced Cryptographic Provider v1.0
  • UserKeySet
    • SubjectName is still System.Security.Cryptography.X509Certificates.X500DistinguishedName
    • HasPrivateKey: true
    • Type of private key is RSACng
    • Machine Key: false
    • Provider Name: Microsoft Enhanced Cryptographic Provider v1.0

In .Net 9.0 running similar code in LinqPad

  • MachineKeySet: error Keyset does not exist
  • UserKeySet: same as .Net 8.0 when running in LinqPad.

Summary

From all the test I have done, I think the permission access has certainly changed between .Net 8 and .Net 9. The error may different depends on situation. It is either Keyset does not exist or The system cannot find the file specified

@Nness
Copy link

Nness commented Dec 4, 2024

RavenDB.Client fail error is below. where the error on X509Certificate2 to get private key I have posted in previous comment.

Raven.Client.Exceptions.RavenException
  HResult=0x80131500
  Message=An exception occurred while contacting {url removed}.
System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception.
 ---> System.Security.Authentication.AuthenticationException: Authentication failed, see inner exception.
 ---> System.ComponentModel.Win32Exception (0x8009030D): The credentials supplied to the package were not recognized
   at System.Net.SSPIWrapper.AcquireCredentialsHandle(ISSPIInterface secModule, String package, CredentialUse intent, SCH_CREDENTIALS* scc)
   at System.Net.Security.SslStreamPal.AcquireCredentialsHandle(CredentialUse credUsage, SCH_CREDENTIALS* secureCredential)
   at System.Net.Security.SslStreamPal.AcquireCredentialsHandleSchCredentials(SslAuthenticationOptions authOptions)
   at System.Net.Security.SslStreamPal.AcquireCredentialsHandle(SslAuthenticationOptions sslAuthenticationOptions, Boolean newCredentialsRequested)
   --- End of inner exception stack trace ---
   at System.Net.Security.SslStreamPal.AcquireCredentialsHandle(SslAuthenticationOptions sslAuthenticationOptions, Boolean newCredentialsRequested)
   at System.Net.Security.SslStream.AcquireCredentialsHandle(SslAuthenticationOptions sslAuthenticationOptions, Boolean newCredentialsRequested)
   at System.Net.Security.SslStream.AcquireClientCredentials(Byte[]& thumbPrint, Boolean newCredentialsRequested)
   at System.Net.Security.SslStream.GenerateToken(ReadOnlySpan`1 inputBuffer, Int32& consumed)
   at System.Net.Security.SslStream.NextMessage(ReadOnlySpan`1 incomingBuffer, Int32& consumed)
   at System.Net.Security.SslStream.ProcessTlsFrame(Int32 frameSize)
   at System.Net.Security.SslStream.ForceAuthenticationAsync[TIOAdapter](Boolean receiveFirst, Byte[] reAuthenticationData, CancellationToken cancellationToken)
   at System.Net.Security.SslStream.ProcessAuthenticationWithTelemetryAsync(Boolean isAsync, CancellationToken cancellationToken)
   at System.Net.Http.ConnectHelper.EstablishSslConnectionAsync(SslClientAuthenticationOptions sslOptions, HttpRequestMessage request, Boolean async, Stream stream, CancellationToken cancellationToken)
   --- End of inner exception stack trace ---
   at System.Net.Http.ConnectHelper.EstablishSslConnectionAsync(SslClientAuthenticationOptions sslOptions, HttpRequestMessage request, Boolean async, Stream stream, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.ConnectAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.InjectNewHttp2ConnectionAsync(QueueItem queueItem)
   at System.Threading.Tasks.TaskCompletionSourceWithCancellation`1.WaitWithCancellationAsync(CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionWaiter`1.WaitForConnectionWithTelemetryAsync(HttpRequestMessage request, HttpConnectionPool pool, Boolean async, CancellationToken requestCancellationToken)
   at System.Net.Http.HttpConnectionPool.SendWithVersionDetectionAndRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken)
   at System.Net.Http.DiagnosticsHandler.SendAsyncCore(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
   at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
   at System.Net.Http.DecompressionHandler.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
   at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
   at Raven.Client.Http.RequestExecutor.SendAsync[TResult](ServerNode chosenNode, RavenCommand`1 command, SessionInfo sessionInfo, HttpRequestMessage request, CancellationToken token) in D:\Builds\RavenDB-Stable-6.0\60035\src\Raven.Client\Http\RequestExecutor.cs:line 1201
   at Raven.Client.Http.RequestExecutor.SendRequestToServer[TResult](ServerNode chosenNode, Nullable`1 nodeIndex, JsonOperationContext context, RavenCommand`1 command, Boolean shouldRetry, SessionInfo sessionInfo, HttpRequestMessage request, String url, CancellationToken token) in D:\Builds\RavenDB-Stable-6.0\60035\src\Raven.Client\Http\RequestExecutor.cs:line 1145.

@AbakumovAlexandr
Copy link

I've got the same error with my code loading Apple Push Notification Service (APNS) certificate to communicate with APNS server. It worked fine in .NET 8, but started to fail right after switching my app to .NET 9.

Like pointed in this post, it works when commenting the MachineKeySet flag only:

var certificateCollection = X509CertificateLoader.LoadPkcs12CollectionFromFile(
    "Path to .p12",
    "123456",
    /*X509KeyStorageFlags.MachineKeySet |*/ X509KeyStorageFlags.PersistKeySet
                        | X509KeyStorageFlags.Exportable);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-System.Net.Security os-windows untriaged New issue has not been triaged by the area owner
Projects
None yet
Development

No branches or pull requests

6 participants