Настройка проверки подлинности сертификата в ASP.NET Core

Microsoft.AspNetCore.Authentication.Certificate содержит реализацию, аналогичную проверке подлинности сертификатов для ASP.NET Core. Проверка подлинности сертификата выполняется на уровне TLS, до того как она попадает в ASP.NET Core. Более точно, эта функция представляет собой обработчик аутентификации, который проверяет сертификат, а затем предоставляет событие, где вы можете обработать этот сертификат ClaimsPrincipal.

Вы должнынастроить сервер для проверки подлинности сертификатов с помощью IIS, Kestrel, Azure веб-приложения или предпочтительного решения.

В этой статье описывается настройка проверки подлинности сертификата в ASP.NET Core для IIS и HTTP.sys, а также примеры вызова различных методов и работы со свойствами.

Просмотр сценариев прокси-сервера и подсистемы балансировки нагрузки

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

  • Осуществляет проверку подлинности.
  • Передает данные аутентификации пользователя приложению (например, в заголовке запроса), которое использует эту информацию для аутентификации.

Альтернативой проверке подлинности на основе сертификатов в средах, где используются прокси-серверы и подсистемы балансировки нагрузки, являются федеративные службы Active Directory (ADFS) с OpenID Connect (OIDC).

Начало работы

Получите сертификат HTTPS, примените его и настройте сервер для требования сертификатов.

В веб-приложении:

  • Добавьте ссылку на пакет NuGet Microsoft.AspNetCore.Authentication.Certificate .

  • В файле Program.cs вызовите builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...); метод. Предоставьте делегата обработчику событий OnCertificateValidated, чтобы произвести любую дополнительную проверку сертификата клиента, передаваемого с запросами. Превратите эти сведения в ClaimsPrincipal значение и задайте его для context.Principal свойства.

Если проверка подлинности завершается ошибкой 403 (Forbidden) , этот обработчик возвращает ответ, а не ответ 401 (Unauthorized), как можно ожидать. Обработчик возвращает другой ответ, так как ожидается, что проверка подлинности будет происходить во время первоначального подключения TLS. К тому времени, когда оно достигает обработчика, это уже слишком поздно. Невозможно обновить подключение с анонимного подключения к одному с сертификатом.

Метод UseAuthentication требуется для установки HttpContext.User в значение ClaimsPrincipal, созданное из сертификата. Например:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(
        CertificateAuthenticationDefaults.AuthenticationScheme)
    .AddCertificate();

var app = builder.Build();

app.UseAuthentication();

app.MapGet("/", () => "Hello World!");

app.Run();

В предыдущем примере показано, как добавить проверку подлинности сертификата по умолчанию. Обработчик создает учетную запись пользователя с помощью общих свойств сертификата.

Настройка проверки сертификата

Обработчик CertificateAuthenticationOptions имеет некоторые встроенные проверки, которые являются минимальными проверками, которые необходимо выполнить в сертификате. Каждый из этих параметров включен по умолчанию. В следующих разделах описывается, как работать с параметрами.

AllowedCertificateTypes = Chained, SelfSigned или All (Chained | SelfSigned)

Значение по умолчанию: CertificateTypes.Chained

Эта проверка проверяет, разрешен ли только соответствующий тип сертификата. Если приложение использует самозаверяющие сертификаты, этот параметр необходимо задать либо на CertificateTypes.All, либо на CertificateTypes.SelfSigned.

ChainTrustValidationMode

Значение по умолчанию: X509ChainTrustMode.System

Сертификат, представленный клиентом, должен быть связан с доверенным корневым сертификатом. Эта проверка определяет, какое хранилище доверия содержит эти корневые сертификаты.

По умолчанию обработчик использует хранилище доверия системы. Если представленный сертификат клиента должен быть привязан к корневому сертификату, который не отображается в хранилище доверия системы, можно задать значение X509ChainTrustMode.CustomRootTrust , чтобы обработчик использовал CustomTrustStore свойство.

CustomTrustStore

Значение по умолчанию: пустое X509Certificate2Collection

Если для свойства обработчика ChainTrustValidationMode задано X509ChainTrustMode.CustomRootTrust значение, этот X509Certificate2Collection объект содержит каждый сертификат, используемый для проверки сертификата клиента до доверенного корневого сертификата, включая сам корневой сертификат.

Когда клиент представляет сертификат, который является частью цепочки сертификатов с несколькими уровнями, CustomTrustStore свойство должно содержать каждый выдавающий сертификат в цепочке.

ПроверитьИспользованиеСертификата

Значение по умолчанию: true

Эта проверка удостоверяется, что сертификат, представленный клиентом, имеет расширенное использование ключа аутентификации клиента (EKU) или они совсем отсутствуют. Как говорят спецификации, если EKU не указан, все EKU считаются допустимыми.

ПроверкаСрокаДействия

Значение по умолчанию: true

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

ОтзывФлаг

Значение по умолчанию: X509RevocationFlag.ExcludeRoot

Флаг, указывающий, какие сертификаты в цепочке проверяются для отзыва.

Проверки отзыва выполняются только при привязке сертификата к корневому сертификату.

Режим отзыва

Значение по умолчанию: X509RevocationMode.Online

Флаг, указывающий, как выполняются проверки отзыва.

Указание на необходимость онлайн-проверки может привести к длительной задержке при обращении к центру сертификации.

Проверки отзыва выполняются только в том случае, если сертификат привязан к корневому сертификату.

Часто задаваемые вопросы. Можно ли настроить приложение для требования сертификата только по определенным путям?

Такой подход невозможен. Обмен сертификатами завершается в начале беседы HTTPS. Операция выполняется сервером до того, как первый запрос будет получен в этом соединении, поэтому область действия невозможна в зависимости от полей запроса.

События обработчика процессов

Обработчик имеет два события:

  • OnAuthenticationFailed: вызывается, если исключение происходит во время проверки подлинности и позволяет реагировать.

  • OnCertificateValidated: вызывается после того, как сертификат проверен, проверка пройдена и создан субъект по умолчанию. Это событие позволяет выполнять собственную проверку, дополнение или замену принципала. Вот некоторые примеры.

    • Определение того, известен ли сертификат вашим службам.

    • Создание собственного принципа, например в следующем примере:

      builder.Services.AddAuthentication(
              CertificateAuthenticationDefaults.AuthenticationScheme)
          .AddCertificate(options =>
          {
              options.Events = new CertificateAuthenticationEvents
              {
                  OnCertificateValidated = context =>
                  {
                      var claims = new[]
                      {
                          new Claim(
                              ClaimTypes.NameIdentifier,
                              context.ClientCertificate.Subject,
                              ClaimValueTypes.String, context.Options.ClaimsIssuer),
                          new Claim(
                              ClaimTypes.Name,
                              context.ClientCertificate.Subject,
                              ClaimValueTypes.String, context.Options.ClaimsIssuer)
                      };
      
                      context.Principal = new ClaimsPrincipal(
                          new ClaimsIdentity(claims, context.Scheme.Name));
                      context.Success();
      
                      return Task.CompletedTask;
                  }
              };
          });
      

Если входящий сертификат не удовлетворяет вашей дополнительной проверке, вызовите context.Fail("failure reason"), указав причину сбоя.

Для повышения функциональности обратитесь к службе, зарегистрированной через механизм внедрения зависимостей, которая подключается к базе данных или к другому типу хранилища пользователей. Доступ к службе с помощью контекста, переданного делегату. Рассмотрим следующий пример:

builder.Services.AddAuthentication(
        CertificateAuthenticationDefaults.AuthenticationScheme)
    .AddCertificate(options =>
    {
        options.Events = new CertificateAuthenticationEvents
        {
            OnCertificateValidated = context =>
            {
                var validationService = context.HttpContext.RequestServices
                    .GetRequiredService<ICertificateValidationService>();

                if (validationService.ValidateCertificate(context.ClientCertificate))
                {
                    var claims = new[]
                    {
                        new Claim(
                            ClaimTypes.NameIdentifier,
                            context.ClientCertificate.Subject,
                            ClaimValueTypes.String, context.Options.ClaimsIssuer),
                        new Claim(
                            ClaimTypes.Name,
                            context.ClientCertificate.Subject,
                            ClaimValueTypes.String, context.Options.ClaimsIssuer)
                    };

                    context.Principal = new ClaimsPrincipal(
                        new ClaimsIdentity(claims, context.Scheme.Name));
                    context.Success();
                }

                return Task.CompletedTask;
            }
        };
    });

Концептуально проверка сертификата является проблемой авторизации. Например, можно добавить проверку издателя или отпечатка в политике авторизации, а не внутри обработчика OnCertificateValidated .

Настройка сервера для требования сертификатов

В следующих разделах описывается настройка сервера для требования сертификатов для конкретного решения, включая Kestrel, IIS, Azure, настраиваемые веб-прокси и Azure веб-приложения.

Kestrel

В файле Program.cs настройте Kestrel следующее:

var builder = WebApplication.CreateBuilder(args);

builder.Services.Configure<KestrelServerOptions>(options =>
{
    options.ConfigureHttpsDefaults(options =>
        options.ClientCertificateMode = ClientCertificateMode.RequireCertificate);
});

