SSL on Self Hosting Web API

loftty

Member
Joined
Aug 6, 2018
Messages
14
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
5,251
Location
Chesapeake, VA
Programming Experience
10+
If you are using IIS, normally you would just add STAR_dogoffice_co_uk certificate to the HTTPS binding for your web service using the IIS Manager GUI. (I think that the 'netsh http app' command would also do the same thing that the GUI does, but I'm not sure.)

If you are running Kestrel, it has its own way using that certificate. I vaguely recall being able to use an environment variable. Another alternative would be having some explicit code to load and use the certificate. My memories of these is kind of hazy right now.

Be warned that Microsoft recommends not exposing Kestrel directly to the internet. They recommend using IIS, Nginx, or Apache act as a reverse proxy in front of Kestrel. Nginx and Apache have their own procedures for using a certificate.
 

loftty

Member
Joined
Aug 6, 2018
Messages
14
Programming Experience
3-5
The last image I sent was from IIS UI. The cert installed is dogofficewildcard and I have a domain called testing.dogoffice.co.uk pointing to that server.

I just can't get the code to host over https

Regards,

Ben
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
5,251
Location
Chesapeake, VA
Programming Experience
10+
That screenshot seems to show that the cert is in your personal key store. I think that it should be in the machine key store.

I general, the way I do things is put the cert in the HTTPS bindings for the site in IIS. Then on the web root directory, I put in a simple default.htm file and then try to access the site, and validate the cert seen by the browser If that works, then anything hosted in that site will also share the same certificate.
 

loftty

Member
Joined
Aug 6, 2018
Messages
14
Programming Experience
3-5
That screenshot seems to show that the cert is in your personal key store. I think that it should be in the machine key store.

I general, the way I do things is put the cert in the HTTPS bindings for the site in IIS. Then on the web root directory, I put in a simple default.htm file and then try to access the site, and validate the cert seen by the browser If that works, then anything hosted in that site will also share the same certificate.

Hi Skydiver,

I will try it in the machine store,

As you can see from the code, it is trying to to do a self host, so there won't be no website. Saying that, i would expect it to kinda be the same principal.

Regards,

Ben
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
5,251
Location
Chesapeake, VA
Programming Experience
10+
A web API has a website. It might not have any UI, but it still has a site. Recall that in your post #1, you were trying to access your API via the browser. You were trying to access a site.
 
Top Bottom