using Crestron.SimplSharp; using System; using System.Collections.Generic; using System.IO; using System.Linq; using Org.BouncyCastle.Asn1; using Org.BouncyCastle.Asn1.X509; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Generators; using Org.BouncyCastle.Crypto.Prng; using Org.BouncyCastle.Pkcs; using Org.BouncyCastle.Security; using Org.BouncyCastle.Utilities; using Org.BouncyCastle.X509; using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2; using X509KeyStorageFlags = System.Security.Cryptography.X509Certificates.X509KeyStorageFlags; using X509ContentType = System.Security.Cryptography.X509Certificates.X509ContentType; using Org.BouncyCastle.Crypto.Operators; using BigInteger = Org.BouncyCastle.Math.BigInteger; using X509Certificate = Org.BouncyCastle.X509.X509Certificate; namespace PepperDash.Core { /// /// Taken From https://github.com/rlipscombe/bouncy-castle-csharp/ /// internal class BouncyCertificate { public string CertificatePassword { get; set; } = "password"; public X509Certificate2 LoadCertificate(string issuerFileName, string password) { // We need to pass 'Exportable', otherwise we can't get the private key. var issuerCertificate = new X509Certificate2(issuerFileName, password, X509KeyStorageFlags.Exportable); return issuerCertificate; } /// /// IssueCertificate method /// public 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, 2048); var issuerKeyPair = DotNetUtilities.GetKeyPair(issuerCertificate.PrivateKey); var serialNumber = GenerateSerialNumber(random); var issuerSerialNumber = new 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); } /// /// CreateCertificateAuthorityCertificate method /// public 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, 2048); // 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); return ConvertCertificate(certificate, subjectKeyPair, random); } /// /// CreateSelfSignedCertificate method /// public 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, 2048); // 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); } private 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; } private X509Certificate GenerateCertificate(SecureRandom random, string subjectName, AsymmetricCipherKeyPair subjectKeyPair, BigInteger subjectSerialNumber, string[] subjectAlternativeNames, string issuerName, AsymmetricCipherKeyPair issuerKeyPair, BigInteger issuerSerialNumber, bool isCertificateAuthority, KeyPurposeID[] usages) { 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 = DateTime.UtcNow.Date; var notAfter = notBefore.AddYears(2); 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. This is used to generate the thumbprint which is then signed // with the issuer's private key. We'll use SHA-256, which is (currently) considered fairly strong. const string signatureAlgorithm = "SHA256WithRSA"; // The certificate is signed with the issuer's private key. ISignatureFactory signatureFactory = new Asn1SignatureFactory(signatureAlgorithm, issuerKeyPair.Private, random); var certificate = certificateGenerator.Generate(signatureFactory); return certificate; } /// /// 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. /// /// /// private BigInteger GenerateSerialNumber(SecureRandom random) { var serialNumber = BigIntegers.CreateRandomInRange( BigInteger.One, BigInteger.ValueOf(Int64.MaxValue), random); return serialNumber; } /// /// Generate a key pair. /// /// The random number generator. /// The key length in bits. For RSA, 2048 bits should be considered the minimum acceptable these days. /// private 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; } /// /// 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. /// /// /// /// /// private void AddAuthorityKeyIdentifier(X509V3CertificateGenerator certificateGenerator, X509Name issuerDN, AsymmetricCipherKeyPair issuerKeyPair, BigInteger issuerSerialNumber) { var authorityKeyIdentifierExtension = new AuthorityKeyIdentifier( SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(issuerKeyPair.Public), new GeneralNames(new GeneralName(issuerDN)), issuerSerialNumber); certificateGenerator.AddExtension( X509Extensions.AuthorityKeyIdentifier.Id, false, authorityKeyIdentifierExtension); } /// /// Add the "Subject Alternative Names" extension. Note that you have to repeat /// the value from the "Subject Name" property. /// /// /// private void AddSubjectAlternativeNames(X509V3CertificateGenerator certificateGenerator, IEnumerable subjectAlternativeNames) { var subjectAlternativeNamesExtension = new DerSequence( subjectAlternativeNames.Select(name => new GeneralName(GeneralName.DnsName, name)) .ToArray()); certificateGenerator.AddExtension( X509Extensions.SubjectAlternativeName.Id, false, subjectAlternativeNamesExtension); } /// /// Add the "Extended Key Usage" extension, specifying (for example) "server authentication". /// /// /// private void AddExtendedKeyUsage(X509V3CertificateGenerator certificateGenerator, KeyPurposeID[] usages) { certificateGenerator.AddExtension( X509Extensions.ExtendedKeyUsage.Id, false, new ExtendedKeyUsage(usages)); } /// /// Add the "Basic Constraints" extension. /// /// /// private void AddBasicConstraints(X509V3CertificateGenerator certificateGenerator, bool isCertificateAuthority) { certificateGenerator.AddExtension( X509Extensions.BasicConstraints.Id, true, new BasicConstraints(isCertificateAuthority)); } /// /// Add the Subject Key Identifier. /// /// /// private void AddSubjectKeyIdentifier(X509V3CertificateGenerator certificateGenerator, AsymmetricCipherKeyPair subjectKeyPair) { var subjectKeyIdentifierExtension = new SubjectKeyIdentifier( SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(subjectKeyPair.Public)); certificateGenerator.AddExtension( X509Extensions.SubjectKeyIdentifier.Id, false, subjectKeyIdentifierExtension); } private X509Certificate2 ConvertCertificate(X509Certificate certificate, AsymmetricCipherKeyPair subjectKeyPair, SecureRandom random) { // 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 private key to that. var store = new Pkcs12StoreBuilder().Build(); // 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 private 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, CertificatePassword.ToCharArray(), random); var convertedCertificate = new X509Certificate2(stream.ToArray(), CertificatePassword, X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable); return convertedCertificate; } /// /// WriteCertificate method /// public void WriteCertificate(X509Certificate2 certificate, string outputDirectory, string certName) { // This password is the one attached to the PFX file. Use 'null' for no password. // Create PFX (PKCS #12) with private key try { var pfx = certificate.Export(X509ContentType.Pfx, CertificatePassword); File.WriteAllBytes(string.Format("{0}.pfx", Path.Combine(outputDirectory, certName)), pfx); } catch (Exception ex) { CrestronConsole.PrintLine(string.Format("Failed to write x509 cert pfx\r\n{0}", ex.Message)); } // Create Base 64 encoded CER (public key only) using (var writer = new StreamWriter($"{Path.Combine(outputDirectory, certName)}.cer", false)) { try { var contents = string.Format("-----BEGIN CERTIFICATE-----\r\n{0}\r\n-----END CERTIFICATE-----", Convert.ToBase64String(certificate.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks)); writer.Write(contents); } catch (Exception ex) { CrestronConsole.PrintLine(string.Format("Failed to write x509 cert cer\r\n{0}", ex.Message)); } } } /// /// AddCertToStore method /// public bool AddCertToStore(X509Certificate2 cert, System.Security.Cryptography.X509Certificates.StoreName st, System.Security.Cryptography.X509Certificates.StoreLocation sl) { bool bRet = false; try { var store = new System.Security.Cryptography.X509Certificates.X509Store(st, sl); store.Open(System.Security.Cryptography.X509Certificates.OpenFlags.ReadWrite); store.Add(cert); store.Close(); bRet = true; } catch (Exception ex) { CrestronConsole.PrintLine(string.Format("AddCertToStore Failed\r\n{0}\r\n{1}", ex.Message, ex.StackTrace)); } return bRet; } } }