Примечание.

Когда конечная точка создается при вызове метода Listenперед вызовом метода ConfigureHttpsDefaults, к конечной точке не применяются настройки по умолчанию.

IIS

Выполните следующие действия в диспетчере IIS.

  1. На вкладке "Подключения" выберите свой сайт.
  2. В окне Представление функций дважды щелкните Настройки SSL.
  3. Установите флажок "Требовать SSL ".
  4. Для параметра "Сертификаты клиента" выберите "Требовать".

Снимок экрана: настройка параметров сертификата клиента в IIS.

Azure и настраиваемые веб-прокси

Дополнительные сведения о настройке ПО промежуточного слоя пересылки сертификатов см. в документации по размещению и развертыванию.

Проверка подлинности сертификата в Azure веб-приложения

Для Azure не требуется конфигурация пересылки. Программное обеспечение пересылки сертификатов настраивает конфигурацию.

Примечание.

Для этого сценария требуется промежуточное ПО для пересылки сертификатов.

Дополнительные сведения см. в разделе Использование TLS/SSL-сертификатов в коде приложения (Azure документации).

Проверка подлинности на основе сертификатов в пользовательских прокси-серверах

Метод AddCertificateForwarding используется для указания:

  • Имя клиентского заголовка.
  • Загрузка сертификата (через HeaderConverter свойство).

Например, в пользовательских прокси-серверах сертификат передается в виде пользовательского заголовка запроса X-SSL-CERT. Чтобы использовать сертификат, настройте пересылку сертификатов в файле Program.cs :

builder.Services.AddCertificateForwarding(options =>
{
    options.CertificateHeader = "X-SSL-CERT";

    options.HeaderConverter = headerValue =>
    {
        X509Certificate2? clientCertificate = null;

        if (!string.IsNullOrWhiteSpace(headerValue))
        {
            clientCertificate = new X509Certificate2(StringToByteArray(headerValue));
        }

        return clientCertificate!;

        static byte[] StringToByteArray(string hex)
        {
            var numberChars = hex.Length;
            var bytes = new byte[numberChars / 2];

            for (int i = 0; i < numberChars; i += 2)
            {
                bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
            }

            return bytes;
        }
    };
});

Если NGINX используется с конфигурацией proxy_set_header ssl-client-cert $ssl_client_escaped_cert для обратного прокси-сервера или приложение развертывается в Kubernetes с помощью NGINX Ingress, сертификат клиента передается приложению в формате с кодировкой URL-адреса. Чтобы использовать сертификат, расшифруйте его следующим образом:

builder.Services.AddCertificateForwarding(options =>
{
    options.CertificateHeader = "ssl-client-cert";

    options.HeaderConverter = (headerValue) =>
    {
        X509Certificate2? clientCertificate = null;

        if (!string.IsNullOrWhiteSpace(headerValue))
        {
            clientCertificate = X509Certificate2.CreateFromPem(
                WebUtility.UrlDecode(headerValue));
        }

        return clientCertificate!;
    };
});

Добавьте ПО промежуточного слоя в файл Program.cs . Метод UseCertificateForwarding вызывается перед вызовами методов UseAuthentication и UseAuthorization:

var app = builder.Build();

app.UseCertificateForwarding();

app.UseAuthentication();
app.UseAuthorization();

Для реализации логики проверки можно использовать отдельный класс. Так как в этом примере используется тот же самозаверяющий сертификат, убедитесь, что можно использовать только ваш сертификат. Убедитесь, что отпечаток сертификата клиента и сертификата сервера совпадают. В противном случае можно использовать любой сертификат, и он будет достаточен для проверки подлинности. Затем сертификат используется внутри AddCertificate метода. Вы также можете проверить субъект или издателя здесь, если вы используете промежуточные или дочерние сертификаты.

using System.Security.Cryptography.X509Certificates;

namespace CertAuthSample.Snippets;

public class SampleCertificateValidationService : ICertificateValidationService
{
    public bool ValidateCertificate(X509Certificate2 clientCertificate)
    {
        // Don't hardcode passwords in production code.
        // Use a certificate thumbprint or Azure Key Vault.
        var expectedCertificate = new X509Certificate2(
            Path.Combine("/path/to/pfx"), "1234");

        return clientCertificate.Thumbprint == expectedCertificate.Thumbprint;
    }
}

Реализация HttpClient с помощью сертификата и IHttpClientFactory

В следующем примере сертификат клиента добавляется объекту HttpClientHandler с помощью свойства ClientCertificates из обработчика. Затем этот обработчик можно использовать в именованном экземпляре HttpClient с помощью метода ConfigurePrimaryHttpMessageHandler. Этот сценарий настраивается в файле Program.cs :

var clientCertificate =
    new X509Certificate2(
      Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234");

builder.Services.AddHttpClient("namedClient", c =>
{
}).ConfigurePrimaryHttpMessageHandler(() =>
{
    var handler = new HttpClientHandler();
    handler.ClientCertificates.Add(clientCertificate);
    return handler;
});

Затем IHttpClientFactory может быть использован для получения именованного экземпляра, вместе с обработчиком и сертификатом. Метод CreateClient с именем клиента, определенного в файле Program.cs , используется для получения экземпляра. HTTP-запрос можно отправить с помощью клиента по мере необходимости:

public class SampleHttpService
{
    private readonly IHttpClientFactory _httpClientFactory;

    public SampleHttpService(IHttpClientFactory httpClientFactory)
        => _httpClientFactory = httpClientFactory;

    public async Task<JsonDocument> GetAsync()
    {
        var httpClient = _httpClientFactory.CreateClient("namedClient");
        var httpResponseMessage = await httpClient.GetAsync("https://example.com");

        if (httpResponseMessage.IsSuccessStatusCode)
        {
            return JsonDocument.Parse(
                await httpResponseMessage.Content.ReadAsStringAsync());
        }

        throw new ApplicationException($"Status code: {httpResponseMessage.StatusCode}");
    }
}

Если на сервер отправляется правильный сертификат, возвращаются данные. Если сертификат или неправильный сертификат не отправлен, сервер возвращает код состояния HTTP 403.

Сертификаты в PowerShell

Создание сертификатов является самой сложной частью при настройке этого потока. Корневой сертификат можно создать с помощью командлета New-SelfSignedCertificate PowerShell. При создании сертификата используйте надежный пароль. Важно добавить KeyUsageProperty параметр и KeyUsage параметр, как показано в примерах.

Создание корневого удостоверяющего центра

В следующем коде показано, как создать удостоверяющий центр (УЦ) на корневом уровне:

New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "root_ca_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath root_ca_dev_damienbod.crt

Примечание.

Значение -DnsName параметра должно соответствовать целевому объекту развертывания приложения. Например, localhost для разработки.

Установить в доверенном корне

Корневой сертификат должен быть доверенным на вашей хост-системе. По умолчанию доверенны только корневые сертификаты, созданные центром сертификации. Сведения о том, как доверять корневому сертификату, см. в документации Windows или командлете PowerShell Import-Certificate.

Использование промежуточного сертификата

Теперь промежуточный сертификат можно создать из корневого сертификата. Этот подход не требуется для всех вариантов использования, но может потребоваться создать множество сертификатов или активировать или отключить группы сертификатов. Параметр TextExtension требуется для задания длины пути в основных ограничениях сертификата.

Затем промежуточный сертификат можно добавить в доверенный промежуточный сертификат в системе узлов Windows.

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint of the root..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "intermediate_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "intermediate_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\intermediate_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath intermediate_dev_damienbod.crt

Создание дочернего сертификата из промежуточного сертификата

Дочерний сертификат можно создать из промежуточного сертификата. Этот дочерний сертификат является конечной сущностью. Вам не нужно создавать дополнительные дочерние сертификаты.

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the Intermediate certificate..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com"

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt

Создание дочернего сертификата из корневого сертификата

Дочерний сертификат также можно создать непосредственно из корневого сертификата.

$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the root cert..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com"

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt

Пример: корневой — промежуточный сертификат — сертификат

В следующем примере показана конфигурация корневого ЦС, промежуточного сертификата и дочернего сертификата:

$mypwdroot = ConvertTo-SecureString -String "1234" -Force -AsPlainText
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "root_ca_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature

Get-ChildItem -Path cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwdroot

Export-Certificate -Cert cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 -FilePath root_ca_dev_damienbod.crt

$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\0C89639E4E2998A93E423F919B36D4009A0F9991 )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")

Get-ChildItem -Path cert:\localMachine\my\BA9BF91ED35538A01375EFC212A2F46104B33A44 | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\BA9BF91ED35538A01375EFC212A2F46104B33A44 -FilePath child_a_dev_damienbod.crt

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\BA9BF91ED35538A01375EFC212A2F46104B33A44 )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_b_from_a_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_b_from_a_dev_damienbod.com" 

Get-ChildItem -Path cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_b_from_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A -FilePath child_b_from_a_dev_damienbod.crt

При использовании корневых, промежуточных или дочерних сертификатов сертификаты можно проверить с помощью отпечатка или PublicKey по мере необходимости:

using System.Security.Cryptography.X509Certificates;

namespace CertAuthSample.Snippets;

