Обмен ключами Диффи Хеллмана с использованием сертификатов ECDSA x509

Я пытаюсь выполнить обмен ключами Диффи-Хеллмана, используя 2 сертификата ECDSA x509.

Вот метод, в котором я извлекаю ключи из сертификатов для вычисления производного ключа.

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;
    }

Я прокомментировал строку, в которой privateKey.ExportParameters(true) возникает ошибка:

System.Security.Cryptography.CryptographicException: запрошенная операция не поддерживается.

в System.Security.Cryptography.NCryptNative.ExportKey (ключ SafeNCryptKeyHandle, строковый формат)
в System.Security.Cryptography.CngKey.Export (формат CngKeyBlobFormat)
в System.Security.CryptographyParacmeters ( CngKey key, Boolean includePrivateParameters, ECParameters & ecparams)
в System.Security.Cryptography.ECDsaCng.ExportParameters (логическое includePrivateParameters)

Поскольку это самоподписанный сертификат, который я генерирую, я предполагаю, что делаю что-то не так.

Сначала я создаю сертификат корневого ЦС и передаю закрытый ключ для подписи моего сертификата.

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;
    }

Метод .HasPrivateKey() возвращает истину, которая, как я читал, может возвращать ложное срабатывание.

Когда я добавляю свои сертификаты в магазин, я могу проверить цепочку сертификатов.

    [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");
    }

Сначала я подумал, что, возможно, частный сертификат должен быть из хранилища сертификатов, поэтому я импортировал его, а затем вытащил обратно, но получаю ту же ошибку, что является еще одной причиной, по которой я считаю, что я что-то делаю не совсем правильно.

РЕДАКТИРОВАТЬ:

У меня есть другой класс, генерирующий RSA x509, использующий тот же код для помещения закрытого ключа в сертификат. Это позволяет мне экспортировать закрытый ключ RSA.

Переменная _keyStrength - 384, а моя фабрика подписей использует "SHA256withECDSA". Я также пробовал использовать "SHA384withECDSA", но получаю ту же ошибку.


person Josh    schedule 07.12.2018    source источник


Ответы (2)


OK. Это слепой выстрел, но, посмотрев на ваш код, я заметил две вещи:

  • Когда вы создаете PFX, вы устанавливаете нулевой пароль. Но когда вы загружаете PFX в класс X509Certificate2, вы используете неправильный конструктор. Вы должны использовать один с параметром пароля и указать в нем нуль
  • Когда вы загружаете PFX в класс X509Certificate2, вы не указываете, следует ли экспортировать закрытый ключ. Я думаю, что именно по этой причине privateKey.ExportParameters(true) дает вам исключение. Вы должны использовать этот конструктор и укажите null в качестве пароля

Сделал это работающим

Я думал, что это ошибка. Вполне возможно, что это так. В X509Constructor мы четко заявили, что закрытый ключ должен быть экспортируемым. Я тоже использовал X509KeyStorageFlags.EphemeralKeySet | X509KeyStorageFlags.Exportable флаги. Но когда я посмотрел на CngKey, для него было установлено значение ExportPolicy _ 6_, но не AllowPlaintextExport.

Это можно было как-то экспортировать. privateKey.Key.Export(CngKeyBlobFormat.OpaqueTransportBlob) работал. Но privateKey.ExportParameters(true) этого не сделал.

Я искал решение, как изменить ExportPolicy CngKey. Я нашел этот вопрос SO, который помог мне его изменить. После этого ExportParameters заработал.

Фиксированная версия вашего GetDerivedKey метода:

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;
}
person pepo    schedule 07.12.2018
comment
x509 = new X509Certificate2(pfxStream.ToArray(), (SecureString)null, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet); по-прежнему возвращает ту же ошибку. - person Josh; 07.12.2018
comment
@pepo FYI, использование конструктора, который не принимает пароль, аналогично передаче null тому, который делает. - person bartonjs; 07.12.2018
comment
Потрясающая работа! Реализация вашего решения привела к тому, что я нашел кое-что интересное. GetECDsaPrivateKey возвращает объект ECDsaCng, но вы должны его преобразовать, а затем вы можете получить ключ от ECDsaCng.Key - person Josh; 07.12.2018
comment
@bartonjs Если я правильно помню, была ошибка это при использовании Mono, но теперь она исправлена. Поэтому я использовал конструктор с параметром пароля. Приятно знать, что мне это больше не нужно. - person pepo; 07.12.2018

Я начал использовать опубликованное решение @pepo, которое привело меня к выводу, что «GetECDsaPrivateKey» не возвращает объект ECDsa, а возвращает ECDsaCng. Я упростил ключевой вывод до этого.

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;
person Josh    schedule 07.12.2018
comment
GetECDsaPrivateKey возвращает ECDsa. В Windows оказывается, что он поддерживается CNG (когда он есть). Вам следует выполнять безопасное приведение, а если это не ECDsaCng, просто попробуйте исходный экспорт / импорт. - person bartonjs; 07.12.2018