Register certificate to SSL port
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;
}
}
}
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, 2022Comments
-
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?