Search code examples
c#bouncycastlex509certificate2ecdsadiffie-hellman

Diffie Hellman Key Exchange using ECDSA x509 certificates


I am trying to perform a Diffie-Hellman key exchange using 2 ECDSA x509 certificates.

Here is the method where I extract the keys from the certificates for computation of the derived key.

private byte[] GetDerivedKey(X509Certificate2 publicCertificate, X509Certificate2 privateCertificate)
    {
        byte[] derivedKey;

        using (var privateKey = privateCertificate.GetECDsaPrivateKey())
        using (var publicKey = publicCertificate.GetECDsaPublicKey())
        {
            var privateParams = privateKey.ExportParameters(true);  //This line is failing
            var publicParams = publicKey.ExportParameters(false);

            using (var privateCng = ECDiffieHellmanCng.Create(privateParams))
            using (var publicCng = ECDiffieHellmanCng.Create(publicParams))
            {
                derivedKey = privateCng.DeriveKeyMaterial(publicCng.PublicKey);
            }
        }
        

        return derivedKey;
    }

I've commented on the line that is failing privateKey.ExportParameters(true) with the error:

System.Security.Cryptography.CryptographicException : The requested operation is not supported.

at System.Security.Cryptography.NCryptNative.ExportKey(SafeNCryptKeyHandle key, String format)
at System.Security.Cryptography.CngKey.Export(CngKeyBlobFormat format)
at System.Security.Cryptography.ECCng.ExportParameters(CngKey key, Boolean includePrivateParameters, ECParameters& ecparams)
at System.Security.Cryptography.ECDsaCng.ExportParameters(Boolean includePrivateParameters)

Because this is a self signed certificate that I am generating, I assume I am doing something wrong.

I first create a root CA certificate and pass in the private key to sign my certificate.

private X509Certificate2 CreateECSDACertificate(string certificateName,
        string issuerCertificateName,
        TimeSpan lifetime,
        AsymmetricKeyParameter issuerPrivateKey,
        string certificateFriendlyName = null)
    {
        // Generating Random Numbers
        var randomGenerator = new CryptoApiRandomGenerator();
        var random = new SecureRandom(randomGenerator);

        var signatureFactory = new Asn1SignatureFactory("SHA256WithECDSA", issuerPrivateKey, random);

        // The Certificate Generator
        var certificateGenerator = new X509V3CertificateGenerator();

        // Serial Number
        var serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(Int64.MaxValue), random);
        certificateGenerator.SetSerialNumber(serialNumber);

        // Issuer and Subject Name
        var subjectDistinguishedName = new X509Name($"CN={certificateName}");
        var issuerDistinguishedName = new X509Name($"CN={issuerCertificateName}");
        certificateGenerator.SetSubjectDN(subjectDistinguishedName);
        certificateGenerator.SetIssuerDN(issuerDistinguishedName);

        // Valid For
        var notBefore = DateTime.UtcNow.Date;
        var notAfter = notBefore.Add(lifetime);

        certificateGenerator.SetNotBefore(notBefore);
        certificateGenerator.SetNotAfter(notAfter);

        //key generation
        var keyGenerationParameters = new KeyGenerationParameters(random, _keyStrength);
        var keyPairGenerator = new ECKeyPairGenerator();
        keyPairGenerator.Init(keyGenerationParameters);
        var subjectKeyPair = keyPairGenerator.GenerateKeyPair();

        certificateGenerator.SetPublicKey(subjectKeyPair.Public);

        var certificate = certificateGenerator.Generate(signatureFactory);

        var store = new Pkcs12Store();
        var certificateEntry = new X509CertificateEntry(certificate);
        store.SetCertificateEntry(certificateName, certificateEntry);
        store.SetKeyEntry(certificateName, new AsymmetricKeyEntry(subjectKeyPair.Private), new[] { certificateEntry });

        X509Certificate2 x509;

        using (var pfxStream = new MemoryStream())
        {
            store.Save(pfxStream, null, new SecureRandom());
            pfxStream.Seek(0, SeekOrigin.Begin);
            x509 = new X509Certificate2(pfxStream.ToArray());
        }

        x509.FriendlyName = certificateFriendlyName;

        return x509;
    }

