SSL on Self Hosting Web API

loftty

Member
Joined
Aug 6, 2018
Messages
9
Programming Experience
3-5
Hi All,

I have been having some trouble getting SSL enabled for my self hosting web api.

I have been trying with the code below, but if there is a better way of doing this, i would like to know :)

I use this code to generate the cert and register it against a port:

C#:
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;
            }
        }

        public 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 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
            );
        }

I call this code from:

C#:
private static void SetUpWebApi()
        {
            
            try
            {
                Trace.WriteLine("Setting up web service on " + appsettings.ApiUrl);


                var certSubjectName = "TestCert";
                var expiresIn = TimeSpan.FromDays(7);
                var cert = Cert.RegisterCertificate.GenerateCert(certSubjectName, expiresIn);

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

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

                string url = appsettings.ApiUrl.Replace("http", "https");
                WebApp.Start<StartUp>(url);
          
                Trace.WriteLine($"Web service started at: {DateTime.UtcNow:D}  at Url: {url}");
                
            }
            catch (Exception ex)
            {
                Trace.WriteLine(ex.Message);
            }

        }

When i first start up my app on port 9822 and load in the following URL https://localhost:9822/api/system/connecttest I get the following

1403435.png


1403436.png


1403437.png


1403438.png


On image 3 it states the SSL cert is invalid, but on the last image it states the cert is ok, so assume the cert got installed fine

Any ideas as to what I am doing wrong here?

Regards,

Loftty
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
4,029
Location
Chesapeake, VA
Programming Experience
10+
It's likely being marked as invalid by the the browser because it can't find your self-signed certificate in the trusted root CAs for which ever certificate store is being used by your browser.

I was just briefly scanning through your code above, so I must have missed where you installed your certificate into the current user's trusted root CAs.
 

loftty

Member
Joined
Aug 6, 2018
Messages
9
Programming Experience
3-5
Hi Skydiver,

Thanks for the quick reply.

The code where I install is:

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

Regards,

Loftty
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
4,029
Location
Chesapeake, VA
Programming Experience
10+
That's for installing into the HTTP.SYS module. As far as I know, putting a certificate into HTTP doesn't automatically put it into the machine's trusted root CAs. Recall that in the normal case, an SSL certificate that you get (either from a commercial entity, Let's Encrypt, or your Active Directory domain controller) has been signed by various CAs going all the way back to a trusted root CA. Adding the SSL certificate doesn't necessarily put in all other certificates along the chain of that SSL certificate into their proper stores. As I recall, it also doesn't do validation on the certificate to ensure it's validity in terms of the entire certificate chain being correct and trusted.
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
4,029
Location
Chesapeake, VA
Programming Experience
10+
Thanks for posting that. It looks like some things have advanced since I was last dealing with some SSL cert issues.

In that for per user or per machine cert store?

Is your browser set to use the per use or per machine cert store, or does it use a .CRT file for its store?
 

loftty

Member
Joined
Aug 6, 2018
Messages
9
Programming Experience
3-5
Thanks for posting that. It looks like some things have advanced since I was last dealing with some SSL cert issues.

In that for per user or per machine cert store?

Is your browser set to use the per use or per machine cert store, or does it use a .CRT file for its store?

That is for Current User
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
4,029
Location
Chesapeake, VA
Programming Experience
10+
I think I see what the issue is. A browser also verifies that the subject name matches the authority part of the URL. If it doesn't match, it'll try to look at the subject alternative names in the certificate for a match. Try adding localhost to a SAN section of your certificate.
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
4,029
Location
Chesapeake, VA
Programming Experience
10+
(Moved this thread out of "WinForms" and into "Hosting" since the issue more about trying to get a web service setup for hosting rather than WinForms. Although with the 7 day long self-signed certificate, "Testing" could also be an alternative subforum.)
 

loftty

Member
Joined
Aug 6, 2018
Messages
9
Programming Experience
3-5
I think I see what the issue is. A browser also verifies that the subject name matches the authority part of the URL. If it doesn't match, it'll try to look at the subject alternative names in the certificate for a match. Try adding localhost to a SAN section of your certificate.

Sorry, but how would you do this?
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
4,029
Location
Chesapeake, VA
Programming Experience
10+

loftty

Member
Joined
Aug 6, 2018
Messages
9
Programming Experience
3-5

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
4,029
Location
Chesapeake, VA
Programming Experience
10+
Ummm... someplace where you create the certificate, but before you save and install in would probably be good place to start.
 
Top Bottom