1

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.

Community
  • 1
  • 1
Josh
  • 16,286
  • 25
  • 113
  • 158

2 Answers2

2

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;
}
pepo
  • 8,644
  • 2
  • 27
  • 42
  • `x509 = new X509Certificate2(pfxStream.ToArray(), (SecureString)null, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet);` still returns the same error. – Josh Dec 07 '18 at 14:07
  • @pepo FYI, using the constructor which doesn't take a password is the same as passing `null` to the one that does. – bartonjs Dec 07 '18 at 16:04
  • Awesome work! Implementing your solution led me to find something interesting. `GetECDsaPrivateKey` returns a `ECDsaCng` object, but you have to cast it, and then you can get the key from `ECDsaCng.Key` – Josh Dec 07 '18 at 16:31
  • @bartonjs If I remember correctly there was an error with [this when using Mono](https://stackoverflow.com/a/24172972/3245057) but it's fixed now. Therefore I was using the constructor with password parameter. Good to know that I don't need to anymore. – pepo Dec 07 '18 at 17:10
0

I started using the solution @pepo posted which lead me to discover 'GetECDsaPrivateKey' does not return an ECDsa object but an ECDsaCng. I simplified the key derivation to this.

byte[] derivedKey;

using (var privateKey = (ECDsaCng)certificate.GetECDsaPrivateKey())
using (var publicKey = (ECDsaCng)certificate.GetECDsaPublicKey())
{
    var publicParams = publicKey.ExportParameters(false);

    using (var publicCng = ECDiffieHellmanCng.Create(publicParams))
    using (var diffieHellman = new ECDiffieHellmanCng(privateKey.Key))
    {
        derivedKey = diffieHellman.DeriveKeyMaterial(publicCng.PublicKey);
    }
}

return derivedKey;
Josh
  • 16,286
  • 25
  • 113
  • 158
  • 1
    GetECDsaPrivateKey returns ECDsa. On Windows it happens to expose that it’s backed by CNG (when it is). You should do safe casts, and if it’s not an ECDsaCng just try your original Export/Import. – bartonjs Dec 07 '18 at 17:13