The .HasPrivateKey() method returns true, which I've read can return a false positive.

When I add my certificates to the store, I can verify the cert chain.

    [Test]
    public void CreateSelfSignedCertificate_AfterAddingToStore_CanBuildChain()
    {
        var result = _target.CreateSelfSignedCertificate(_subject, _issuer, TimeSpan.FromDays(356), _certificateFriendlyName, _issuerFriendlyName);

        _store.TryAddCertificateToStore(result.CertificateAuthority, _caStoreName, _location);
        _store.TryAddCertificateToStore(result.Certificate, _certStoreName, _location);

        var chain = new X509Chain
        {
            ChainPolicy =
            {
                RevocationMode = X509RevocationMode.NoCheck
            }
        };

        var chainBuilt = chain.Build(result.Certificate);

        if (!chainBuilt)
        {
            foreach (var status in chain.ChainStatus)
            {
                Assert.Warn(string.Format("Chain error: {0} {1}", status.Status, status.StatusInformation));
            }
        }

        Assert.IsTrue(chainBuilt, "Chain");
    }

I thought at first that maybe the private cert had to come from the cert store, so I imported it and then pulled it back out, but I get the same error, which is another reason I believe I'm not doing something quite right.

EDIT:

I have another class generating RSA x509's using the same code for putting the private key into the certificate. It allows me to export the RSA private key.

The variable _keyStrength is 384 and my signature factory is using "SHA256withECDSA". I have also tried using "SHA384withECDSA" but I get the same error.


Solution

  • OK. It's a blind shot but after looking at your code I noticed two things:

    • When you create PFX you set null password. But when you load the PFX into X509Certificate2 class you are using wrong constructor. You should use one with a password parameter and give a null into it
    • When you load PFX into X509Certificate2 class you do not specify, if the private key should be exportable. I think that this is the reason why privateKey.ExportParameters(true) gives you an exception. You should use this constructor and specify null as password

    Made it working

    I thought it was a bug. It's possible that it is. We clearly stated in X509Constructor that the private key should be exportable. I used X509KeyStorageFlags.EphemeralKeySet | X509KeyStorageFlags.Exportable flags too. But when I looked at the CngKey it had ExportPolicy set to AllowExport but not AllowPlaintextExport.

    It was exportable in some way. privateKey.Key.Export(CngKeyBlobFormat.OpaqueTransportBlob) worked. But privateKey.ExportParameters(true) did not.

    I've searched for a solution how to change ExportPolicy of CngKey. I found this SO question that helped me to change it. After that the ExportParameters worked.

    The fixed version of your GetDerivedKey method is

    private byte[] GetDerivedKey(X509Certificate2 publicCertificate, X509Certificate2 privateCertificate)
    {
        byte[] derivedKey;
    
        using (var privateKey = privateCertificate.GetECDsaPrivateKey())
        using (var publicKey = privateCertificate.GetECDsaPublicKey())
        {
            var myPrivateKeyToMessWith = privateKey as ECDsaCng;
    
            // start - taken from https://stackoverflow.com/q/48542233/3245057 
            // make private key exportable:
            byte[] bytes = BitConverter.GetBytes((int)(CngExportPolicies.AllowExport | CngExportPolicies.AllowPlaintextExport));
            CngProperty pty = new CngProperty(NCryptExportPolicyProperty, bytes, CngPropertyOptions.Persist);
            myPrivateKeyToMessWith.Key.SetProperty(pty);
            // end - taken from https://stackoverflow.com/q/48542233/3245057
    
            var privateParams = myPrivateKeyToMessWith.ExportParameters(true);  //This line is NOT failing anymore
            var publicParams = publicKey.ExportParameters(false);
    
            using (var privateCng = ECDiffieHellmanCng.Create(privateParams))
            using (var publicCng = ECDiffieHellmanCng.Create(publicParams))
            {
                derivedKey = privateCng.DeriveKeyMaterial(publicCng.PublicKey);
            }
        }
    
        return derivedKey;
    }