C# Automated Certificate Generation, Local Certificate Installation, and Trust Certificate Issues Resolution

2022年11月9日 44点热度 5人点赞 0条评论
内容目录

Background

During local development, the internal network's HTTPS is considered insecure.
file

This can result in JS requests not being sent.
To secure HTTPS, the automatic generation and installation of certificates for local localhost is implemented here.

Writing Code

The certificate generation uses the built-in libraries of .NET, without the need for third-party packages.

using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;

Refer to the method for generating certificates from the FastGithub project.

The first step is to write a certificate generator, where the code is directly copied from here: https://github.com/dotnetcore/FastGithub/blob/9f9cbce624310c207b01699de2a5818a742e11ca/FastGithub.HttpServer/Certs/CertGenerator.cs

Next, define the service for managing the generated certificates. The original author used .NET 7, and since the current stable version is .NET 6, many APIs cannot be used, so modifications are necessary. The original address is:
https://github.com/dotnetcore/FastGithub/blob/9f9cbce624310c207b01699de2a5818a742e11ca/FastGithub.HttpServer/Certs/CertService.cs

Define the certificate location and name:

        private const string CACERT_PATH = "cacert";

        /// <summary>
        /// Get the certificate file path
        /// </summary>
        public string CaCerFilePath { get; } = OperatingSystem.IsLinux() ? $"{CACERT_PATH}/https.crt" : $"{CACERT_PATH}/https.cer";

        /// <summary>
        /// Get the private key file path
        /// </summary>
        public string CaKeyFilePath { get; } = $"{CACERT_PATH}/https.key";

This involves two files, the client certificate and the private key.
.key is the private key that can be used to generate both server and client certificates, so only the .key private key needs to be saved, and there is no need to export the server certificate.
.csr and .cer are client certificates, and in Windows, the .cer format can be used. The reason for exporting the client certificate is to install it, and it only needs to be installed once, without the need for dynamic generation.

The rules for the certificate management service are as follows: if the ssl directory does not contain certificates, then generate and install them; if the files already exist, load them into memory without reinstalling.

