9

I have a windows service (running as LocalSystem) that is self-hosting an OWIN service (SignalR) and needs to be accessed over SSL.

I can set up the SSL binding on my local development machine just fine - and I can access my service over SSL on that same machine. However, when I go to another machine and try to run the following command I receive an error:

Command:

netsh http add sslcert ipport=0.0.0.0:9389 appid={...guid here...} certhash=...cert hash here...

Error:

SSL Certificate add failed, Error: 1312

A specified logon session does not exist. It may have already been terminated.

The certificate I am using is a fully signed cert (not a development cert) and works on my local dev box. Here's what I am doing:

Windows service starts up and registers my certificate using the following code:

var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadWrite);
var path = AppDomain.CurrentDomain.BaseDirectory;
var cert = new X509Certificate2(path + @"\mycert.cer");
var existingCert = store.Certificates.Find(X509FindType.FindByThumbprint, cert.Thumbprint, false);
if (existingCert.Count == 0)
    store.Add(cert);
store.Close();

I then attempt to bind the certificate to port 9389 using netsh and the following code:

var process = new Process {
    StartInfo = new ProcessStartInfo {
        WindowStyle = ProcessWindowStyle.Hidden,
        FileName = "cmd.exe",
        Arguments = "/c netsh http add sslcert ipport=0.0.0.0:9389 appid={12345678-db90-4b66-8b01-88f7af2e36bf} certhash=" + cert.thumbprint
    }
};
process.Start();

The code above successfully installs the certificate to the "Local Machine - Certificates\Trusted Root Certification Authorities\Certificates" certificate folder - but the netsh command fails to run with the error I described above. If I take the netsh command and run it in a command prompt as an administrator on that box it also throws out the same error - so I don't believe that it's a code related issue...

I have to imagine that this is possible to accomplish - plenty of other applications create self-hosted services and host them over ssl - but I cannot seem to get this to work at all...anyone have any suggestions? Perhaps programmatic alternatives to netsh?

Community
  • 1
  • 1
Robert Petz
  • 2,718
  • 4
  • 23
  • 52
  • I found that if I generate a self-signed certificate on the machine that I'm having trouble with and use netsh on that certificate's thumbprint it works - I wonder if there is a way to generate a self signed cert in code? – Robert Petz Oct 24 '14 at 19:42
  • 2
    You are importing the certificate from a .cer file, which doesn't include the certificate's private key. You need its private key in order to bind it to a port. To get your "fully signed cert" to work, you need to export it from the machine it works on (your development machine) _along with the private key_ into a .pfx file. Then import that on the machine where you are installing the service. The reason generating a self-signed certificate on the machine works is because this creates a private key on that machine where the certificate is generated. – Scott Jan 29 '16 at 00:43

2 Answers2

6

Okay I found the answer:

If you are bringing in a certificate from another machine it will NOT work on the new machine. You have to create a self-signed certificate on the new machine and import it into the Local Computer's Trusted Root Certificates.

The answer is from here: How to create a self-signed certificate using C#?

For posterity's sake this is the process used to create a self signed cert (from the above referenced answer):

Import the CertEnroll 1.0 Type Library from the COM tab in your project's references

Add the following method to your code:

//This method credit belongs to this StackOverflow Answer:
//https://stackoverflow.com/a/13806300/594354
using CERTENROLLLib;