public class SampleCertificateThumbprintsValidationService : ICertificateValidationService
{
    private readonly string[] validThumbprints = new[]
    {
        "141594A0AE38CBBECED7AF680F7945CD51D8F28A",
        "0C89639E4E2998A93E423F919B36D4009A0F9991",
        "BA9BF91ED35538A01375EFC212A2F46104B33A44"
    };

    public bool ValidateCertificate(X509Certificate2 clientCertificate)
        => validThumbprints.Contains(clientCertificate.Thumbprint);
}

Результаты проверки сертификата кэша

.NET 5 и более поздних версий поддерживают возможность кэширования результатов проверки. Кэширование значительно повышает производительность проверки подлинности сертификата, так как проверка является дорогостоящей операцией.

По умолчанию проверка подлинности сертификата отключает кэширование. Чтобы включить кэширование, вызовите AddCertificateCache метод в файле Program.cs :

builder.Services.AddAuthentication(
        CertificateAuthenticationDefaults.AuthenticationScheme)
    .AddCertificate()
    .AddCertificateCache(options =>
    {
        options.CacheSize = 1024;
        options.CacheEntryExpiration = TimeSpan.FromMinutes(2);
    });

Реализация кэширования по умолчанию сохраняет результаты в памяти. Вы можете предоставить собственный кэш, реализуя ICertificateValidationCache и регистрируя его с помощью внедрения зависимостей. Например, services.AddSingleton<ICertificateValidationCache, YourCache>().

Использование необязательных сертификатов клиента

В этом разделе содержатся сведения о приложениях, которые должны защищать подмножество приложения с помощью сертификата. Например, для Razor страницы или контроллера в приложении могут потребоваться сертификаты клиента. В этом сценарии возникают некоторые проблемы:

  • Сертификаты клиента — это функция TLS, а не функция HTTP.
  • Сертификаты клиента согласовываются по каждому подключению и обычно в начале подключения перед доступностью данных HTTP.

Существует два подхода для реализации необязательных сертификатов клиента:

  • Вариант 1. Используйте отдельные имена узлов (SNI) и перенаправление. Хотя этот параметр включает больше работы по настройке, рекомендуется использовать этот подход, так как он работает в большинстве сред и протоколов.
  • Вариант 2. Повторное согласование во время HTTP-запроса. Этот подход имеет несколько ограничений и не рекомендуется.

Отдельные хосты (SNI)

В начале подключения известно только имя сервера (SNI)†. Сертификаты клиента можно настроить для каждого имени узла, поэтому для одного узла требуются сертификаты, а другой — нет.

  • Настройте привязку для домена и поддомена.

    Например, настройте привязки в contoso.com и myClient.contoso.com. Узел contoso.com не требует клиентского сертификата, но myClient.contoso.com требует.

    Дополнительные сведения см. в следующих ресурсах:

    .NET 5 и более поздних версий предлагает более удобную поддержку перенаправления для получения необязательных сертификатов клиента. Для получения дополнительной информации см. образец опциональных сертификатов.

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

    Например, перенаправьте на myClient.contoso.com/requestedPage. Так как запрос myClient.contoso.com/requestedPage отличается от имени contoso.com/requestedPageузла, клиент устанавливает другое подключение и предоставляется сертификат клиента.

    Дополнительные сведения см. в разделе "Общие сведения о авторизации" в ASP.NET Core.

† указание имени сервера (SNI) — это расширение TLS, которое используется для включения виртуального домена в рамках согласования SSL. Этот подход фактически означает, что имя виртуального домена или имя узла можно использовать для идентификации конечной точки сети.

Повторное согласование TLS

Повторное согласование TLS — это процесс, с помощью которого клиент и сервер могут повторно оценить требования шифрования для отдельного подключения, включая запрос сертификата клиента, если он не указан ранее. Перенацеливание TLS является угрозой безопасности и не рекомендуется, так как:

  • В HTTP/1.1 сервер должен сначала буферировать или использовать любые находящиеся в процессе данные HTTP, такие как тела запросов POST, чтобы убедиться, что соединение готово для пересогласования. В противном случае повторное согласование может перестать отвечать или завершиться ошибкой.
  • HTTP/2 и HTTP/3 явно запрещают повторное согласование.
  • Существуют риски безопасности, связанные с повторным согласованием. TLS 1.3 удалил повторное согласование всего подключения и заменил его новым расширением для запроса только сертификата клиента после начала подключения. Этот механизм предоставляется через те же API и по-прежнему подвергается предыдущим ограничениям буферизации и версий протокола HTTP.

Реализация и конфигурация этой функции зависят от версии сервера и платформы, как описано в следующих разделах.

IIS

IIS управляет согласованием сертификата клиента от вашего имени. Подраздел приложения может включить SslRequireCert возможность согласования сертификата клиента для этих запросов. Дополнительные сведения см. в разделе "Конфигурация" в документации по IIS.

IIS автоматически буферизирует тело запроса полностью до заданного предела размера перед повторным согласованием. Запросы, превышающие ограничение, отклоняются с ответом 413. Это ограничение по умолчанию составляет 48 КБ и настраивается путем задания свойства uploadReadAheadSize .

HttpSys

HttpSys имеет два параметра, которые управляют согласованием сертификата клиента, и оба должны быть заданы. Первый находится в файлеnetsh.exe в разделе http add sslcert clientcertnegotiation=enable/disable. Этот флаг указывает, следует ли согласовывать сертификат клиента в начале подключения. Задайте значение disable для необязательных сертификатов клиента. Дополнительные сведения см. параметр http add sslcert в документации netsh.

Второй настройкой является свойство ClientCertificateMethod. Если задано значение AllowRenegotation, сертификат клиента можно перенастроить во время запроса.

Примечание.

Приложение должно буферировать или использовать любые данные текста запроса перед попыткой повторного обмена данными. В противном случае запрос может перестать отвечать.

Приложение может сначала проверить ClientCertificate свойство, чтобы узнать, доступен ли сертификат. Если он недоступен, убедитесь, что тело запроса обработано перед вызовом метода GetClientCertificateAsync для согласования. GetClientCertificateAsync может возвращать пустой сертификат, если клиент отказывается предоставить его.

Примечание.

Поведение свойства ClientCertificate изменилось в .NET 6. См. дополнительные сведения в задаче GitHub № 466.

Kestrel

Kestrel контролирует согласование сертификата клиента со свойством ClientCertificateMode.

.NET 6 и более поздних версий предоставляет параметр DelayCertificate для свойства ClientCertificateMode. Если этот параметр задан, приложение может проверить ClientCertificate свойство, чтобы узнать, доступен ли сертификат. Если он недоступен, убедитесь, что тело запроса обработано перед вызовом метода GetClientCertificateAsync, чтобы выполнить согласование. GetClientCertificateAsync может возвращать пустой сертификат, если клиент отказывается предоставить его.

Примечание.

Приложение должно буферировать или использовать любые данные текста запроса перед попыткой повторного обмена данными. В противном случае GetClientCertificateAsync может вызвать исключение InvalidOperationException: поток клиента должен быть освобожден перед повторным согласованием.

При программной настройке параметров TLS для имени узла SNI используйте перегрузку метода UseHttps (.NET 6 или более поздняя версия), которая принимает объект класса TlsHandshakeCallbackOptions. Этот параметр управляет повторным согласованием сертификата клиента через свойство AllowDelayedClientCertificateNegotation. Дополнительные сведения см. в методе ListenOptionsHttpsExtensions.UseHttps .

Microsoft.AspNetCore.Authentication.Certificate содержит реализацию, аналогичную проверке подлинности сертификатов для ASP.NET Core. Проверка подлинности сертификата происходит на уровне TLS, задолго до того, как он достигает ASP.NET Core. Точнее, этот обработчик аутентификации проверяет сертификат и предоставляет событие, в котором можно сопоставить сертификат значению ClaimsPrincipal.

Настройте сервер для проверки подлинности сертификата, будь то IIS, KestrelAzure веб-приложения или все, что вы используете.

Сценарии прокси-сервера и подсистемы балансировки нагрузки

Проверка подлинности сертификата — это сценарий с отслеживанием состояния, в основном используемый, когда прокси-сервер или подсистема балансировки нагрузки не обрабатывает трафик между клиентами и серверами. Если используется прокси-сервер или подсистема балансировки нагрузки, проверка подлинности сертификатов выполняется только в том случае, если прокси-сервер или подсистема балансировки нагрузки:

  • Обрабатывает проверку подлинности.
  • Передает сведения о проверке подлинности пользователя приложению (например, в заголовке запроса), которое использует эти сведения для аутентификации.

Альтернативой проверке подлинности на основе сертификатов в средах, где используются прокси-серверы и подсистемы балансировки нагрузки, являются федеративные службы Active Directory (ADFS) с OpenID Connect (OIDC).

Начало работы

Получите сертификат HTTPS, примените его и настройте сервер для требования сертификатов.

В веб-приложении добавьте ссылку на пакет Microsoft.AspNetCore.Authentication.Certificate . Затем в методе Startup.ConfigureServices вызовите services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...); с вашими параметрами, предоставив делегата для OnCertificateValidated, чтобы выполнить дополнительную проверку клиентского сертификата, отправленного с запросами. Превратите эту информацию в ClaimsPrincipal и установите его в свойство context.Principal.

