Pki.cs

using Microsoft.Win32;
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Operators;
using Org.BouncyCastle.Crypto.Prng;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Utilities;
using Org.BouncyCastle.X509;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Text;
 
namespace GenXdev.Helpers
{
    public static class Pki
  {
        public static Pkcs12Store CreatePkcs12Store()
    {
        // Generate the key pair
        var random = new SecureRandom();
        var keyGenerationParameter = new KeyGenerationParameters(random, 4096);
        var keyPairGenerator = new RsaKeyPairGenerator();
        keyPairGenerator.Init(keyGenerationParameter);
        var keyPair = keyPairGenerator.GenerateKeyPair();
 
            // Generate the certificate
            var generator = new X509V3CertificateGenerator();
 
            string signatureAlgorithm = "SHA256WithRSA"; // Change this as needed
            ISignatureFactory signatureFactory = new Asn1SignatureFactory(signatureAlgorithm, keyPair.Private);
 
            var cert = generator.Generate(signatureFactory);
             
            generator.SetPublicKey(keyPair.Public);
         
        // Create the PKCS12 store
        var builder = new Pkcs12StoreBuilder();
        builder.SetUseDerEncoding(true);
        var store = builder.Build();
 
        // Add a certificate entry to the store
        var certEntry = new X509CertificateEntry(cert);
        store.SetCertificateEntry("mycertificates.net", certEntry);
 
        // Add a private key entry to the store
        var keyEntry = new AsymmetricKeyEntry(keyPair.Private);
        store.SetKeyEntry("mycertificates.net", keyEntry, new[] { certEntry });
 
        return store;
    }
        static int bitStrength = 2048;
 
        /// <summary>
        /// Export a certificate to a PEM format string
        /// </summary>
        /// <param name="cert">The certificate to export</param>
        /// <returns>A PEM encoded string</returns>
        public static string ExportToPEM(System.Security.Cryptography.X509Certificates.X509Certificate cert)
        {
            StringBuilder builder = new StringBuilder();
 
            builder.AppendLine("-----BEGIN CERTIFICATE-----");
            builder.AppendLine(Convert.ToBase64String(cert.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks));
            builder.AppendLine("-----END CERTIFICATE-----");
 
            return builder.ToString();
        }
 
        public static X509Certificate2 LoadCertificate(string issuerFileName, string password = null)
        {
            try
            {
                // We need to pass 'Exportable', otherwise we can't get the public key.
                return new X509Certificate2(issuerFileName, password, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet);
            }
            catch (System.Security.Cryptography.CryptographicException)
            {
                // We need to pass 'Exportable', otherwise we can't get the public key.
                return new X509Certificate2(issuerFileName, default(String), X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet);
            }
        }
 
        public static X509Certificate2 IssueCertificate(string subjectName, X509Certificate2 issuerCertificate, string[] subjectAlternativeNames, KeyPurposeID[] usages)
        {
            // It's self-signed, so these are the same.
            var issuerName = issuerCertificate.Subject;
 
            var random = GetSecureRandom();
            var subjectKeyPair = GenerateKeyPair(random, bitStrength);
 
            var issuerKeyPair = DotNetUtilities.GetKeyPair(issuerCertificate.GetRSAPrivateKey());
 
            var serialNumber = GenerateSerialNumber(random);
            var issuerSerialNumber = new Org.BouncyCastle.Math.BigInteger(issuerCertificate.GetSerialNumber());
 
            const bool isCertificateAuthority = false;
            var certificate = GenerateCertificate(random, subjectName, subjectKeyPair, serialNumber,
                                                  subjectAlternativeNames, issuerName, issuerKeyPair,
                                                  issuerSerialNumber, isCertificateAuthority,
                                                  usages);
            return ConvertCertificate(certificate, subjectKeyPair, random);
        }
 
