Добавить сертификат клиента в .NET Core HttpClient

#c# #.net #asp.net-core #.net-core #dotnet-httpclient

#c# #.net #asp.net-core #.net-core #dotnet-httpclient

Вопрос:

Я играл с .NET Core и создавал API, который использует платежные API. Существует сертификат клиента, который необходимо добавить к запросу на двустороннюю SSL-аутентификацию. Как я могу добиться этого с помощью .NET Core HttpClient ?

Я просмотрел различные статьи и обнаружил, что HttpClientHandler в них нет возможности добавлять сертификаты клиентов.

Ответ №1:

Я выполнил новую установку для своей платформы (Linux Mint 17.3), выполнив следующие действия: Учебное пособие по .NET — Привет, мир за 5 минут. Я создал новое консольное приложение, ориентированное на netcoreapp1.0 платформу, смог отправить сертификат клиента; однако во время тестирования я получил сообщение «Ошибка SSL connect» ( CURLE_SSL_CONNECT_ERROR 35 ), хотя я использовал действительный сертификат. Моя ошибка может быть специфичной для моего libcurl.

Я запустил то же самое в Windows 7, и это сработало именно так, как нужно.

 // using System.Net.Http;
// using System.Security.Authentication;
// using System.Security.Cryptography.X509Certificates;

var handler = new HttpClientHandler();
handler.ClientCertificateOptions = ClientCertificateOption.Manual;
handler.SslProtocols = SslProtocols.Tls12;
handler.ClientCertificates.Add(new X509Certificate2("cert.crt"));
var client = new HttpClient(handler);
var result = client.GetAsync("https://apitest.startssl.com").GetAwaiter().GetResult();
  

Комментарии:

1. вау, это сработало, приятель, большое спасибо, что нашли время, чтобы помочь мне. Все эти дни я был в отпуске.

2. Мне любопытно. Вы заставили это работать на компьютере с Linux или Windows?

3. Я заставил его работать как на Windows (10), так и на Linux (Mint 17.3, 18 и 18.1). Я получил «ошибку SSL connect» (CURLE_SSL_CONNECT_ERROR 35), когда я попытался использовать сертификат без закрытого ключа.

4. @yfisaqt можете ли вы подробнее рассказать о том, как вы увидели ошибку CURL? Я обнаружил, что могу подписать сертификат в Windows, но тот же код в Ubuntu 17.04 вызывает ошибки.

5. @Tom Чтобы быть техническим, это зависит от формата файла. Имейте в виду, что на этот вопрос был дан ответ до того, как я обновился до .NET Core SDK 2, поэтому в версии 2.0 это может вести себя немного по-другому. В то время, когда я отвечал на это, Windows требовала, чтобы файл был PFX, но в Linux я смог использовать обычный сертификат (без закрытого ключа).

Ответ №2:

У меня есть аналогичный проект, в котором я взаимодействую между службами, а также между мобильными и настольными компьютерами с помощью службы.

Мы используем сертификат Authenticode из EXE-файла, чтобы убедиться, что запросы выполняются нашими двоичными файлами.

На стороне запроса (упрощено для post).

 Module m = Assembly.GetEntryAssembly().GetModules()[0];
using (var cert = m.GetSignerCertificate())
using (var cert2 = new X509Certificate2(cert))
{
   var _clientHandler = new HttpClientHandler();
   _clientHandler.ClientCertificates.Add(cert2);
   _clientHandler.ClientCertificateOptions = ClientCertificateOption.Manual;
   var myModel = new Dictionary<string, string>
   {
       { "property1","value" },
       { "property2","value" },
   };
   using (var content = new FormUrlEncodedContent(myModel))
   using (var _client = new HttpClient(_clientHandler))
   using (HttpResponseMessage response = _client.PostAsync($"{url}/{controler}/{action}", content).Result)
   {
       response.EnsureSuccessStatusCode();
       string jsonString = response.Content.ReadAsStringAsync().Resu<
       var myClass = JsonConvert.DeserializeObject<MyClass>(jsonString);
    }
}
  

Затем я использую следующий код для действия, которое получает запрос:

 X509Certificate2 clientCertInRequest = Request.HttpContext.Connection.ClientCertificate;
if (!clientCertInRequest.Verify() || !AllowedCerialNumbers(clientCertInRequest.SerialNumber))
{
    Response.StatusCode = 404;
    return null;
}
  

Мы скорее предоставляем 404, чем 500, поскольку нам нравятся те, которые пытаются получить неверный запрос по URL, а затем сообщают им, что они «на правильном пути»

В .NET Core способ получения сертификата больше не заключается в переходе по модулю. Современный способ, который может сработать для вас, это:

 private static X509Certificate2? Signer()
{
    using var cert = X509Certificate2.CreateFromSignedFile(Assembly.GetExecutingAssembly().Location);
    if (cert is null)
        return null;

    return new X509Certificate2(cert);
}
  

Комментарии:

