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
 
Ummm... someplace where you create the certificate, but before you save and install in would probably be good place to start.

I am revisiting this as I still have not resolved it.

I can't see anywhere to add X509Certificate.SubjectAlternativeName, the property does not exist.

I have written a test bed and the code errors with the following: AuthenticationException: The remote certificate is invalid according to the validation procedure.

I don't know what I am missing here?

Regards,

Loftty
 
The part you are missing is that you are programmatically creating your certificate. You are supposed to purchase a signed SSL certificate from a vendor (or go down the free signed SSL certificate via Let's Encrypt).
 
The part you are missing is that you are programmatically creating your certificate. You are supposed to purchase a signed SSL certificate from a vendor (or go down the free signed SSL certificate via Let's Encrypt).

Hi Skydiver,

Thanks for your reply.

I have purchased a SSL cert, which is siting on my Azure server. There is 5 files 'AAACertificateServices.crt', 'My_CA_Bundle.ca-bundle', 'SectigoRSADomainValidationSecureServerCA.crt', 'STAR_dogoffice_co_uk.crt', 'USERTrustRSAAAACA.crt'.

I brought a wild card, so "*.dogoffice.co.uk". Should I be using the code above with these?

Regards,

Loftty
 
Sort of. You would install the certificates are part of your installation script or manually. It's not something that you can install on first run as you seem to be trying to do based on my quick re-reading of post #1 and #3.
 
Sort of. You would install the certificates are part of your installation script or manually. It's not something that you can install on first run as you seem to be trying to do based on my quick re-reading of post #1 and #3.

I have that certificate installed on my server (see image)

2022-05-18_15-56-27.png


How would I make use of that with the code above? or would I not do that?

Regards,

Loftty
 
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.
 
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
 
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.
 
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
 
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.
 
Back
Top Bottom