Trouble getting self hosting web api working over SSL

Viewed 36

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:

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:

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

            int port = 50244;
            var certSubjectName = "*.dogoffice.co.uk";
            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(50243, cert.Thumbprint);


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

    }

I then have a test app, which calls the code like so:

        public static bool ConnectTest(string username, string password)
    {
        using (HttpClient client = new HttpClient())
        {
            var auth = Convert.ToBase64String(
                System.Text.Encoding.ASCII.GetBytes(
                    string.Format("{0}:{1}", username, password)));

            client.DefaultRequestHeaders.Authorization =
                new AuthenticationHeaderValue(
                    "Basic",
                   auth);


            HttpResponseMessage httpr = client.GetAsync("https://127.0.0.1:50244/" + "/api/system/connecttest").Result;
            var result = httpr.Content.ReadAsStringAsync();

            bool connected = false;
            bool.TryParse(result.Result, out connected);


            return connected;
        }
    }

When I run the code I get the following error:

AuthenticationException: The remote certificate is invalid according to the validation procedure.

When I browse to the URL https://127.0.0.1:50244/api/system/connecttest. I hit the end point, however it says the certificate is not secure and the certificate is OK.

Any ideas to how to get this working?

Regards,

Ben

1 Answers

I see you are using a self signed certificate, that's why the browser is warning you about the authenticity of your certificate. For your local testing you can enable trust for your self signed certificate by installing that certificate on your machine. You can read more about this here

For production, I recommend you using getting a free one from CloudFlare. Another option would be using Let's Encrypt certbot.

I hope this helped you,

Have a nice day !

Related