Bläddra i källkod

使用基础库实现CertGenerator

xljiulang 2 år sedan
förälder
incheckning
0ddc22a299

+ 109 - 156
FastGithub.HttpServer/Certs/CertGenerator.cs

@@ -1,23 +1,9 @@
-using Org.BouncyCastle.Asn1.Pkcs;
-using Org.BouncyCastle.Asn1.X509;
-using Org.BouncyCastle.Asn1.X9;
-using Org.BouncyCastle.Crypto;
-using Org.BouncyCastle.Crypto.Generators;
-using Org.BouncyCastle.Crypto.Operators;
-using Org.BouncyCastle.Crypto.Parameters;
-using Org.BouncyCastle.Math;
-using Org.BouncyCastle.OpenSsl;
-using Org.BouncyCastle.Pkcs;
-using Org.BouncyCastle.Security;
-using Org.BouncyCastle.X509;
-using Org.BouncyCastle.X509.Extension;
-using System;
+using System;
 using System.Collections.Generic;
-using System.IO;
 using System.Linq;
 using System.Net;
-using System.Text;
-using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
 
 namespace FastGithub.HttpServer.Certs
 {
@@ -26,178 +12,145 @@ namespace FastGithub.HttpServer.Certs
     /// </summary>
     static class CertGenerator
     {
-        private static readonly SecureRandom secureRandom = new();
+        private static readonly Oid tlsServerOid = new("1.3.6.1.5.5.7.3.1");
+        private static readonly Oid tlsClientOid = new("1.3.6.1.5.5.7.3.2");
 
         /// <summary>
-        /// 生成自签名证书
+        /// 生成ca证书
         /// </summary>
-        /// <param name="domains"></param>
-        /// <param name="keySizeBits"></param>
-        /// <param name="validFrom"></param>
-        /// <param name="validTo"></param>
-        /// <param name="caPublicCerPath"></param>
-        /// <param name="caPrivateKeyPath"></param>
-        public static void GenerateBySelf(IEnumerable<string> domains, int keySizeBits, DateTime validFrom, DateTime validTo, string caPublicCerPath, string caPrivateKeyPath)
-        {
-            var keys = GenerateRsaKeyPair(keySizeBits);
-            var cert = GenerateCertificate(domains, keys.Public, validFrom, validTo, domains.First(), null, keys.Private, 1);
-
-            using var priWriter = new StreamWriter(caPrivateKeyPath);
-            var priPemWriter = new PemWriter(priWriter);
-            priPemWriter.WriteObject(keys.Private);
-            priPemWriter.Writer.Flush();
-
-            using var pubWriter = new StreamWriter(caPublicCerPath);
-            var pubPemWriter = new PemWriter(pubWriter);
-            pubPemWriter.WriteObject(cert);
-            pubPemWriter.Writer.Flush();
-        }
-
-        /// <summary>
-        /// 生成CA签名证书
-        /// </summary>
-        /// <param name="domains"></param>
-        /// <param name="keySizeBits"></param>
-        /// <param name="validFrom"></param>
-        /// <param name="validTo"></param>
-        /// <param name="caPublicCerPath"></param>
-        /// <param name="caPrivateKeyPath"></param>
+        /// <param name="subjectName"></param>
+        /// <param name="notBefore"></param>
+        /// <param name="notAfter"></param>
+        /// <param name="rsaKeySizeInBits"></param>
+        /// <param name="pathLengthConstraint"></param>
         /// <returns></returns>
-        public static X509Certificate2 GenerateByCa(IEnumerable<string> domains, int keySizeBits, DateTime validFrom, DateTime validTo, string caPublicCerPath, string caPrivateKeyPath, string? password = default)
+        public static X509Certificate2 CreateCACertificate(
+            X500DistinguishedName subjectName,
+            DateTimeOffset notBefore,
+            DateTimeOffset notAfter,
+            int rsaKeySizeInBits = 2048,
+            int pathLengthConstraint = 1)
         {
-            if (File.Exists(caPublicCerPath) == false)
-            {
-                throw new FileNotFoundException(caPublicCerPath);
-            }
+            using var rsa = RSA.Create(rsaKeySizeInBits);
+            var request = new CertificateRequest(subjectName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
 
-            if (File.Exists(caPrivateKeyPath) == false)
-            {
-                throw new FileNotFoundException(caPublicCerPath);
-            }
+            var basicConstraints = new X509BasicConstraintsExtension(true, pathLengthConstraint > 0, pathLengthConstraint, true);
+            request.CertificateExtensions.Add(basicConstraints);
 
-            using var pubReader = new StreamReader(caPublicCerPath, Encoding.ASCII);
-            var caCert = (X509Certificate)new PemReader(pubReader).ReadObject();
+            var keyUsage = new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.CrlSign | X509KeyUsageFlags.KeyCertSign, true);
+            request.CertificateExtensions.Add(keyUsage);
 
-            using var priReader = new StreamReader(caPrivateKeyPath, Encoding.ASCII);
-            var reader = new PemReader(priReader);
-            var caPrivateKey = ((AsymmetricCipherKeyPair)reader.ReadObject()).Private;
+            var oids = new OidCollection { tlsServerOid, tlsClientOid };
+            var enhancedKeyUsage = new X509EnhancedKeyUsageExtension(oids, true);
+            request.CertificateExtensions.Add(enhancedKeyUsage);
 
-            var caSubjectName = GetSubjectName(caCert);
-            var keys = GenerateRsaKeyPair(keySizeBits);
-            var cert = GenerateCertificate(domains, keys.Public, validFrom, validTo, caSubjectName, caCert.GetPublicKey(), caPrivateKey, null);
+            var dnsBuilder = new SubjectAlternativeNameBuilder();
+            dnsBuilder.Add(subjectName.Name[3..]);
+            request.CertificateExtensions.Add(dnsBuilder.Build());
 
-            return GeneratePfx(cert, keys.Private, password);
-        }
+            var subjectKeyId = new X509SubjectKeyIdentifierExtension(request.PublicKey, false);
+            request.CertificateExtensions.Add(subjectKeyId);
 
-        /// <summary>
-        /// 生成私钥
-        /// </summary>
-        /// <param name="length"></param>
-        /// <returns></returns>
-        private static AsymmetricCipherKeyPair GenerateRsaKeyPair(int length)
-        {
-            var keygenParam = new KeyGenerationParameters(secureRandom, length);
-            var keyGenerator = new RsaKeyPairGenerator();
-            keyGenerator.Init(keygenParam);
-            return keyGenerator.GenerateKeyPair();
+            return request.CreateSelfSigned(notBefore, notAfter);
         }
 
         /// <summary>
-        /// 生成证书
+        /// 生成服务器证书
         /// </summary>
-        /// <param name="domains"></param>
-        /// <param name="subjectPublic"></param>
-        /// <param name="validFrom"></param>
-        /// <param name="validTo"></param>
-        /// <param name="issuerName"></param>
-        /// <param name="issuerPublic"></param>
-        /// <param name="issuerPrivate"></param>
-        /// <param name="caPathLengthConstraint"></param>
+        /// <param name="issuerCertificate"></param>
+        /// <param name="subjectName"></param>
+        /// <param name="extraDnsNames"></param>
+        /// <param name="notBefore"></param>
+        /// <param name="notAfter"></param>
+        /// <param name="rsaKeySizeInBits"></param>
         /// <returns></returns>
-        private static X509Certificate GenerateCertificate(IEnumerable<string> domains, AsymmetricKeyParameter subjectPublic, DateTime validFrom, DateTime validTo, string issuerName, AsymmetricKeyParameter? issuerPublic, AsymmetricKeyParameter issuerPrivate, int? caPathLengthConstraint)
+        public static X509Certificate2 CreateEndCertificate(
+            X509Certificate2 issuerCertificate,
+            X500DistinguishedName subjectName,
+            IEnumerable<string>? extraDnsNames = default,
+            DateTimeOffset? notBefore = default,
+            DateTimeOffset? notAfter = default,
+            int rsaKeySizeInBits = 2048)
         {
-            var signatureFactory = issuerPrivate is ECPrivateKeyParameters
-                ? new Asn1SignatureFactory(X9ObjectIdentifiers.ECDsaWithSha256.ToString(), issuerPrivate)
-                : new Asn1SignatureFactory(PkcsObjectIdentifiers.Sha256WithRsaEncryption.ToString(), issuerPrivate);
-
-            var certGenerator = new X509V3CertificateGenerator();
-            certGenerator.SetIssuerDN(new X509Name("CN=" + issuerName));
-            certGenerator.SetSubjectDN(new X509Name("CN=" + domains.First()));
-            certGenerator.SetSerialNumber(BigInteger.ProbablePrime(120, new Random()));
-            certGenerator.SetNotBefore(validFrom);
-            certGenerator.SetNotAfter(validTo);
-            certGenerator.SetPublicKey(subjectPublic);
-
-            if (issuerPublic != null)
-            {
-                var akis = new AuthorityKeyIdentifierStructure(issuerPublic);
-                certGenerator.AddExtension(X509Extensions.AuthorityKeyIdentifier, false, akis);
-            }
+            using var rsa = RSA.Create(rsaKeySizeInBits);
+            var request = new CertificateRequest(subjectName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
+
+            var basicConstraints = new X509BasicConstraintsExtension(false, false, 0, true);
+            request.CertificateExtensions.Add(basicConstraints);
+
+            var keyUsage = new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment, true);
+            request.CertificateExtensions.Add(keyUsage);
+
+            var oids = new OidCollection { tlsServerOid, tlsClientOid };
+            var enhancedKeyUsage = new X509EnhancedKeyUsageExtension(oids, true);
+            request.CertificateExtensions.Add(enhancedKeyUsage);
+
+            var authorityKeyId = GetAuthorityKeyIdentifierExtension(issuerCertificate);
+            request.CertificateExtensions.Add(authorityKeyId);
+
+            var subjectKeyId = new X509SubjectKeyIdentifierExtension(request.PublicKey, false);
+            request.CertificateExtensions.Add(subjectKeyId);
+
+            var dnsBuilder = new SubjectAlternativeNameBuilder();
+            dnsBuilder.Add(subjectName.Name[3..]);
 
-            if (caPathLengthConstraint != null && caPathLengthConstraint >= 0)
+            if (extraDnsNames != null)
             {
-                var basicConstraints = new BasicConstraints(caPathLengthConstraint.Value);
-                certGenerator.AddExtension(X509Extensions.BasicConstraints, true, basicConstraints);
-                certGenerator.AddExtension(X509Extensions.KeyUsage, false, new KeyUsage(KeyUsage.DigitalSignature | KeyUsage.CrlSign | KeyUsage.KeyCertSign));
+                foreach (var dnsName in extraDnsNames)
+                {
+                    dnsBuilder.Add(dnsName);
+                }
             }
-            else
+
+            var dnsNames = dnsBuilder.Build();
+            request.CertificateExtensions.Add(dnsNames);
+
+            if (notBefore == null || notBefore.Value < issuerCertificate.NotBefore)
             {
-                var basicConstraints = new BasicConstraints(cA: false);
-                certGenerator.AddExtension(X509Extensions.BasicConstraints, true, basicConstraints);
-                certGenerator.AddExtension(X509Extensions.KeyUsage, false, new KeyUsage(KeyUsage.DigitalSignature | KeyUsage.KeyEncipherment));
+                notBefore = issuerCertificate.NotBefore;
             }
-            certGenerator.AddExtension(X509Extensions.ExtendedKeyUsage, true, new ExtendedKeyUsage(KeyPurposeID.IdKPServerAuth));
 
-            var names = domains.Select(domain =>
+            if (notAfter == null || notAfter.Value > issuerCertificate.NotAfter)
             {
-                var nameType = GeneralName.DnsName;
-                if (IPAddress.TryParse(domain, out _))
-                {
-                    nameType = GeneralName.IPAddress;
-                }
-                return new GeneralName(nameType, domain);
-            }).ToArray();
+                notAfter = issuerCertificate.NotAfter;
+            }
 
-            var subjectAltName = new GeneralNames(names);
-            certGenerator.AddExtension(X509Extensions.SubjectAlternativeName, false, subjectAltName);
-            return certGenerator.Generate(signatureFactory);
+            var serialNumber = BitConverter.GetBytes(Random.Shared.NextInt64());
+            using var certOnly = request.Create(issuerCertificate, notBefore.Value, notAfter.Value, serialNumber);
+            return certOnly.CopyWithPrivateKey(rsa);
         }
 
 
-        /// <summary>
-        /// 生成pfx
-        /// </summary>
-        /// <param name="cert"></param>
-        /// <param name="privateKey"></param>
-        /// <param name="password"></param>
-        /// <returns></returns>
-        private static X509Certificate2 GeneratePfx(X509Certificate cert, AsymmetricKeyParameter privateKey, string? password)
+
+        private static void Add(this SubjectAlternativeNameBuilder builder, string name)
         {
-            var subject = GetSubjectName(cert);
-            var pkcs12Store = new Pkcs12Store();
-            var certEntry = new X509CertificateEntry(cert);
-            pkcs12Store.SetCertificateEntry(subject, certEntry);
-            pkcs12Store.SetKeyEntry(subject, new AsymmetricKeyEntry(privateKey), new[] { certEntry });
-
-            using var pfxStream = new MemoryStream();
-            pkcs12Store.Save(pfxStream, password?.ToCharArray(), secureRandom);
-            return new X509Certificate2(pfxStream.ToArray());
+            if (IPAddress.TryParse(name, out var address))
+            {
+                builder.AddIpAddress(address);
+            }
+            else
+            {
+                builder.AddDnsName(name);
+            }
         }
 
 
-        /// <summary>
-        /// 获取Subject
-        /// </summary>
-        /// <param name="cert"></param>
-        /// <returns></returns>
-        private static string GetSubjectName(X509Certificate cert)
+        private static X509Extension GetAuthorityKeyIdentifierExtension(X509Certificate2 certificate)
         {
-            var subject = cert.SubjectDN.ToString();
-            if (subject.StartsWith("CN=", StringComparison.OrdinalIgnoreCase))
-            {
-                subject = subject[3..];
-            }
-            return subject;
+#if NET7_0_OR_GREATER
+            return X509AuthorityKeyIdentifierExtension.CreateFromCertificate(certificate, true, false);
+#else
+            var extension = certificate.Extensions.OfType<X509SubjectKeyIdentifierExtension>().First();
+            var subjectKeyIdentifier = extension.RawData.AsSpan(2);
+            var rawData = new byte[subjectKeyIdentifier.Length + 4];
+            rawData[0] = 0x30;
+            rawData[1] = 0x16;
+            rawData[2] = 0x80;
+            rawData[3] = 0x14;
+            subjectKeyIdentifier.CopyTo(rawData);
+
+            return new X509Extension("2.5.29.35", rawData, false);
+#endif
         }
     }
 }