Если проверка подлинности завершается ошибкой, этот обработчик возвращает ответ 403 (Forbidden) вместо 401 (Unauthorized), как можно было бы ожидать. Причина заключается в том, что проверка подлинности должна происходить во время начального подключения TLS. К тому времени, когда данные достигают обработчика, уже слишком поздно. Невозможно обновить подключение с анонимного подключения к одному с сертификатом.

Также добавьте app.UseAuthentication(); в метод Startup.Configure. В противном случае HttpContext.User не будет установлен на ClaimsPrincipal, созданное из сертификата. Например:

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(
        CertificateAuthenticationDefaults.AuthenticationScheme)
        .AddCertificate()
        // Adding an ICertificateValidationCache results in certificate auth caching the results.
        // The default implementation uses a memory cache.
        .AddCertificateCache();

    // All other service configuration
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseAuthentication();

    // All other app configuration
}

В предыдущем примере показано, как добавить проверку подлинности сертификата по умолчанию. Обработчик создает пользовательский принципал с помощью общих свойств сертификата.

Настройка проверки сертификата

Обработчик CertificateAuthenticationOptions имеет некоторые встроенные проверки, которые являются минимальными проверками, которые необходимо выполнить в сертификате. Каждый из этих параметров включен по умолчанию.

AllowedCertificateTypes = Chained, SelfSigned или All (Chained | SelfSigned)

Значение по умолчанию: CertificateTypes.Chained

Эта проверка проверяет, разрешен ли только соответствующий тип сертификата. Если приложение использует самозаверяющие сертификаты, этот параметр необходимо задать как CertificateTypes.All или CertificateTypes.SelfSigned.

ПроверитьИспользованиеСертификата

Значение по умолчанию: true

Эта проверка удостоверяется, что сертификат, представленный клиентом, имеет расширенный ключ использования для аутентификации клиента (EKU) или не содержит EKU совсем. Согласно спецификациям, если EKU не указан, то все EKU считаются действительными.

ПроверкаСрокаДействия

Значение по умолчанию: true

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

RevocationFlag

Значение по умолчанию: X509RevocationFlag.ExcludeRoot

Флаг, указывающий, какие сертификаты в цепочке проверяются для отзыва.

Проверки отзыва выполняются только при привязке сертификата к корневому сертификату.

Режим аннулирования

Значение по умолчанию: X509RevocationMode.Online

Флаг, указывающий, как выполняются проверки отзыва.

Указание онлайн-проверки может привести к длительной задержке при обращении к центру сертификации.

Проверки отзыва выполняются только при привязке сертификата к корневому сертификату.

Можно ли настроить приложение для требования сертификата только по определенным путям?

Это невозможно. Помните, что обмен сертификатами производится в начале HTTPS-соединения: сервер выполняет это до получения первого запроса по этому подключению, поэтому область действия не может основываться на каких-либо полях запроса.

События обработчиков

Обработчик имеет два события:

  • OnAuthenticationFailed: вызывается, если исключение происходит во время проверки подлинности и позволяет реагировать.
  • OnCertificateValidated: Вызывается после того, как сертификат был проверен и прошел проверку, и создан принципал по умолчанию. Это событие позволяет выполнять собственную проверку и дополнить или заменить главное лицо (principal). Например:
    • Определение того, известен ли сертификат вашим службам.

    • Создание собственного основного компонента. Рассмотрим следующий пример в Startup.ConfigureServices:

      services.AddAuthentication(
          CertificateAuthenticationDefaults.AuthenticationScheme)
          .AddCertificate(options =>
          {
              options.Events = new CertificateAuthenticationEvents
              {
                  OnCertificateValidated = context =>
                  {
                      var claims = new[]
                      {
                          new Claim(
                              ClaimTypes.NameIdentifier, 
                              context.ClientCertificate.Subject,
                              ClaimValueTypes.String, 
                              context.Options.ClaimsIssuer),
                          new Claim(ClaimTypes.Name,
                              context.ClientCertificate.Subject,
                              ClaimValueTypes.String, 
                              context.Options.ClaimsIssuer)
                      };
      
                      context.Principal = new ClaimsPrincipal(
                          new ClaimsIdentity(claims, context.Scheme.Name));
                      context.Success();
      
                      return Task.CompletedTask;
                  }
              };
          });
      

Если вы обнаружите, что входящий сертификат не проходит дополнительную проверку, позвоните по context.Fail("failure reason") с указанием причины сбоя.

Для реальных функциональных возможностей может потребоваться вызвать службу, зарегистрированную в внедрении зависимостей, которая подключается к базе данных или другому типу хранилища пользователей. Получите доступ к вашей службе, используя контекст, переданный в делегат. Рассмотрим следующий пример в Startup.ConfigureServices:

services.AddAuthentication(
    CertificateAuthenticationDefaults.AuthenticationScheme)
    .AddCertificate(options =>
    {
        options.Events = new CertificateAuthenticationEvents
        {
            OnCertificateValidated = context =>
            {
                var validationService =
                    context.HttpContext.RequestServices
                        .GetRequiredService<ICertificateValidationService>();

                if (validationService.ValidateCertificate(
                    context.ClientCertificate))
                {
                    var claims = new[]
                    {
                        new Claim(
                            ClaimTypes.NameIdentifier, 
                            context.ClientCertificate.Subject, 
                            ClaimValueTypes.String, 
                            context.Options.ClaimsIssuer),
                        new Claim(
                            ClaimTypes.Name, 
                            context.ClientCertificate.Subject, 
                            ClaimValueTypes.String, 
                            context.Options.ClaimsIssuer)
                    };

                    context.Principal = new ClaimsPrincipal(
                        new ClaimsIdentity(claims, context.Scheme.Name));
                    context.Success();
                }                     

                return Task.CompletedTask;
            }
        };
    });

Концептуально проверка сертификата является проблемой авторизации. Добавление проверки, например, на издателя или отпечаток в политике авторизации, а не внутри OnCertificateValidated, вполне допустимо.

Настройка сервера для требования сертификатов

Kestrel

В Program.cs настройте Kestrel следующим образом:

public static void Main(string[] args)
{
    CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args)
{
    return Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
            webBuilder.ConfigureKestrel(o =>
            {
                o.ConfigureHttpsDefaults(o => 
                    o.ClientCertificateMode =  ClientCertificateMode.RequireCertificate);
            });
        });
}

Примечание.

К конечным точкам, созданным путем вызова Listenперед вызовом ConfigureHttpsDefaults, не будут применяться значения по умолчанию.

IIS

Выполните следующие действия в диспетчере IIS.

  1. Выберите сайт на вкладке "Подключения ".
  2. Дважды щелкните Настройки SSL в окне Представление функций.
  3. Установите флажок "Требовать SSL" и нажмите переключатель "Требовать" в разделе "Сертификаты клиента".

Параметры сертификата клиента в IIS

Azure и настраиваемые веб-прокси

Сведения о настройке ПО промежуточного слоя пересылки сертификатов см. в документации по размещению и развертыванию.

Использование проверки подлинности с сертификатом в Azure веб-приложения

Для Azure не требуется конфигурация пересылки. Конфигурация пересылки настраивается промежуточным программным обеспечением пересылки сертификатов.

Примечание.

Для этого сценария требуется промежуточное ПО пересылки сертификатов.

Дополнительные сведения см. в статье о использовании сертификата TLS/SSL в коде в службе приложений Azure (документация Azure).

Использование проверки подлинности сертификата в пользовательских веб-прокси

Метод AddCertificateForwarding используется для указания:

  • Имя заголовка клиента.
  • Как следует загружать сертификат (используя свойство HeaderConverter).

Например, в пользовательских веб-прокси сертификат передается в виде пользовательского заголовка запроса, например X-SSL-CERT. Чтобы использовать его, настройте пересылку сертификатов в Startup.ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.AddCertificateForwarding(options =>
    {
        options.CertificateHeader = "X-SSL-CERT";
        options.HeaderConverter = (headerValue) =>
        {
            X509Certificate2 clientCertificate = null;

            if(!string.IsNullOrWhiteSpace(headerValue))
            {
                byte[] bytes = StringToByteArray(headerValue);
                clientCertificate = new X509Certificate2(bytes);
            }

            return clientCertificate;
        };
    });
}

private static byte[] StringToByteArray(string hex)
{
    int NumberChars = hex.Length;
    byte[] bytes = new byte[NumberChars / 2];

    for (int i = 0; i < NumberChars; i += 2)
    {
        bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
    }

    return bytes;
}

Если приложение является обратным прокси-сервером NGINX с конфигурацией proxy_set_header ssl-client-cert $ssl_client_escaped_cert или развернуто в Kubernetes с помощью NGINX Ingress, сертификат клиента передается приложению в формате, закодированном URL-адресом. Чтобы использовать сертификат, расшифруйте его следующим образом:

В Startup.ConfigureServices (Startup.cs):