public static X509Certificate2 CreateSelfSignedCertificate(string subjectName)
{
    // create DN for subject and issuer
    var dn = new CX500DistinguishedName();
    dn.Encode("CN=" + subjectName, X500NameFlags.XCN_CERT_NAME_STR_NONE);

    // create a new private key for the certificate
    CX509PrivateKey privateKey = new CX509PrivateKey();
    privateKey.ProviderName = "Microsoft Base Cryptographic Provider v1.0";
    privateKey.MachineContext = true;
    privateKey.Length = 2048;
    privateKey.KeySpec = X509KeySpec.XCN_AT_SIGNATURE; // use is not limited
    privateKey.ExportPolicy = X509PrivateKeyExportFlags.XCN_NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG;
    privateKey.Create();

    // Use the stronger SHA512 hashing algorithm
    var hashobj = new CObjectId();
    hashobj.InitializeFromAlgorithmName(ObjectIdGroupId.XCN_CRYPT_HASH_ALG_OID_GROUP_ID,
        ObjectIdPublicKeyFlags.XCN_CRYPT_OID_INFO_PUBKEY_ANY, 
        AlgorithmFlags.AlgorithmFlagsNone, "SHA512");

    // add extended key usage if you want - look at MSDN for a list of possible OIDs
    var oid = new CObjectId();
    oid.InitializeFromValue("1.3.6.1.5.5.7.3.1"); // SSL server
    var oidlist = new CObjectIds();
    oidlist.Add(oid);
    var eku = new CX509ExtensionEnhancedKeyUsage();
    eku.InitializeEncode(oidlist); 

    // Create the self signing request
    var cert = new CX509CertificateRequestCertificate();
    cert.InitializeFromPrivateKey(X509CertificateEnrollmentContext.ContextMachine, privateKey, "");
    cert.Subject = dn;
    cert.Issuer = dn; // the issuer and the subject are the same
    cert.NotBefore = DateTime.Now;
    // this cert expires immediately. Change to whatever makes sense for you
    cert.NotAfter = DateTime.Now; 
    cert.X509Extensions.Add((CX509Extension)eku); // add the EKU
    cert.HashAlgorithm = hashobj; // Specify the hashing algorithm
    cert.Encode(); // encode the certificate

    // Do the final enrollment process
    var enroll = new CX509Enrollment();
    enroll.InitializeFromRequest(cert); // load the certificate
    enroll.CertificateFriendlyName = subjectName; // Optional: add a friendly name
    string csr = enroll.CreateRequest(); // Output the request in base64
    // and install it back as the response
    enroll.InstallResponse(InstallResponseRestrictionFlags.AllowUntrustedCertificate,
        csr, EncodingType.XCN_CRYPT_STRING_BASE64, ""); // no password
    // output a base64 encoded PKCS#12 so we can import it back to the .Net security classes
    var base64encoded = enroll.CreatePFX("", // no password, this is for internal consumption
        PFXExportOptions.PFXExportChainWithRoot);

    // instantiate the target class with the PKCS#12 data (and the empty password)
    return new System.Security.Cryptography.X509Certificates.X509Certificate2(
        System.Convert.FromBase64String(base64encoded), "", 
        // mark the private key as exportable (this is usually what you want to do)
        System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.Exportable
    );
}

For anyone else reading this answer - the code for importing the certificate from the original question should now change to the following:

var certName = "Your Cert Subject Name";
var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadWrite);
var existingCert = store.Certificates.Find(X509FindType.FindBySubjectName, certName, false);
if (existingCert.Count == 0)
{
    var cert = CreateSelfSignedCertificate(certName);
    store.Add(cert);
    RegisterCertForSSL(cert.Thumbprint);
}
store.Close();
Community
  • 1
  • 1