The complete code is as follows:

    /// <summary>
    /// Certificate generation service
    /// </summary>
    internal class CertService
    {

        private const string CACERT_PATH = "cacert";

        /// <summary>
        /// Get the certificate file path
        /// </summary>
        public string CaCerFilePath { get; } = OperatingSystem.IsLinux() ? $"{CACERT_PATH}/https.crt" : $"{CACERT_PATH}/https.cer";

        /// <summary>
        /// Get the private key file path
        /// </summary>
        public string CaKeyFilePath { get; } = $"{CACERT_PATH}/https.key";

        private X509Certificate2? caCert;

        /*
         Local will generate both cer and key files, the cer file is imported into the Windows certificate manager.
         The key file is used to generate the X509 certificate each time at startup for the web service.
         */

        /// <summary>
        /// Generate CA certificate
        /// </summary> 
        public bool CreateCaCertIfNotExists()
        {
            if (!Directory.Exists(CACERT_PATH)) Directory.CreateDirectory(CACERT_PATH);
            if (File.Exists(this.CaCerFilePath) && File.Exists(this.CaKeyFilePath))
            {
                return false;
            }

            File.Delete(this.CaCerFilePath);
            File.Delete(this.CaKeyFilePath);

            var notBefore = DateTimeOffset.Now.AddDays(-1);
            var notAfter = DateTimeOffset.Now.AddYears(10);

            var subjectName = new X500DistinguishedName($"CN=痴者工良");
            this.caCert = CertGenerator.CreateCACertificate(subjectName, notBefore, notAfter);

            var privateKeyPem = ExportRSAPrivateKeyPem(this.caCert.GetRSAPrivateKey());
            File.WriteAllText(this.CaKeyFilePath, new string(privateKeyPem), Encoding.ASCII);

            var certPem = ExportCertificatePem(this.caCert);
            File.WriteAllText(this.CaCerFilePath, new string(certPem), Encoding.ASCII);

            return true;
        }

        /// <summary>
        /// Get the certificate issued for the specified domain
        /// </summary>
        /// <param name="domain"></param> 
        /// <returns></returns>
        public X509Certificate2 GetOrCreateServerCert(string? domain)
        {
            if (this.caCert == null)
            {
                using var rsa = RSA.Create();
                rsa.ImportFromPem(File.ReadAllText(this.CaKeyFilePath));
                this.caCert = new X509Certificate2(this.CaCerFilePath).CopyWithPrivateKey(rsa);
            }

            var key = $"{nameof(CertService)}:{domain}";
            var endCert = GetOrCreateCert();
            return endCert!;

            // Generate a 1-year certificate for the domain
            X509Certificate2 GetOrCreateCert()
            {
                var notBefore = DateTimeOffset.Now.AddDays(-1);
                var notAfter = DateTimeOffset.Now.AddYears(1);

                var extraDomains = GetExtraDomains();

                var subjectName = new X500DistinguishedName($"CN={domain}");
                var endCert = CertGenerator.CreateEndCertificate(this.caCert, subjectName, extraDomains, notBefore, notAfter);

                // Reinitialize the certificate to be compatible with the Windows platform which cannot use in-memory certificates
                return new X509Certificate2(endCert.Export(X509ContentType.Pfx));
            }
        }
        private static IEnumerable<string> GetExtraDomains()
        {
            yield return Environment.MachineName;
            yield return IPAddress.Loopback.ToString();
            yield return IPAddress.IPv6Loopback.ToString();
        }

        internal const string RasPrivateKey = "RSA PRIVATE KEY";

        private static string ExportRSAPrivateKeyPem(RSA rsa)
        {
            var key = rsa.ExportRSAPrivateKey();
            var chars = PemEncoding.Write(RasPrivateKey, key);
            return new string(chars);
        }

        private static string ExportCertificatePem(X509Certificate2 x509)
        {
            var chars = PemEncoding.Write(PemLabels.X509Certificate, x509.Export(X509ContentType.Cert));
            return new string(chars);
        }

        /// <summary>
        /// Install CA certificate
        /// </summary>
        /// <exception cref="Exception">Cannot install the certificate</exception>
        public void Install()
        {
            var caCertFilePath = CaCerFilePath;
            try
            {
                using var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine);
                store.Open(OpenFlags.ReadWrite);

                var caCert = new X509Certificate2(caCertFilePath);
                var subjectName = caCert.Subject[3..];
                foreach (var item in store.Certificates.Find(X509FindType.FindBySubjectName, subjectName, false))
                {
                    if (item.Thumbprint != caCert.Thumbprint)
                    {
                        store.Remove(item);
                    }
                }
                if (store.Certificates.Find(X509FindType.FindByThumbprint, caCert.Thumbprint, true).Count == 0)
                {
                    store.Add(caCert);
                }
                store.Close();
            }
            catch (Exception ex)
            {
                throw new Exception($"Please manually install the CA certificate {caCertFilePath} into \"Place all certificates in the following store\"\\\"Trusted Root Certification Authorities\"" + ex);
            }
        }
    }
}

Using in ASP.NET Core

Load the server certificate in ASP.NET Core (generate X509 certificates each time on startup).

            var sslService = new CertService();
            if(sslService.CreateCaCertIfNotExists())
            {
                try
                {
                    sslService.Install();
                }
                catch (Exception)
                {

                }
            }

           var webhost =  WebHost.CreateDefaultBuilder()
                .UseStartup<Startup>()
                .UseKestrel(serverOptions =>
                {
                    serverOptions.ListenAnyIP(39999,
                        listenOptions =>
                        {
                            var certificate = sslService.GetOrCreateServerCert("localhost");
                            listenOptions.UseHttps(certificate);
                        });
                })
                .Build();
            await webhost.RunAsync();  

痴者工良

高级程序员劝退师

文章评论