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

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

Необходимо настроить сервер для проверки подлинности сертификата, будь то IIS, KestrelAzure веб-приложения или любой другой вариант.

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

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

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

Альтернативой проверке подлинности на основе сертификатов в средах, где используются прокси-серверы и подсистемы балансировки нагрузки, являются федеративные службы 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 должен содержать каждый выдавающий сертификат в цепочке.

ValidateCertificateUse

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

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

ValidateValidityPeriod

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

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

ОтзывFlag

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

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

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

ОтзывMode

Значение по умолчанию: 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

В 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" и нажмите переключатель "Требовать" в разделе "Сертификаты клиента".

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

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

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

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

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

Примечание.

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

Дополнительные сведения см. в статье об использовании TLS/SSL-сертификата в коде в приложение Azure службе (документация 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, см . в документации по Windows или командлете Import-Certificate PowerShell.

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

Теперь промежуточный сертификат можно создать из корневого сертификата. Это не обязательно для всех вариантов использования, но может потребоваться создать множество сертификатов или активировать или отключить группы сертификатов. Параметр 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);
}

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

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

По умолчанию проверка подлинности сертификата отключает кэширование. Чтобы включить кэширование, вызовите 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)†. Сертификаты клиента можно настроить для каждого имени узла, чтобы один узел их требует, а другой — нет.

ASP.NET Core 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, сертификат клиента можно перенастроить во время запроса.

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

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

ПРИМЕЧАНИЕ. Поведение ClientCertificate свойства изменилось в .NET 6. Дополнительные сведения см. здесь на GitHub.

Kestrel

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

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

ПРИМЕЧАНИЕ. Приложение должно буферировать или использовать любые данные текста запроса перед попыткой повторного выполнения, в противном случае GetClientCertificateAsync может вызвать исключение InvalidOperationException: Client stream needs to be drained before renegotiation..

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

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. К тому времени, когда он достигает обработчика, это слишком поздно. Невозможно обновить подключение с анонимного подключения к одному с сертификатом.

Кроме того, Startup.Configure добавьте app.UseAuthentication(); в метод. 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.

ValidateCertificateUse

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

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

ValidateValidityPeriod

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

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

ОтзывFlag

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

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

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

ОтзывMode

Значение по умолчанию: 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-адресом. Чтобы использовать сертификат, расшифруйте его следующим образом:

В 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;
        }
    }
}

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

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

По умолчанию проверка подлинности сертификата отключает кэширование. Чтобы включить кэширование, вызовите 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)†. Сертификаты клиента можно настроить для каждого имени узла, чтобы один узел их требует, а другой — нет.

ASP.NET Core 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. К тому времени, когда он достигает обработчика, это слишком поздно. Невозможно обновить подключение с анонимного подключения к одному с сертификатом.

Кроме того, Startup.Configure добавьте app.UseAuthentication(); в метод. 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.

ValidateCertificateUse

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

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

ValidateValidityPeriod

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

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

ОтзывFlag

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

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

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

ОтзывMode

Значение по умолчанию: 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)†. Сертификаты клиента можно настроить для каждого имени узла, чтобы один узел их требует, а другой — нет.

ASP.NET Core 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.