1
\$\begingroup\$

Reaserching BouncyCastle for C# is not for the faint of heart. After navigating an ocean of Java dead ends, I have arrived at the following code.

I believe it's very useful for someone who wants to create these files in C# to use it anywhere.

It basically generates the 3 files every webdev who uses NGINX http server would need to configure SSL certificates.

I wanted to make sure some of this BouncyCastle code isn't outdated and if it could be streamlined and improved.

There's no need for class implementations. Just pure, openssl equivalent, output.

Take a look:

// .NET Imports
using System.Collections;
using System.Text.RegularExpressions;

// BouncyCastle for .NET Imports
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.Parameters;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Security;

class Program
{
    static void Main(string[] args)
    {
        // RootLenght.RootLength1024 for testing only!
        // It's faster to visualize. Recommended is RootLenght.RootLength2048
        GeneratePkcs3(RootLenght.RootLength1024);
        GeneratePkcs8Pkcs10("US", "Utah", "Salt Lake City", "XYZ Inc.", "IT Subdivision", "www.example.com", "[email protected]", RootLenght.RootLength2048);

    }

    /// <summary>
    ///     Generates dhparam in PKCS#3 format as defined by RFC 2631.
    ///     
    ///     OpenSSL equivalent command:
    ///     $ openssl dhparam -out dhparam.pem 2048
    ///     
    ///     The minimum root length recommended for NGINX dhparam is 2048bit
    ///     
    ///     *******************************************
    ///     Notes / Handy references:
    ///     http://www.keylength.com/en/compare/
    ///     
    /// </summary>
    static void GeneratePkcs3(RootLenght rootLength)
    {
        string dhparam = null;

        try
        {

            Console.WriteLine($"Generating {(int)rootLength}bit DH parameter...");
            Console.WriteLine("");
            Console.WriteLine("Choosing a Root Length > 1024bit may take a while.");
            Console.WriteLine("");

            const int DefaultPrimeProbability = 30;

            DHParametersGenerator generator = new DHParametersGenerator();
            generator.Init((int)rootLength, DefaultPrimeProbability, new SecureRandom());
            DHParameters parameters = generator.GenerateParameters();

            DHParameters realParams = new DHParameters(parameters.P, BigInteger.ValueOf(2));

            Asn1EncodableVector seq = new Asn1EncodableVector();
            seq.Add(new DerInteger(realParams.P));
            seq.Add(new DerInteger(realParams.G));
            byte[] derEncoded = new DerSequence(seq).GetDerEncoded();
            dhparam = Convert.ToBase64String(derEncoded);

            Console.WriteLine("-----BEGIN DH PARAMETERS-----");
            Console.WriteLine(SpliceText(dhparam, 64));
            Console.WriteLine("-----END DH PARAMETERS-----");

            Console.WriteLine("");
            Console.WriteLine("");

            Console.WriteLine($"{(int)rootLength}bit DH parameter succesfully generated.");

            Console.WriteLine("");
            Console.WriteLine("");

        }
        catch (Exception ex)
        {
            // Note: handles errors on the page. Redirect to error page.
            //ErrorHandler(ex);
            Console.WriteLine(ex.Message);
        }
    }

