Configurar a autenticação de certificado no ASP.NET Core

Microsoft.AspNetCore.Authentication.Certificatecontém uma implementação semelhante à Autenticação de Certificado para ASP.NET Core. A autenticação de certificado ocorre no nível do TLS, muito antes de chegar ao ASP.NET Core. Mais precisamente, esse é um manipulador de autenticação que valida o certificado e, em seguida, fornece um evento em que você pode resolve esse certificado para um ClaimsPrincipal.

Você deveconfigurar seu servidor para autenticação de certificado, seja IIS, Kestrel, Aplicativos Web do Azure ou qualquer outra coisa que você esteja usando.

Cenários de proxy e balanceador de carga

A autenticação de certificado é um cenário com estado usado principalmente em que um proxy ou balanceador de carga não manipula o tráfego entre clientes e servidores. Se um proxy ou balanceador de carga for usado, a autenticação de certificado só funcionará se o proxy ou o balanceador de carga:

  • Trata a autenticação.
  • Passa as informações de autenticação do usuário para o aplicativo (por exemplo, em um cabeçalho de solicitação), que atua nas informações de autenticação.

Uma alternativa à autenticação de certificado em ambientes em que proxies e balanceadores de carga são usados é o ADFS (Serviços Federados do Active Directory) com o OIDC (OpenID Connect).

Introdução

Adquira um certificado HTTPS, aplique-o e configure o servidor para exigir certificados.

No aplicativo Web:

  • Adicione uma referência ao pacote NuGet Microsoft.AspNetCore.Authentication.Certificate.
  • Em Program.cs, chame builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...);. Forneça um delegado para OnCertificateValidated fazer qualquer validação suplementar no certificado do cliente enviado com solicitações. Transforme essas informações em um ClaimsPrincipal e defina-as na propriedade context.Principal.

Se a autenticação falhar, esse manipulador retornará uma 403 (Forbidden) resposta em vez de 401 (Unauthorized), como você poderia esperar. O raciocínio é que a autenticação deve ocorrer durante a conexão TLS inicial. Quando ele chega ao manipulador, é tarde demais. Não há como atualizar a conexão de uma conexão anônima para uma com um certificado.

UseAuthentication é necessário definir HttpContext.User como um ClaimsPrincipal criado a partir do certificado. Por exemplo:

var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

app.UseAuthentication();

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

app.Run();

O exemplo anterior demonstra a maneira padrão de adicionar a autenticação de certificado. O manipulador constrói uma entidade de segurança de usuário usando as propriedades comuns do certificado.

Configurar a validação do certificado

O CertificateAuthenticationOptions manipulador tem algumas validações internas que são as validações mínimas que você deve executar em um certificado. Cada uma dessas configurações é habilitada por padrão.

AllowedCertificateTypes = Chained, SelfSigned ou All (Encadeado | AutoSigned)

Valor padrão: CertificateTypes.Chained

Esse marcar valida que somente o tipo de certificado apropriado é permitido. Se o aplicativo estiver usando certificados autoassinados, essa opção precisará ser definida como CertificateTypes.All ou CertificateTypes.SelfSigned.

ChainTrustValidationMode

Valor padrão: X509ChainTrustMode.System

O certificado apresentado pelo cliente deve ser encadeado a um certificado raiz confiável. Esta verificação controla qual repositório de confiança contém esses certificados raiz.

Por padrão, o manipulador usa o repositório de confiança do sistema. Se o certificado do cliente apresentado precisar ser encadeado a um certificado raiz que não aparece no repositório de confiança do sistema, essa opção poderá ser definida como X509ChainTrustMode.CustomRootTrust para fazer com que o manipulador use o CustomTrustStore.

CustomTrustStore

Valor padrão: X509Certificate2Collection vazio

Se a propriedade do manipulador ChainTrustValidationMode estiver definida como X509ChainTrustMode.CustomRootTrust, X509Certificate2Collection conterá todos os certificados que serão usados para validar o certificado do cliente até uma raiz confiável, incluindo a raiz confiável.

Quando o cliente apresenta um certificado que faz parte de uma cadeia de certificados de vários níveis, CustomTrustStore deve conter todos os certificados emissores na cadeia.

ValidateCertificateUse

Valor padrão: true

Esse marcar valida se o certificado apresentado pelo cliente tem o EKU (uso de chave estendida) da Autenticação do Cliente ou nenhuma EKUs. Como dizem as especificações, se nenhum EKU for especificado, todas as EKUs serão consideradas válidas.

ValidateValidityPeriod

Valor padrão: true

Esse marcar valida se o certificado está dentro de seu período de validade. Em cada solicitação, o manipulador garante que um certificado válido quando ele foi apresentado não expirou durante sua sessão atual.

RevocationFlag

Valor padrão: X509RevocationFlag.ExcludeRoot

Um sinalizador que especifica quais certificados na cadeia são verificados quanto à revogação.

As verificações de revogação só são executadas quando o certificado é encadeado a um certificado raiz.

RevocationMode

Valor padrão: X509RevocationMode.Online

Um sinalizador que especifica como as verificações de revogação são executadas.

Especificar uma marcar online pode resultar em um longo atraso enquanto a autoridade de certificação é contatada.

As verificações de revogação só são executadas quando o certificado é encadeado a um certificado raiz.

Posso configurar meu aplicativo para exigir um certificado somente em determinados caminhos?

Isso não é possível. Lembre-se de que a troca de certificados é feita no início da conversa HTTPS, ela é feita pelo servidor antes que a primeira solicitação seja recebida nessa conexão para que não seja possível definir o escopo com base em nenhum campo de solicitação.

Eventos de manipulador

O manipulador tem dois eventos:

  • OnAuthenticationFailed: Chamado se uma exceção ocorrer durante a autenticação e permitir que você reaja.
  • OnCertificateValidated: Chamado depois que o certificado tiver sido validado, a validação aprovada e uma entidade de segurança padrão tiver sido criada. Esse evento permite que você execute sua própria validação e aumente ou substitua a entidade de segurança. Para obter exemplos, incluem:
    • Determinando se o certificado é conhecido por seus serviços.

    • Construindo sua própria entidade de segurança. Considere o seguinte exemplo:

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

Se você encontrar que o certificado de entrada não atende à sua validação extra, chame context.Fail("failure reason") com um motivo de falha.

Para obter uma melhor funcionalidade, chame um serviço registrado na injeção de dependência que se conecta a um banco de dados ou outro tipo de repositório de usuários. Acesse o serviço usando o contexto passado para o delegado. Considere o seguinte exemplo:

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

