Настройка проверки подлинности сертификата в 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.
- Выберите сайт на вкладке "Подключения ".
- Дважды щелкните параметр "Параметры SSL" в окне представления компонентов.
- Установите флажок "Требовать SSL" и нажмите переключатель "Требовать" в разделе "Сертификаты клиента".
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.
Существует два подхода к реализации необязательных сертификатов клиента:
- Использование отдельных имен узлов (SNI) и перенаправления. Несмотря на более эффективную настройку, рекомендуется использовать эту функцию, так как она работает в большинстве сред и протоколов.
- Повторное согласование во время HTTP-запроса. Это имеет несколько ограничений и не рекомендуется.
Отдельные узлы (SNI)
В начале подключения известно только имя сервера (SNI)†. Сертификаты клиента можно настроить для каждого имени узла, чтобы один узел их требует, а другой — нет.
- Настройте привязку для домена и поддомена:
- Например, настройте привязки в
contoso.com
иmyClient.contoso.com
. Узелcontoso.com
не требует сертификата клиента, ноmyClient.contoso.com
делает. - Для получения дополнительной информации см.
- Например, настройте привязки в
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.
- Выберите сайт на вкладке "Подключения ".
- Дважды щелкните параметр "Параметры SSL" в окне представления компонентов.
- Установите флажок "Требовать SSL" и нажмите переключатель "Требовать" в разделе "Сертификаты клиента".
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.
Существует два подхода к реализации необязательных сертификатов клиента:
- Использование отдельных имен узлов (SNI) и перенаправления. Несмотря на более эффективную настройку, рекомендуется использовать эту функцию, так как она работает в большинстве сред и протоколов.
- Повторное согласование во время HTTP-запроса. Это имеет несколько ограничений и не рекомендуется.
Отдельные узлы (SNI)
В начале подключения известно только имя сервера (SNI)†. Сертификаты клиента можно настроить для каждого имени узла, чтобы один узел их требует, а другой — нет.
- Настройте привязку для домена и поддомена:
- Например, настройте привязки в
contoso.com
иmyClient.contoso.com
. Узелcontoso.com
не требует сертификата клиента, ноmyClient.contoso.com
делает. - Для получения дополнительной информации см.
- Например, настройте привязки в
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.
- Выберите сайт на вкладке "Подключения ".
- Дважды щелкните параметр "Параметры SSL" в окне представления компонентов.
- Установите флажок "Требовать SSL" и нажмите переключатель "Требовать" в разделе "Сертификаты клиента".
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.
Существует два подхода к реализации необязательных сертификатов клиента:
- Использование отдельных имен узлов (SNI) и перенаправления. Несмотря на более эффективную настройку, рекомендуется использовать эту функцию, так как она работает в большинстве сред и протоколов.
- Повторное согласование во время HTTP-запроса. Это имеет несколько ограничений и не рекомендуется.
Отдельные узлы (SNI)
В начале подключения известно только имя сервера (SNI)†. Сертификаты клиента можно настроить для каждого имени узла, чтобы один узел их требует, а другой — нет.
- Настройте привязку для домена и поддомена:
- Например, настройте привязки в
contoso.com
иmyClient.contoso.com
. Узелcontoso.com
не требует сертификата клиента, ноmyClient.contoso.com
делает. - Для получения дополнительной информации см.
- Kestrel веб-сервер в ASP.NET Core:
- ListenOptions.UseHttps
- ClientCertificateMode
- Примечание Kestrel в настоящее время не поддерживает несколько конфигураций TLS в одной привязке, вам потребуется две привязки с уникальными IP-адресами или портами. Дополнительные сведения см. здесь на GitHub.
- Службы IIS
- HTTP.sys. Настройка Windows Server
- Kestrel веб-сервер в ASP.NET Core:
- Например, настройте привязки в
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.
ASP.NET Core