    /// <summary>
    ///     Generates RSA private key in PKCS#8 format as defined by RFC 5208.
    ///     Generates Certificate Signing Reuqest in PKCS#10 format as defined by RFC 2986.
    ///     
    ///     OpenSSL equivalent command:
    ///     $ openssl req -new -newkey rsa:2048 -nodes -keyout www_example_com.key -out www_example_com.csr -subj "/C=US/ST=UT/L=Salt Lak City/O=XYZ Inc./OU=IT Division/CN=www.example.com/[email protected]"
    ///     
    ///     The minimum root length recommended for NGINX dhparam is 2048bit
    ///     
    ///     *******************************************
    ///     Notes / Handy references:
    ///     http://www.keylength.com/en/compare/
    ///     
    /// </summary>
    static void GeneratePkcs8Pkcs10(string countryIso2Characters, string state, string city, string companyName, string division, string domainName, string email, RootLenght rootLength)
    {
        string csr = null;
        string privateKey = null;

        try
        {
            var rsaKeyPairGenerator = new RsaKeyPairGenerator();

            // Note: the numbers {3, 5, 17, 257 or 65537} as Fermat primes.
            // NIST doesn't allow a public exponent smaller than 65537, since smaller exponents are a problem if they aren't properly padded.
            // Note: the default in openssl is '65537', i.e. 0x10001.
            var genParam = new RsaKeyGenerationParameters(BigInteger.ValueOf(0x10001), new SecureRandom(), (int)rootLength, 128);

            rsaKeyPairGenerator.Init(genParam);

            AsymmetricCipherKeyPair pair = rsaKeyPairGenerator.GenerateKeyPair();

            IDictionary attrs = new Hashtable();

            attrs.Add(X509Name.C, countryIso2Characters);
            attrs.Add(X509Name.L, city);
            attrs.Add(X509Name.ST, state);
            attrs.Add(X509Name.O, companyName);
            if (division != null)
            {
                attrs.Add(X509Name.OU, division);
            }
            attrs.Add(X509Name.CN, domainName);
            if (email != null)
            {
                attrs.Add(X509Name.EmailAddress, email);
            }

            var subject = new X509Name(new ArrayList(attrs.Keys), attrs);

            var pkcs10CertificationRequest = new Pkcs10CertificationRequest(PkcsObjectIdentifiers.Sha256WithRsaEncryption.Id, subject, pair.Public, null, pair.Private);

            csr = Convert.ToBase64String(pkcs10CertificationRequest.GetEncoded());

            var pkInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(pair.Private);
            privateKey = Convert.ToBase64String(pkInfo.GetDerEncoded());

            Console.WriteLine("-----BEGIN PRIVATE KEY-----");
            Console.WriteLine(SpliceText(privateKey, 64));
            Console.WriteLine("-----END PRIVATE KEY-----");

            Console.WriteLine("");
            Console.WriteLine("");

            Console.WriteLine($"{(int)rootLength}bit Private key succesfully generated.");

            Console.WriteLine("");
            Console.WriteLine("");

            Console.WriteLine("-----BEGIN CERTIFICATE REQUEST-----");
            Console.WriteLine(SpliceText(csr, 64));
            Console.WriteLine("-----END CERTIFICATE REQUEST-----");

            Console.WriteLine("");
            Console.WriteLine("");

            Console.WriteLine($"Certificate Signing Request succesfully generated.");

            Console.ReadKey();

        }
        catch (Exception ex)
        {
            // Note: handles errors on the page. Redirect to error page.
            //ErrorHandler(ex);
            Console.WriteLine(ex.Message);
        }
    }

    private enum RootLenght
    {
        RootLength1024 = 1024, // Test ONLY!
        RootLength2048 = 2048,
        RootLength3072 = 3072,
        RootLength4096 = 4096,
    }

    public static string SpliceText(string text, int lineLength)
    {
        return Regex.Replace(text, "(.{" + lineLength + "})", "$1" + Environment.NewLine);
    }

}

Here's the output:

Generating 1024bit DH parameter...

Choosing a Root Length > 1024bit may take a while.

-----BEGIN DH PARAMETERS-----
MIGHAoGBAMP7G34Trw22TCnfYzIpkv/4hJKGGbEYalW4okSq2Zkxvk8Oeikel3k4
M+2b6KbBUMETpKqMhm18M3bCVE8ENFRGyTtrhYGXfPrWAF+cbhZVttgnpnyGOpcp
7rsuI3/NHkhp8n/Fuxy+9CDzh6mva4nCNSwvbzzkzhh0tOhtkMezAgEC
-----END DH PARAMETERS-----


1024bit DH parameter succesfully generated.


