I'm trying to generate (self-signed) certificate with private key using ECDSA. The goals is to get "the same" (pkcs12) certificate as when using openssl:
openssl ecparam -genkey -name secp256r1 -out mykey.key
openssl req -new -key mykey.key -out myreq.csr
openssl req -x509 -days 7 -key mykey.key -in myreq.csr -out mycert.crt
openssl pkcs12 -export -out mycert.pfx -inkey mykey.key -in mycert.crt
I already use BouncyCastle to help me with creating RSA-based certificate(s), so next steps more or less follow the way I use to create RSA certs.
(note that BC
prefix is used for classes from BouncyCastle, MS
for .NET classes)
1 generate key pair: private and public keys
BC.IAsymmetricCipherKeyPairGenerator bcKpGen = BC.GeneratorUtilities.GetKeyPairGenerator("ECDSA");
bcKpGen.Init(new BC.ECKeyGenerationParameters(BC.SecObjectIdentifiers.SecP256r1, new BC.SecureRandom()));
BC.AsymmetricCipherKeyPair bcSubjKeys = bcKpGen.GenerateKeyPair();
2 use private key to sign public key with some additional data (subject, validity period etc)
BC.X509V3CertificateGenerator bcXgen = new BC.X509V3CertificateGenerator();
// .. set subject, validity period etc
bcXgen.SetPublicKey(bcSubjKeys.Public);
BC.ISignatureFactory bcSigFac = new BC.Asn1SignatureFactory("SHA256WITHECDSA", bcSubjKeys.Private);
BC.X509Certificate bcCert = bcXgen.Generate(bcSigFac);
3 "join" private key from step1 and certificate from step2 to get certificate with private key.
If I'm ok with certificate without private key, I could do something like:
MS.X509Certificate mcCert = new MS.X509Certificate2(bcCert.GetEncoded(), null);
and I'm done.
The issue(s) come when trying to set private-key:
msCert.PrivateKey = ConvertBouncyToNetSomehow(bcSubjKeys.Private)
(note that typeof msCert.PrivateKey
is MS.AsymmetricAlgorithm
and the type of bcSubjKeys.Private
is BC.ECPrivateKeyParameters
)
It seems that suitable way is using MS.ECDsaCng
class (which inherits from MS.AsymmetricAlgorithm
), but:
1 The only way I found to convert BC.ECPrivateKeyParameters
to MS.CngKey
(required by MS.ECDsaCng
) is via pkcs8 format:
BC.PrivateKeyInfo bcPKInfo = BC.PrivateKeyInfoFactory.CreatePrivateKeyInfo(bcSubjKeys.Private);
byte[] pkArr = bcPKInfo.GetDerEncoded();
MS.CngKey msPKCng = MS.CngKey.Import(pkArr, MS.CngKeyBlobFormat.Pkcs8PrivateBlob);
but using this approach some information is lost because value of msPKCng.AlgorithmGroup
is "ECDH"
while bcSubjKeys.Private.AlgorithmName
says "ECDSA"
. Also ECDH-key cannot be used with MS.ECDsaCng
.
Nevertheless.. I could continue with MS.ECDiffieHellmanCng
instead of requested MS.ECDsaCng
if..
2 implementation of MS.X509Certificate2.set_PrivateKey
requires the object implements interface MS.ICspAsymmetricAlgorithm
. But neither one of them (ECDsaCng
, ECDiffieHellmanCng
) implement it.
At this point it seems different approach must be used (because of MS.ICspAsymmetricAlgorithm
condition), e.g. export certificate and private key to pkcs file and use X509Certificate2.Import(..)
.
Any hint? Regards
Unfortunately, it's not possible to do straight out of the box right now. You can get the rest of the way with P/Invokes and .NET 4.6.2 (currently in preview). Or, with a detour through .NET Core you can build a PFX that works in .NET 4.6.1.
The Windows CNG libraries split ECC into ECDSA and ECDH. ECDSA key objects can only be used for ECDSA; but whenever Windows can't determine the usage during a PFX import (or PKCS#8 import) it calls a private key ECDH. Why? Because Windows lets ECDH key objects do both key agreement (ECDH) and digital signature (ECDSA), so ECDH is more flexible.
But .NET 4.6.1 didn't know that.
.NET Core doesn't have this limitation (see https://github.com/dotnet/corefx/pull/5850), and .NET 4.6.2 has also removed the restriction (per https://github.com/Microsoft/dotnet/blob/master/releases/net462/dotnet462-changes.md#user-content-bcl).
.NET Core now has an ImportParameters method on ECDsa. If you can translate the BC.ECPrivateKeyProperty object to an MS.ECParameters structure you can import the blob into an ECDsaCng object. (Be sure to use it as a named curve, instead of explicitly copying all of the curve parameters).
Since it was purposefully imported into an ECDsa object it gets an ECDSA key, and that information will be embedded in the PFX.
With a bit of P/Invoking you can convince Windows to build a PFX using an ephemeral key. While .NET can't access ephemeral private keys from certificates, it will be able to make use of it if loaded from a PFX:
[DllImport(Libraries.Crypt32, CharSet = CharSet.Unicode, SetLastError = true)]
private static extern unsafe bool CertSetCertificateContextProperty(IntPtr pCertContext, CertContextPropId dwPropId, CertSetPropertyFlags dwFlags, SafeNCryptKeyHandle pvData);
internal enum CertContextPropId : int
{
CERT_NCRYPT_KEY_HANDLE_PROP_ID = 78,
}
[Flags]
internal enum CertSetPropertyFlags : int
{
None = 0,
}
private static X509Certificate2 MateECDsaPrivateKey(
X509Certificate2 cert,
CngKey privateKey)
{
// Make a new certificate instance which isn't tied to the current one
using (var tmpCert = new X509Certificate2(cert.RawData))
{
SafeNCryptKeyHandle keyHandle = privateKey.Handle;
// Set the ephemeral key handle property
if (!CertSetCertificateContextProperty(
tmpCert.Handle,
CertContextPropId.CERT_NCRYPT_KEY_HANDLE_PROP_ID,
CertSetPropertyFlags.None,
keyHandle))
{
throw new CryptographicException(Marshal.GetLastWin32Error());
}
// You could emit this, if you prefer.
byte[] pfxBytes = tmpCert.Export(X509ContentType.Pkcs12);
// Clear the key handle out again to prevent double-free
keyHandle = new SafeNCryptKeyHandle();
if (!CertSetCertificateContextProperty(
tmpCert.Handle,
CertContextPropId.CERT_NCRYPT_KEY_HANDLE_PROP_ID,
CertSetPropertyFlags.None,
keyHandle))
{
throw new CryptographicException(Marshal.GetLastWin32Error());
}
// Now load a new certificate which has a temporary keyfile on disk.
// Note: If you don't want exportability here, don't request it.
var matedCert = new X509Certificate2(pfxBytes, (string)null, X509KeyStorageFlags.Exportable);
using (ECDsa ecdsa = matedCert.GetECDsaPrivateKey())
{
if (ecdsa == null)
{
throw new InvalidOperationException("It didn't work");
}
}
return matedCert;
}
}
You'll need .NET 4.6.1 (or newer) to have access to GetECDsaPrivateKey().
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With