Conceitualmente, a validação do certificado é uma preocupação de autorização. Adicionar um marcar, por exemplo, um emissor ou impressão digital em uma política de autorização, em vez de dentro de OnCertificateValidated, é perfeitamente aceitável.

Configurar seu servidor para exigir certificados

Kestrel

No Program.cs, configure o Kestrel da seguinte maneira:

var builder = WebApplication.CreateBuilder(args);

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

Observação

Os pontos de extremidade criados fazendo a chamada de Listenantes da chamada de ConfigureHttpsDefaults não terão os padrões aplicados.

IIS

Conclua as seguintes etapas no Gerenciador do IIS:

  1. Selecione seu site na guia Conexões.
  2. Clique duas vezes na opção Configurações de SSL na janela Exibição de Recursos.
  3. Marque a caixa de seleção Exigir SSL e selecione o botão de opção Exigir na seção Certificados do cliente.

Client certificate settings in IIS

Proxies web personalizados e do Azure

Consulte o host e a documentação de implantação para saber como configurar o middleware de encaminhamento de certificado.

Usar a autenticação de certificado no Azure Aplicativos Web

Nenhuma configuração de encaminhamento é necessária para o Azure. A configuração de encaminhamento é configurada pelo middleware de encaminhamento de certificado.

Observação

O Middleware de Encaminhamento de Certificado é necessário para esse cenário.

Para obter mais informações, confira Usar um certificado TLS/SSL no código do Serviço de Aplicativo do Azure.

Usar a autenticação de certificado em proxies Web personalizados

O método AddCertificateForwarding é usado para especificar:

  • O nome do cabeçalho do cliente.
  • Como o certificado deve ser carregado (usando a propriedade HeaderConverter).

Em proxies web personalizados, o certificado é passado como um cabeçalho de solicitação personalizado, por exemplo X-SSL-CERT. Para usá-lo, configure o encaminhamento de certificado em 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;
        }
    };
});

Se o aplicativo for proxy reverso pelo NGINX com a configuração proxy_set_header ssl-client-cert $ssl_client_escaped_cert ou implantado no Kubernetes usando a Entrada NGINX, o certificado do cliente será passado para o aplicativo no formulário codificado em URL. Para usar o certificado, decodifica-o da seguinte maneira:

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!;
    };
});

Adicione o middleware em Program.cs. UseCertificateForwarding é chamado antes das chamadas para UseAuthentication e UseAuthorization:

var app = builder.Build();

app.UseCertificateForwarding();

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

Uma classe separada pode ser usada para implementar a lógica de validação. Como o mesmo certificado autoassinado é usado neste exemplo, verifique se apenas o certificado pode ser usado. Valide se as impressões digitais do certificado do cliente e do certificado do servidor correspondem, caso contrário, qualquer certificado pode ser usado e será suficiente para autenticar. Isso seria usado dentro do método AddCertificate. Você também pode validar o assunto ou o emissor aqui se estiver usando certificados intermediários ou filho.

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

Implementar um HttpClient usando um certificado e IHttpClientFactory

No exemplo a seguir, um certificado do cliente é adicionado a um HttpClientHandler usando a propriedade ClientCertificates do manipulador. Esse manipulador pode ser usado em uma instância nomeada de um HttpClient usando o método ConfigurePrimaryHttpMessageHandler. Isso é configurado em 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;
});

O IHttpClientFactory pode ser usado para obter a instância nomeada com o manipulador e o certificado. O CreateClient método com o nome do cliente definido em Program.cs é usado para obter a instância. A solicitação HTTP pode ser enviada usando o cliente conforme necessário:

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}");
    }
}

Se o certificado correto for enviado ao servidor, os dados serão retornados. Se nenhum certificado ou certificado incorreto for enviado, um código http 403 status será retornado.

Criar certificados no PowerShell

Criar os certificados é a parte mais difícil na configuração desse fluxo. Um certificado raiz pode ser criado usando o New-SelfSignedCertificate cmdlet do PowerShell. Ao criar o certificado, use uma senha forte. É importante adicionar o KeyUsageProperty parâmetro e o KeyUsage parâmetro, conforme mostrado.

Criar CA raiz

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

Observação

O valor do parâmetro -DnsName deve corresponder ao destino de implantação do aplicativo. Por exemplo, "localhost" para desenvolvimento.

Instalar na raiz confiável

O certificado raiz precisa ser confiável em seu sistema host. Um certificado raiz que não foi criado por uma autoridade de certificação não será confiável por padrão. Para obter informações sobre como confiar no certificado raiz no Windows, consulte esta pergunta.

Certificado intermediário

Um certificado intermediário agora pode ser criado com base no certificado raiz. Isso não é necessário para todos os casos de uso, mas talvez seja necessário criar muitos certificados ou precisar ativar ou desabilitar grupos de certificados. O parâmetro TextExtension é necessário para definir o comprimento do caminho nas restrições básicas do certificado.

Em seguida, o certificado intermediário pode ser adicionado ao certificado intermediário confiável no sistema host do 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

Criar certificado filho com base no certificado intermediário

Um certificado filho pode ser criado com base no certificado intermediário. Essa é a entidade final e não precisa criar mais certificados filho.

$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

Criar certificado filho com base no certificado raiz

Um certificado filho também pode ser criado diretamente do certificado raiz.

$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

Raiz de exemplo – certificado intermediário – certificado

$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

Ao usar os certificados raiz, intermediário ou filho, os certificados podem ser validados usando a Impressão Digital ou PublicKey conforme necessário:

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

Cache de validação de certificado

ASP.NET Core 5.0 e versões posteriores dão suporte à capacidade de habilitar o cache de resultados de validação. O cache melhora drasticamente o desempenho da autenticação de certificado, pois a validação é uma operação cara.

Por padrão, a autenticação de certificado desabilita o cache. Para habilitar o cache, chame AddCertificateCache em Program.cs:

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

A implementação de cache padrão armazena os resultados na memória. Você pode fornecer seu próprio cache implementando ICertificateValidationCache e registrando-o com injeção de dependência. Por exemplo, services.AddSingleton<ICertificateValidationCache, YourCache>().

Certificados do cliente opcionais

Esta seção fornece informações para aplicativos que devem proteger um subconjunto do aplicativo com um certificado. Por exemplo, uma Página ou controlador Razor no aplicativo pode exigir certificados de cliente. Isso apresenta desafios como certificados de cliente:

  • São um recurso TLS, não um recurso HTTP.
  • São negociados por conexão e geralmente no início da conexão antes que todos os dados HTTP estejam disponíveis.

