C# 自动化生成证书、本地安装证书、解决信任证书问题

2022年11月9日 4310点热度 4人点赞 0条评论
内容纲要

背景

因为本地开发时,内网的 https 是不安全的 https。
file

会导致 js 发不出请求。
为了让 https 安全,这里实现了本地 localhost 自动生成证书以及安装的过程。

写代码

生成证书使用的是 .NET 自带的库,不需要引入第三方包。

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

生成证书的方法参考 https://github.com/dotnetcore/FastGithub 项目。

第一步是编写一个证书生成器,其中,代码直接从这里复制: https://github.com/dotnetcore/FastGithub/blob/9f9cbce624310c207b01699de2a5818a742e11ca/FastGithub.HttpServer/Certs/CertGenerator.cs

然后,定义管理生成证书的服务,原版作者使用的是 .NET 7,而且当前稳定版本是 .NET 6,很多 API 不能使用,因此需要对其改造。原版地址:
https://github.com/dotnetcore/FastGithub/blob/9f9cbce624310c207b01699de2a5818a742e11ca/FastGithub.HttpServer/Certs/CertService.cs

定义证书位置和名称:

        private const string CACERT_PATH = "cacert";

        /// <summary>
        /// 获取证书文件路径
        /// </summary>
        public string CaCerFilePath { get; } = OperatingSystem.IsLinux() ? $"{CACERT_PATH}/https.crt" : $"{CACERT_PATH}/https.cer";

        /// <summary>
        /// 获取私钥文件路径
        /// </summary>
        public string CaKeyFilePath { get; } = $"{CACERT_PATH}/https.key";

这里涉及到两个文件,客户端证书和私钥。
.key 是私钥,可以通过私钥来生成服务端证书和客户端证书,因此这里只需要保存 .key 私钥,不需要导出服务器证书。
.csr.cer 是客户端证书,在 Windows 下可以使用 .cer 格式。导出客户端证书的原因是要安装证书,而且安装一次即可,不需要动态生成。

证书管理服务的规则是,如果 ssl 目录下没有证书,那么就生成并安装;如果发现文件已经存在,则加载文件到内存,不会重新安装。

完整代码如下:

    /// <summary>
    /// 证书生成服务
    /// </summary>
    internal class CertService
    {

        private const string CACERT_PATH = "cacert";

        /// <summary>
        /// 获取证书文件路径
        /// </summary>
        public string CaCerFilePath { get; } = OperatingSystem.IsLinux() ? $"{CACERT_PATH}/https.crt" : $"{CACERT_PATH}/https.cer";

        /// <summary>
        /// 获取私钥文件路径
        /// </summary>
        public string CaKeyFilePath { get; } = $"{CACERT_PATH}/https.key";

        private X509Certificate2? caCert;

        /*
         本地会生成 cer 和 key 两个文件,cer 文件导入到 Window 证书管理器中。
         key 文件用于每次启动时生成 X509 证书,让 Web 服务使用。
         */

        /// <summary>
        /// 生成 CA 证书
        /// </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>
        /// 获取颁发给指定域名的证书
        /// </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!;

            // 生成域名的1年证书
            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);

                // 重新初始化证书,以兼容win平台不能使用内存证书
                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>
        /// 安装ca证书
        /// </summary>
        /// <exception cref="Exception">不能安装证书</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($"请手动安装CA证书{caCertFilePath}到“将所有的证书都放入下列存储”\\“受信任的根证书颁发机构”" + ex);
            }
        }
    }
}

在 ASP.NET Core 中使用

在 ASP.NET Core 中加载服务端证书(每次启动时生成 X509 证书)。

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

痴者工良

高级程序员劝退师

文章评论