Register certificate to SSL port

11,106

Solution 1

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();

Solution 2

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;
        }
    }
}
Share:
11,106
Robert Petz
Author by

Robert Petz

.Net developer focusing on WPF and the Entity Framework. Spends most of his time trying to solve large scale data synchronization woes and working on best practices for round-tripping data between applications. Highly interested in implementing real-time technologies, such as SignalR, wherever absolutely possible. An Adobe Flex certified expert and professional software engineer. Proficient in many languages including C#, Java, Actionscript 3, and WPF XAML markup.

Updated on July 24, 2022

Comments

  • Robert Petz
    Robert Petz almost 2 years

    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?