Há duas abordagens para implementar certificados de cliente opcionais:

  1. Usando SNI (nomes de host) separados e redirecionamento. Embora haja mais trabalho para configurar, isso é recomendado porque ele funciona na maioria dos ambientes e protocolos.
  2. Renegociação durante uma solicitação HTTP. Isso tem várias limitações e não é recomendado.

Hosts separados (SNI)

No início da conexão, somente a SNI (Indicação de Nome do Servidor)† é conhecida. Os certificados do cliente podem ser configurados por nome de host para que um host os exija e outro não.

ASP.NET Core 5 e posteriores adiciona suporte mais conveniente para redirecionamento para adquirir certificados de cliente opcionais. Para obter mais informações, consulte o exemplo certificados opcionais.

  • Para solicitações para o aplicativo Web que exigem um certificado de cliente e não têm um:
    • Redirecione para a mesma página usando o subdomínio protegido pelo certificado do cliente.
    • Por exemplo, redirecionar para myClient.contoso.com/requestedPage. Como a solicitação para myClient.contoso.com/requestedPage é um nome de host diferente de contoso.com/requestedPage, o cliente estabelece uma conexão diferente e o certificado do cliente é fornecido.
    • Para obter mais informações, consulte Introdução à autorização no ASP.NET Core.

† A SNI (Indicação de Nome de Servidor) é uma extensão TLS para incluir um domínio virtual como parte da negociação SSL. Isso significa que o nome de domínio virtual, ou um nome de host, pode ser usado para identificar o ponto final da rede.

Renegociação

A renegociação do TLS é um processo pelo qual o cliente e o servidor podem reavaliar os requisitos de criptografia para uma conexão individual, incluindo a solicitação de um certificado do cliente, se não for fornecido anteriormente. A renegociação do TLS é um risco à segurança e não é recomendada porque:

  • Em HTTP/1.1, o servidor deve primeiro armazenar ou consumir todos os dados HTTP que estão em pré-lançamento, como corpos de solicitação POST, para garantir que a conexão esteja clara para a renegociação. Caso contrário, a renegociação poderá parar de responder ou falhar.
  • HTTP/2 e HTTP/3 proíbem explicitamente a renegociação.
  • Há riscos de segurança associados à renegociação. O TLS 1.3 removeu a renegociação de toda a conexão e a substituiu por uma nova extensão para solicitar apenas o certificado do cliente após o início da conexão. Esse mecanismo é exposto por meio das mesmas APIs e ainda está sujeito às restrições anteriores de buffer e versões de protocolo HTTP.

A implementação e a configuração desse recurso variam de acordo com a versão do servidor e da estrutura.

IIS

O IIS gerencia a negociação de certificado do cliente em seu nome. Uma subseção do aplicativo pode habilitar a opção SslRequireCert de negociar o certificado do cliente para essas solicitações. Consulte Configuração na documentação do IIS para obter detalhes.

O IIS armazenará automaticamente os dados do corpo da solicitação em buffer até um limite de tamanho configurado antes da renegociação. As solicitações que excedem o limite são rejeitadas com uma resposta 413. Esse limite usa como padrão 48 KB e é configurável definindo o uploadReadAheadSize.

HttpSys

O HttpSys tem duas configurações que controlam a negociação de certificado do cliente e ambas devem ser definidas. O primeiro está em netsh.exe em http add sslcert clientcertnegotiation=enable/disable. Esse sinalizador indica se o certificado do cliente deve ser negociado no início de uma conexão e deve ser definido disable como para certificados de cliente opcionais. Consulte os documentos do netsh para obter detalhes.

A outra configuração é ClientCertificateMethod. Quando definido como AllowRenegotation, o certificado do cliente pode ser renegociado durante uma solicitação.

NOTA O aplicativo deve armazenar ou consumir dados do corpo da solicitação antes de tentar a renegociação, caso contrário, a solicitação poderá ficar sem resposta.

Um aplicativo pode primeiro marcar a propriedade ClientCertificate para ver se o certificado está disponível. Se ele não estiver disponível, verifique se o corpo da solicitação foi consumido antes de chamar GetClientCertificateAsync para negociar um. Nota GetClientCertificateAsync pode retornar um certificado nulo se o cliente se recusar a fornecer um.

NOTA O comportamento da propriedade ClientCertificate foi alterado no .NET 6. Saiba mais neste tópico do GitHub.

Kestrel

Kestrel controla a negociação de certificado do cliente com a opção ClientCertificateMode.

ClientCertificateMode.DelayCertificate é uma nova opção disponível no .NET 6 ou posterior. Quando definido, um aplicativo pode marcar a propriedade ClientCertificate para ver se o certificado está disponível. Se ele não estiver disponível, verifique se o corpo da solicitação foi consumido antes de chamar GetClientCertificateAsync para negociar um. Nota GetClientCertificateAsync pode retornar um certificado nulo se o cliente se recusar a fornecer um.

NOTA O aplicativo deve armazenar em buffer ou consumir quaisquer dados do corpo da solicitação antes de tentar a renegociação; caso contrário, GetClientCertificateAsync pode lançar InvalidOperationException: Client stream needs to be drained before renegotiation..

Se você estiver definindo programaticamente as configurações de TLS por host, haverá uma nova sobrecarga UseHttps disponível no .NET 6 e posterior que usa TlsHandshakeCallbackOptions e controla a renegociação de certificado do cliente por meio de TlsHandshakeCallbackContext.AllowDelayedClientCertificateNegotation.

Microsoft.AspNetCore.Authentication.Certificatecontém uma implementação semelhante à Autenticação de Certificado para ASP.NET Core. A autenticação de certificado ocorre no nível do TLS, muito antes de chegar ao ASP.NET Core. Mais precisamente, esse é um manipulador de autenticação que valida o certificado e, em seguida, fornece um evento em que você pode resolve esse certificado para um ClaimsPrincipal.

Configure seu servidorde rede para autenticação de certificado, seja ele o IIS, Kestrel, o Azure Web Apps ou qualquer outro que você esteja usando.

Cenários de proxy e balanceador de carga

A autenticação de certificado é um cenário com estado usado principalmente em que um proxy ou balanceador de carga não manipula o tráfego entre clientes e servidores. Se um proxy ou balanceador de carga for usado, a autenticação de certificado só funcionará se o proxy ou o balanceador de carga:

  • Trata a autenticação.
  • Passa as informações de autenticação do usuário para o aplicativo (por exemplo, em um cabeçalho de solicitação), que atua nas informações de autenticação.