+ 43 - 23
FastGithub.HttpServer/Certs/CertService.cs

@@ -6,7 +6,9 @@ using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Net;
+using System.Security.Cryptography;
 using System.Security.Cryptography.X509Certificates;
+using System.Text;
 
 namespace FastGithub.HttpServer.Certs
 {
@@ -16,10 +18,10 @@ namespace FastGithub.HttpServer.Certs
     sealed class CertService
     {
         private const string CACERT_PATH = "cacert";
-        private const int KEY_SIZE_BITS = 2048;
         private readonly IMemoryCache serverCertCache;
         private readonly IEnumerable<ICaCertInstaller> certInstallers;
         private readonly ILogger<CertService> logger;
+        private X509Certificate2? caCert;
 
 
         /// <summary>
@@ -54,17 +56,28 @@ namespace FastGithub.HttpServer.Certs
         /// </summary> 
         public bool CreateCaCertIfNotExists()
         {
-            if (File.Exists(CaCerFilePath) && File.Exists(CaKeyFilePath))
+            if (File.Exists(this.CaCerFilePath) && File.Exists(this.CaKeyFilePath))
             {
                 return false;
             }
 
-            File.Delete(CaCerFilePath);
-            File.Delete(CaKeyFilePath);
+            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={nameof(FastGithub)}");
+            this.caCert = CertGenerator.CreateCACertificate(subjectName, notBefore, notAfter);
+
+            var privateKey = this.caCert.GetRSAPrivateKey()?.ExportRSAPrivateKey();
+            var privateKeyPem = PemEncoding.Write("RSA PRIVATE KEY", privateKey);
+            File.WriteAllText(this.CaKeyFilePath, new string(privateKeyPem), Encoding.ASCII);
+
+            var cert = this.caCert.Export(X509ContentType.Cert);
+            var certPem = PemEncoding.Write("CERTIFICATE", cert);
+            File.WriteAllText(this.CaCerFilePath, new string(certPem), Encoding.ASCII);
 
-            var validFrom = DateTime.Today.AddDays(-1);
-            var validTo = DateTime.Today.AddYears(10);
-            CertGenerator.GenerateBySelf(new[] { nameof(FastGithub) }, KEY_SIZE_BITS, validFrom, validTo, CaCerFilePath, CaKeyFilePath);
             return true;
         }
 
@@ -73,14 +86,14 @@ namespace FastGithub.HttpServer.Certs
         /// </summary> 
         public void InstallAndTrustCaCert()
         {
-            var installer = certInstallers.FirstOrDefault(item => item.IsSupported());
+            var installer = this.certInstallers.FirstOrDefault(item => item.IsSupported());
             if (installer != null)
             {
-                installer.Install(CaCerFilePath);
+                installer.Install(this.CaCerFilePath);
             }
             else
             {
-                logger.LogWarning($"请根据你的系统平台手动安装和信任CA证书{CaCerFilePath}");
+                this.logger.LogWarning($"请根据你的系统平台手动安装和信任CA证书{this.CaCerFilePath}");
             }
 
             GitConfigSslverify(false);
@@ -118,18 +131,31 @@ namespace FastGithub.HttpServer.Certs
         /// <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}";
-            return serverCertCache.GetOrCreate(key, GetOrCreateCert);
+            var endCert = this.serverCertCache.GetOrCreate(key, GetOrCreateCert);
+            return endCert!;
 
             // 生成域名的1年证书
             X509Certificate2 GetOrCreateCert(ICacheEntry entry)
             {
-                var domains = GetDomains(domain).Distinct();
-                var validFrom = DateTime.Today.AddDays(-1);
-                var validTo = DateTime.Today.AddYears(1);
+                var notBefore = DateTimeOffset.Now.AddDays(-1);
+                var notAfter = DateTimeOffset.Now.AddYears(1);
+                entry.SetAbsoluteExpiration(notAfter);
+
+                var extraDomains = GetExtraDomains();
+
+                var subjectName = new X500DistinguishedName($"CN={domain}");
+                var endCert = CertGenerator.CreateEndCertificate(this.caCert, subjectName, extraDomains, notBefore, notAfter);
 
-                entry.SetAbsoluteExpiration(validTo);
-                return CertGenerator.GenerateByCa(domains, KEY_SIZE_BITS, validFrom, validTo, CaCerFilePath, CaKeyFilePath);
+                // 重新初始化证书,以兼容win平台不能使用内存证书
+                return new X509Certificate2(endCert.Export(X509ContentType.Pfx));
             }
         }
 
@@ -138,14 +164,8 @@ namespace FastGithub.HttpServer.Certs
         /// </summary>
         /// <param name="domain"></param>
         /// <returns></returns>
-        private static IEnumerable<string> GetDomains(string? domain)
+        private static IEnumerable<string> GetExtraDomains()
         {
-            if (string.IsNullOrEmpty(domain) == false)
-            {
-                yield return domain;
-                yield break;
-            }
-
             yield return Environment.MachineName;
             yield return IPAddress.Loopback.ToString();
             yield return IPAddress.IPv6Loopback.ToString();

+ 0 - 1
FastGithub.HttpServer/FastGithub.HttpServer.csproj

@@ -6,7 +6,6 @@
 
 	<ItemGroup>
 		<FrameworkReference Include="Microsoft.AspNetCore.App" />
-		<PackageReference Include="Portable.BouncyCastle" Version="1.9.0" />
 		<PackageReference Include="Yarp.ReverseProxy" Version="1.1.0" />
 	</ItemGroup>