Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

generate certificate using ECDSA in c#

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.AlgorithmNamesays "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

like image 990
grim.ub Avatar asked Jan 06 '23 06:01

grim.ub


1 Answers

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.

"ECDSA" vs "ECDH"

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).

Generating "ECDSA" keys, instead of "ECDH"

.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.

Building the PFX (tying it all together)

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().

like image 188
bartonjs Avatar answered Jan 22 '23 01:01

bartonjs