Uma alternativa à autenticação de certificado em ambientes em que proxies e balanceadores de carga são usados é o ADFS (Serviços Federados do Active Directory) com o OIDC (OpenID Connect).

Introdução

Adquira um certificado HTTPS, aplique-o e configure o servidor para exigir certificados.

Em seu aplicativo Web, adicione uma referência ao pacote Microsoft.AspNetCore.Authentication.Certificate . Em seguida, no método Startup.ConfigureServices, chame services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...); com suas opções, fornecendo um delegado para OnCertificateValidated fazer qualquer validação suplementar no certificado do cliente enviado com solicitações. Transforme essas informações em um ClaimsPrincipal e defina-as na propriedade context.Principal.

Se a autenticação falhar, esse manipulador retornará uma 403 (Forbidden) resposta em vez de 401 (Unauthorized), como você poderia esperar. O raciocínio é que a autenticação deve ocorrer durante a conexão TLS inicial. Quando ele chega ao manipulador, é tarde demais. Não há como atualizar a conexão de uma conexão anônima para uma com um certificado.

Adicione app.UseAuthentication(); também no método Startup.Configure. Caso contrário, o HttpContext.User não será definido como ClaimsPrincipal criado a partir do certificado. Por exemplo:

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
}

O exemplo anterior demonstra a maneira padrão de adicionar a autenticação de certificado. O manipulador constrói uma entidade de segurança de usuário usando as propriedades comuns do certificado.

Configurar a validação do certificado

O CertificateAuthenticationOptions manipulador tem algumas validações internas que são as validações mínimas que você deve executar em um certificado. Cada uma dessas configurações é habilitada por padrão.

AllowedCertificateTypes = Chained, SelfSigned ou All (Encadeado | AutoSigned)

Valor padrão: CertificateTypes.Chained

Esse marcar valida que somente o tipo de certificado apropriado é permitido. Se o aplicativo estiver usando certificados autoassinados, essa opção precisará ser definida como CertificateTypes.All ou CertificateTypes.SelfSigned.

ValidateCertificateUse

Valor padrão: true

Esse marcar valida se o certificado apresentado pelo cliente tem o EKU (uso de chave estendida) da Autenticação do Cliente ou nenhuma EKUs. Como dizem as especificações, se nenhum EKU for especificado, todas as EKUs serão consideradas válidas.

ValidateValidityPeriod

Valor padrão: true

Esse marcar valida se o certificado está dentro de seu período de validade. Em cada solicitação, o manipulador garante que um certificado válido quando ele foi apresentado não expirou durante sua sessão atual.

RevocationFlag

Valor padrão: X509RevocationFlag.ExcludeRoot

Um sinalizador que especifica quais certificados na cadeia são verificados quanto à revogação.

As verificações de revogação só são executadas quando o certificado é encadeado a um certificado raiz.

RevocationMode

Valor padrão: X509RevocationMode.Online

Um sinalizador que especifica como as verificações de revogação são executadas.

Especificar uma marcar online pode resultar em um longo atraso enquanto a autoridade de certificação é contatada.

As verificações de revogação só são executadas quando o certificado é encadeado a um certificado raiz.

Posso configurar meu aplicativo para exigir um certificado somente em determinados caminhos?

Isso não é possível. Lembre-se de que a troca de certificados é feita no início da conversa HTTPS, ela é feita pelo servidor antes que a primeira solicitação seja recebida nessa conexão para que não seja possível definir o escopo com base em nenhum campo de solicitação.

Eventos de manipulador

O manipulador tem dois eventos:

  • OnAuthenticationFailed: Chamado se uma exceção ocorrer durante a autenticação e permitir que você reaja.
  • OnCertificateValidated: Chamado depois que o certificado tiver sido validado, a validação aprovada e uma entidade de segurança padrão tiver sido criada. Esse evento permite que você execute sua própria validação e aumente ou substitua a entidade de segurança. Para obter exemplos, incluem:
    • Determinando se o certificado é conhecido por seus serviços.

    • Construindo sua própria entidade de segurança. Considere o exemplo a seguir em 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;
                  }
              };
          });
      

Se você encontrar que o certificado de entrada não atende à sua validação extra, chame context.Fail("failure reason") com um motivo de falha.

Para uma funcionalidade real, você provavelmente desejará chamar um serviço registrado na injeção de dependência que se conecta a um banco de dados ou outro tipo de repositório de usuários. Acesse o serviço usando o contexto passado para o delegado. Considere o exemplo a seguir em 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;
            }
        };
    });

Conceitualmente, a validação do certificado é uma preocupação de autorização. Adicionar um marcar, por exemplo, um emissor ou impressão digital em uma política de autorização, em vez de dentro de OnCertificateValidated, é perfeitamente aceitável.

Configurar seu servidor para exigir certificados

Kestrel

No Program.cs, configure o Kestrel da seguinte maneira:

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

Observação

Os pontos de extremidade criados fazendo a chamada de Listenantes da chamada de ConfigureHttpsDefaults não terão os padrões aplicados.

IIS

Conclua as seguintes etapas no Gerenciador do IIS:

  1. Selecione seu site na guia Conexões.
  2. Clique duas vezes na opção Configurações de SSL na janela Exibição de Recursos.
  3. Marque a caixa de seleção Exigir SSL e selecione o botão de opção Exigir na seção Certificados do cliente.

Client certificate settings in IIS

Proxies web personalizados e do Azure

Consulte o host e a documentação de implantação para saber como configurar o middleware de encaminhamento de certificado.

Usar a autenticação de certificado no Azure Aplicativos Web

Nenhuma configuração de encaminhamento é necessária para o Azure. A configuração de encaminhamento é configurada pelo middleware de encaminhamento de certificado.

Observação

O Middleware de Encaminhamento de Certificado é necessário para esse cenário.

Para obter mais informações, confira Usar um certificado TLS/SSL no código do Serviço de Aplicativo do Azure.

Usar a autenticação de certificado em proxies Web personalizados

O método AddCertificateForwarding é usado para especificar:

  • O nome do cabeçalho do cliente.
  • Como o certificado deve ser carregado (usando a propriedade HeaderConverter).

Em proxies web personalizados, o certificado é passado como um cabeçalho de solicitação personalizado, por exemplo X-SSL-CERT. Para usá-lo, configure o encaminhamento de certificado em 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;
}