services.AddCertificateForwarding(options =>
{
    options.CertificateHeader = "ssl-client-cert";
    options.HeaderConverter = (headerValue) =>
    {
        X509Certificate2 clientCertificate = null;

        if (!string.IsNullOrWhiteSpace(headerValue))
        {
            string certPem = WebUtility.UrlDecode(headerValue);
            clientCertificate = X509Certificate2.CreateFromPem(certPem);
        }

        return clientCertificate;
    };
});

Затем метод Startup.Configure добавляет ПО промежуточного слоя. UseCertificateForwarding вызывается перед вызовами UseAuthentication и UseAuthorization:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...

    app.UseRouting();

    app.UseCertificateForwarding();
    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

Для реализации логики проверки можно использовать отдельный класс. Так как в этом примере используется тот же самозаверяющий сертификат, убедитесь, что можно использовать только ваш сертификат. Убедитесь, что отпечаток сертификата клиента и сертификата сервера совпадают, в противном случае можно использовать любой сертификат и будет достаточно для проверки подлинности. Это будет использоваться внутри AddCertificate метода. Вы также можете проверить данные субъекта или издателя, если вы используете промежуточные или дочерние сертификаты.

using System.IO;
using System.Security.Cryptography.X509Certificates;

namespace AspNetCoreCertificateAuthApi
{
    public class MyCertificateValidationService
    {
        public bool ValidateCertificate(X509Certificate2 clientCertificate)
        {
            // Do not hardcode passwords in production code
            // Use thumbprint or key vault
            var cert = new X509Certificate2(
                Path.Combine("sts_dev_cert.pfx"), "1234");

            if (clientCertificate.Thumbprint == cert.Thumbprint)
            {
                return true;
            }

            return false;
        }
    }
}

Реализация HttpClient с помощью сертификата и HttpClientHandler

Его HttpClientHandler можно добавить непосредственно в конструктор класса HttpClient. Следует соблюдать осторожность при создании экземпляров HttpClient. Затем сертификат HttpClient отправляется с каждым запросом.

private async Task<JsonDocument> GetApiDataUsingHttpClientHandler()
{
    var cert = new X509Certificate2(Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234");
    var handler = new HttpClientHandler();
    handler.ClientCertificates.Add(cert);
    var client = new HttpClient(handler);

    var request = new HttpRequestMessage()
    {
        RequestUri = new Uri("https://localhost:44379/api/values"),
        Method = HttpMethod.Get,
    };
    var response = await client.SendAsync(request);
    if (response.IsSuccessStatusCode)
    {
        var responseContent = await response.Content.ReadAsStringAsync();
        var data = JsonDocument.Parse(responseContent);
        return data;
    }

    throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
}

Реализуйте HttpClient, используя сертификат и именованный HttpClient, управляемый через IHttpClientFactory.

В следующем примере сертификат клиента добавляется в HttpClientHandler с использованием свойства ClientCertificates из обработчика. Затем этот обработчик можно использовать в именованном экземпляре HttpClient, применяя метод ConfigurePrimaryHttpMessageHandler. Эта настройка выполняется в Startup.ConfigureServices:

var clientCertificate = 
    new X509Certificate2(
      Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234");

services.AddHttpClient("namedClient", c =>
{
}).ConfigurePrimaryHttpMessageHandler(() =>
{
    var handler = new HttpClientHandler();
    handler.ClientCertificates.Add(clientCertificate);
    return handler;
});

Затем IHttpClientFactory можно использовать для получения экземпляра с указанным именем, обработчиком и сертификатом. Метод CreateClient с именем клиента, определенного в Startup классе, используется для получения экземпляра. HTTP-запрос можно отправить с помощью клиента по мере необходимости.

private readonly IHttpClientFactory _clientFactory;

public ApiService(IHttpClientFactory clientFactory)
{
    _clientFactory = clientFactory;
}

private async Task<JsonDocument> GetApiDataWithNamedClient()
{
    var client = _clientFactory.CreateClient("namedClient");

    var request = new HttpRequestMessage()
    {
        RequestUri = new Uri("https://localhost:44379/api/values"),
        Method = HttpMethod.Get,
    };
    var response = await client.SendAsync(request);
    if (response.IsSuccessStatusCode)
    {
        var responseContent = await response.Content.ReadAsStringAsync();
        var data = JsonDocument.Parse(responseContent);
        return data;
    }

    throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
}

Если на сервер отправляется правильный сертификат, возвращаются данные. Если сертификат или неправильный сертификат не отправлен, возвращается код состояния HTTP 403.

Создание сертификатов в PowerShell

Создание сертификатов является самой сложной частью при настройке этого потока. Корневой сертификат можно создать с помощью командлета New-SelfSignedCertificate PowerShell. При создании сертификата используйте надежный пароль. Важно добавить KeyUsageProperty параметр и KeyUsage параметр, как показано ниже.

Создание корневого центра сертификации

New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "root_ca_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath root_ca_dev_damienbod.crt

Примечание.

Значение -DnsName параметра должно соответствовать целевому объекту развертывания приложения. Например, localhost для разработки.

Установить в доверенном корне

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

Промежуточный сертификат

Теперь промежуточный сертификат можно создать из корневого сертификата. Это не обязательно для всех вариантов использования, но может потребоваться создать множество сертификатов или активировать или отключить группы сертификатов. Параметр TextExtension требуется для задания длины пути в основных ограничениях сертификата.

Затем промежуточный сертификат можно добавить в доверенный промежуточный сертификат в системе узлов Windows.

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint of the root..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "intermediate_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "intermediate_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\intermediate_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath intermediate_dev_damienbod.crt

Создание дочернего сертификата из промежуточного сертификата

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

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the Intermediate certificate..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com"

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt

Создание дочернего сертификата из корневого сертификата

Дочерний сертификат также можно создать непосредственно из корневого сертификата.

$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the root cert..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com"

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt

Пример: корневой сертификат — промежуточный сертификат — сертификат

$mypwdroot = ConvertTo-SecureString -String "1234" -Force -AsPlainText
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "root_ca_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature

Get-ChildItem -Path cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwdroot

Export-Certificate -Cert cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 -FilePath root_ca_dev_damienbod.crt

$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\0C89639E4E2998A93E423F919B36D4009A0F9991 )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")

Get-ChildItem -Path cert:\localMachine\my\BA9BF91ED35538A01375EFC212A2F46104B33A44 | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\BA9BF91ED35538A01375EFC212A2F46104B33A44 -FilePath child_a_dev_damienbod.crt

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\BA9BF91ED35538A01375EFC212A2F46104B33A44 )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_b_from_a_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_b_from_a_dev_damienbod.com" 

Get-ChildItem -Path cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_b_from_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A -FilePath child_b_from_a_dev_damienbod.crt

При использовании корневых, промежуточных или дочерних сертификатов сертификаты можно проверить с помощью отпечатка или PublicKey по мере необходимости.

using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography.X509Certificates;

namespace AspNetCoreCertificateAuthApi
{
    public class MyCertificateValidationService 
    {
        public bool ValidateCertificate(X509Certificate2 clientCertificate)
        {
            return CheckIfThumbprintIsValid(clientCertificate);
        }

        private bool CheckIfThumbprintIsValid(X509Certificate2 clientCertificate)
        {
            var listOfValidThumbprints = new List<string>
            {
                "141594A0AE38CBBECED7AF680F7945CD51D8F28A",
                "0C89639E4E2998A93E423F919B36D4009A0F9991",
                "BA9BF91ED35538A01375EFC212A2F46104B33A44"
            };

            if (listOfValidThumbprints.Contains(clientCertificate.Thumbprint))
            {
                return true;
            }

            return false;
        }
    }
}

Кэширование проверки сертификата

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

По умолчанию проверка подлинности сертификата отключает кэширование. Чтобы включить кэширование, вызовите AddCertificateCache в Startup.ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(
        CertificateAuthenticationDefaults.AuthenticationScheme)
            .AddCertificate()
            .AddCertificateCache(options =>
            {
                options.CacheSize = 1024;
                options.CacheEntryExpiration = TimeSpan.FromMinutes(2);
            });
}

Реализация кэширования по умолчанию сохраняет результаты в памяти. Вы можете предоставить собственный кэш, реализуя ICertificateValidationCache и регистрируя его с помощью внедрения зависимостей. Например, services.AddSingleton<ICertificateValidationCache, YourCache>().

Необязательные сертификаты клиентов

В этом разделе содержатся сведения о приложениях, которые должны защищать подмножество приложения с помощью сертификата. Например, для Razor страницы или контроллера в приложении могут потребоваться сертификаты клиента. Это создает проблемы в качестве сертификатов клиента:

  • Это функция TLS, а не функция HTTP.
  • Согласовываются для каждого подключения и обычно в начале подключения до того, как становятся доступны данные HTTP.

Существует два подхода к реализации необязательных сертификатов клиента:

  1. Использование отдельных имен узлов (SNI) и их перенаправление. Несмотря на более эффективную настройку, рекомендуется использовать эту функцию, так как она работает в большинстве сред и протоколов.
  2. Повторное согласование во время HTTP-запроса. Это имеет несколько ограничений и не рекомендуется.

Отдельные хосты (SNI)

В начале подключения известно только имя сервера (SNI)†. Сертификаты клиента можно настроить для каждого имени узла, чтобы один узел их требует, а другой — нет.

