【问题标题】:Diffie Hellman Key Exchange using ECDSA x509 certificates使用 ECDSA x509 证书的 Diffie Hellman 密钥交换
【发布时间】:2023-04-02 18:38:01
【问题描述】:

我正在尝试使用 2 个 ECDSA x509 证书执行 Diffie-Hellman 密钥交换。

这是我从证书中提取密钥以计算派生密钥的方法。

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.Cryptography.ECCng.ExportParameters(CngKey 密钥,布尔值 includePrivateParameters,ECParameters& ecparams)
在 System.Security.Cryptography.ECDsaCng.ExportParameters(Boolean includePrivateParameters)

因为这是我正在生成的自签名证书,所以我认为我做错了什么。

我首先创建一个根 CA 证书并传入私钥来签署我的证书。

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() 方法返回 true,我读过它可以返回误报。

当我将证书添加到商店时,我可以验证证书链。

    [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",但我得到了同样的错误。

【问题讨论】:

    标签: c# bouncycastle x509certificate2 ecdsa diffie-hellman


    【解决方案1】:

    好的。这是一个盲目的镜头,但是在查看了您的代码后,我注意到了两件事:

    • 当您创建 PFX 时,您设置了空密码。但是,当您将 PFX 加载到 X509Certificate2 类中时,您使用了错误的构造函数。你应该使用一个带密码参数的变量,并给它一个空值
    • 当您将 PFX 加载到 X509Certificate2 类时,您没有指定私钥是否可以导出。我认为这就是privateKey.ExportParameters(true) 给你一个例外的原因。您应该使用this 构造函数并指定null 作为密码

    让它工作

    我认为这是一个错误。有可能是这样。我们在 X509Constructor 中明确指出,私钥应该是可导出的。我也使用了X509KeyStorageFlags.EphemeralKeySet | X509KeyStorageFlags.Exportable 标志。但是当我查看 CngKey 时,它的 ExportPolicy 设置为 AllowExport 而不是 AllowPlaintextExport

    它可以以某种方式导出。 privateKey.Key.Export(CngKeyBlobFormat.OpaqueTransportBlob) 工作。但是privateKey.ExportParameters(true) 没有。

    我已经搜索了如何更改 CngKey 的 ExportPolicy 的解决方案。我发现 this SO question 帮助我改变了它。之后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;
    }
    

    【讨论】:

    • x509 = new X509Certificate2(pfxStream.ToArray(), (SecureString)null, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet); 仍然返回相同的错误。
    • @pepo 仅供参考,使用不带密码的构造函数与将 null 传递给需要密码的构造函数相同。
    • 很棒的工作!实施您的解决方案使我发现了一些有趣的东西。 GetECDsaPrivateKey 返回一个ECDsaCng 对象,但是你必须强制转换它,然后你才能从ECDsaCng.Key 获取密钥
    • @bartonjs 如果我没记错的话,this when using Mono 有一个错误,但现在已修复。因此我使用了带有密码参数的构造函数。很高兴知道我不再需要了。
    【解决方案2】:

    我开始使用@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;
    

    【讨论】:

    • GetECDsaPrivateKey 返回 ECDsa。在 Windows 上,它碰巧暴露了它由 CNG 支持(当它支持时)。您应该进行安全转换,如果不是 ECDsaCng,请尝试您的原始导出/导入。
    猜你喜欢
    • 1970-01-01
    • 2023-03-19
    • 1970-01-01
    • 1970-01-01
    • 2014-05-13
    • 1970-01-01
    • 2018-06-10
    • 1970-01-01
    相关资源
    最近更新 更多