Robert Petz
  • 2,718
  • 4
  • 23
  • 52
  • I'd say it's easier to use [certutil.exe](http://technet.microsoft.com/en-us/library/cc732443.aspx) rather than write the code in C#. – abatishchev Oct 24 '14 at 20:47
  • 2
    I went down that path and it definitely works - however when you are running an installshield and trying to execute certutil you run into a bunch of permissions issues...the next best thing would be to have the C# application execute certutil from a windows service running under LocalSystem but I honestly prefer this way because it's easier to debug – Robert Petz Oct 24 '14 at 21:12
  • Thanks. I had tried using CLRSecurity but it didn't work (I could create the certificate but not register it for some reason). – Erwin Mayer Jan 21 '15 at 17:40
  • You don't have to generate it on the machine, you just need to import it WITH the private key intact (i.e. via a ".pfx" file)! -- Obviously, just the ".cer" file won't work, because it only contains the public key. – BrainSlugs83 Jun 29 '17 at 00:27
  • 7
    What is `RegisterCertForSSL`‽ – David Murdoch Nov 07 '17 at 16:25
  • 2
    This answer doesn't actually answer the question. It discusses how to create a certificate - which wasn't part of the question - then fails to discuss how to actually bind it to the port - which the question was about. Seriously what is RegisterCertForSSL(cert.Thumbprint) ?? Is it some sort of a framework method? is it involved in a 3rd party component? Did you write it? If yes, how exactly does that work? – mg30rg Dec 11 '17 at 12:01
  • @mg30rg this absolutely does answer the question as the question was why the cert would only work on the local machine. This answer states that the locally generated cert won’t work and then provides how to get a new cert created in code so you can do so on the target machine. If you review the question itself you would see that the thumbprint I am passing to `RegisterCertForSSL` is used in a CMD call that binds that thumbprint to my appid. Since that part of the process wasn’t related to answering the question I didn’t feel the need to repeat that chunk of code. – Robert Petz Dec 21 '17 at 17:50
  • 5
    @RobertPetz The question was "[How to] register certificate to SSL port?". The one thing this answer didn't tell was **how to register a certificate to an SSL port** therefore no, it didn't answer the question. – mg30rg Dec 23 '17 at 11:39
  • @mg30rg as the person asking the question, I’m pretty sure I can tell if this answers it...just because my original question thought it was an issue of how to actually do the registration doesn’t mean the answer can’t be that I was registering it fine but the generation of the cert needed to be changed to make it register on another machine. the registration part in the OP is still the same and still functions exactly as intended and correctly. – Robert Petz Dec 23 '17 at 15:45
  • System.InvalidCastException: Unable to cast COM object of type 'System.__ComObject' to interface type 'CERTENROLLLib.CX509PrivateKey'. got this error when run this on windows server 2012 R2 – King_Fisher Sep 04 '19 at 13:22
6

Here is the full code including:

  • Generating certificate
  • Registering ssl on port
  • Running simple HTTPS server on that port

** Forgive me the quality of code. It's just a very dirty proof of concept glued form different pieces of code I found on the web and Robert Petz answer. I didn't have time to clean it up:

Remember to

  • Run Visual Studio as admin (admin priveleges are requied for this code)
  • Add Reference to the project: COM > TypeLibraries > CertEnroll 1.0 Type Library

Code:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using System.Web.Http.SelfHost;
using CERTENROLLLib;

namespace SelfhostSSLProofOfConcept
{
    /// <summary>
    /// Add Reference: COM > TypeLibraries > CertEnroll 1.0 Type Library
    /// </summary>
    class Program
    {
        static void Main(string[] args)
        {
            var port = 1234;

            var certSubjectName = "Your cert subject name";
            var expiresIn = TimeSpan.FromDays(7);
            var cert = GenerateCert(certSubjectName, expiresIn);

            Console.WriteLine("Generated certificate, {0}Thumbprint: {1}{0}", Environment.NewLine, cert.Thumbprint);

            RegisterSslOnPort(port, cert.Thumbprint);
            Console.WriteLine($"Registerd SSL on port: {port}");

            var config = new HttpSelfHostConfiguration($"https://localhost:{port}");

            var server = new HttpSelfHostServer(config, new MyWebAPIMessageHandler());
            var task = server.OpenAsync();
            task.Wait();

            Process.Start($"https://localhost:{port}"); // automatically run browser

            Console.WriteLine($"Web API Server has started at https://localhost:{port}");
            Console.ReadLine();
        }

        private static void RegisterSslOnPort(int port, string certThumbprint)
        {
            var appId = Guid.NewGuid();
            string arguments = $"http add sslcert ipport=0.0.0.0:{port} certhash={certThumbprint} appid={{{appId}}}";
            ProcessStartInfo procStartInfo = new ProcessStartInfo("netsh", arguments);

            procStartInfo.RedirectStandardOutput = true;
            procStartInfo.UseShellExecute = false;
            procStartInfo.CreateNoWindow = true;

            var process = Process.Start(procStartInfo);
            while (!process.StandardOutput.EndOfStream)
            {
                string line = process.StandardOutput.ReadLine();
                Console.WriteLine(line);
            }

            process.WaitForExit();
        }

        public static X509Certificate2 GenerateCert(string certName, TimeSpan expiresIn)
        {
            var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine);
            store.Open(OpenFlags.ReadWrite);
            var existingCert = store.Certificates.Find(X509FindType.FindBySubjectName, certName, false);
            if (existingCert.Count > 0)
            {
                store.Close();
                return existingCert[0];
            }
            else
            {
                var cert = CreateSelfSignedCertificate(certName, expiresIn);
                store.Add(cert);

                store.Close();
                return cert;
            }
        }

        /// <summary>
        /// Add Reference: COM > TypeLibraries > CertEnroll 1.0 Type Library
        /// source: https://stackoverflow.com/a/13806300/594354
        /// </summary>
        /// <param name="subjectName"></param>
        /// <returns></returns>
        public static X509Certificate2 CreateSelfSignedCertificate(string subjectName, TimeSpan expiresIn)
        {
            // create DN for subject and issuer
            var dn = new CX500DistinguishedName();
            dn.Encode("CN=" + subjectName, X500NameFlags.XCN_CERT_NAME_STR_NONE);

            // create a new private key for the certificate
            CX509PrivateKey privateKey = new CX509PrivateKey();
            privateKey.ProviderName = "Microsoft Base Cryptographic Provider v1.0";
            privateKey.MachineContext = true;
            privateKey.Length = 2048;
            privateKey.KeySpec = X509KeySpec.XCN_AT_SIGNATURE; // use is not limited
            privateKey.ExportPolicy = X509PrivateKeyExportFlags.XCN_NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG;
            privateKey.Create();

            // Use the stronger SHA512 hashing algorithm
            var hashobj = new CObjectId();
            hashobj.InitializeFromAlgorithmName(ObjectIdGroupId.XCN_CRYPT_HASH_ALG_OID_GROUP_ID,
                ObjectIdPublicKeyFlags.XCN_CRYPT_OID_INFO_PUBKEY_ANY,
                AlgorithmFlags.AlgorithmFlagsNone, "SHA512");

            // add extended key usage if you want - look at MSDN for a list of possible OIDs
            var oid = new CObjectId();
            oid.InitializeFromValue("1.3.6.1.5.5.7.3.1"); // SSL server
            var oidlist = new CObjectIds();
            oidlist.Add(oid);
            var eku = new CX509ExtensionEnhancedKeyUsage();
            eku.InitializeEncode(oidlist);

            // Create the self signing request
            var cert = new CX509CertificateRequestCertificate();
            cert.InitializeFromPrivateKey(X509CertificateEnrollmentContext.ContextMachine, privateKey, "");
            cert.Subject = dn;
            cert.Issuer = dn; // the issuer and the subject are the same
            cert.NotBefore = DateTime.Now;
            // this cert expires immediately. Change to whatever makes sense for you
            cert.NotAfter = DateTime.Now.Add(expiresIn);
            cert.X509Extensions.Add((CX509Extension)eku); // add the EKU
            cert.HashAlgorithm = hashobj; // Specify the hashing algorithm
            cert.Encode(); // encode the certificate

            // Do the final enrollment process
            var enroll = new CX509Enrollment();
            enroll.InitializeFromRequest(cert); // load the certificate
            enroll.CertificateFriendlyName = subjectName; // Optional: add a friendly name
            string csr = enroll.CreateRequest(); // Output the request in base64
            // and install it back as the response
            enroll.InstallResponse(InstallResponseRestrictionFlags.AllowUntrustedCertificate,
                csr, EncodingType.XCN_CRYPT_STRING_BASE64, ""); // no password
            // output a base64 encoded PKCS#12 so we can import it back to the .Net security classes
            var base64encoded = enroll.CreatePFX("", // no password, this is for internal consumption
                PFXExportOptions.PFXExportChainWithRoot);

            // instantiate the target class with the PKCS#12 data (and the empty password)
            return new System.Security.Cryptography.X509Certificates.X509Certificate2(
                System.Convert.FromBase64String(base64encoded), "",
                // mark the private key as exportable (this is usually what you want to do)
                System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.Exportable
            );
        }
    }

    class MyWebAPIMessageHandler : HttpMessageHandler
    {
        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
        {
            var task = new Task<HttpResponseMessage>(() => {
                var resMsg = new HttpResponseMessage();
                resMsg.Content = new StringContent("Hello World!");
                return resMsg;
            });

            task.Start();
            return task;
        }
    }
}
Andrzej Gis
  • 13,706
  • 14
  • 86
  • 130