runtime: SslStream not working with ephemeral keys

This issue was opened as result of the discussion in issue dotnet/runtime#21761.

I have a .net core 2.0 application which hosts a service and secures it via SSL.

My key material is a a key pair where I have the certificate and the private keys in two files. So I load the cert and the keys from disk and use the X509Certificate2.CopyWithPrivateKey method to construct a certificate with a private key.

That works as it should. However, if I pass this certificate to an SslStream via SslStream.AuthenticateAsServer, I get this error:

System.ComponentModel.Win32Exception (0x80004005): No credentials are available in the security package
   at System.Net.SSPIWrapper.AcquireCredentialsHandle(SSPIInterface secModule, String package, CredentialUse intent, SCHANNEL_CRED scc)
   at System.Net.Security.SslStreamPal.AcquireCredentialsHandle(CredentialUse credUsage, SCHANNEL_CRED secureCredential)
   at System.Net.Security.SslStreamPal.AcquireCredentialsHandle(X509Certificate certificate, SslProtocols protocols, EncryptionPolicy policy, Boolean isServer)
   at System.Net.Security.SecureChannel.AcquireServerCredentials(Byte[]& thumbPrint)
   at System.Net.Security.SecureChannel.GenerateToken(Byte[] input, Int32 offset, Int32 count, Byte[]& output)
   at System.Net.Security.SecureChannel.NextMessage(Byte[] incoming, Int32 offset, Int32 count)
   at System.Net.Security.SslState.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslState.ProcessReceivedBlob(Byte[] buffer, Int32 count, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslState.StartReadFrame(Byte[] buffer, Int32 readBytes, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslState.StartReceiveBlob(Byte[] buffer, 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.Security.SslStream.BeginAuthenticateAsServer(X509Certificate serverCertificate, Boolean clientCertificateRequired, SslProtocols enabledSslProtocols, Boolean checkCertificateRevocation, AsyncCallback asyncCallback, Object asyncState)
   at System.Net.Security.SslStream.<>c__DisplayClass35_0.<AuthenticateAsServerAsync>b__0(AsyncCallback callback, Object state)
   at System.Threading.Tasks.TaskFactory`1.FromAsyncImpl(Func`3 beginMethod, Func`2 endFunction, Action`1 endAction, Object state, TaskCreationOptions creationOptions)
   at System.Threading.Tasks.TaskFactory.FromAsync(Func`3 beginMethod, Action`1 endMethod, Object state)
   at System.Net.Security.SslStream.AuthenticateAsServerAsync(X509Certificate serverCertificate, Boolean clientCertificateRequired, SslProtocols enabledSslProtocols, Boolean checkCertificateRevocation)

@bartonjs suggested that the SslStream on Windows might not support certificates with an ephemeral key, so this might be the issue here.

Everything works fine on Linux.

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 1
  • Comments: 41 (23 by maintainers)

Commits related to this issue

Most upvoted comments

@natemcmaster

using (X509Certificate2 certWithKey = certOnly.CopyWithPrivateKey(key))
{
    return new X509Certificate2(certWithKey.Export(X509ContentType.Pkcs12));
}

If you don’t specify Ephemeral in the load on that one then it’ll get a classic Perphemeral (persisted, with cleanup as long as there’s no abnormal termination) private key.

FWIW, I’m pretty far along on dotnet/runtime#22020. A few more integration scenarios to work through on the API and then it should be in the home stretch.

@henning-krause: I figured that much when it worked on Linux… But it’s been 3 years…

So to work around it:

return new System.Security.Cryptography.X509Certificates.X509Certificate2(
     thisCert.Value.Export(
        System.Security.Cryptography.X509Certificates.X509ContentType.Pkcs12
    )
);

In case it helps raise the priority, I just spent over a day trying to figure out why I couldn’t take a private key used for mutual TLS stored in Azure Key Vault and use it in my Azure App Services App (Windows based) to use SslStream.AuthenticateAsClientAsync()

At first, I tried var privateCert = new X509Certificate2(Convert.FromBase64String(<certstring>), (string)null, X509KeyStorageFlags.DefaultKeySet); but I realized that a Windows based Azure App Service doesn’t like that because it tries to write the cert to disk, but the security context App Services runs under doesn’t allow that.

So, I switched to var privateCert = new X509Certificate2(Convert.FromBase64String(<certstring>), (string)null, X509KeyStorageFlags.EphemeralKeySet); This results in a No credentials are available in the security package (when running locally or in Windows Azure App Service when calling SslStream.AuthenticateAsClientAsync())

Then, I finally stumbled here, which made me realize that EphemeralKeySet, Windows, and SSlStream.AuthenticateAsClientAsync() don’t work together and it is a known issue.

I think this use case will become more common as Azure KeyVault gains in popularity due to its awesome certificate management features.

None of the workarounds I can come up with are attractive. I can’t switch to Linux based Azure App Service without incurring additional costs because I have some Windows only apps running in my Azure App Services plan.

I was able to get this to work: https://github.com/MicrosoftDocs/azure-docs/blob/master/articles/app-service/app-service-web-ssl-cert-load.md but that involves a lot of setup (as well as setup on each developer machine), leaking the private key to disk and a lot more maintenance when the certificate is renewed.

Please don’t feel obligated to respond, I’m just documenting it here in case it helps triage or if it helps someone else trying the same thing.

Bug still exists in .NET Core 2.1.5

I too have hit this issue with using certificate based authentication in Kestrel. Switching to ephemeral keys breaks it and fails to negotiate TLS. I see that this is more of a Windows issue so I don’t have much hope for a workaround any time soon. Unfortunately I have millions of file entries in C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys and will have to resort to scripted deletions rather than a code based fix. Relying on calling Dispose on the certificate when loaded has proven very difficult in this scenario due to IOC complications and program terminations caused by IISExpress.

Sure:

	var publicCertificate = new X509Certificate2(File.ReadAllBytes("Path/to/Certificate");
	var key = DecodePrivateKey("Path/to/key"); // Using an ASN.1 Parser to decode the key
	var rsa = RSA.Create();
	rsa.ImportParameters(key.GetRsaParameters());
	cert = publicCertificate.CopyWithPrivateKey(rsa);
       
        // If I execute the follwowing two lines, everything works as expected. 
	/*var buffer = cert.Export(X509ContentType.Pfx, (string) null);
	cert = new X509Certificate2(buffer, (string) null);*/


        var sslStream = new SslStream(networkStream, false);
        await sslStream.AuthenticateAsServerAsync(cert, false, SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, false);

Please take into account that SmartCard based X509Certificate2 cannot be exported (the private key part) and this exporting and re-importing simply does not work as a workaround.

Minor change for @ststeiger 's work around is it’s cert.Export. Looks like nobody tested loading SSL certs from PEM files on Windows 😏 Here’s how I do it in .NET Core 3 (.NET 5 lets you load a single PEM file with the private key inside, so all the RSA private key code below is redundant)

public static class CertHelper
{
    // dotnet dev-certs https -ep $pwd/selfsigned.pem --format Pem -np
    public static X509Certificate2 GetCertificate()
    {
        X509Certificate2 sslCert = CreateFromPublicPrivateKey("certs/selfsigned.pem", "certs/selfsigned.key");
        
        // work around for Windows (WinApi) problems with PEMS, still in .NET 5
        return new X509Certificate2(sslCert.Export(X509ContentType.Pkcs12));
    }
    
    public static X509Certificate2 CreateFromPublicPrivateKey(string publicCert="certs/public.pem", string privateCert="certs/private.pem")
    {
        byte[] publicPemBytes = File.ReadAllBytes(publicCert);
        using var publicX509 = new X509Certificate2(publicPemBytes);
        var privateKeyText = File.ReadAllText(privateCert);
        var privateKeyBlocks = privateKeyText.Split("-", StringSplitOptions.RemoveEmptyEntries);
        var privateKeyBytes = Convert.FromBase64String(privateKeyBlocks[1]);

        using RSA rsa = RSA.Create();
        if (privateKeyBlocks[0] == "BEGIN PRIVATE KEY")
        {
            rsa.ImportPkcs8PrivateKey(privateKeyBytes, out _);
        }
        else if (privateKeyBlocks[0] == "BEGIN RSA PRIVATE KEY")
        {
            rsa.ImportRSAPrivateKey(privateKeyBytes, out _);
        }
        X509Certificate2 keyPair = publicX509.CopyWithPrivateKey(rsa);
        return keyPair;
    }
}
    
public static IHostBuilder CreateHostBuilder(string[] args)
{
    return Host.CreateDefaultBuilder(args)
        .UseSerilog()
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.ConfigureKestrel(options =>
            {
                options.ConfigureHttpsDefaults(adapterOptions =>
                {
                    adapterOptions.ServerCertificate = CertHelper.GetCertificate();
                });
            });

            webBuilder.UseStartup<Startup>();
        });
}

The code is also on my blog in this post, I’ll add this to that post I think as it’s a fairly common scenario to use PEMs instead of the archaic PFX horse that Microsoft tried flogging!

@scegg The fix (when it comes) will be in the Windows OS, not in .NET. Since the only known problem with ephemeral private keys is SslStream on Windows I’m not sure that I’d expect a doc-warning on either of the APIs you mentioned… SslStream.AuthenticateAsServer(Async) would be a more likely target in my mind… but would you (did you) look there? If not, then there may not be a better place than this issue.

Though, one can hope that Windows will fix the underlying problem…

After discussions with the SCHANNEL team, it has been confirmed that this won’t work in the current versions of Windows due to SCHANNEL’s cross-process architecture with LSASS.EXE. The in-memory TLS client certificate private key is not marshaled between SCHANNEL and LSASS. That is why SEC_E_NO_CREDENTIALS is returned from SCHANNEL AcquireCredentialHandle() call.

We are working with the SCHANNEL team to define a feature request for this but the timeline is uncertain.

I started debugging this a little bit.

The problem is not in SslStream. The problem is in the X509Certificate2 created. There is some problem when using X509KeyStorageFlags.EphemeralKeySet. The X509Certificate2.HasPrivateKey property is true. But there is something wrong with how the private key material is being stored.

SslStream internally calls AcquireCredentialsHandle with the PCCERT_CONTEXT of the X509Certificate2. But ACH returns SEC_E_NO_CREDENTIALS. After turning on SCHANNEL logging an error message can be seen in the System Event Log.

The TLS client credential’s certificate does not have a private key information property attached to it. This most often occurs when a certificate is backed up incorrectly and then later restored. This message can also indicate a certificate enrollment failure.

Normally, the private key needs to be stored into a CSP and then that information is attached to the public key portion of the certificate:

After this, you have to update the CERT_KEY_PROV_INFO_PROP_ID property of the newly create PCCERT_CONTEXT using CertSetCertificateContextProperty with the parameters of the private key (container name, CSP name, key spec…).

See: https://social.msdn.microsoft.com/Forums/vstudio/en-US/9d906fe1-a5bf-4005-adb5-de8964e817b4/cryptoapi-import-private-key-in-pem-format

I found this article on StackOverflow. It implies that the private key must be stored in a particular CSP.

Afaik this error means that the SSPI SChannel package did not find the private key for the certificate or the certificate is not valid for SSL/TLS. Make sure the certificate/private key are loaded in the PROV_RSA_SCHANNEL Crypto provider (CSP), not in the Enhanced CSP.

I don’t know if that is compatible with the memory based certificate store (CERT_STORE_PROV_MEMORY) implied by the use of X509KeyStorageFlags.EphemeralKeySet.