【问题标题】:HttpClient with X509Certificate2 and ECDsa failing (server response "No required SSL certificate was sent")具有 X509Certificate2 和 ECDsa 的 HttpClient 失败(服务器响应“未发送所需的 SSL 证书”)
【发布时间】:2022-12-30 20:59:07
【问题描述】:

更新

事实证明,SslStreamHttpClient 所基于的)存在问题。在当前版本 (7.xx) 中,证书链在通过客户端发送时不会提交给服务器。这是一个已知问题并讨论了 herehere

我会把这篇文章留在网上,因为下面的代码可能对其他人有帮助(如果你只想在你的请求中使用客户端证书,它不会造成问题)。


我花了很多时间试图找到我们使用基于ECDsa 的证书和 .Net Core 的本地HttpClient 的客户端证书身份验证有什么问题(版本 7.0.100 但也尝试过 v.6xxx)但是从来没有得到这个东西在跑(顺便说一句,我对基于 RSA 的客户端证书使用了相同的方法,没有任何问题)。

出于安全原因,我必须使用 ECDsa 客户端证书 + 链。

我不明白或找不到信息为什么这不起作用/不支持,结果让我感到困惑。

加载证书和密钥并使用它们对某些数据进行签名和验证时,所有测试均通过(见代码)。最后,所需的客户端证书没有发送到服务器,导致 SSL 异常(使用 Python 脚本测试了相同的证书以验证它们是否正确,我遇到了零问题)。

我无法想象(至少我希望不会)不支持这些类型的客户端证书。对于替代解决方法的任何帮助或提示,我将不胜感激。在这一点上切换到一些不同的语言将是非常糟糕的:/

参考代码

包含一个测试证书和可以使用的密钥。此示例中缺少链,可以简单地附加到证书字符串部分测试摘自:Use X509Certificate2 to sign and validate ECDSA-SHA256 signatures

[Test]
// Just some test to better understand whether the certificate and key is working and belong together
public void TestEcdsaFunctionality()
{
    var ecdsaCertificate = @"-----BEGIN CERTIFICATE-----
                            MIIBgzCCASoCCQD+iUUbrH+BOzAKBggqhkjOPQQDAjBKMQswCQYDVQQGEwJQRDEL
                            MAkGA1UECAwCQlcxEDAOBgNVBAcMB1BhbmRvcmExDzANBgNVBAoMBkFueU9yZzEL
                            MAkGA1UEAwwCNDIwHhcNMjIxMTIxMTE0MjExWhcNMjMxMTIxMTE0MjExWjBKMQsw
                            CQYDVQQGEwJQRDELMAkGA1UECAwCQlcxEDAOBgNVBAcMB1BhbmRvcmExDzANBgNV
                            BAoMBkFueU9yZzELMAkGA1UEAwwCNDIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC
                            AAT6vBU2iIcESep8UeQhfNFgfTArFYvtb2Pmlbk1+R9gdNaWEg1UK7dlt3/mH/X3
                            Mrg80JaTY3OPM92MY9e9gs7ZMAoGCCqGSM49BAMCA0cAMEQCIA3p2mMOYqGEzReY
                            br7nYLsLdF0+dV6iZSZaG1iMHwblAiA5UaJlVr5CsCkG+j1ZJEICSOnVMyx4DjA5
                            oZuoMYa42w==
                            -----END CERTIFICATE-----";
    var ecdsaPrivateKey = @"MDECAQEEIM6BExC2G7P1KpViQmZ/Z65nukv8yQmvw6PqGGQcKn9boAoGCCqGSM49
                            AwEH";


    var cert = X509Certificate2.CreateFromPem(ecdsaCertificate.ToCharArray());
    var key = ECDsa.Create("ECDsa");
    var keybytes = Convert.FromBase64String(ecdsaPrivateKey);
    key.ImportECPrivateKey(keybytes, out _);

    var helloBytes = Encoding.UTF8.GetBytes("Hello World");

    // Sign data with the ECDsa key
    var signed = key.SignData(helloBytes, 0, helloBytes.Count(), HashAlgorithmName.SHA256);
    // Verify the data signature with the certificates public key
    var verified = cert.GetECDsaPublicKey().VerifyData(helloBytes, signed, HashAlgorithmName.SHA256);
    // Assume that everything went well and the data signature is valid
    Assert.That(verified, Is.EqualTo(true));


    // Additional tests with the X509Certificate2 object type
    X509Certificate2 keyCert = ECDsaCertificateExtensions.CopyWithPrivateKey(cert, key);

    // Sing using the certificate that contains the private key
    using (ECDsa ecdsa = keyCert.GetECDsaPrivateKey())
    {
        if (ecdsa == null)
            throw new ArgumentException("Cert must have an ECDSA private key", nameof(cert));

        signed = ecdsa.SignData(helloBytes, HashAlgorithmName.SHA256);
    }
    // Verify signed data using the certificate that contains the private key
    using (ECDsa ecdsa = keyCert.GetECDsaPublicKey())
    {
        if (ecdsa == null)
            throw new ArgumentException("Cert must be an ECDSA cert", nameof(cert));

        Assert.That(ecdsa.VerifyData(helloBytes, signed, HashAlgorithmName.SHA256), Is.EqualTo(true));
    }

    WorkshopRegistration(keyCert);
}

