Search code examples
c#curl.net-corehttpclientx509certificate2

Unable make a certificate authorized API call to ING (api.sandbox.ing.com) endpoint using C# .NET Core


The task

I'm trying to follow this get started guide to make an access token request. It provides a bash script as an example of how to do that. Another requirement is to make this call from Azure Function, so I've created an HTTP triggered Azure Function project in Visual Studio 2019.

My attempt of a solution

It consists of 4 parts:

  1. Loading certificates
  2. Computing digest
  3. Generating signature
  4. Making a correct request to a provided API endpoint

Loading the certificates

There are two certificates provided as key pairs in .cer and .key files. One is to authenticate a request, another to create a signature with. I've combined the public and private keys into a .pfx container with openssl command like so:

openssl pkcs12 -export -in my.cer -inkey my.key -out mycert.pfx

and loaded them using:

public static X509Certificate2 GetCertificate(this ExecutionContext ctx, string certFileName)
{
    return new X509Certificate2(Path.GetFullPath(Path.Combine(ctx.FunctionAppDirectory, @$"Certs\{certFileName}")), "mypass");
}

Computing digest

I'm using FormUrlEncodedContent class for my payload, so digest is computed like so:

public static string ComputeSHA256HashAsBase64String(this string stringToHash)
{
    using (var hash = SHA256.Create())
    {
        Byte[] result = hash.ComputeHash(Encoding.UTF8.GetBytes(stringToHash));
        return Convert.ToBase64String(result);
    }
}

public static async Task<string> DigestValue(this FormUrlEncodedContent content)
{
    var payload = await content.ReadAsStringAsync();
    return "SHA-256=" + payload.ComputeSHA256HashAsBase64String();
}

Generating signature

var currentDate = DateTime.Now.ToUniversalTime().ToString("r");

var signingString =
@$"(request-target): {IgnAccountApi.HttpMethodStr} {IgnAccountApi.AccessTokenPath}
date: {currentDate}
digest: {digest}";

var signature = cert.SignData(signingString);