Se o aplicativo for proxy reverso pelo NGINX com a configuração proxy_set_header ssl-client-cert $ssl_client_escaped_cert ou implantado no Kubernetes usando a Entrada NGINX, o certificado do cliente será passado para o aplicativo no formulário codificado em URL. Para usar o certificado, decodifica-o da seguinte maneira:

Em 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;
    };
});

O método Startup.Configure adiciona o middleware. UseCertificateForwarding é chamado antes das chamadas para UseAuthentication e UseAuthorization:

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

    app.UseRouting();

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

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

Uma classe separada pode ser usada para implementar a lógica de validação. Como o mesmo certificado autoassinado é usado neste exemplo, verifique se apenas o certificado pode ser usado. Valide se as impressões digitais do certificado do cliente e do certificado do servidor correspondem, caso contrário, qualquer certificado pode ser usado e será suficiente para autenticar. Isso seria usado dentro do método AddCertificate. Você também pode validar o assunto ou o emissor aqui se estiver usando certificados intermediários ou filho.

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

Implementar um HttpClient usando um certificado e o HttpClientHandler

O HttpClientHandler pode ser adicionado diretamente no construtor da classe HttpClient. Deve-se tomar cuidado ao criar instâncias do HttpClient. O HttpClient enviará o certificado com cada solicitação.

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}");
}

Implementar um HttpClient usando um certificado e um HttpClient nomeado de IHttpClientFactory

No exemplo a seguir, um certificado do cliente é adicionado a um HttpClientHandler usando a propriedade ClientCertificates do manipulador. Esse manipulador pode ser usado em uma instância nomeada de um HttpClient usando o método ConfigurePrimaryHttpMessageHandler. Isso é configurado em 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;
});

O IHttpClientFactory pode ser usado para obter a instância nomeada com o manipulador e o certificado. O método CreateClient com o nome do cliente definido na classe Startup é usado para obter a instância . A solicitação HTTP pode ser enviada usando o cliente conforme necessário.

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}");
}

Se o certificado correto for enviado ao servidor, os dados serão retornados. Se nenhum certificado ou certificado incorreto for enviado, um código http 403 status será retornado.

Criar certificados no PowerShell

Criar os certificados é a parte mais difícil na configuração desse fluxo. Um certificado raiz pode ser criado usando o New-SelfSignedCertificate cmdlet do PowerShell. Ao criar o certificado, use uma senha forte. É importante adicionar o KeyUsageProperty parâmetro e o KeyUsage parâmetro, conforme mostrado.

Criar CA raiz

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

Observação

O valor do parâmetro -DnsName deve corresponder ao destino de implantação do aplicativo. Por exemplo, "localhost" para desenvolvimento.

Instalar na raiz confiável

O certificado raiz precisa ser confiável em seu sistema host. Um certificado raiz que não foi criado por uma autoridade de certificação não será confiável por padrão. Para obter informações sobre como confiar no certificado raiz no Windows, consulte esta pergunta.

Certificado intermediário

Um certificado intermediário agora pode ser criado com base no certificado raiz. Isso não é necessário para todos os casos de uso, mas talvez seja necessário criar muitos certificados ou precisar ativar ou desabilitar grupos de certificados. O parâmetro TextExtension é necessário para definir o comprimento do caminho nas restrições básicas do certificado.

Em seguida, o certificado intermediário pode ser adicionado ao certificado intermediário confiável no sistema host do 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

Criar certificado filho com base no certificado intermediário

Um certificado filho pode ser criado com base no certificado intermediário. Essa é a entidade final e não precisa criar mais certificados filho.

$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

Criar certificado filho com base no certificado raiz

Um certificado filho também pode ser criado diretamente do certificado raiz.

$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

Raiz de exemplo – certificado intermediário – certificado

$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

Ao usar os certificados raiz, intermediário ou filho, os certificados podem ser validados usando a Impressão Digital ou PublicKey conforme necessário.

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

Cache de validação de certificado

ASP.NET Core 5.0 e versões posteriores dão suporte à capacidade de habilitar o cache de resultados de validação. O cache melhora drasticamente o desempenho da autenticação de certificado, pois a validação é uma operação cara.

Por padrão, a autenticação de certificado desabilita o cache. Para habilitar o cache, chame AddCertificateCache em Startup.ConfigureServices:

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

A implementação de cache padrão armazena os resultados na memória. Você pode fornecer seu próprio cache implementando ICertificateValidationCache e registrando-o com injeção de dependência. Por exemplo, services.AddSingleton<ICertificateValidationCache, YourCache>().

Certificados do cliente opcionais

Esta seção fornece informações para aplicativos que devem proteger um subconjunto do aplicativo com um certificado. Por exemplo, uma Página ou controlador Razor no aplicativo pode exigir certificados de cliente. Isso apresenta desafios como certificados de cliente:

  • São um recurso TLS, não um recurso HTTP.
  • São negociados por conexão e geralmente no início da conexão antes que todos os dados HTTP estejam disponíveis.

Há duas abordagens para implementar certificados de cliente opcionais:

  1. Usando SNI (nomes de host) separados e redirecionamento. Embora haja mais trabalho para configurar, isso é recomendado porque ele funciona na maioria dos ambientes e protocolos.
  2. Renegociação durante uma solicitação HTTP. Isso tem várias limitações e não é recomendado.

Hosts separados (SNI)

No início da conexão, somente a SNI (Indicação de Nome do Servidor)† é conhecida. Os certificados do cliente podem ser configurados por nome de host para que um host os exija e outro não.

ASP.NET Core 5 e posteriores adiciona suporte mais conveniente para redirecionamento para adquirir certificados de cliente opcionais. Para obter mais informações, consulte o exemplo certificados opcionais.

  • Para solicitações para o aplicativo Web que exigem um certificado de cliente e não têm um:
    • Redirecione para a mesma página usando o subdomínio protegido pelo certificado do cliente.
    • Por exemplo, redirecionar para myClient.contoso.com/requestedPage. Como a solicitação para myClient.contoso.com/requestedPage é um nome de host diferente de contoso.com/requestedPage, o cliente estabelece uma conexão diferente e o certificado do cliente é fornecido.
    • Para obter mais informações, consulte Introdução à autorização no ASP.NET Core.

† A SNI (Indicação de Nome de Servidor) é uma extensão TLS para incluir um domínio virtual como parte da negociação SSL. Isso significa que o nome de domínio virtual, ou um nome de host, pode ser usado para identificar o ponto final da rede.

Renegociação

