alexrainman / ModernHttpClient

ModernHttpClient
MIT License
126 stars 28 forks source link

Certificate Pinning #15

Closed nologinatgit closed 5 years ago

nologinatgit commented 5 years ago

As ModernHttpClient solves the issue of connecting with the server, the client should be able to use its own certificate to communicate with the server.

Can this be done somehow?

alexrainman commented 5 years ago

What you want is called Certificate Pinning. I am adding that functionality in next release.

nologinatgit commented 5 years ago

Do you have any guess when to release it? We really need to depend on it and I'd rather not implement any custom solution, if it is part of the same client.

alexrainman commented 5 years ago

You can do it with the current implementation setting customSSLVerification to true:

var httpClient = new HttpClient(new NativeMessageHandler(
    throwOnCaptiveNetwork: false, 
    customSSLVerification: true
));

Set ServicePointManager SecurityProtocol and assign ServerCertificateValidationCallback:

ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
ServicePointManager.ServerCertificateValidationCallback = ValidateServerCertficate;

Then the Certificate Pinning:

private const string SupportedPublicKey = "{PLACE HERE THE PUBLIC KEY}";

private static bool ValidateServerCertficate(
    object sender,
    X509Certificate certificate,
    X509Chain chain,
    SslPolicyErrors sslPolicyErrors
)
{
    return SupportedPublicKey == certificate?.GetPublicKeyString();
}

This is not leveraging the underlying network stacks of both platforms to do the pinning and that's what i want to add.

nologinatgit commented 5 years ago

We might have misunderstood each other. I don't want the client to valididate the server certificate, I want the client to send its own client certificate to the server after the TSL connection is established.

There is some code, that I tried to use, but ended up with a Method not supported exception.

This is what I tried to use:

var handler = new HttpClientHandler();
handler.ClientCertificateOptions = ClientCertificateOption.Manual;
handler.SslProtocols = SslProtocols.Tls12;
handler.ClientCertificates.Add(new X509Certificate2("cert.crt"));

Will ModernHttpClient support this kind of handling?

alexrainman commented 5 years ago

This:

handler.ClientCertificates.Add(new X509Certificate2("cert.crt")); 

Is the source of your Method not supported exception.

You have to do it this way:

client.DefaultRequestHeaders.Add("X-Client-Cert", "CERT AS BASE64 STRING");

And that you can do with the current implementation.

In your backend, you can parse the X-Client-Cert header and return SSL Handshakes errors if the certificate is missing, authority or ciphers mismatch.

var cs = Request.Headers["X-Client-Cert"];

byte[] certdata = Convert.FromBase64String(cs);

var cert = new X509Certificate(certdata);

You have now two ways certificate pinning.

nologinatgit commented 5 years ago

Is this header modification the official way to add a client certificate?

I have a custom middleware that checks for client certificates upon connection.

However, even after adding the modified header value, the Context.Connection.ClientCertificate value is null.

protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
  var certificate = Context.Connection.ClientCertificate;
  if (certificate == null)
  {
    var cc = Context.Request.Headers["X-Client-Cert"];
    byte[] certdata = Convert.FromBase64String(cc);
    certificate = new X509Certificate2(certdata);
  }
  if (certificate != null && certificate.Verify())
  {
    //client cert is okay
  }

If I use Postman and add a certificate, the value contains it. Processing the header manually works, but I don't understand why it is not present in the Connection.ClientCertificate.

Any idea how to make it appear?