We are generating some self-signed certificates for testing using BouncyCastle, but the code throws an exception when we try to add a private key to the certificate. Here's the code in question:
private static X509Certificate2 CreateCertificate(string subject, DateTimeOffset notBefore, DataTimeOffset notAfter, string issuer, AsymmetricKeyParamter issuerPrivateKey)
{
// Setup
X509V3CertificateGenerator certGenerator = new X509V3CertificateGenerator();
SecureRandom random = new SecureRandom(new CryptoApiRandomGenerator());
RsaKeyPairGenerator keyPairGenerator = new RsaKeyPairGenerator();
keyPairGenerator.Init(new KeyGenerationParameters(random, KeyStrength));
// Randomly generate a serial number
BigInteger serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(long.MaxValue), random);
certGenerator.SetSerialNumber(serialNumber);
// Set the issuer and subject names
X509Name issuerName = new X509Name(issuer);
X509Name subjectName = new X509Name(subject);
certGenerator.SetIssuerDN(issuerName);
certGenerator.SetSubjectDN(subjectName);
// Set the validity period
certGenerator.SetNotBefore(notBefore.UtcDateTime);
certGenerator.SetNotAfter(notAfter.UtcDateTime);
// Randomly generate the public key
AsymmetricCipherKeyPair subjectKeyPair = keyPairGenerator.GenerateKeyPair();
certGenerator.SetPublicKey(subjectKeyPair.Public);
// Generate the signed certificate
ISignatureFactory signatureFactory = new Asn1SignatureFactory(SHA256RSASignatureAlgorithm, issuerPrivateKey ?? subjectKeyPair.Private, random);
X509Certificate2 certificate = new X509Certificate2(certGenerator.Generate(signatureFactory).GetEncoded());
// Include the private key with the response
// ERROR HERE!
certificate.PrivateKey = DotNetUtilities.ToRSA(subjectKeyPair.Private as RsaPrivateCrtKeyParameters);
return certificate;
}
This code is in a library that targets .NET Standard 2.0, and the library is a dependency of two different applications: one targeting .NET Core 2.1 and the other targeting .NET Framework 4.7.2. I believe this works fine in the .NET Framework app, but in the .NET Core app I'm getting an exception with this message on the indicated line above:
Operation is not supported on this platform.
Apparently this is expected behavior in .NET Core. I am aware of the CopyWithPrivateKey
method as mentioned in this question, which in theory is what I should be using. However, this method is not supported in .NET Standard 2.0 (note the error at the top of the page indicating the redirect). Furthermore, the .NET Framework app cannot be converted to .NET Core at the moment because of some other dependencies which are .NET Framework. According to this document, .NET Standard 2.1 is not supported by .NET Framework at all, which means I cannot upgrade to .NET Standard 2.1 and use CopyWithPrivateKey
!
How can I create an X509Certificate2
with a private key in .NET Standard 2.0 in a way that's compatible with .NET Core?
After much digging, the only solution I found was to convert the certificate to a PKCS12-formatted byte array, append the private key, and then read it back into an X509Certificate2
object. It sucks, but from what I can tell in .NET Core the only way you can get one with the private key is to either call CopyWithPrivateKey
(unavailable in .NET Standard 2.0 as mentioned in the question) or to load PKCS12 data which contains the private key. The following code can do that:
private static X509Certificate2 AddPrivateKeyPlatformIndependent(Org.BouncyCastle.X509.X509Certificate bouncyCastleCert, AsymmetricKeyParameter privateKey)
{
string alias = bouncyCastleCert.SubjectDN.ToString();
Pkcs12Store store = new Pkcs12StoreBuilder().Build();
X509CertificateEntry certEntry = new X509CertificateEntry(bouncyCastleCert);
store.SetCertificateEntry(alias, certEntry);
// TODO: This needs extra logic to support a certificate chain
AsymmetricKeyEntry keyEntry = new AsymmetricKeyEntry(privateKey);
store.SetKeyEntry(alias, keyEntry, new X509CertificateEntry[] { certEntry });
byte[] certificateData;
string password = GenerateRandomString();
using (MemoryStream memoryStream = new MemoryStream())
{
store.Save(memoryStream, password.ToCharArray(), new SecureRandom());
memoryStream.Flush();
certificateData = memoryStream.ToArray();
}
return new X509Certificate2(certificateData, password, X509KeyStorageFlags.Exportable);
}
Use this in place of the last two lines in the question code, and instead of creating an X509Certificate2
with certGenerator
, use this to create the equivalent Bouncy Castle Type to pass to it: Org.BouncyCastle.X509.X509Certificate bouncyCastleCert = certGenerator.Generate(signatureFactory);
Since documentation on Bouncy Castle and .NET certificates is sparse at best, here are some quirks I found during this process:
The aliases for the certificate entry and the key entry in the PKCS12 container must be the same. Otherwise, you will get a "Keyset does not exist" exception when trying to use the private key.
You must use X509KeyStorageFlags.Exportable
when loading the byte array, otherwise you may get a "Key not valid for use in specified state" exception depending on how you use the cert. This also means you have to supply a password because there's no overload without it, but you can use any old string since it's a temporary password.