0

Update

It turned out, that we have a problem with the SslStream (on which the HttpClient is based on). In the current version (7.xx) the certificate chain is not submitted to the server, when sent via the client. This is a known issue and discussed here and here.

I will leave this post online, since the code below might be helpful to others (it does not cause problems if you want to use the client certificate only in your requests).


I have spent a lot of time trying to find our what's wrong with the Client Certificate authentication using ECDsa based certificates with the native HttpClient of .Net Core (version 7.0.100 but also tried v.6xxx) but never got this thing running. (Btw. I used the same approach for RSA based Client Certificates without any problems).

Due to security reasons, I MUST use ECDsa client certificate + chain.

I can not understand or find information why this is not working / supported and the results are confusing to me.

When loading the certificate and the key and using them to sign and verify some data, all tests pass (see code). In the end, the required Client Certificates are not sent to the server, resulting in an SSL exception (tested the same certificates with a Python script to verify that they are correct, there I had zero problems).

I can not imagine (at least I hope not) that these kind of Client Certificates are not supported. I would greatly appreciate any help or hints for alternative workarounds. It would be quite terrible to switch to some different language at this point :/

Reference Code

Contains a test certificate and key to play around with. The chain is missing in this example and can simply be attached to the certificate string Part of the tests are taken from: 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());
}
hbertsch
  • 367
  • 2
  • 12
  • can you provide the code that fails and point at which line the code fails? – Crypt32 Nov 21 '22 at 12:34
  • 1
    BTW, you are receiving exception only when accessing `cert.PrivateKey` getter, which is obsolete and supports only legacy RSA/DSA keys, so this exception is expected. – Crypt32 Nov 21 '22 at 12:41
  • Well, the code itself is not failing. Only the HttpClient is not sending out the client certificate as expected. The exception I was picturing above is only visible if you debug the code and try to inspect the `keyCert` after it was loaded with the private key. – hbertsch Nov 21 '22 at 12:42
  • Hi Crypt32, thank you for pointing this out. This explains why the debugger is raising this exception and explains my confusion why signing and verifying is working (since this requires to X509Certificate2 to have cert and key loaded). Other than that, I only can guess that I am using the certificate incorrectly(?), since the same certificate is accepted by the server, when sending it via `curl` or `python` – hbertsch Nov 21 '22 at 12:46

0 Answers0