// This would be what I really want to use the client certificate for
private void WorkshopRegistration(X509Certificate2 clientCert)
{
    
    var payload = "{somepayload}";

    var handler = new HttpClientHandler();
    handler.ClientCertificateOptions = ClientCertificateOption.Manual;
    handler.SslProtocols = SslProtocols.Tls12;
    handler.ClientCertificates.Add(clientCert);
    handler.ServerCertificateCustomValidationCallback =
        (httpRequestMessage, cert, cetChain, policyErrors) =>
        {
            return true;
        };

    var content = new StringContent(payload, System.Text.Encoding.UTF8, "application/json");

    var client = new HttpClient(handler);
    client.DefaultRequestHeaders.Add("Accept", "application/json");

    var result = client.PutAsync("https://someHostname.com/registration",
        content).GetAwaiter().GetResult();

    if (result.StatusCode != HttpStatusCode.OK)
        throw new SuccessException("Registration failed with conent: " + result.Content.ToString());
}

【问题讨论】:

  • 你能提供失败的代码并指出代码失败的那一行吗?
  • 顺便说一句,您仅在访问 cert.PrivateKey getter 时收到异常,它已过时并且仅支持旧版 RSA/DSA 密钥,因此预计会出现此异常。
  • 好吧,代码本身并没有失败。只有 HttpClient 没有按预期发送客户端证书。我在上面描绘的异常仅在您调试代码并尝试在加载私钥后检查 keyCert 时才可见。
  • 您好 Crypt32,感谢您指出这一点。这解释了调试器引发此异常的原因,并解释了我为何对签名和验证工作感到困惑(因为这需要 X509Certificate2 加载证书和密钥)。除此之外,我只能猜测我使用的证书不正确(?),因为服务器接受了相同的证书,当通过curlpython发送时

标签: c# ssl x509certificate dotnet-httpclient ecdsa


【解决方案1】:

我有同样的问题要解决(使用从 Windows .NET 6 到 Linux 的 ECDSA SHA-384 证书身份验证)并尝试使用我在上面找到的代码,直到调用给我一个“Win32Exception:没有可用的凭据”在安全包中”,

//it may looks like we need to convert from pkcs8 to pkcs12 before sending the certificate, see https://github.com/dotnet/runtime/issues/45680

keyCert = new System.Security.Cryptography.X509Certificates.X509Certificate2(
 keyCert.Export(
    System.Security.Cryptography.X509Certificates.X509ContentType.Pkcs12
)
);

使用此 SSL 异常不再存在,但在我的情况下,我在发送请求时一直收到 403 Forbidden ...而 CURL 似乎可能出于其他原因给出 500。无论如何,我应该使用 NET Framework 4.8 来完成这项工作,所以我很困惑。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-07-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-07-29
    • 2017-09-26
    • 2017-08-06
    相关资源
    最近更新 更多