2

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).

Boris Vaskin
  • 141
  • 1
  • 11
  • Maybe you can start by viewing how the request sent by example curl looks (https://stackoverflow.com/a/3121175/5521670). And then comparing it to what you are sending (`request.ToString()` should be enough). – Šimon Kocúrek Apr 18 '20 at 07:58
  • 1
    I figured. The headers seem fine. I'm guessing the problem is with the TLS handshake. – Boris Vaskin Apr 18 '20 at 08:05
  • 1
    Some things you may try are: In case your certificate is in a chain, extract the correct certificate (https://stackoverflow.com/a/9260696/5521670). Import the `.pfx` with appropriate flags (Tip 4: http://paulstovell.com/blog/x509certificate2, https://stackoverflow.com/a/3826390/5521670). – Šimon Kocúrek Apr 18 '20 at 08:26
  • 1
    Thanks for the suggestion. There is no chain, just a key pair. I've tried different flags, but the result is still the same. – Boris Vaskin Apr 18 '20 at 09:09

5 Answers5

2

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.

Boris Vaskin
  • 141
  • 1
  • 11
0

I think we are working on the same problem.

But it looks like in your code they are asking for SSL certificate, not TLS (or both). May be you need:

        ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Ssl3 |
                                               SecurityProtocolType.Tls | SecurityProtocolType.Tls11;

On ING dev environment there are 2 separate certificates - SSL and TLS

  • As I understand it from the "get started" guide, there are 2 certificates: one is for request authentication (example_client_tls.cer) and another is to generate the proper signature (example_client_signing.cer). I use both of these, as I described in my question. The Ssl2 and Ssl3 are obsolete and deprecated, I get `System.Private.CoreLib: Exception while executing function: IGNAccountInfo. System.Net.ServicePoint: The requested security protocol is not supported.` error if using the line of code you suggested. – Boris Vaskin Apr 21 '20 at 09:01
0

can you please post the request actually send? Did you check the differences between the ones documented in the Getting Started Guide on the ING developer portal?

Also, there you will find a summary of commonly made mistakes. The HTTP Signing requires attention to detail.

You will need to use a different key pair for signing and for TLS! Both of the certificates need to be added to the client config on the developer portal.Unless you use eIDAS. These are onboarded automatically.

Pls post the request made.

Rob C.
  • 1
  • 1
  • Updated the answer with the sent request. I did compare the two requests, but couldn't identify the problem. I use both certificates as described in the "get started" guide. For now, I'm trying to make it work for the simplest case possible with provided certificates without any registration and certificate generation. The provided bash script is working, so my solution should too :) – Boris Vaskin Apr 21 '20 at 09:21
0

good that you have found it. I am busy documenting additional errors that are not yet in the current docs. The current code does return this error when for example the request signature is not included.

On developers.ing.com you will find in the documentation tab of OAUTH 2.0 more info on errors returned.

As I am not part of the ING open banking support team but do work currently for ING, it is preferred by Open banking to handle all support questions themselves. You are in good hands. It is just that I wanted to mention this as I found it a bit strange for you not to hear anything back from me.

Rob C.
  • 1
  • 1
0

I have build a Postman collection to play around with the ING OAuth PSD2 API. It can be used to experiment with all parameters to get acquainted with error handling of the API and to view its client side requirements in Javascript instead of shells script.

Use it at your own discretion. My project has no official link with ING at all!

I hope is helpful to other developers wanting to get started with ING PSD2 OAuth 2.0 API.

One can find it here: https://github.com/robcordes/ING-PSD2-OAUTH2.O-API-Test-Client

Rob C.
  • 1
  • 1