Search code examples
c#google-chromessl-certificatecertificatex509certificate2

Win32Exception when authenticating as server using certificate Ephemeral key


I'm trying to established an HTTPS connexion to a web server through SSL stream using google chrome (last version), I'm generating my certificate using CreateSelfSignedCertificate(string commonName) and when I call https://localhost/ it allways raises a System.Security.Authentication.AuthenticationException : A call to SSPI failed, see inner exception -> Win32Exception: An unknown error occurred while processing the certificate

I'm using an Ephemeral Key and I don't want to store the certificate.

here's my code:

...
ServerCertificate = CreateSelfSignedCertificate("localhost");
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
SslStream s  = new SslStream(_stream, false, ValidateServerCertificate);
s.AuthenticateAsServer(ServerCertificate, false, SslProtocols.Tls12, false);
_stream = s;
...

...
public bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
    return true;
}

public static X509Certificate2 CreateSelfSignedCertificate(string commonName)
{
    X500DistinguishedName subjectName = new X500DistinguishedName($"CN={commonName}");

    using (RSA rsa = RSA.Create(2048))
    {
        CertificateRequest certificateRequest = new CertificateRequest(subjectName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);

        certificateRequest.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature, false));

        X509Certificate2 certificate = certificateRequest.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(1));

        byte[] pfxData = certificate.Export(X509ContentType.Pkcs12);

        return new X509Certificate2(pfxData, "", X509KeyStorageFlags.Exportable | X509KeyStorageFlags.EphemeralKeySet);
    }
}

UPDATE:

  • had to use SAN because google chrome requires SAN and never uses CNN fallback.

Solution

  • ANSWER

    Authenticating using an Ephemeral key is not possible on Windows, because the underlying OS component that provides TLS/SSL doesn’t work with ephemeral keys.

    see github issue here

    Also:

    byte[] pfxData = certificate.Export(X509ContentType.Pkcs12, (string)null);
                    return new X509Certificate2(pfxData, (string)null, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.EphemeralKeySet);
    

    is basically the same as just return certificate.

    The reason that people do the pkcs12 export and re-import is to NOT specify EphemeralKeySet (you should also not assert PersistKeySet, unless you really mean to). And you likely don’t need Exportable, either.

    byte[] pfxData = certificate.Export(X509ContentType.Pkcs12, (string)null);
                    return new X509Certificate2(pfxData);
    

    would work much better.

    The issue that I was having was best described by an issue that has gotten locked due to old age on the github Issue linked here.

    Thanks to Barton Jeremy Barton at Microsoft for helping me on this and pointing some light on this subject.


    SOLUTION

    To work around that behaviour :

    Either switch to a Linux-based Azure App Service using Azure Key Vault to manage your certificates. Azure Key Vault can securely store certificates and private keys and automatically handle renewals.

    OR

    You have to persist the Certificate to a particular CSP using X509KeyStorageFlags.MachineKeySet

    here is my code so far, this basically stores the self signed certificate once it has been created so your server is able to AuthenticateAsServer() without throwing a Win32 Exception.

    Function to create the selfsigned certificate on demand (free to tweak it as needed):

    public void CreateSelfSignedCertificate()
    {
        string commonName = "My Authority CA";
    
        using (RSA rsa = RSA.Create(2048))
        {
            // Create a subject name
            X500DistinguishedName subjectName = new X500DistinguishedName($"CN={commonName}");
    
            // Create a self-signed certificate
            CertificateRequest certificateRequest = new CertificateRequest(subjectName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
    
            // Add a "Key Usage" extension to the certificate during its creation.
            // The "Key Usage" extension defines the purposes for which the public key of the certificate can be used.
            // X509KeyUsageFlags.DataEncipherment: The public key can be used to encrypt data, typically by encrypting a session key that is then used to encrypt the actual data.
            // X509KeyUsageFlags.KeyEncipherment: The public key can be used to encrypt other keys, for example, in the TLS protocol during key exchange.
            // X509KeyUsageFlags.DigitalSignature: The public key can be used to verify digital signatures.
            // The second parameter of X509KeyUsageExtension specifies whether the extension is critical or not.
            // If it is critical (true), applications that do not understand this extension must reject the certificate.
            // If it is non-critical (false), applications that do not understand this extension can still accept the certificate.
            certificateRequest.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature, false));
    
            // oid 1.3.6.1.5.5.7.3.1 = "Server Authentication"
            // oid 1.3.6.1.5.5.7.3.2 = "Client Authentication"
            // oid 1.3.6.1.5.5.7.3.3 = "Code Signing"
            // ...
            certificateRequest.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, false));
    
            // Add SAN extension (fallback)
            var sanBuilder = new SubjectAlternativeNameBuilder();
            sanBuilder.AddDnsName("localhost");
            sanBuilder.AddIpAddress(IPAddress.Parse("127.0.0.1"));
    
            // Add all Machine IPv4 ou IPv6 configuration to SAN extension
            foreach (var ipAddress in Dns.GetHostAddresses(Dns.GetHostName()))
                if (ipAddress.AddressFamily == AddressFamily.InterNetwork || ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
                    sanBuilder.AddIpAddress(ipAddress);
    
            certificateRequest.CertificateExtensions.Add(sanBuilder.Build());
    
            // Set the certificate date and duration
            X509Certificate2 certificate = certificateRequest.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(1));
    
            // Export the certificat
            pfxData = certificate.Export(X509ContentType.Pkcs12);
        }
    }
    

    code to store the certificate to the specific CSP before authenticating only if it doesn't exist yet (here we store it to the LocalMachine trusted certificate as Google shares those and have access to it)

    ...
    
    ServerCertificate = new X509Certificate2(pfxData, (string)null, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet);
    X509Store store = new X509Store(StoreName.Root, StoreLocation.LocalMachine);
    store.Open(OpenFlags.ReadWrite);
    bool certificateExists = false;
    foreach (X509Certificate2 existingCert in store.Certificates)
    {
        if (existingCert.Subject == ServerCertificate.Subject && existingCert.HasPrivateKey == ServerCertificate.HasPrivateKey && existingCert.GetCertHashString() == ServerCertificate.GetCertHashString())
        {
            certificateExists = true;
            break;
        }
    }
    if (!certificateExists)
        store.Add(new X509Certificate2(_certificateContent));
    store.Close();
    
    ...
    

    I hope it helped you

    Thanks to @Charlieface for is help