        public static X509Certificate2 CreateCertificateAuthorityCertificate(string subjectName, string[] subjectAlternativeNames, KeyPurposeID[] usages)
        {
            // It's self-signed, so these are the same.
            var issuerName = subjectName;
 
            var random = GetSecureRandom();
            var subjectKeyPair = GenerateKeyPair(random, bitStrength);
 
            // It's self-signed, so these are the same.
            var issuerKeyPair = subjectKeyPair;
 
            var serialNumber = GenerateSerialNumber(random);
            var issuerSerialNumber = serialNumber; // Self-signed, so it's the same serial number.
 
            const bool isCertificateAuthority = true;
            var certificate = GenerateCertificate(random, subjectName, subjectKeyPair, serialNumber,
                                                  subjectAlternativeNames, issuerName, issuerKeyPair,
                                                  issuerSerialNumber, isCertificateAuthority,
                                                  usages, 3650 * 2);
            return ConvertCertificate(certificate, subjectKeyPair, random);
        }
 
        public static X509Certificate2 CreateSelfSignedCertificate(string subjectName, string[] subjectAlternativeNames, KeyPurposeID[] usages)
        {
            // It's self-signed, so these are the same.
            var issuerName = subjectName;
 
            var random = GetSecureRandom();
            var subjectKeyPair = GenerateKeyPair(random, bitStrength);
 
            // It's self-signed, so these are the same.
            var issuerKeyPair = subjectKeyPair;
 
            var serialNumber = GenerateSerialNumber(random);
            var issuerSerialNumber = serialNumber; // Self-signed, so it's the same serial number.
 
            const bool isCertificateAuthority = false;
            var certificate = GenerateCertificate(random, subjectName, subjectKeyPair, serialNumber,
                                                  subjectAlternativeNames, issuerName, issuerKeyPair,
                                                  issuerSerialNumber, isCertificateAuthority,
                                                  usages);
            return ConvertCertificate(certificate, subjectKeyPair, random);
        }
 
        public static SecureRandom GetSecureRandom()
        {
            // Since we're on Windows, we'll use the CryptoAPI one (on the assumption
            // that it might have access to better sources of entropy than the built-in
            // Bouncy Castle ones):
            var randomGenerator = new CryptoApiRandomGenerator();
            var random = new SecureRandom(randomGenerator);
            return random;
        }
 
        public static Org.BouncyCastle.X509.X509Certificate GenerateCertificate(SecureRandom random,
                                                           string subjectName,
                                                           AsymmetricCipherKeyPair subjectKeyPair,
                                                           Org.BouncyCastle.Math.BigInteger subjectSerialNumber,
                                                           string[] subjectAlternativeNames,
                                                           string issuerName,
                                                           AsymmetricCipherKeyPair issuerKeyPair,
                                                           Org.BouncyCastle.Math.BigInteger issuerSerialNumber,
                                                           bool isCertificateAuthority,
                                                           KeyPurposeID[] usages,
                                                           uint LifeSpanDays = 365 * 2
                                                           )
        {
            var certificateGenerator = new X509V3CertificateGenerator();
 
            certificateGenerator.SetSerialNumber(subjectSerialNumber);
 
            var issuerDN = new X509Name(issuerName);
            certificateGenerator.SetIssuerDN(issuerDN);
 
            // Note: The subject can be omitted if you specify a subject alternative name (SAN).
            var subjectDN = new X509Name(subjectName);
            certificateGenerator.SetSubjectDN(subjectDN);
 
            // Our certificate needs valid from/to values.
            var notBefore = System.DateTime.UtcNow.Date.AddDays(-2);
            var notAfter = notBefore.AddDays(LifeSpanDays);
 
            certificateGenerator.SetNotBefore(notBefore);
            certificateGenerator.SetNotAfter(notAfter);
 
            // The subject's public key goes in the certificate.
            certificateGenerator.SetPublicKey(subjectKeyPair.Public);
 
            AddAuthorityKeyIdentifier(certificateGenerator, issuerDN, issuerKeyPair, issuerSerialNumber);
            AddSubjectKeyIdentifier(certificateGenerator, subjectKeyPair);
            AddBasicConstraints(certificateGenerator, isCertificateAuthority);
 
            if (usages != null && usages.Any())
                AddExtendedKeyUsage(certificateGenerator, usages);
 
            if (subjectAlternativeNames != null && subjectAlternativeNames.Any())
                AddSubjectAlternativeNames(certificateGenerator, subjectAlternativeNames);
 
            // Set the signature algorithm.
            ISignatureFactory signatureFactory = new Asn1SignatureFactory("SHA512WITHRSA", issuerKeyPair.Private, random);
 
            // The certificate is signed with the issuer's public key.
            var certificate = certificateGenerator.Generate(signatureFactory);
            return certificate;
        }
 