.NET 5 или более поздней версии добавляет более удобную поддержку перенаправления для получения необязательных сертификатов клиента. Для получения дополнительной информации см. образец опциональных сертификатов.

  • Для запросов к веб-приложению, для которых требуется клиентский сертификат, но его нет:
    • Перенаправление на ту же страницу через защищенный с помощью клиентского сертификата поддомен.
    • Например, перенаправьте на myClient.contoso.com/requestedPage. Так как запрос myClient.contoso.com/requestedPage отличается от имени contoso.com/requestedPageузла, клиент устанавливает другое подключение и предоставляется сертификат клиента.
    • Дополнительные сведения см. в разделе "Общие сведения о авторизации" в ASP.NET Core.

† указание имени сервера (SNI) — это расширение TLS для включения виртуального домена в рамках согласования SSL. Это фактически означает, что имя виртуального домена или имя узла можно использовать для идентификации конечной точки сети.

Повторные переговоры

Повторное согласование TLS — это процесс, с помощью которого клиент и сервер могут повторно оценить требования шифрования для отдельного подключения, включая запрос сертификата клиента, если он не указан ранее. Перенацеливание TLS является угрозой безопасности и не рекомендуется, так как:

  • В HTTP/1.1 сервер должен сначала буферизовать или обработать все HTTP-данные, находящиеся в обработке, например тела POST-запросов, чтобы убедиться, что соединение свободно для повторного обмена данными. В противном случае повторное согласование может перестать откликаться или завершиться ошибкой.
  • HTTP/2 и HTTP/3 явно запрещают повторное согласование.
  • Существуют риски безопасности, связанные с повторным согласованием. TLS 1.3 удалил повторное согласование всего подключения и заменил его новым расширением для запроса только сертификата клиента после начала подключения. Этот механизм предоставляется через те же API и по-прежнему подвергается предыдущим ограничениям буферизации и версий протокола HTTP.

Реализация и конфигурация этой функции зависят от версии сервера и платформы.

IIS

IIS управляет согласованием сертификата клиента от вашего имени. Подраздел приложения может включить SslRequireCert опцию согласования клиентского сертификата для этих запросов. См. раздел "Конфигурация" в документации IIS для получения подробной информации.

Служба IIS автоматически буферизует все данные тела запроса до заданного ограничения размера перед повторным согласованием. Запросы, превышающие ограничение, отклоняются с ответом 413. Это ограничение по умолчанию равно 48 КБ и настраивается путем задания uploadReadAheadSize.

HttpSys

HttpSys имеет два параметра, которые управляют согласованием сертификата клиента и оба должны быть заданы. Первый находится в netsh.exe под http add sslcert clientcertnegotiation=enable/disable. Этот флаг указывает, должен ли сертификат клиента согласовываться в начале подключения, и его необходимо задать disable для необязательных сертификатов клиента. Дополнительные сведения см. в документации по netsh.

Другой параметр — ClientCertificateMethod. Если задано значение AllowRenegotation, сертификат клиента можно перенастроить во время запроса.

ПРИМЕЧАНИЕ. Приложение должно буферировать или использовать любые данные текста запроса перед попыткой повторного выполнения, в противном случае запрос может стать неответственным.

Существует известная проблема, из-за которой включение AllowRenegotation может привести к синхронному перезаключению при доступе к свойству ClientCertificate. Вызовите метод GetClientCertificateAsync, чтобы избежать этого. Это было решено в .NET 6. Дополнительные сведения см. здесь на GitHub. Примечание GetClientCertificateAsync может возвращать пустой сертификат, если клиент отказывается предоставить его.

Kestrel

Kestrel управляет согласованием сертификата клиента с параметром ClientCertificateMode .

Для версий .NET 5 или более ранних Kestrel не поддерживает повторное установление условий после начала подключения для получения клиентского сертификата. Эта функция добавлена в .NET 6.

Microsoft.AspNetCore.Authentication.Certificate содержит реализацию, аналогичную проверке подлинности сертификатов для ASP.NET Core. Проверка подлинности по сертификату происходит на уровне TLS, задолго до его попадания в ASP.NET Core. Более точно, это обработчик аутентификации, который проверяет сертификат, а затем предоставляет событие, где можно завершить обработку этого сертификата ClaimsPrincipal.

Настройте сервер для проверки подлинности сертификата, будь то IIS, KestrelAzure веб-приложения или все, что вы используете.

Сценарии прокси-сервера и подсистемы балансировки нагрузки

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

  • Осуществляет проверку подлинности.
  • Передает данные аутентификации пользователя приложению (например, в заголовке запроса), которое использует эту информацию для аутентификации.

Альтернативой проверке подлинности на основе сертификатов в средах, где используются прокси-серверы и подсистемы балансировки нагрузки, являются федеративные службы Active Directory (ADFS) с OpenID Connect (OIDC).

Начало работы

Получите сертификат HTTPS, примените его и настройте сервер для требования сертификатов.

В веб-приложении добавьте ссылку на пакет Microsoft.AspNetCore.Authentication.Certificate . Затем в методе Startup.ConfigureServices вызовите services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...); с вашими параметрами, предоставив делегата для OnCertificateValidated, чтобы выполнить дополнительную проверку клиентского сертификата, отправленного с запросами. Превратите эту информацию в ClaimsPrincipal и установите его в свойство context.Principal.

Если проверка подлинности завершается ошибкой, этот обработчик возвращает ответ 403 (Forbidden) вместо 401 (Unauthorized), как можно было бы ожидать. Причина заключается в том, что проверка подлинности должна происходить во время начального подключения TLS. К тому времени, когда оно достигает обработчика, это уже слишком поздно. Невозможно обновить подключение с анонимного подключения к одному с сертификатом.

Также добавьте app.UseAuthentication(); в метод Startup.Configure. В противном случае HttpContext.User не будет установлен на ClaimsPrincipal, созданное из сертификата. Например:

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(
        CertificateAuthenticationDefaults.AuthenticationScheme)
        .AddCertificate();

    // All other service configuration
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseAuthentication();

    // All other app configuration
}

В предыдущем примере показано, как добавить проверку подлинности сертификата по умолчанию. Обработчик создает пользовательский принципал с помощью общих свойств сертификата.

Настройка проверки сертификата

Обработчик CertificateAuthenticationOptions имеет некоторые встроенные проверки, которые являются минимальными проверками, которые необходимо выполнить в сертификате. Каждый из этих параметров включен по умолчанию.

AllowedCertificateTypes = Chained, SelfSigned или All (Chained | SelfSigned)

Значение по умолчанию: CertificateTypes.Chained

Эта проверка проверяет, разрешен ли только соответствующий тип сертификата. Если приложение использует самозаверяющие сертификаты, этот параметр необходимо задать либо на CertificateTypes.All, либо на CertificateTypes.SelfSigned.

ПроверитьИспользованиеСертификата

Значение по умолчанию: true

Эта проверка удостоверяется, что сертификат, представленный клиентом, имеет расширенное использование ключа аутентификации клиента (EKU) или они совсем отсутствуют. Как говорят спецификации, если EKU не указан, все EKU считаются допустимыми.

ПроверкаСрокаДействия

Значение по умолчанию: true

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

ОтзывФлаг

Значение по умолчанию: X509RevocationFlag.ExcludeRoot

Флаг, указывающий, какие сертификаты в цепочке проверяются для отзыва.

Проверки отзыва выполняются только при привязке сертификата к корневому сертификату.

Режим отзыва

Значение по умолчанию: X509RevocationMode.Online

Флаг, указывающий, как выполняются проверки отзыва.

Указание на необходимость онлайн-проверки может привести к длительной задержке при обращении к центру сертификации.

Проверки отзыва выполняются только при привязке сертификата к корневому сертификату.

Можно ли настроить приложение для требования сертификата только по определенным путям?

Это невозможно. Помните, что обмен сертификатами производится в начале HTTPS-соединения: сервер выполняет это до получения первого запроса по этому подключению, поэтому область действия не может основываться на каких-либо полях запроса.

События обработчиков

Обработчик имеет два события:

  • OnAuthenticationFailed: вызывается, если исключение происходит во время проверки подлинности и позволяет реагировать.
  • OnCertificateValidated: Вызывается после того, как сертификат был проверен и прошел проверку, и создан принципал по умолчанию. Это событие позволяет выполнять собственную проверку, дополнение или замену принципала. Например:
    • Определение того, известен ли сертификат вашим службам.

    • Создание собственного основного компонента. Рассмотрим следующий пример в Startup.ConfigureServices:

      services.AddAuthentication(
          CertificateAuthenticationDefaults.AuthenticationScheme)
          .AddCertificate(options =>
          {
              options.Events = new CertificateAuthenticationEvents
              {
                  OnCertificateValidated = context =>
                  {
                      var claims = new[]
                      {
                          new Claim(
                              ClaimTypes.NameIdentifier, 
                              context.ClientCertificate.Subject,
                              ClaimValueTypes.String, 
                              context.Options.ClaimsIssuer),
                          new Claim(ClaimTypes.Name,
                              context.ClientCertificate.Subject,
                              ClaimValueTypes.String, 
                              context.Options.ClaimsIssuer)
                      };
      
                      context.Principal = new ClaimsPrincipal(
                          new ClaimsIdentity(claims, context.Scheme.Name));
                      context.Success();
      
                      return Task.CompletedTask;
                  }
              };
          });
      