1. Как вы подтверждаете, что запрос клиента выполнен с помощью HTTP / 1.1? В HTTP / 2 нет аутентификации по сертификату клиента.

2. Я знаю, что это устарело, но чтобы ответить на ваш комментарий: handler.ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true;

Ответ №3:

Я не использую .NET для моего клиента, но на стороне сервера его можно настроить просто через IIS, развернув мой ASP.NET Основной веб-сайт IIS, настройка IIS для HTTPS клиентских сертификатов:

Настройка сертификата клиента IIS:

Затем вы можете получить его просто в коде:

         var clientCertificate = await HttpContext.Connection.GetClientCertificateAsync();

        if(clientCertificate!=null)
            return new ContentResult() { Content = clientCertificate.Subject };
  

У меня все работает нормально, но я использую curl или chrome в качестве клиентов, а не .ЧИСТЫЕ. Во время квитирования HTTPS клиент получает запрос от сервера на предоставление сертификата и отправляет его на сервер.

Если вы используете клиент .NET Core, у него не может быть кода, зависящего от платформы, и было бы разумно, если бы он не мог подключиться к какому-либо хранилищу сертификатов, специфичному для ОС, чтобы извлечь его и отправить на сервер. Если вы компилировали с .NET 4.5.x, то это кажется простым:

Использование HttpClient с аутентификацией на стороне клиента на основе SSL / TLS

Это похоже на компиляцию curl. Если вы хотите иметь возможность подключать его к хранилищу сертификатов Windows, вы должны скомпилировать его для какой-либо конкретной библиотеки Windows.

Комментарии:

1. спасибо, что уделили время ответу, но я использовал это для asp.net ядро. Приведенный выше код сработал для меня.

Ответ №4:

После долгих испытаний с этой проблемой я закончил с этим.

  1. Используя SSL , я создал pfx файл из сертификата и ключа.
  2. Создайте HttpClient следующий:
 _httpClient = new(new HttpClientHandler
{
    ClientCertificateOptions = ClientCertificateOption.Manual,
    SslProtocols = SslProtocols.Tls12,
    ClientCertificates = { new X509Certificate2(@"C:kambiDev.pfx") }
});
  

Комментарии:

1. Он не работает на компьютере с Linux.

Ответ №5:

Может использоваться как для .NET Core 2.0< и .NET Framework 4.7.1<:

 var handler = new HttpClientHandler();
handler.ClientCertificates.Add(new X509Certificate2("cert.crt"));
var client = new HttpClient(handler);
  

https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclienthandler?view=netframework-4.7.1

Комментарии:

1. обработчик. ClientCertificates. Add(new X509Certificate2(«cert.crt»)) работает только с Framework 4.8 ( learn.microsoft.com/ru-ru/dotnet/api /… )

2. @Kate Пропустил от меня, что это не сработало с .Net Framework 4.5 < но код работает с. .Net Framework 4.7.1 Протестировал его локально, и в документации тоже так сказано. Обновлен ответ.

3. Спецификация версии, похоже, перевернута, я думаю, она должна гласить: Может использоваться как для .NET Core> 2.0, так и для .NET Framework> 4.7.1: обратите внимание на инверсию знака (ов).

Ответ №6:

Сделайте всю конфигурацию в Main() следующим образом:

 public static void Main(string[] args)
{
    var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();
    var logger = new LoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger();
    string env="", sbj="", crtf = "";

    try
    {
        var whb = WebHost.CreateDefaultBuilder(args).UseContentRoot(Directory.GetCurrentDirectory());

        var environment = env = whb.GetSetting("environment");
        var subjectName = sbj = CertificateHelper.GetCertificateSubjectNameBasedOnEnvironment(environment);
        var certificate = CertificateHelper.GetServiceCertificate(subjectName);

        crtf = certificate != null ? certificate.Subject : "It will after the certification";

        if (certificate == null) // present apies even without server certificate but dont give permission on authorization
        {
            var host = whb
                .ConfigureKestrel(_ => { })
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseIISIntegration()
                .UseStartup<Startup>()
                .UseConfiguration(configuration)
                .UseSerilog((context, config) =>
                {
                    config.ReadFrom.Configuration(context.Configuration);
                })
                .Build();
            host.Run();
        }
        else
        {
            var host = whb
                .ConfigureKestrel(options =>
                {
                    options.Listen(new IPEndPoint(IPAddress.Loopback, 443), listenOptions =>
                    {
                        var httpsConnectionAdapterOptions = new HttpsConnectionAdapterOptions()
                        {
                            ClientCertificateMode = ClientCertificateMode.AllowCertificate,
                            SslProtocols = System.Security.Authentication.SslProtocols.Tls12,
                            ServerCertificate = certificate
                        };
                        listenOptions.UseHttps(httpsConnectionAdapterOptions);
                    });
                })
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseIISIntegration()
                .UseUrls("https://*:443")
                .UseStartup<Startup>()
                .UseConfiguration(configuration)
                .UseSerilog((context, config) =>
                {
                    config.ReadFrom.Configuration(context.Configuration);
                })
                .Build();
            host.Run();
        }

        Log.Logger.Information("Information: Environment = "   env  
            " Subject = "   sbj  
            " Certificate Subject = "   crtf);
    }
    catch(Exception ex)
    {
        Log.Logger.Error("Main handled an exception: Environment = "   env  
            " Subject = "   sbj  
            " Certificate Subject = "   crtf  
            " Exception Detail = "   ex.Message);
    }
}
  