        /// <summary>
        /// The certificate needs a serial number. This is used for revocation,
        /// and usually should be an incrementing index (which makes it easier to revoke a range of certificates).
        /// Since we don't have anywhere to store the incrementing index, we can just use a random number.
        /// </summary>
        /// <param name="random"></param>
        /// <returns></returns>
        public static Org.BouncyCastle.Math.BigInteger GenerateSerialNumber(SecureRandom random)
        {
            var serialNumber =
                BigIntegers.CreateRandomInRange(
                    Org.BouncyCastle.Math.BigInteger.One, Org.BouncyCastle.Math.BigInteger.ValueOf(Int64.MaxValue), random);
            return serialNumber;
        }
 
        /// <summary>
        /// Generate a key pair.
        /// </summary>
        /// <param name="random">The random number generator.</param>
        /// <param name="strength">The key length in bits. For RSA, 2048 bits should be considered the minimum acceptable these days.</param>
        /// <returns></returns>
        public static AsymmetricCipherKeyPair GenerateKeyPair(SecureRandom random, int strength)
        {
            var keyGenerationParameters = new KeyGenerationParameters(random, strength);
 
            var keyPairGenerator = new RsaKeyPairGenerator();
            keyPairGenerator.Init(keyGenerationParameters);
            var subjectKeyPair = keyPairGenerator.GenerateKeyPair();
            return subjectKeyPair;
        }
 
        /// <summary>
        /// Add the Authority Key Identifier. According to http://www.alvestrand.no/objectid/2.5.29.35.html, this
        /// identifies the public key to be used to verify the signature on this certificate.
        /// In a certificate chain, this corresponds to the "Subject Key Identifier" on the *issuer* certificate.
        /// The Bouncy Castle documentation, at http://www.bouncycastle.org/wiki/display/JA1/X.509+Public+Key+Certificate+and+Certification+Request+Generation,
        /// shows how to create this from the issuing certificate. Since we're creating a self-signed certificate, we have to do this slightly differently.
        /// </summary>
        /// <param name="certificateGenerator"></param>
        /// <param name="issuerDN"></param>
        /// <param name="issuerKeyPair"></param>
        /// <param name="issuerSerialNumber"></param>
        public static void AddAuthorityKeyIdentifier(X509V3CertificateGenerator certificateGenerator,
                                                      X509Name issuerDN,
                                                      AsymmetricCipherKeyPair issuerKeyPair,
                                                      Org.BouncyCastle.Math.BigInteger issuerSerialNumber)
        {
            var authorityKeyIdentifierExtension =
                new AuthorityKeyIdentifier(
                    SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(issuerKeyPair.Public),
                    new GeneralNames(new GeneralName(issuerDN)),
                    issuerSerialNumber);
            certificateGenerator.AddExtension(
                X509Extensions.AuthorityKeyIdentifier.Id, false, authorityKeyIdentifierExtension);
        }
 
        /// <summary>
        /// Add the "Subject Alternative Names" extension. Note that you have to repeat
        /// the value from the "Subject Name" property.
        /// </summary>
        /// <param name="certificateGenerator"></param>
        /// <param name="subjectAlternativeNames"></param>
        public static void AddSubjectAlternativeNames(X509V3CertificateGenerator certificateGenerator,
                                                       IEnumerable<string> subjectAlternativeNames)
        {
            var subjectAlternativeNamesExtension =
                new DerSequence(
                    subjectAlternativeNames.Select(name => new GeneralName(GeneralName.DnsName, name))
                                           .ToArray<Asn1Encodable>());
 
            certificateGenerator.AddExtension(
                X509Extensions.SubjectAlternativeName.Id, false, subjectAlternativeNamesExtension);
        }
 