Если вы обнаружите, что входящий сертификат не проходит дополнительную проверку, позвоните по context.Fail("failure reason") с указанием причины сбоя.

Для реальных функциональных возможностей может потребоваться вызвать службу, зарегистрированную в внедрении зависимостей, которая подключается к базе данных или другому типу хранилища пользователей. Получите доступ к вашей службе, используя контекст, переданный в делегат. Рассмотрим следующий пример в Startup.ConfigureServices:

services.AddAuthentication(
    CertificateAuthenticationDefaults.AuthenticationScheme)
    .AddCertificate(options =>
    {
        options.Events = new CertificateAuthenticationEvents
        {
            OnCertificateValidated = context =>
            {
                var validationService =
                    context.HttpContext.RequestServices
                        .GetRequiredService<ICertificateValidationService>();

                if (validationService.ValidateCertificate(
                    context.ClientCertificate))
                {
                    var claims = new[]
                    {
                        new Claim(
                            ClaimTypes.NameIdentifier, 
                            context.ClientCertificate.Subject, 
                            ClaimValueTypes.String, 
                            context.Options.ClaimsIssuer),
                        new Claim(
                            ClaimTypes.Name, 
                            context.ClientCertificate.Subject, 
                            ClaimValueTypes.String, 
                            context.Options.ClaimsIssuer)
                    };

                    context.Principal = new ClaimsPrincipal(
                        new ClaimsIdentity(claims, context.Scheme.Name));
                    context.Success();
                }                     

                return Task.CompletedTask;
            }
        };
    });

Концептуально проверка сертификата является проблемой авторизации. Добавление проверки, например, на издателя или отпечаток в политике авторизации, а не внутри OnCertificateValidated, вполне допустимо.

Настройка сервера для требования сертификатов

Kestrel

В Program.cs настройте Kestrel следующим образом:

public static void Main(string[] args)
{
    CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args)
{
    return Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
            webBuilder.ConfigureKestrel(o =>
            {
                o.ConfigureHttpsDefaults(o => 
                    o.ClientCertificateMode =  ClientCertificateMode.RequireCertificate);
            });
        });
}

Примечание.

К конечным точкам, созданным путем вызова Listenперед вызовом ConfigureHttpsDefaults, не будут применяться значения по умолчанию.

IIS

Выполните следующие действия в диспетчере IIS.

  1. Выберите сайт на вкладке "Подключения ".
  2. Дважды щелкните Настройки SSL в окне Представление функций.
  3. Установите флажок "Требовать SSL" и нажмите переключатель "Требовать" в разделе "Сертификаты клиента".

Параметры сертификата клиента в IIS

Azure и настраиваемые веб-прокси

Сведения о настройке ПО промежуточного слоя пересылки сертификатов см. в документации по размещению и развертыванию.

Использование проверки подлинности с сертификатом в Azure веб-приложения

Для Azure не требуется конфигурация пересылки. Конфигурация пересылки настраивается промежуточным программным обеспечением пересылки сертификатов.

Примечание.

Для этого сценария требуется промежуточное ПО пересылки сертификатов.

Дополнительные сведения см. в статье о использовании сертификата TLS/SSL в коде в службе приложений Azure (документация Azure).

Использование проверки подлинности сертификата в пользовательских веб-прокси

Метод AddCertificateForwarding используется для указания:

  • Имя заголовка клиента.
  • Как следует загружать сертификат (используя свойство HeaderConverter).

Например, в пользовательских веб-прокси сертификат передается в виде пользовательского заголовка запроса, например X-SSL-CERT. Чтобы использовать его, настройте пересылку сертификатов в Startup.ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.AddCertificateForwarding(options =>
    {
        options.CertificateHeader = "X-SSL-CERT";
        options.HeaderConverter = (headerValue) =>
        {
            X509Certificate2 clientCertificate = null;

            if(!string.IsNullOrWhiteSpace(headerValue))
            {
                byte[] bytes = StringToByteArray(headerValue);
                clientCertificate = new X509Certificate2(bytes);
            }

            return clientCertificate;
        };
    });
}

private static byte[] StringToByteArray(string hex)
{
    int NumberChars = hex.Length;
    byte[] bytes = new byte[NumberChars / 2];

    for (int i = 0; i < NumberChars; i += 2)
    {
        bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
    }

    return bytes;
}

Если приложение является обратным прокси-сервером NGINX с конфигурацией proxy_set_header ssl-client-cert $ssl_client_escaped_cert или развернуто в Kubernetes с помощью NGINX Ingress, сертификат клиента передается приложению в формате, закодированном URL-адресом. Чтобы использовать сертификат, расшифруйте его следующим образом:

Добавьте пространство имен для System.Net в верхнюю часть Startup.cs.

using System.Net;

В Startup.ConfigureServices:

services.AddCertificateForwarding(options =>
{
    options.CertificateHeader = "ssl-client-cert";
    options.HeaderConverter = (headerValue) =>
    {
        X509Certificate2 clientCertificate = null;

        if (!string.IsNullOrWhiteSpace(headerValue))
        {
            var bytes = UrlEncodedPemToByteArray(headerValue);
            clientCertificate = new X509Certificate2(bytes);
        }

        return clientCertificate;
    };
});

Добавьте метод UrlEncodedPemToByteArray.

private static byte[] UrlEncodedPemToByteArray(string urlEncodedBase64Pem)
{
    var base64Pem = WebUtility.UrlDecode(urlEncodedBase64Pem);
    var base64Cert = base64Pem
        .Replace("-----BEGIN CERTIFICATE-----", string.Empty)
        .Replace("-----END CERTIFICATE-----", string.Empty)
        .Trim();

    return Convert.FromBase64String(base64Cert);
}

Затем метод Startup.Configure добавляет ПО промежуточного слоя. UseCertificateForwarding вызывается перед вызовами UseAuthentication и UseAuthorization:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...

    app.UseRouting();

    app.UseCertificateForwarding();
    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

Для реализации логики проверки можно использовать отдельный класс. Так как в этом примере используется тот же самозаверяющий сертификат, убедитесь, что можно использовать только ваш сертификат. Убедитесь, что отпечаток сертификата клиента и сертификата сервера совпадают, в противном случае можно использовать любой сертификат и будет достаточно для проверки подлинности. Это будет использоваться внутри AddCertificate метода. Вы также можете проверить данные субъекта или издателя, если вы используете промежуточные или дочерние сертификаты.

using System.IO;
using System.Security.Cryptography.X509Certificates;

namespace AspNetCoreCertificateAuthApi
{
    public class MyCertificateValidationService
    {
        public bool ValidateCertificate(X509Certificate2 clientCertificate)
        {
            // Do not hardcode passwords in production code
            // Use thumbprint or key vault
            var cert = new X509Certificate2(
                Path.Combine("sts_dev_cert.pfx"), "1234");

            if (clientCertificate.Thumbprint == cert.Thumbprint)
            {
                return true;
            }

            return false;
        }
    }
}

Реализация HttpClient с помощью сертификата и HttpClientHandler

Его HttpClientHandler можно добавить непосредственно в конструктор класса HttpClient. Следует соблюдать осторожность при создании экземпляров HttpClient. Затем сертификат HttpClient отправляется с каждым запросом.