-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDQMMj7KQMzVc7x
oDXF5mqvuJS/7LH7hJEWQnpbsYH5oIu1/dfL9+xd0yBZ25xcA4NFSlWf34otTnBj
qrRLzq/86PZNHJn239odbM+1XRqPnC6XLyHBmOcQAa4eIQL3zHL6JQcCKWVcOBy4
HvY+OTwE2IksvEBgybj6HdSbdJnGLkyCpiG9F9tjcc3OUmflev554Vpwb6Pdxt0d
2zQt3UHHoNKRJYnhZMAGVm2X0c1s5QGx33sDQxBEZp7dhO4Wq9gnU8DVzJzXmvN6
9eJykIN42TEHdQr/MycQP0B5M92BDLuIy4mf/gCj9eIarJlDFnkKBu5QAF3wVtHt
13imkCEXAgMBAAECggEAVm+iyhBNiV87c7tCCB+xtLIsZAC4JoZ0HGtOVMp+fa8n
wlAZa9BLvOqemGAi61r0Ae0qXp2XR1c1N1QU1hKCo7zvIbXZwJNRAf1+wTxd/jjo
aRGC8Nd0O4OPEGhBTLOAGxYLclzQffi4B8OnFc96eUKTtVSVX/nwiDuhXUeMXqhz
AZynm9CqhDN4fVJhMlUCVokKmaPD5Y/toUTQ1hkrsb72HYX1tcAnp9j02UwUobZW
/GJKC9d0WVkUw1Rb0cWhPlyz1vV/l4nlif1mQVa9pSWFcGyjmi34ERGqGah+iCJe
H6br82dng8VvWcSJrjD+iloa5LmwFMQbLBax1sX+ZQKBgQD6AvKQZ0jq1AGwAwXj
Hv10zaW4RhiW3RI6Qnk4w+WxuR6tqHq8ONsyK6a97k9MSp00TglNo8udiBOdBcIK
F3WjhZX2i6qMIedIWwEg1ZEYHikTCsAcx/JyQP3YqMqTMBLJEfHVZURNkS/NUiDx
xCzF+5yQfA5MhnRWznwoMglcUwKBgQDVLWUBPWtDXZZWJyLkArxS28s/dstrUdYp
Vc+jDOEmIwWoPfCzkkTLCtGpveh0k1Z1dzTC5pAcWBngvDeB3rXrvCf7VtOd7Q1R
AIhHaLSYCA5yG93tf28WeCBICK/taAhkNAMcGcOfG106LOvB9t/B19q3LoxoEiJ/
L2PWy2CvrQKBgEiPxHDu2TX2tEquhe3mV6+n5Bo4lfhrT1gDZQV5rdfIH8RNKtoo
Mk48Zxem6/L7kObWY1LaYEVncjHXsvqU1nrQUbcN0ED9sg/JAenSslrqngc9zoZL
7e0FOefBDZJsmHctMyA5VPqiUdpopNEmm8wWe9lfeBLzzx5GrbhI1XirAoGBAJGs
+LX1M8SQQrjS+7vWxrzUqDrRQkcvYGtU0ZR5q641Bpum4ELGNK6P0SDuvthTCyUw
deSuTFKiPeTemgvslmLmbGgMOIZWROBSvc4WljrCXqTJuEmg6Nfw9RZkoVxZ2Eop
ZOqiDJVAzN+BbQZaXyQHDtZZP+eqZNqHFvHkf0jtAoGAEggGixhY79ZoSpRejlHS
oTCWqtsjB9xLPNC6JLD1TP7UhHNXhpnvBRpxanqeUKwvfnMLZKR5ZH2mE+aA3hij
oiU87X9iDf7H9oQ91dzvsoDh3WbEFvTFo88kBUxb7Q2lIqWG0OjhKDrj/3TGWzpQ
XznRAR43mzbkukKE39ddYns=
-----END PRIVATE KEY-----


2048bit Private key succesfully generated.