        /// <summary>
        /// Add the "Extended Key Usage" extension, specifying (for example) "server authentication".
        /// </summary>
        /// <param name="certificateGenerator"></param>
        /// <param name="usages"></param>
        public static void AddExtendedKeyUsage(X509V3CertificateGenerator certificateGenerator, KeyPurposeID[] usages)
        {
            certificateGenerator.AddExtension(
                X509Extensions.ExtendedKeyUsage.Id, false, new ExtendedKeyUsage(usages));
        }
 
        /// <summary>
        /// Add the "Basic Constraints" extension.
        /// </summary>
        /// <param name="certificateGenerator"></param>
        /// <param name="isCertificateAuthority"></param>
        public static void AddBasicConstraints(X509V3CertificateGenerator certificateGenerator,
                                                bool isCertificateAuthority)
        {
            certificateGenerator.AddExtension(
                X509Extensions.BasicConstraints.Id, true, new BasicConstraints(isCertificateAuthority));
        }
 
        /// <summary>
        /// Add the Subject Key Identifier.
        /// </summary>
        /// <param name="certificateGenerator"></param>
        /// <param name="subjectKeyPair"></param>
        public static void AddSubjectKeyIdentifier(X509V3CertificateGenerator certificateGenerator,
                                                    AsymmetricCipherKeyPair subjectKeyPair)
        {
            var subjectKeyIdentifierExtension =
                new SubjectKeyIdentifier(
                    SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(subjectKeyPair.Public));
            certificateGenerator.AddExtension(
                X509Extensions.SubjectKeyIdentifier.Id, false, subjectKeyIdentifierExtension);
        }
 
        public static X509Certificate2 ConvertCertificate(Org.BouncyCastle.X509.X509Certificate certificate,
                                                           AsymmetricCipherKeyPair subjectKeyPair,
                                                           SecureRandom random,
                                                           string password = null)
        {
            // Now to convert the Bouncy Castle certificate to a .NET certificate.
            // See http://web.archive.org/web/20100504192226/http://www.fkollmann.de/v2/post/Creating-certificates-using-BouncyCastle.aspx
            // ...but, basically, we create a PKCS12 store (a .PFX file) in memory, and add the public and public key to that.
 
            var store = CreatePkcs12Store();
 
            // What Bouncy Castle calls "alias" is the same as what Windows terms the "friendly name".
            string friendlyName = certificate.SubjectDN.ToString();
 
            // Add the certificate.
            var certificateEntry = new X509CertificateEntry(certificate);
            store.SetCertificateEntry(friendlyName, certificateEntry);
 
            // Add the public key.
            store.SetKeyEntry(friendlyName, new AsymmetricKeyEntry(subjectKeyPair.Private), new[] { certificateEntry });
 
            // Convert it to an X509Certificate2 object by saving/loading it from a MemoryStream.
            // It needs a password. Since we'll remove this later, it doesn't particularly matter what we use.
            var stream = new MemoryStream();
            store.Save(stream, "password".ToCharArray(), random);
 
            var convertedCertificate =
                new X509Certificate2(stream.ToArray(),
                                     "password",
                                     X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);
            return convertedCertificate;
        }
 
        public static void WriteCertificate(X509Certificate2 certificate, string targetFilePath, string password = null)
        {
            var bytes = certificate.Export(X509ContentType.Pfx, password);
            FileSystem.ForciblyPrepareTargetFilePath(targetFilePath);
            File.WriteAllBytes(targetFilePath, bytes);
        }
 
        public static void WriteCertificateToPemFormat(X509Certificate2 certificate, string targetFilePath)
        {
            var text = ExportToPEM(certificate);
            FileSystem.ForciblyPrepareTargetFilePath(targetFilePath);
            File.WriteAllText(targetFilePath, text, new UTF8Encoding(false));
        }
 