Настройте файл startup.cs следующим образом:

 #region 2way SSL settings
services.AddMvc();
services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = CertificateAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = CertificateAuthenticationDefaults.AuthenticationScheme;
})
.AddCertificateAuthentication(certOptions =>
{
    var certificateAndRoles = new List<CertficateAuthenticationOptions.CertificateAndRoles>();
    Configuration.GetSection("AuthorizedCertficatesAndRoles:CertificateAndRoles").Bind(certificateAndRoles);
    certOptions.CertificatesAndRoles = certificateAndRoles.ToArray();
});

services.AddAuthorization(options =>
{
    options.AddPolicy("CanAccessAdminMethods", policy => policy.RequireRole("Admin"));
    options.AddPolicy("CanAccessUserMethods", policy => policy.RequireRole("User"));
});
#endregion
  

Помощник по сертификату

 public class CertificateHelper
{
    protected internal static X509Certificate2 GetServiceCertificate(string subjectName)
    {
        using (var certStore = new X509Store(StoreName.Root, StoreLocation.LocalMachine))
        {
            certStore.Open(OpenFlags.ReadOnly);
            var certCollection = certStore.Certificates.Find(
                                       X509FindType.FindBySubjectDistinguishedName, subjectName, true);
            X509Certificate2 certificate = null;
            if (certCollection.Count > 0)
            {
                certificate = certCollection[0];
            }
            return certificate;
        }
    }

    protected internal static string GetCertificateSubjectNameBasedOnEnvironment(string environment)
    {
        var builder = new ConfigurationBuilder()
         .SetBasePath(Directory.GetCurrentDirectory())
        .AddJsonFile($"appsettings.{environment}.json", optional: false);

        var configuration = builder.Build();
        return configuration["ServerCertificateSubject"];
    }
}
  

Ответ №7:

Если вы посмотрите на стандартную ссылку .NET для класса HttpClientHandler, вы увидите, что свойство ClientCertificates существует, но скрыто из-за использования EditorBrowsableState .Никогда. Это не позволяет IntelliSense отображать его, но все равно будет работать в коде, который его использует.

 [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
public System.Security.Cryptography.X509Certificates.X509CertificateCollection ClientCertificates { get; }
  

Ответ №8:

Я подумал, что лучший ответ на этот вопрос был предоставлен здесь.

Используя заголовок X-ARR-ClientCert, вы можете предоставить информацию о сертификате.

Адаптированное решение здесь:

 X509Certificate2 certificate;
var handler = new HttpClientHandler {
    ClientCertificateOptions = ClientCertificateOption.Manual,
    SslProtocols = SslProtocols.Tls12
};
handler.ClientCertificates.Add(certificate);
handler.CheckCertificateRevocationList = false;
// this is required to get around self-signed certs
handler.ServerCertificateCustomValidationCallback =
    (httpRequestMessage, cert, cetChain, policyErrors) => {
        return true;
    };
var client = new HttpClient(handler);
requestMessage.Headers.Add("X-ARR-ClientCert", certificate.GetRawCertDataString());
requestMessage.Content = new StringContent(JsonConvert.SerializeObject(requestData), Encoding.UTF8, "application/json");
var response = await client.SendAsync(requestMessage);

if (response.IsSuccessStatusCode)
{
    var responseContent = await response.Content.ReadAsStringAsync();
    var keyResponse = JsonConvert.DeserializeObject<KeyResponse>(responseContent);

    return keyResponse;
}
  

И в процедуре запуска вашего сервера .net core:

 public IServiceProvider ConfigureServices(IServiceCollection services)
{
    services.AddCertificateForwarding(options => {
        options.CertificateHeader = "X-ARR-ClientCert";
        options.HeaderConverter = (headerValue) => {
            X509Certificate2 clientCertificate = null;
            try
            {
                if (!string.IsNullOrWhiteSpace(headerValue))
                {
                    var bytes = ConvertHexToBytes(headerValue);
                    clientCertificate = new X509Certificate2(bytes);
                }
            }
            catch (Exception)
            {
                // invalid certificate
            }

            return clientCertificate;
        };
    });
}
  

Комментарии:

1. X-ARR-ClientCert — это специфичная для Azure вещь, и этот код фактически не подтверждает, что у клиента есть закрытый ключ для сертификата. Он просто прикрепляет открытый ключ к запросу. Обратите внимание, что связанная статья была исправлена, чтобы действительно правильно использовать сертификат клиента.