-----BEGIN CERTIFICATE REQUEST-----
MIIC4TCCAckCAQAwgZ0xFzAVBgNVBAsMDklUIFN1YmRpdmlzaW9uMRcwFQYDVQQH
DA5TYWx0IExha2UgQ2l0eTEgMB4GCSqGSIb3DQEJARYRYWRtaW5AZXhhbXBsZS5j
b20xGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTENMAsGA1UECAwEVXRhaDELMAkG
A1UEBhMCVVMxETAPBgNVBAoMCFhZWiBJbmMuMIIBIjANBgkqhkiG9w0BAQEFAAOC
AQ8AMIIBCgKCAQEA0DDI+ykDM1XO8aA1xeZqr7iUv+yx+4SRFkJ6W7GB+aCLtf3X
y/fsXdMgWducXAODRUpVn9+KLU5wY6q0S86v/Oj2TRyZ9t/aHWzPtV0aj5wuly8h
wZjnEAGuHiEC98xy+iUHAillXDgcuB72Pjk8BNiJLLxAYMm4+h3Um3SZxi5MgqYh
vRfbY3HNzlJn5Xr+eeFacG+j3cbdHds0Ld1Bx6DSkSWJ4WTABlZtl9HNbOUBsd97
A0MQRGae3YTuFqvYJ1PA1cyc15rzevXicpCDeNkxB3UK/zMnED9AeTPdgQy7iMuJ
n/4Ao/XiGqyZQxZ5CgbuUABd8FbR7dd4ppAhFwIDAQABMA0GCSqGSIb3DQEBCwUA
A4IBAQCwaMkLKKioFy1XUSRjkjDupuvsSXT070oov3F5Vwr0TT5rxki16PeXaLFf
lFzhaiwMx88ql2vHLeq6WADrvRtW/dB2OPTbceMC+CuRb7Mk0WKI+SMfrDbSYc4I
oXdDE8NfdMWV2ntWbKptVjJUfU0OmNnsh2W9973JpiRVMl+UJv/eMXMjKhH4Trzf
sW+voJLNajjVTJwYIQktOpGM7rmfjb9rVtZ1f5ZlgCCJUeTttdFSqSPG/yRh+9yJ
HKBeBFKptJ0HsZTLMhcRWc8rEiqTmag2QmL9jJHP4krwLf5aYQVUZAABJJ2LNyR8
8yYUVt5KsHKYksMu3yOnOq5BOHNu
-----END CERTIFICATE REQUEST-----


Certificate Signing Request succesfully generated.
\$\endgroup\$
1
  • \$\begingroup\$ Console.WriteLine(""); == Console.WriteLine(); :) \$\endgroup\$
    – aepot
    Commented Dec 28, 2021 at 23:40

1 Answer 1

1
\$\begingroup\$

Security remarks:

  • generally a 3072 bit RSA key and DH domain parameters would be preferred; 2048 bit keys don't even reach 128 bits of security
  • commonly named parameters are used, so there is no need to generate DH domain parameters (and it is questionable if they should be linked to the lifetime of the private key / certificate)
  • dumping private keys on the console is a very bad idea; instead a PKCS#12 or an password encrypted PKCS#8 should be generated
  • generally we use the faster / more secure ECDH instead of DH nowadays, although it is less secure against quantum computers - once they become available

Other remarks:

  • generally you'd use a line size of 76 (ASCII) characters within PEM encoded files, not 64
  • the message is printed to standard out instead of standard error
  • the "success" messages are entirely spurious and may harm code that tries to read out the PEM parts from the printout
  • it seems that a space between the bit size and the word "bit" would make your output more readable
  • the text "Choosing a Root Length > 1024bit may take a while." does not need to be printed for 1024 bit keys
  • it is strange that code edits are required to generate the output
  • parameter documentation is missing and it is unclear from the method definition which parameters may be set to null

Coding remarks:

  • input parameters such as country code size are not tested
  • csr and privateKey are never returned, so they don't need to be defined before the try loop, and assigning null is never needed
  • all exceptions are caught and printed, but you don't return any error code from your Main method
  • commented out code is present for the exception handling, without TODO or anything like that
  • the generation and DER encoding / printout of the generated parameter / key / certificate should be placed in separate methods
  • more functions should be used, e.g. Bouncy Castle has code to perform PEM encoding and probably has functions to perform DH parameter encoding as well
  • the enum is generally placed on top of the class definition, not at the bottom
  • there doesn't seem to be any need to make the SpliceText method public; you would not use this class as utility class in other applications for that
  • it's probably more performant and more readable to use a StringBuilder class for SpliceText than a regular expression Replace; you could simply use Substring after all
  • the entire SpliceText could be avoided if you had taken a look at the base 64 encoder provided with C#
  • it is unclear why the size of the private key is called rootLength
  • 8 method parameters is a lot, and it makes it easy to e.g. swap parameters, a builder / class could be used instead - especially since you may want to increase them later on
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.