Задача
Я пытаюсь следовать этому руководству start , чтобы запросить токен доступа. В качестве примера того, как это сделать, приводится сценарий bash . Другим требованием является выполнение этого вызова из Azure Function, поэтому я создал запущенный HTTP-проект Azure Function в Visual Studio 2019.
Моя попытка решения
Он состоит из 4 частей:
- Загрузка сертификатов
- Компьютерный дайджест
- Создание подписи
- Создание правильного запроса к предоставленной конечной точке API
Загрузка сертификатов
В файлах ключей .cer
и .key
предусмотрено два сертификата в виде пар ключей. Один для аутентификации запроса, другой для создания подписи. Я объединил publi c и закрытые ключи в контейнер .pfx
с помощью команды openssl
следующим образом:
openssl pkcs12 -export -in my.cer -inkey my.key -out mycert.pfx
и загрузил их, используя:
public static X509Certificate2 GetCertificate(this ExecutionContext ctx, string certFileName)
{
return new X509Certificate2(Path.GetFullPath(Path.Combine(ctx.FunctionAppDirectory, @$"Certs\{certFileName}")), "mypass");
}
Computing Дайджест
Я использую класс FormUrlEncodedContent
для своей полезной нагрузки, поэтому дайджест вычисляется так:
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();
}
Генерация подписи
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);
}
}
Создание запроса
Запрос curl
, который я пытаюсь воспроизвести ( из упомянутого 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"
Я использую HttpClientHandler
для добавления tls.pfx
Я создал с помощью openssl
до HttpClient
и сделал авторизованный запрос следующим образом:
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
}
}
}
Чтобы ничего не пропустить, вот метод расширения AddHeaders
:
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}\"");
}
Приведенный выше код генерирует следующий запрос:
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
Получаемый ответ довольно разочаровывает:
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."
}
Для сравнения запросов, сделанных из curl, вот запрос, сгенерированный с использованием предоставленного скрипта bash (к сожалению, fiddler этого не видит, поэтому он просто копирует / вставляет из окна консоли):
* 1 070 *
Вопросы
Что я делаю не так? Можно ли аутентифицировать запрос с помощью сертификатов, используя класс HttpClient
? Пожалуйста, помогите!
ОБНОВЛЕНИЕ 1: добавлен HTTP-запрос, сгенерированный моим кодом, и HTTP-запрос, сгенерированный вышеупомянутым bash скриптом, предоставленным ( руководство по началу работы ).