using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Cryptography.X509Certificates; using System.Text; using Org.BouncyCastle.Asn1; using Org.BouncyCastle.Asn1.Pkcs; using Org.BouncyCastle.Asn1.X509; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Generators; using Org.BouncyCastle.Crypto.Operators; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Crypto.Prng; using Org.BouncyCastle.Math; using Org.BouncyCastle.OpenSsl; using Org.BouncyCastle.Pkcs; using Org.BouncyCastle.Security; using Org.BouncyCastle.Utilities; using Org.BouncyCastle.X509; using Attribute = Org.BouncyCastle.Asn1.Cms.Attribute; using SysHashAlgorName = System.Security.Cryptography.HashAlgorithmName; using BcCertificate = Org.BouncyCastle.X509.X509Certificate; using BigInteger = Org.BouncyCastle.Math.BigInteger; using X509Extension = Org.BouncyCastle.Asn1.X509.X509Extension; namespace PRSServer { /// /// Wrapper class around a native BouncyCastle Asymmetric Key Pair. /// public class CertPrivateKey { public AsymmetricCipherKeyPair KeyPair { get; set; } } /// /// Collection of static routines for working with basic entities needs to /// support X509 Certificate operations, including request generation, /// private key management, standards-based serialization and export. /// /// /// Unfortunately there is not yet enough out-of-the-box support for /// general certificate management in .NET Standard, so we rely on a 3rd-party /// library to handle most of this work for us, in this case the very capable /// BouncyCastle C# library. /// public static class CertHelper { // Useful references and examples for BC: // CSR: // http://www.bouncycastle.org/wiki/display/JA1/X.509+Public+Key+Certificate+and+Certification+Request+Generation // https://gist.github.com/Venomed/5337717aadfb61b09e58 // http://codereview.stackexchange.com/questions/84752/net-bouncycastle-csr-and-private-key-generation // Other: // https://www.txedo.com/blog/java-read-rsa-keys-pem-file/ public const int RSA_BITS_DEFAULT = 2048; public const int RSA_BITS_MINIMUM = 1024 + 1; // LE no longer allows 1024-bit PrvKeys // This is based on the BC RSA Generator code: // https://github.com/bcgit/bc-csharp/blob/fba5af528ce7dcd0ac0513363196a62639b82a86/crypto/src/crypto/generators/RsaKeyPairGenerator.cs#L37 private const int DEFAULT_CERTAINTY = 100; public static readonly BigInteger RSA_E_3 = BigInteger.Three; public static readonly BigInteger RSA_E_F4 = BigInteger.ValueOf(0x10001); public static CertPrivateKey GenerateRsaPrivateKey(int bits, string PubExp = null) { // Bits less than 1024 are weak Ref: http://openssl.org/docs/manmaster/crypto/RSA_generate_key_ex.html if (bits < RSA_BITS_MINIMUM) bits = RSA_BITS_DEFAULT; BigInteger e; if (string.IsNullOrEmpty(PubExp)) e = RSA_E_F4; else if (PubExp.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) e = new BigInteger(PubExp, 16); else e = new BigInteger(PubExp); var rsaKgp = new RsaKeyGenerationParameters( e, new SecureRandom(), bits, DEFAULT_CERTAINTY); var rkpg = new RsaKeyPairGenerator(); rkpg.Init(rsaKgp); var ackp = rkpg.GenerateKeyPair(); return new CertPrivateKey { KeyPair = ackp }; } public static CertPrivateKey GenerateEcPrivateKey(int bits) { // From: // http://www.bouncycastle.org/wiki/display/JA1/Elliptic+Curve+Key+Pair+Generation+and+Key+Factories // var csr = new Pkcs10CertificationRequest() // string curveName; // switch (bits) // { // case 256: // curveName = "P-256"; // break; // case 384: // curveName = "P-384"; // break; // default: // throw new ArgumentException("bit length is unsupported or unknown", nameof(bits)); // } // var ecParamSpec = ECNamedCurveTable.GetByName(curveName); // From: // https://www.codeproject.com/Tips/1150485/Csharp-Elliptical-Curve-Cryptography-with-Bouncy-C var ecKpg = new ECKeyPairGenerator("ECDSA"); ecKpg.Init(new KeyGenerationParameters(new SecureRandom(), bits)); var ecKp = ecKpg.GenerateKeyPair(); return new CertPrivateKey { KeyPair = ecKp }; } public static void ExportPrivateKey(CertPrivateKey pk, EncodingFormat fmt, Stream target) { switch (fmt) { case EncodingFormat.PEM: var pem = ToPrivatePem(pk.KeyPair); var bytes = Encoding.UTF8.GetBytes(pem); target.Write(bytes, 0, bytes.Length); break; case EncodingFormat.DER: var der = PrivateKeyInfoFactory.CreatePrivateKeyInfo(pk.KeyPair.Private).GetDerEncoded(); target.Write(der, 0, der.Length); break; default: throw new NotSupportedException("unsupported encoding format"); } } public static CertPrivateKey ImportPrivateKey(EncodingFormat fmt, Stream source) { if (fmt != EncodingFormat.PEM) throw new NotSupportedException("Unsupported archive format"); using (var tr = new StreamReader(source)) { var pr = new PemReader(tr); var pem = pr.ReadObject(); var ackp = pem as AsymmetricCipherKeyPair; if (ackp != null) { if(ackp.Private is RsaPrivateCrtKeyParameters rsa || ackp.Private is ECPrivateKeyParameters ec) { return new CertPrivateKey { KeyPair = ackp }; } } throw new InvalidDataException("could not read source as PEM private key"); } } public static byte[] GenerateRsaCsr(IEnumerable names, CertPrivateKey pk, SysHashAlgorName? hashAlgor = null) { if (hashAlgor == null) hashAlgor = SysHashAlgorName.SHA256; var attrs = new Dictionary { [X509Name.CN] = names.First() }; var subj = new X509Name(attrs.Keys.ToList(), attrs.Values.ToList()); var ackp = pk.KeyPair; var sigAlg = $"{hashAlgor.Value.Name}withRSA"; var csrAttrs = new List(); var gnames = new List( names.Select(x => new GeneralName(GeneralName.DnsName, x))); var altNames = new GeneralNames(gnames.ToArray()); #pragma warning disable CS0612 // Type or member is obsolete var x509Ext = new X509Extensions(new Dictionary() { [X509Extensions.SubjectAlternativeName] = new X509Extension( false, new DerOctetString(altNames)) }); #pragma warning restore CS0612 // Type or member is obsolete csrAttrs.Add(new Attribute( PkcsObjectIdentifiers.Pkcs9AtExtensionRequest, new DerSet(x509Ext))); #pragma warning disable CS0618 // Type or member is obsolete var csr = new Pkcs10CertificationRequest(sigAlg, subj, ackp.Public, new DerSet(csrAttrs.ToArray()), ackp.Private); #pragma warning restore CS0618 // Type or member is obsolete return csr.GetDerEncoded(); } public static byte[] GenerateEcCsr(IEnumerable names, CertPrivateKey pk, SysHashAlgorName? hashAlgor = null) { if (hashAlgor == null) hashAlgor = SysHashAlgorName.SHA256; var attrs = new Dictionary { [X509Name.CN] = names.First() }; var subj = new X509Name(attrs.Keys.ToList(), attrs.Values.ToList()); var ackp = pk.KeyPair; var sigAlg = $"{hashAlgor.Value.Name}withECDSA"; var csrAttrs = new List(); var gnames = new List( names.Select(x => new GeneralName(GeneralName.DnsName, x))); var altNames = new GeneralNames(gnames.ToArray()); #pragma warning disable CS0612 // Type or member is obsolete var x509Ext = new X509Extensions(new Dictionary() { [X509Extensions.SubjectAlternativeName] = new X509Extension( false, new DerOctetString(altNames)) }); #pragma warning restore CS0612 // Type or member is obsolete csrAttrs.Add(new Attribute( PkcsObjectIdentifiers.Pkcs9AtExtensionRequest, new DerSet(x509Ext))); #pragma warning disable CS0618 // Type or member is obsolete var csr = new Pkcs10CertificationRequest(sigAlg, subj, ackp.Public, new DerSet(csrAttrs.ToArray()), ackp.Private); #pragma warning restore CS0618 // Type or member is obsolete return csr.GetDerEncoded(); } public static (CertPrivateKey, BcCertificate) GenerateRsaCACertificate(string subjectName, int keyStrength = 2048) { // Generating Random Numbers var randomGenerator = new CryptoApiRandomGenerator(); var random = new SecureRandom(randomGenerator); // Subject Public Key var keyGenerationParameters = new KeyGenerationParameters(random, keyStrength); var keyPairGenerator = new RsaKeyPairGenerator(); keyPairGenerator.Init(keyGenerationParameters); AsymmetricCipherKeyPair subjectKeyPair = keyPairGenerator.GenerateKeyPair(); // Generating the Certificate var issuerKeyPair = subjectKeyPair; // The Certificate Generator var certificateGenerator = new X509V3CertificateGenerator(); // Serial Number var serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(long.MaxValue), random); certificateGenerator.SetSerialNumber(serialNumber); // Signature Algorithm.. // const string signatureAlgorithm = "SHA256WithRSA"; // #pragma warning disable CS0618 // Type or member is obsolete // certificateGenerator.SetSignatureAlgorithm(signatureAlgorithm); // #pragma warning restore CS0618 // Type or member is obsolete ISignatureFactory signatureFactory = new Asn1SignatureFactory("SHA512WITHRSA", issuerKeyPair.Private, random); // Issuer and Subject Name var subjectDN = new X509Name(subjectName); var issuerDN = subjectDN; certificateGenerator.SetIssuerDN(issuerDN); certificateGenerator.SetSubjectDN(subjectDN); // Valid For var notBefore = DateTime.UtcNow.Date; var notAfter = notBefore.AddYears(2); certificateGenerator.SetNotBefore(notBefore); certificateGenerator.SetNotAfter(notAfter); certificateGenerator.SetPublicKey(subjectKeyPair.Public); // selfsign certificate // #pragma warning disable CS0618 // Type or member is obsolete // var certificate = certificateGenerator.Generate(issuerKeyPair.Private, random); // #pragma warning restore CS0618 // Type or member is obsolete var certificate = certificateGenerator.Generate(signatureFactory); // var x509 = new System.Security.Cryptography.X509Certificates.X509Certificate2(certificate.GetEncoded()); // // Add CA certificate to Root store // addCertToStore(cert, StoreName.Root, StoreLocation.CurrentUser); var key = new CertPrivateKey { KeyPair = issuerKeyPair }; return (key, certificate); } public static (CertPrivateKey, BcCertificate) GenerateRsaSelfSignedCertificate(string subjectName, string issuerName, AsymmetricKeyParameter issuerPrivKey, int keyStrength = 2048) { // Generating Random Numbers var randomGenerator = new CryptoApiRandomGenerator(); var random = new SecureRandom(randomGenerator); // Subject Public Key var keyGenerationParameters = new KeyGenerationParameters(random, keyStrength); var keyPairGenerator = new RsaKeyPairGenerator(); keyPairGenerator.Init(keyGenerationParameters); AsymmetricCipherKeyPair subjectKeyPair = keyPairGenerator.GenerateKeyPair(); var issuerKeyPair = subjectKeyPair; // The Certificate Generator var certificateGenerator = new X509V3CertificateGenerator(); // Serial Number var serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(long.MaxValue), random); certificateGenerator.SetSerialNumber(serialNumber); // Signature Algorithm // const string signatureAlgorithm = "SHA256WithRSA"; // #pragma warning disable CS0618 // Type or member is obsolete // certificateGenerator.SetSignatureAlgorithm(signatureAlgorithm); // #pragma warning restore CS0618 // Type or member is obsolete ISignatureFactory signatureFactory = new Asn1SignatureFactory("SHA512WITHRSA", issuerKeyPair.Private, random); // Issuer and Subject Name var subjectDN = new X509Name(subjectName); var issuerDN = new X509Name(issuerName); certificateGenerator.SetIssuerDN(issuerDN); certificateGenerator.SetSubjectDN(subjectDN); // Valid For var notBefore = DateTime.UtcNow.AddMonths(-1).Date; var notAfter = DateTime.UtcNow.AddMonths(1).Date; certificateGenerator.SetNotBefore(notBefore); certificateGenerator.SetNotAfter(notAfter); // Generating the Certificate certificateGenerator.SetPublicKey(subjectKeyPair.Public); // selfsign certificate // #pragma warning disable CS0618 // Type or member is obsolete // var certificate = certificateGenerator.Generate(issuerPrivKey, random); // #pragma warning restore CS0618 // Type or member is obsolete var certificate = certificateGenerator.Generate(signatureFactory); var key = new CertPrivateKey { KeyPair = subjectKeyPair }; return (key, certificate); } // public static X509Certificate2 // ToDotNetCert(CertPrivateKey key, BcCertificate certificate) // { // // merge into X509Certificate2 // // // correcponding private key // var info = PrivateKeyInfoFactory.CreatePrivateKeyInfo(key.KeyPair.Private); // // var x509 = new X509Certificate2( // certificate.GetEncoded()); // // // #pragma warning disable CS0618 // Type or member is obsolete // var seq = (Asn1Sequence)Asn1Object.FromByteArray(info.PrivateKeyData.GetDerEncoded()); // #pragma warning restore CS0618 // Type or member is obsolete // if (seq.Count != 9) // throw new PemException("malformed sequence in RSA private key"); // // #pragma warning disable CS0618 // Type or member is obsolete // var rsa = new RsaPrivateKeyStructure(seq); // #pragma warning restore CS0618 // Type or member is obsolete // var rsaparams = new RsaPrivateCrtKeyParameters( // rsa.Modulus, rsa.PublicExponent, rsa.PrivateExponent, // rsa.Prime1, rsa.Prime2, rsa.Exponent1, rsa.Exponent2, rsa.Coefficient); // // x509.PrivateKey = DotNetUtilities.ToRSA(rsaparams); // return x509; // } public static BcCertificate ImportCertificate(EncodingFormat fmt, Stream source) { BcCertificate bcCert = null; if (fmt == EncodingFormat.DER) { var certParser = new X509CertificateParser(); bcCert = certParser.ReadCertificate(source); } else if (fmt == EncodingFormat.PEM) { using (var tr = new StreamReader(source)) { var pr = new PemReader(tr); bcCert = (BcCertificate)pr.ReadObject(); } } else { throw new NotSupportedException("encoding format has not been implemented"); } return bcCert; } public static void ExportCertificate(BcCertificate cert, EncodingFormat fmt, Stream target) { if (fmt == EncodingFormat.PEM) { using (var tw = new StringWriter()) { var pw = new PemWriter(tw); pw.WriteObject(cert); var pemBytes = Encoding.UTF8.GetBytes(tw.GetStringBuilder().ToString()); target.Write(pemBytes, 0, pemBytes.Length); } } else if (fmt == EncodingFormat.DER) { var der = cert.GetEncoded(); target.Write(der, 0, der.Length); } else { throw new NotSupportedException("unsupported encoding format"); } } public static void ExportArchive(CertPrivateKey pk, IEnumerable certs, ArchiveFormat fmt, Stream target, string password = null) { if (fmt == ArchiveFormat.PKCS12) { var bcCerts = certs.Select(x => new X509CertificateEntry(x)).ToArray(); // Need to check this var pfx = new Pkcs12StoreBuilder().Build(); pfx.SetCertificateEntry(bcCerts[0].Certificate.SubjectDN.ToString(), bcCerts[0]); pfx.SetKeyEntry(bcCerts[0].Certificate.SubjectDN.ToString(), new AsymmetricKeyEntry(pk.KeyPair.Private), new[] { bcCerts[0] }); for (var i = 1; i < bcCerts.Length; ++i) //pfx.SetCertificateEntry(bcCerts[i].Certificate.SubjectDN.ToString(), pfx.SetCertificateEntry(i.ToString(), bcCerts[i]); // It used to be pretty straight forward to export this... pfx.Save(target, password?.ToCharArray(), new SecureRandom()); } else { throw new NotSupportedException("unsupported archive format"); } } private static string ToPrivatePem(AsymmetricCipherKeyPair ackp) { string pem; using (var tw = new StringWriter()) { var pw = new PemWriter(tw); pw.WriteObject(ackp.Private); pem = tw.GetStringBuilder().ToString(); tw.GetStringBuilder().Clear(); } return pem; } } }