private async Task<JsonDocument> GetApiDataUsingHttpClientHandler()
{
    var cert = new X509Certificate2(Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234");
    var handler = new HttpClientHandler();
    handler.ClientCertificates.Add(cert);
    var client = new HttpClient(handler);

    var request = new HttpRequestMessage()
    {
        RequestUri = new Uri("https://localhost:44379/api/values"),
        Method = HttpMethod.Get,
    };
    var response = await client.SendAsync(request);
    if (response.IsSuccessStatusCode)
    {
        var responseContent = await response.Content.ReadAsStringAsync();
        var data = JsonDocument.Parse(responseContent);
        return data;
    }

    throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
}

Реализуйте HttpClient, используя сертификат и именованный HttpClient, управляемый через IHttpClientFactory.

В следующем примере сертификат клиента добавляется в HttpClientHandler с использованием свойства ClientCertificates из обработчика. Затем этот обработчик можно использовать в именованном экземпляре HttpClient, применяя метод ConfigurePrimaryHttpMessageHandler. Эта настройка выполняется в Startup.ConfigureServices:

var clientCertificate = 
    new X509Certificate2(
      Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234");

services.AddHttpClient("namedClient", c =>
{
}).ConfigurePrimaryHttpMessageHandler(() =>
{
    var handler = new HttpClientHandler();
    handler.ClientCertificates.Add(clientCertificate);
    return handler;
});

Затем IHttpClientFactory можно использовать для получения экземпляра с указанным именем, обработчиком и сертификатом. Метод CreateClient с именем клиента, определенного в Startup классе, используется для получения экземпляра. HTTP-запрос можно отправить с помощью клиента по мере необходимости.

private readonly IHttpClientFactory _clientFactory;

public ApiService(IHttpClientFactory clientFactory)
{
    _clientFactory = clientFactory;
}

private async Task<JsonDocument> GetApiDataWithNamedClient()
{
    var client = _clientFactory.CreateClient("namedClient");

    var request = new HttpRequestMessage()
    {
        RequestUri = new Uri("https://localhost:44379/api/values"),
        Method = HttpMethod.Get,
    };
    var response = await client.SendAsync(request);
    if (response.IsSuccessStatusCode)
    {
        var responseContent = await response.Content.ReadAsStringAsync();
        var data = JsonDocument.Parse(responseContent);
        return data;
    }

    throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
}

Если на сервер отправляется правильный сертификат, возвращаются данные. Если сертификат или неправильный сертификат не отправлен, возвращается код состояния HTTP 403.

Создание сертификатов в PowerShell

Создание сертификатов является самой сложной частью при настройке этого потока. Корневой сертификат можно создать с помощью командлета New-SelfSignedCertificate PowerShell. При создании сертификата используйте надежный пароль. Важно добавить KeyUsageProperty параметр и KeyUsage параметр, как показано ниже.

Создание корневого ЦС

New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "root_ca_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath root_ca_dev_damienbod.crt

Примечание.

Значение -DnsName параметра должно соответствовать целевому объекту развертывания приложения. Например, localhost для разработки.

Установка в доверенном корне

Корневой сертификат должен быть доверенным на вашей хост-системе. Корневой сертификат, который не был создан центром сертификации, по умолчанию не будет доверенным. Сведения о том, как установить доверие к корневому сертификату в Windows, см. этот вопрос.

Промежуточный сертификат

Теперь промежуточный сертификат можно создать из корневого сертификата. Это не обязательно для всех вариантов использования, но может потребоваться создать множество сертификатов или активировать или отключить группы сертификатов. Параметр TextExtension требуется для задания длины пути в основных ограничениях сертификата.

Затем промежуточный сертификат можно добавить в доверенный промежуточный сертификат в системе узлов Windows.

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint of the root..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "intermediate_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "intermediate_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\intermediate_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath intermediate_dev_damienbod.crt

Создание дочернего сертификата из промежуточного сертификата

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

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the Intermediate certificate..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com"

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt

Создание дочернего сертификата из корневого сертификата

Дочерний сертификат также можно создать непосредственно из корневого сертификата.

$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the root cert..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com"

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt

Пример корневого сертификата — промежуточный сертификат — сертификат

$mypwdroot = ConvertTo-SecureString -String "1234" -Force -AsPlainText
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "root_ca_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature

Get-ChildItem -Path cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwdroot

Export-Certificate -Cert cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 -FilePath root_ca_dev_damienbod.crt

$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\0C89639E4E2998A93E423F919B36D4009A0F9991 )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")

Get-ChildItem -Path cert:\localMachine\my\BA9BF91ED35538A01375EFC212A2F46104B33A44 | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\BA9BF91ED35538A01375EFC212A2F46104B33A44 -FilePath child_a_dev_damienbod.crt

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\BA9BF91ED35538A01375EFC212A2F46104B33A44 )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_b_from_a_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_b_from_a_dev_damienbod.com" 

Get-ChildItem -Path cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_b_from_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A -FilePath child_b_from_a_dev_damienbod.crt

При использовании корневых, промежуточных или дочерних сертификатов сертификаты можно проверить с помощью отпечатка или PublicKey по мере необходимости.

using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography.X509Certificates;

namespace AspNetCoreCertificateAuthApi
{
    public class MyCertificateValidationService 
    {
        public bool ValidateCertificate(X509Certificate2 clientCertificate)
        {
            return CheckIfThumbprintIsValid(clientCertificate);
        }

        private bool CheckIfThumbprintIsValid(X509Certificate2 clientCertificate)
        {
            var listOfValidThumbprints = new List<string>
            {
                "141594A0AE38CBBECED7AF680F7945CD51D8F28A",
                "0C89639E4E2998A93E423F919B36D4009A0F9991",
                "BA9BF91ED35538A01375EFC212A2F46104B33A44"
            };

            if (listOfValidThumbprints.Contains(clientCertificate.Thumbprint))
            {
                return true;
            }

            return false;
        }
    }
}

Необязательные сертификаты клиентов

В этом разделе содержатся сведения о приложениях, которые должны защищать подмножество приложения с помощью сертификата. Например, для Razor страницы или контроллера в приложении могут потребоваться сертификаты клиента. Это создает проблемы в качестве сертификатов клиента:

  • Это функция TLS, а не функция HTTP.
  • Согласовываются для каждого подключения и обычно осуществляются при начале подключения до того, как будут доступны данные HTTP.

Существует два подхода к реализации необязательных сертификатов клиента:

  1. Использование отдельных имен узлов (SNI) и перенаправление. Несмотря на более эффективную настройку, рекомендуется использовать эту функцию, так как она работает в большинстве сред и протоколов.
  2. Повторное согласование во время HTTP-запроса. Это имеет несколько ограничений и не рекомендуется.

Отдельные серверы (SNI)

В начале подключения известно только имя сервера (SNI)†. Сертификаты клиента можно настроить для каждого имени узла, чтобы один узел их требует, а другой — нет.

.NET 5 или более поздней версии добавляет более удобную поддержку перенаправления для получения необязательных сертификатов клиента. Для получения дополнительной информации см. образец опциональных сертификатов.

  • Для запросов к веб-приложению, для которых требуется сертификат клиента, но у них его нет:
    • Перенаправьте на ту же страницу, используя поддомен, защищённый клиентским сертификатом.
    • Например, перенаправьте на myClient.contoso.com/requestedPage. Так как запрос myClient.contoso.com/requestedPage отличается от имени contoso.com/requestedPageузла, клиент устанавливает другое подключение и предоставляется сертификат клиента.
    • Дополнительные сведения см. в разделе "Общие сведения о авторизации" в ASP.NET Core.

† указание имени сервера (SNI) — это расширение TLS для включения виртуального домена в рамках согласования SSL. Это фактически означает, что имя виртуального домена или имя узла можно использовать для идентификации конечной точки сети.

Пересмотр условий

Повторное согласование TLS — это процесс, с помощью которого клиент и сервер могут повторно оценить требования шифрования для отдельного подключения, включая запрос сертификата клиента, если он не указан ранее. Перенацеливание TLS является угрозой безопасности и не рекомендуется, так как:

  • В HTTP/1.1 сервер должен сначала буферировать или обрабатывать все HTTP-данные, находящиеся в процессе передачи, такие как тела запросов POST, чтобы убедиться, что подключение свободно для повторного согласования. В противном случае повторное согласование может прекратить отвечать или завершиться с ошибкой.
  • HTTP/2 и HTTP/3 явно запрещают повторное согласование.
  • Существуют риски безопасности, связанные с повторным согласованием. TLS 1.3 удалил повторное согласование всего подключения и заменил его новым расширением для запроса только сертификата клиента после начала подключения. Этот механизм предоставляется через те же API и по-прежнему подвергается предыдущим ограничениям буферизации и версий протокола HTTP.

Реализация и конфигурация этой функции зависят от версии сервера и платформы.

IIS

IIS управляет согласованием сертификата клиента от вашего имени. Подраздел приложения может включить SslRequireCert возможность согласования сертификата клиента для этих запросов. Дополнительные сведения см. в разделе Конфигурация в документации IIS.

IIS автоматически буферизует любые данные тела запроса до установленного предела размера перед повторными переговорами. Запросы, превышающие ограничение, отклоняются с ответом 413. Это ограничение по умолчанию равно 48 КБ и настраивается путем задания uploadReadAheadSize.

HttpSys

HttpSys имеет два параметра, которые управляют согласованием сертификата клиента и оба должны быть заданы. Первый находится в netsh.exe под http add sslcert clientcertnegotiation=enable/disable. Этот флаг указывает, должен ли сертификат клиента согласовываться в начале подключения, и его необходимо задать disable для необязательных сертификатов клиента. Дополнительные сведения см. в документации netsh.

Другой параметр — ClientCertificateMethod. Если задано значение AllowRenegotation, сертификат клиента можно перенастроить во время запроса.

ПРИМЕЧАНИЕ. Приложение должно буферизовать или потреблять любые данные тела запроса перед попыткой повторного согласования, в противном случае запрос может перестать отвечать.

Существует известная проблема, из-за которой включение AllowRenegotation может привести к синхронному перезаключению при доступе к свойству ClientCertificate. Вызовите метод GetClientCertificateAsync, чтобы избежать этого. Это было решено в .NET 6. Дополнительные сведения см. здесь на GitHub. Примечание GetClientCertificateAsync может возвращать пустой сертификат, если клиент отказывается предоставить его.

Kestrel

Kestrel управляет согласованием клиентского сертификата с параметром ClientCertificateMode .

Для версий .NET 5 или более ранних Kestrel не поддерживает повторное установление условий после начала подключения для получения клиентского сертификата. Эта функция добавлена в .NET 6.

Оставьте вопросы, комментарии и другие отзывы о необязательных клиентских сертификатах в ветке обсуждения вопроса GitHub #18720.