A renegociação do TLS é um processo pelo qual o cliente e o servidor podem reavaliar os requisitos de criptografia para uma conexão individual, incluindo a solicitação de um certificado do cliente, se não for fornecido anteriormente. A renegociação do TLS é um risco à segurança e não é recomendada porque:

  • Em HTTP/1.1, o servidor deve primeiro armazenar ou consumir todos os dados HTTP que estão em pré-lançamento, como corpos de solicitação POST, para garantir que a conexão esteja clara para a renegociação. Caso contrário, a renegociação poderá parar de responder ou falhar.
  • HTTP/2 e HTTP/3 proíbem explicitamente a renegociação.
  • Há riscos de segurança associados à renegociação. O TLS 1.3 removeu a renegociação de toda a conexão e a substituiu por uma nova extensão para solicitar apenas o certificado do cliente após o início da conexão. Esse mecanismo é exposto por meio das mesmas APIs e ainda está sujeito às restrições anteriores de buffer e versões de protocolo HTTP.

A implementação e a configuração desse recurso variam de acordo com a versão do servidor e da estrutura.

IIS

O IIS gerencia a negociação de certificado do cliente em seu nome. Uma subseção do aplicativo pode habilitar a opção SslRequireCert de negociar o certificado do cliente para essas solicitações. Consulte Configuração na documentação do IIS para obter detalhes.

O IIS armazenará automaticamente os dados do corpo da solicitação em buffer até um limite de tamanho configurado antes da renegociação. As solicitações que excedem o limite são rejeitadas com uma resposta 413. Esse limite usa como padrão 48 KB e é configurável definindo o uploadReadAheadSize.

HttpSys

O HttpSys tem duas configurações que controlam a negociação de certificado do cliente e ambas devem ser definidas. O primeiro está em netsh.exe em http add sslcert clientcertnegotiation=enable/disable. Esse sinalizador indica se o certificado do cliente deve ser negociado no início de uma conexão e deve ser definido disable como para certificados de cliente opcionais. Consulte os documentos do netsh para obter detalhes.

A outra configuração é ClientCertificateMethod. Quando definido como AllowRenegotation, o certificado do cliente pode ser renegociado durante uma solicitação.

NOTA O aplicativo deve armazenar ou consumir dados do corpo da solicitação antes de tentar a renegociação, caso contrário, a solicitação poderá ficar sem resposta.

Há um problema conhecido em que a habilitação AllowRenegotation pode fazer com que a renegociação ocorra de forma síncrona ao acessar a propriedade ClientCertificate. Chame o método GetClientCertificateAsync para evitar isso. Isso foi resolvido no .NET 6. Saiba mais neste tópico do GitHub. Nota GetClientCertificateAsync pode retornar um certificado nulo se o cliente se recusar a fornecer um.

Kestrel

Kestrel controla a negociação de certificado do cliente com a opção ClientCertificateMode.

Para o .NET 5 e versões anteriores, o Kestrel não oferece suporte à renegociação após o início de uma conexão para adquirir um certificado de cliente. Esse recurso foi adicionado ao .NET 6.

Microsoft.AspNetCore.Authentication.Certificatecontém uma implementação semelhante à Autenticação de Certificado para ASP.NET Core. A autenticação de certificado ocorre no nível do TLS, muito antes de chegar ao ASP.NET Core. Mais precisamente, esse é um manipulador de autenticação que valida o certificado e, em seguida, fornece um evento em que você pode resolve esse certificado para um ClaimsPrincipal.

Configure seu servidorde rede para autenticação de certificado, seja ele o IIS, Kestrel, o Azure Web Apps ou qualquer outro que você esteja usando.

Cenários de proxy e balanceador de carga

A autenticação de certificado é um cenário com estado usado principalmente em que um proxy ou balanceador de carga não manipula o tráfego entre clientes e servidores. Se um proxy ou balanceador de carga for usado, a autenticação de certificado só funcionará se o proxy ou o balanceador de carga:

  • Trata a autenticação.
  • Passa as informações de autenticação do usuário para o aplicativo (por exemplo, em um cabeçalho de solicitação), que atua nas informações de autenticação.

Uma alternativa à autenticação de certificado em ambientes em que proxies e balanceadores de carga são usados é o ADFS (Serviços Federados do Active Directory) com o OIDC (OpenID Connect).

Introdução

Adquira um certificado HTTPS, aplique-o e configure o servidor para exigir certificados.

Em seu aplicativo Web, adicione uma referência ao pacote Microsoft.AspNetCore.Authentication.Certificate . Em seguida, no método Startup.ConfigureServices, chame services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...); com suas opções, fornecendo um delegado para OnCertificateValidated fazer qualquer validação suplementar no certificado do cliente enviado com solicitações. Transforme essas informações em um ClaimsPrincipal e defina-as na propriedade context.Principal.

Se a autenticação falhar, esse manipulador retornará uma 403 (Forbidden) resposta em vez de 401 (Unauthorized), como você poderia esperar. O raciocínio é que a autenticação deve ocorrer durante a conexão TLS inicial. Quando ele chega ao manipulador, é tarde demais. Não há como atualizar a conexão de uma conexão anônima para uma com um certificado.

Adicione app.UseAuthentication(); também no método Startup.Configure. Caso contrário, o HttpContext.User não será definido como ClaimsPrincipal criado a partir do certificado. Por exemplo:

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
}

O exemplo anterior demonstra a maneira padrão de adicionar a autenticação de certificado. O manipulador constrói uma entidade de segurança de usuário usando as propriedades comuns do certificado.

Configurar a validação do certificado

O CertificateAuthenticationOptions manipulador tem algumas validações internas que são as validações mínimas que você deve executar em um certificado. Cada uma dessas configurações é habilitada por padrão.

AllowedCertificateTypes = Chained, SelfSigned ou All (Encadeado | AutoSigned)

Valor padrão: CertificateTypes.Chained

Esse marcar valida que somente o tipo de certificado apropriado é permitido. Se o aplicativo estiver usando certificados autoassinados, essa opção precisará ser definida como CertificateTypes.All ou CertificateTypes.SelfSigned.

ValidateCertificateUse

Valor padrão: true

Esse marcar valida se o certificado apresentado pelo cliente tem o EKU (uso de chave estendida) da Autenticação do Cliente ou nenhuma EKUs. Como dizem as especificações, se nenhum EKU for especificado, todas as EKUs serão consideradas válidas.

ValidateValidityPeriod

Valor padrão: true

Esse marcar valida se o certificado está dentro de seu período de validade. Em cada solicitação, o manipulador garante que um certificado válido quando ele foi apresentado não expirou durante sua sessão atual.