        public static void EnableTlsProtocols()
        {
            // https://docs.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/runtime/appcontextswitchoverrides-element
 
            try
            {
                const string DisableCachingName = @"TestSwitch.LocalAppContext.DisableCaching";
                AppContext.SetSwitch(DisableCachingName, true);
            }
            catch { }
            try
            {
                const string DontEnableSchUseStrongCrypto = @"Switch.System.Net.DontEnableSchUseStrongCrypto";
                AppContext.SetSwitch(DontEnableSchUseStrongCrypto, true);
            }
            catch { }
            try
            {
                const string DontEnableSystemDefaultTlsVersions = @"TestSwitch.LocalAppContext.DontEnableSystemDefaultTlsVersions";
                AppContext.SetSwitch(DontEnableSystemDefaultTlsVersions, true);
            }
            catch { }
            try
            {
                const string DontEnableTlsAlerts = @"Switch.System.Net.DontEnableTlsAlerts";
                AppContext.SetSwitch(DontEnableTlsAlerts, true);
            }
            catch { }
            try
            {
                const string DontCheckCertificateEKUs = @"TestSwitch.LocalAppContext.DontCheckCertificateEKUs";
                AppContext.SetSwitch(DontCheckCertificateEKUs, true);
            }
            catch { }
            try
            {
                const string DontEnableSchSendAuxRecord = @"Switch.System.Net.DontEnableSchSendAuxRecord";
                AppContext.SetSwitch(DontEnableSchSendAuxRecord, true);
            }
            catch { }
            try
            {
                const string DisableUsingServicePointManagerSecurityProtocols = @"TestSwitch.LocalAppContext.DisableUsingServicePointManagerSecurityProtocols";
                AppContext.SetSwitch(DisableUsingServicePointManagerSecurityProtocols, false);
            }
            catch { }
            try
            {
                const string DontEnableSchUseStrongCrypto = @"TestSwitch.LocalAppContext.DontEnableSchUseStrongCrypto";
                AppContext.SetSwitch(DontEnableSchUseStrongCrypto, true);
            }
            catch { }
            try
            {
                using (var key = Registry.LocalMachine.CreateSubKey(@"SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client", true))
                {
                    key.SetValue("Enabled", "1", RegistryValueKind.DWord);
                }
                using (var key = Registry.LocalMachine.CreateSubKey(@"SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server", true))
                {
                    key.SetValue("Enabled", "1", RegistryValueKind.DWord);
                }
            }
            catch { }
            try
            {
                using (var key = Registry.LocalMachine.CreateSubKey(@"SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.3\Client", true))
                {
                    key.SetValue("Enabled", "1", RegistryValueKind.DWord);
                }
            }
            catch { }
            try
            {
                using (var key = Registry.LocalMachine.CreateSubKey(@"SYSTEM\CurrentControlSet\Control\Cryptography\Configuration\Local\SSL\00010003", true))
                {
                    var currentValue = ((string[])key.GetValue("Functions")).ToList<String>();
 
                    if (currentValue != null && !currentValue.Contains("RSA/SHA512"))
                    {
                        currentValue.Add(System.Environment.NewLine + "RSA/SHA512");
 
                        key.SetValue("Function", currentValue.ToArray<string>(), RegistryValueKind.MultiString);
                    }
                }
            }
            catch { }
 
        }
 
        public static void OptimizeAndReorganizeDNSNames(ref string certSubjectName, ref string[] certSubjectAltNames)
        {
            if ((certSubjectAltNames == null) || (certSubjectAltNames.Length == 0))
            {
                return;
            }
 
            var dnsName = Dns.GetHostName().ToLowerInvariant();
 
            var list = (from q in certSubjectAltNames where q.Trim().ToLowerInvariant() != dnsName select q.Trim().ToLowerInvariant()).ToList<string>();
 
            if ((list.Count > 0) && (string.IsNullOrWhiteSpace(certSubjectName) || (certSubjectName == dnsName)))
            {
                certSubjectName = list[0];
                list.Add(dnsName);
            }
 
            certSubjectAltNames = list.ToArray<string>();
        }
 
        public static string GetPrimaryDnsName(string defaultValue, string[] certSubjectAltNames)
        {
            if ((certSubjectAltNames == null) || (certSubjectAltNames.Length == 0))
            {
                return defaultValue;
            }
 
            var dnsName = Dns.GetHostName().ToLowerInvariant();
 
            var list = (from q in certSubjectAltNames where q.Trim().ToLowerInvariant() != dnsName select q.Trim().ToLowerInvariant());
 
            if (list.Any())
            {
                return list.First<string>();
            }
 
            return defaultValue;
        }
    }
}