public static string SignData(this X509Certificate2 cert, string stringToSign)
{
    using (var hash = SHA256.Create())
    {
        var dataToSign = Encoding.UTF8.GetBytes(stringToSign);
        Byte[] hashToSign = hash.ComputeHash(dataToSign);
        var signedData = cert.GetRSAPrivateKey().SignData(hashToSign, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
        return Convert.ToBase64String(signedData);
    }
}

Making a request

The curl request I'm trying to reproduce (from the mentioned bash script):

curl -v -i -X POST "${httpHost}${reqPath}" \
-H 'Accept: application/json' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H "Digest: ${digest}" \
-H "Date: ${reqDate}" \
-H "authorization: Signature keyId=\"$keyId\",algorithm=\"rsa-sha256\",headers=\"(request-target) date digest\",signature=\"$signature\"" \
-d "${payload}" \
--cert "${certPath}example_client_tls.cer" \
--key "${certPath}example_client_tls.key"

I'm using HttpClientHandler to add tls.pfx I've created using openssl to HttpClient and make an authorized request like so:

using (var cert = ctx.GetCertificate("tls.pfx"))
{
    // https://stackoverflow.com/questions/40014047/add-client-certificate-to-net-core-httpclient
    var _clientHandler = new HttpClientHandler();
    _clientHandler.ClientCertificates.Add(cert);
    _clientHandler.ClientCertificateOptions = ClientCertificateOption.Manual;
    _clientHandler.SslProtocols = SslProtocols.Tls12;

    var dataToSend = new Dictionary<string, string>
    {
        { "grant_type","client_credentials" },
    };

    using (var content = new FormUrlEncodedContent(dataToSend))
    using (var _client = new HttpClient(_clientHandler))
    {
        var request = new HttpRequestMessage(HttpMethod.Post, $"{HttpHost}{AccessTokenPath}");
        request.Content = content;
        request.AddHeaders(ctx.GetCertificate("sign.pfx"), await content.DigestValue());

        using (HttpResponseMessage response = await _client.SendAsync(request))
        {
            // TODO: process the response
        }
    }
}

To not left anything out, here's AddHeaders extension method:

public static void AddHeaders(this HttpRequestMessage request, X509Certificate2 cert, string digest)
{
    var currentDate = DateTime.Now.ToUniversalTime().ToString("r");

    var signingString =
@$"(request-target): {IgnAccountApi.HttpMethodStr} {IgnAccountApi.AccessTokenPath}
date: {currentDate}
digest: {digest}";
    var signature = cert.SignData(signingString);

    request.Headers.Add("Accept", "application/json");
    request.Headers.Add("Digest", digest);
    request.Headers.Add("Date", currentDate);
    request.Headers.Add("authorization", $"Signature keyId=\"{IgnAccountApi.ClienId}\",algorithm=\"rsa-sha256\",headers=\"(request-target) date digest\",signature=\"{signature}\"");
}

The code above is generation the following request:

POST https://api.sandbox.ing.com/oauth2/token HTTP/1.1
Host: api.sandbox.ing.com
Accept: application/json
Digest: SHA-256=w0mymuL8aCrbJmmabs1pytZhon8lQucTuJMUtuKr+uw=
Date: Tue, 21 Apr 2020 09:02:56 GMT
Authorization: Signature keyId="e77d776b-90af-4684-bebc-521e5b2614dd",algorithm="rsa-sha256",headers="(request-target) date digest",signature="BaQgDXTsGBcZfZa+9oeaQhkv7bQwbMw92h4Dwp/EexJnjScScqVMYFwRSskkN1fYfu/1lDE+/K27qEJD9cq8i68C6u29I9wsUWlRtAiHu10d/hzTcZkfWLpoSKSo4mg016I//K/4scdnwf0fcsNgDOXYaoe9/KscltreXn6UQuYuwP98uZDTP3j/V7k34R5VIMPaUm1MSvE3H5opGNbLqpBjK8IenKUHjF0B9aqCzGB30eA7Y+fL025wRko6mGY2f+u4w3mi1RJzTb72Cw3SPejaa5s65sYIAus14g975RPBI4B7A2o/vsZ39Np1yJNvCW1tbZaTGAF4IJUfXQashw=="
Content-Type: application/x-www-form-urlencoded
Content-Length: 29

grant_type=client_credentials

The response I'm getting is rather disappointing:

HTTP/1.1 400
Date: Sat, 18 Apr 2020 06:17:20 GMT
Content-Type: application/json
Content-Length: 98
Connection: keep-alive
X-Frame-Options: deny
X-Content-Type-Options: nosniff
Strict-Transport-Security: max-age=31622400; includeSubDomains
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
Content-Security-Policy: default-src 'self'; prefetch-src 'none'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; upgrade-insecure-requests; block-all-mixed-content; connect-src 'self'; style-src 'self' 'unsafe-inline' data:; img-src https: data:; script-src 'self' data: 'unsafe-inline' 'unsafe-eval'

{
  "message" : "InputValidation failed: Field 'X-ENV-SSL_CLIENT_CERTIFICATE' was not provided."
}

To compare requests made from curl, here is the request generated using provided bash script (unfortunately fiddler isn't seeing this one, so it's just copy/paste form the console window):

*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x7fffe7479580)
> POST /oauth2/token HTTP/2
> Host: api.sandbox.ing.com
> User-Agent: curl/7.58.0
> Accept: application/json
> Content-Type: application/x-www-form-urlencoded
> Digest: SHA-256=w0mymuL8aCrbJmmabs1pytZhon8lQucTuJMUtuKr+uw=
> Date: Fri, 17 Apr 2020 14:56:12 GMT
> authorization: Signature keyId="e77d776b-90af-4684-bebc-521e5b2614dd",algorithm="rsa-sha256",headers="(request-target) date digest",signature="KynniiPdkPoVWu5YqXl+YMXQlmYa5C7Wp5ih+/HQtiSlgcNXZlHiRoKmhrwvaDo/0qXHGexJxxrrcgWYnJz3DusgUpr30Xg6DbcD8VlN6kYvk3DUez2q2+CmYo93ulVdz9W7+V0xQdEr6jLHZc/TLcpMUQly11ADiiBPUMhGd4VfN4XTwCcsoq/mPQ5tqVM+3hln5r85jDzf2sFjt/Is4do8WCwZjfdoNBdgtS3k73oBH1kS/foRxzS5ke6fxFaN2Al1o9dkDMhrOV7TQl0wOCIbmkgBRdQXA4Rq83HO3t3R65x+RVHafRfRT6o8bNTIqgy51aKVzqhdBvUQC6Dwkg=="
> Content-Length: 29

Questions

What am I doing wrong? Is it possible to authenticate a request with certificates using HttpClient class? Please help!

UPDATE 1: Added HTTP request generated by my code and HTTP request generated by the aforementioned bash script provided by (get started guide).


Solution

  • I figured it out.

    First of all, it was Fiddler that somehow broke the request and removed the certificate from it.

    When I got Fiddler out of my way, I discovered, that my method to generate the signature is wrong and data I'm generating it from is also incorrect.

    The correct method to generate the signature is the following:

    public static string SignData(this X509Certificate2 cert, string stringToSign)
    {
        var dataToSign = Encoding.UTF8.GetBytes(stringToSign);
        var signedData = cert.GetRSAPrivateKey().SignData(dataToSign, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
        var base64Signature = Convert.ToBase64String(signedData);
        return base64Signature;
    }
    

    The correct data to sign is the following:

    var stringToSign = $"(request-target): {httpMethod} {httpPath}\ndate: {currentDate}\ndigest: {digest}";
    

    Hope this will help someone trying to integrate with the ING APIs. Thank you, everyone, for help.