RevocationFlag

Valor padrão: X509RevocationFlag.ExcludeRoot

Um sinalizador que especifica quais certificados na cadeia são verificados quanto à revogação.

As verificações de revogação só são executadas quando o certificado é encadeado a um certificado raiz.

RevocationMode

Valor padrão: X509RevocationMode.Online

Um sinalizador que especifica como as verificações de revogação são executadas.

Especificar uma marcar online pode resultar em um longo atraso enquanto a autoridade de certificação é contatada.

As verificações de revogação só são executadas quando o certificado é encadeado a um certificado raiz.

Posso configurar meu aplicativo para exigir um certificado somente em determinados caminhos?

Isso não é possível. Lembre-se de que a troca de certificados é feita no início da conversa HTTPS, ela é feita pelo servidor antes que a primeira solicitação seja recebida nessa conexão para que não seja possível definir o escopo com base em nenhum campo de solicitação.

Eventos de manipulador

O manipulador tem dois eventos:

  • OnAuthenticationFailed: Chamado se uma exceção ocorrer durante a autenticação e permitir que você reaja.
  • OnCertificateValidated: Chamado depois que o certificado tiver sido validado, a validação aprovada e uma entidade de segurança padrão tiver sido criada. Esse evento permite que você execute sua própria validação e aumente ou substitua a entidade de segurança. Para obter exemplos, incluem:
    • Determinando se o certificado é conhecido por seus serviços.

    • Construindo sua própria entidade de segurança. Considere o exemplo a seguir em 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;
                  }
              };
          });
      

Se você encontrar que o certificado de entrada não atende à sua validação extra, chame context.Fail("failure reason") com um motivo de falha.

Para uma funcionalidade real, você provavelmente desejará chamar um serviço registrado na injeção de dependência que se conecta a um banco de dados ou outro tipo de repositório de usuários. Acesse o serviço usando o contexto passado para o delegado. Considere o exemplo a seguir em 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;
            }
        };
    });

Conceitualmente, a validação do certificado é uma preocupação de autorização. Adicionar um marcar, por exemplo, um emissor ou impressão digital em uma política de autorização, em vez de dentro de OnCertificateValidated, é perfeitamente aceitável.

Configurar seu servidor para exigir certificados

Kestrel

No Program.cs, configure o Kestrel da seguinte maneira:

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

Observação

Os pontos de extremidade criados fazendo a chamada de Listenantes da chamada de ConfigureHttpsDefaults não terão os padrões aplicados.

IIS

Conclua as seguintes etapas no Gerenciador do IIS:

  1. Selecione seu site na guia Conexões.
  2. Clique duas vezes na opção Configurações de SSL na janela Exibição de Recursos.
  3. Marque a caixa de seleção Exigir SSL e selecione o botão de opção Exigir na seção Certificados do cliente.

Client certificate settings in IIS

Proxies web personalizados e do Azure

Consulte o host e a documentação de implantação para saber como configurar o middleware de encaminhamento de certificado.

Usar a autenticação de certificado no Azure Aplicativos Web

Nenhuma configuração de encaminhamento é necessária para o Azure. A configuração de encaminhamento é configurada pelo middleware de encaminhamento de certificado.

Observação

O Middleware de Encaminhamento de Certificado é necessário para esse cenário.

Para obter mais informações, confira Usar um certificado TLS/SSL no código do Serviço de Aplicativo do Azure.

Usar a autenticação de certificado em proxies Web personalizados

O método AddCertificateForwarding é usado para especificar:

  • O nome do cabeçalho do cliente.
  • Como o certificado deve ser carregado (usando a propriedade HeaderConverter).

Em proxies web personalizados, o certificado é passado como um cabeçalho de solicitação personalizado, por exemplo X-SSL-CERT. Para usá-lo, configure o encaminhamento de certificado em 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;
}

Se o aplicativo for proxy reverso pelo NGINX com a configuração proxy_set_header ssl-client-cert $ssl_client_escaped_cert ou implantado no Kubernetes usando a Entrada NGINX, o certificado do cliente será passado para o aplicativo no formulário codificado em URL. Para usar o certificado, decodifica-o da seguinte maneira:

Adicione o namespace para System.Net à parte superior de Startup.cs:

using System.Net;

Em 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;
    };
});

Adicione o método 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);
}

O método Startup.Configure adiciona o middleware. UseCertificateForwarding é chamado antes das chamadas para UseAuthentication e UseAuthorization:

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

    app.UseRouting();

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

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

Uma classe separada pode ser usada para implementar a lógica de validação. Como o mesmo certificado autoassinado é usado neste exemplo, verifique se apenas o certificado pode ser usado. Valide se as impressões digitais do certificado do cliente e do certificado do servidor correspondem, caso contrário, qualquer certificado pode ser usado e será suficiente para autenticar. Isso seria usado dentro do método AddCertificate. Você também pode validar o assunto ou o emissor aqui se estiver usando certificados intermediários ou filho.

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

Implementar um HttpClient usando um certificado e o HttpClientHandler

O HttpClientHandler pode ser adicionado diretamente no construtor da classe HttpClient. Deve-se tomar cuidado ao criar instâncias do HttpClient. O HttpClient enviará o certificado com cada solicitação.

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}");
}

Implementar um HttpClient usando um certificado e um HttpClient nomeado de IHttpClientFactory

No exemplo a seguir, um certificado do cliente é adicionado a um HttpClientHandler usando a propriedade ClientCertificates do manipulador. Esse manipulador pode ser usado em uma instância nomeada de um HttpClient usando o método ConfigurePrimaryHttpMessageHandler. Isso é configurado em 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;
});

O IHttpClientFactory pode ser usado para obter a instância nomeada com o manipulador e o certificado. O método CreateClient com o nome do cliente definido na classe Startup é usado para obter a instância . A solicitação HTTP pode ser enviada usando o cliente conforme necessário.

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}");
}

Se o certificado correto for enviado ao servidor, os dados serão retornados. Se nenhum certificado ou certificado incorreto for enviado, um código http 403 status será retornado.

Criar certificados no PowerShell

Criar os certificados é a parte mais difícil na configuração desse fluxo. Um certificado raiz pode ser criado usando o New-SelfSignedCertificate cmdlet do PowerShell. Ao criar o certificado, use uma senha forte. É importante adicionar o KeyUsageProperty parâmetro e o KeyUsage parâmetro, conforme mostrado.

Criar CA raiz

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

Observação

O valor do parâmetro -DnsName deve corresponder ao destino de implantação do aplicativo. Por exemplo, "localhost" para desenvolvimento.

Instalar na raiz confiável

O certificado raiz precisa ser confiável em seu sistema host. Um certificado raiz que não foi criado por uma autoridade de certificação não será confiável por padrão. Para obter informações sobre como confiar no certificado raiz no Windows, consulte esta pergunta.

Certificado intermediário

Um certificado intermediário agora pode ser criado com base no certificado raiz. Isso não é necessário para todos os casos de uso, mas talvez seja necessário criar muitos certificados ou precisar ativar ou desabilitar grupos de certificados. O parâmetro TextExtension é necessário para definir o comprimento do caminho nas restrições básicas do certificado.

Em seguida, o certificado intermediário pode ser adicionado ao certificado intermediário confiável no sistema host do 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

Criar certificado filho com base no certificado intermediário

Um certificado filho pode ser criado com base no certificado intermediário. Essa é a entidade final e não precisa criar mais certificados filho.

$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

Criar certificado filho com base no certificado raiz

Um certificado filho também pode ser criado diretamente do certificado raiz.

$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

Raiz de exemplo – certificado intermediário – certificado

$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

Ao usar os certificados raiz, intermediário ou filho, os certificados podem ser validados usando a Impressão Digital ou PublicKey conforme necessário.

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

Certificados do cliente opcionais

Esta seção fornece informações para aplicativos que devem proteger um subconjunto do aplicativo com um certificado. Por exemplo, uma Página ou controlador Razor no aplicativo pode exigir certificados de cliente. Isso apresenta desafios como certificados de cliente:

  • São um recurso TLS, não um recurso HTTP.
  • São negociados por conexão e geralmente no início da conexão antes que todos os dados HTTP estejam disponíveis.

Há duas abordagens para implementar certificados de cliente opcionais:

  1. Usando SNI (nomes de host) separados e redirecionamento. Embora haja mais trabalho para configurar, isso é recomendado porque ele funciona na maioria dos ambientes e protocolos.
  2. Renegociação durante uma solicitação HTTP. Isso tem várias limitações e não é recomendado.

Hosts separados (SNI)

No início da conexão, somente a SNI (Indicação de Nome do Servidor)† é conhecida. Os certificados do cliente podem ser configurados por nome de host para que um host os exija e outro não.

ASP.NET Core 5 e posteriores adiciona suporte mais conveniente para redirecionamento para adquirir certificados de cliente opcionais. Para obter mais informações, consulte o exemplo certificados opcionais.

  • Para solicitações para o aplicativo Web que exigem um certificado de cliente e não têm um:
    • Redirecione para a mesma página usando o subdomínio protegido pelo certificado do cliente.
    • Por exemplo, redirecionar para myClient.contoso.com/requestedPage. Como a solicitação para myClient.contoso.com/requestedPage é um nome de host diferente de contoso.com/requestedPage, o cliente estabelece uma conexão diferente e o certificado do cliente é fornecido.
    • Para obter mais informações, consulte Introdução à autorização no ASP.NET Core.

† A SNI (Indicação de Nome de Servidor) é uma extensão TLS para incluir um domínio virtual como parte da negociação SSL. Isso significa que o nome de domínio virtual, ou um nome de host, pode ser usado para identificar o ponto final da rede.

Renegociação

A renegociação do TLS é um processo pelo qual o cliente e o servidor podem reavaliar os requisitos de criptografia para uma conexão individual, incluindo a solicitação de um certificado do cliente, se não for fornecido anteriormente. A renegociação do TLS é um risco à segurança e não é recomendada porque:

  • Em HTTP/1.1, o servidor deve primeiro armazenar ou consumir todos os dados HTTP que estão em pré-lançamento, como corpos de solicitação POST, para garantir que a conexão esteja clara para a renegociação. Caso contrário, a renegociação poderá parar de responder ou falhar.
  • HTTP/2 e HTTP/3 proíbem explicitamente a renegociação.
  • Há riscos de segurança associados à renegociação. O TLS 1.3 removeu a renegociação de toda a conexão e a substituiu por uma nova extensão para solicitar apenas o certificado do cliente após o início da conexão. Esse mecanismo é exposto por meio das mesmas APIs e ainda está sujeito às restrições anteriores de buffer e versões de protocolo HTTP.

A implementação e a configuração desse recurso variam de acordo com a versão do servidor e da estrutura.

IIS

O IIS gerencia a negociação de certificado do cliente em seu nome. Uma subseção do aplicativo pode habilitar a opção SslRequireCert de negociar o certificado do cliente para essas solicitações. Consulte Configuração na documentação do IIS para obter detalhes.

O IIS armazenará automaticamente os dados do corpo da solicitação em buffer até um limite de tamanho configurado antes da renegociação. As solicitações que excedem o limite são rejeitadas com uma resposta 413. Esse limite usa como padrão 48 KB e é configurável definindo o uploadReadAheadSize.

HttpSys

O HttpSys tem duas configurações que controlam a negociação de certificado do cliente e ambas devem ser definidas. O primeiro está em netsh.exe em http add sslcert clientcertnegotiation=enable/disable. Esse sinalizador indica se o certificado do cliente deve ser negociado no início de uma conexão e deve ser definido disable como para certificados de cliente opcionais. Consulte os documentos do netsh para obter detalhes.

A outra configuração é ClientCertificateMethod. Quando definido como AllowRenegotation, o certificado do cliente pode ser renegociado durante uma solicitação.

NOTA O aplicativo deve armazenar ou consumir dados do corpo da solicitação antes de tentar a renegociação, caso contrário, a solicitação poderá ficar sem resposta.

Há um problema conhecido em que a habilitação AllowRenegotation pode fazer com que a renegociação ocorra de forma síncrona ao acessar a propriedade ClientCertificate. Chame o método GetClientCertificateAsync para evitar isso. Isso foi resolvido no .NET 6. Saiba mais neste tópico do GitHub. Nota GetClientCertificateAsync pode retornar um certificado nulo se o cliente se recusar a fornecer um.

Kestrel

Kestrel controla a negociação de certificado do cliente com a opção ClientCertificateMode.

Para o .NET 5 e versões anteriores, o Kestrel não oferece suporte à renegociação após o início de uma conexão para adquirir um certificado de cliente. Esse recurso foi adicionado ao .NET 6.

Deixe perguntas, comentários e outros comentários sobre certificados de cliente opcionais neste problema de discussão do GitHub.