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

Microsoft.AspNetCore.Authentication.Certificate contém uma implementação semelhante à Autenticação de Certificado para ASP.NET Core. A autenticação de certificados acontece ao nível do TLS antes mesmo de chegar ao ASP.NET Core. Mais precisamente, esta funcionalidade é um handler de autenticação que valida o certificado e depois lhe dá um evento onde pode resolver esse certificado para um ClaimsPrincipal.

Deve configurar o seu servidor para autenticação de certificados com IIS, , Azure Aplicações Web ou a sua solução preferida.

Este artigo descreve como configurar a autenticação de certificados em ASP.NET Core para IIS e HTTP.sys, e fornece exemplos para chamar vários métodos e trabalhar com propriedades.

Revise cenários de proxy e balanceador de carga

A autenticação de certificado é um cenário com estado, utilizado principalmente quando um proxy ou balanceador de carga não lida com 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 balanceador de carga:

  • Gere 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 sobre as informações de autenticação.

Uma alternativa à autenticação de certificado em ambientes onde proxies e balanceadores de carga são usados é o Ative Directory Federated Services (ADFS) com OpenID Connect (OIDC).

Introdução

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

Na aplicação Web:

  • Adicione uma referência ao pacote NuGet Microsoft.AspNetCore.Authentication.Certificate .

  • No ficheiro Program.cs , chama o builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...); método. Forneça um delegado para que o OnCertificateValidated gestor de eventos complete qualquer validação suplementar no certificado do cliente enviado com pedidos. Transforme essa informação num ClaimsPrincipal valor e defina-a na context.Principal propriedade.

Se a autenticação falhar, este handler devolve uma 403 (Forbidden) resposta em vez de um 401 (Unauthorized), como seria de esperar. O handler devolve uma resposta diferente porque espera que a autenticação ocorra durante a ligação TLS inicial. Quando chega ao responsável, já é tarde demais. Não há como atualizar a conexão de uma conexão anônima para uma com um certificado.

O UseAuthentication método é obrigado a definir HttpContext.User para um ClaimsPrincipal valor 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 autenticação de certificado. O handler constrói um principal de utilizador usando as propriedades comuns do certificado.

Configurar validação de 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. As secções seguintes descrevem como trabalhar com as definições.

AllowedCertificateTypes = Encadeado, Autoassinado ou Todos (Encadeado | Autoassinado)

Valor predefinido: CertificateTypes.Chained

Essa verificação valida que apenas 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.

Modo de Validação de Confiança em Cadeia

Valor padrão: X509ChainTrustMode.System

O certificado apresentado pelo cliente deve ser encadeado a um certificado raiz confiável. Essa verificação controla qual armazenamento confiável contém esses certificados raiz.

Por padrão, o manipulador usa o armazenamento confiável do sistema. Se o certificado cliente apresentado precisar de se encadear a um certificado raiz que não aparece na loja de confiança do sistema, pode definir a opção para X509ChainTrustMode.CustomRootTrust para que o manipulador use a CustomTrustStore propriedade.

CustomTrustStore

Valor padrão: Vazio X509Certificate2Collection

Se a propriedade do ChainTrustValidationMode manipulador estiver definida para X509ChainTrustMode.CustomRootTrust, este objeto X509Certificate2Collection contém todos os certificados 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 multinível, a CustomTrustStore propriedade deve conter todos os certificados emitentes da cadeia.

ValidarUsoDoCertificado

Valor predefinido: true

Esta verificação valida se o certificado apresentado pelo cliente tem o uso estendido de chave para Autenticação de Cliente (EKU) ou não tem nenhum EKU. Como dizem as especificações, se nenhum EKU for especificado, todos os EKUs serão considerados válidos.

ValidarPeriodoDeValidade

Valor predefinido: true

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

Indicador de Revogação

Valor predefinido: X509RevocationFlag.ExcludeRoot

Um indicador que especifica quais certificados na cadeia de certificação são verificados para revogação.

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

Modo de revogação

Valor predefinido: X509RevocationMode.Online

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

Especificar uma verificação online pode resultar num longo atraso enquanto a entidade emissora do certificado é contactada.

As verificações de revogação só são realizadas quando o certificado está encadeado a um certificado raiz.

FAQ: Posso configurar a minha aplicação para exigir um certificado apenas em certos caminhos?

Esta abordagem não é possível. A troca de certificados é concluída no início da conversa HTTPS. A operação é feita pelo servidor antes de o primeiro pedido ser recebido nessa ligação, pelo que não é possível definir o âmbito com base em campos de pedido.

Eventos do manipulador de processos

O manipulador tem dois eventos:

  • OnAuthenticationFailed: Chamado se uma exceção acontecer durante a autenticação, permitindo que você reaja.

  • OnCertificateValidated: Chamado após o certificado ser validado, passar validação e criar um principal padrão. Esse evento permite que você execute sua própria validação e aumente ou substitua o principal. Os exemplos incluem:

    • Verificar se o certificado é conhecido nos seus serviços.

    • Construir o seu próprio princípio, como no 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 o certificado de entrada não satisfizer a sua validação extra, chame context.Fail("failure reason") com uma razão de falha.

Para melhor funcionalidade, utilize um serviço registado no sistema de injeção de dependência que se conecta a uma base de dados ou outro tipo de repositório de utilizadores. Acesse o serviço usando o contexto passado ao 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;
            }
        };
    });

Conceptualmente, a validação do certificado é uma questão de autorização. Por exemplo, pode adicionar uma verificação num emissor ou numa impressão digital (thumbprint) numa política de autorização, em vez de dentro do "handler".

Configurar o servidor para exigir certificados

As secções seguintes descrevem como configurar o seu servidor para exigir certificados para uma solução específica, incluindo Kestrel, IIS, Azure, proxies web personalizados e Azure Aplicações Web.

Kestrel

No ficheiro Program.cs , configure Kestrel da seguinte forma:

var builder = WebApplication.CreateBuilder(args);

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

Observação

Quando um endpoint é criado ao chamar o Listen método antes de chamar o ConfigureHttpsDefaults, o endpoint não tem as configurações padrão aplicadas.

IIS

Conclua as seguintes etapas no Gerenciador do IIS:

  1. No separador Conexões , selecione o seu site.
  2. Na janela de Vista de Funcionalidades , clique duas vezes em Definições SSL.
  3. Seleciona a caixa Exigir SSL .
  4. Para a opção de certificados de cliente , selecione Exigir.

Captura de ecrã que mostra como configurar as definições do certificado do cliente no IIS.

Azure e proxies da Web personalizados

Para mais informações sobre como configurar o middleware de encaminhamento de certificados, consulte a documentação de alojamento e implementação.

Autenticação de certificados em Azure Aplicações Web

Nenhuma configuração de encaminhamento é necessária para o Azure. O Certificate Forwarding Middleware configura a configuração.

Observação

O middleware de encaminhamento de certificados é necessário para este cenário.

Para mais informações, consulte Use certificados TLS/SSL no seu código de aplicação (documentação Azure).

Autenticação de certificados em proxies web personalizados

O AddCertificateForwarding método é utilizado para especificar:

  • O nome do cabeçalho do cliente.
  • Como carregar o certificado (através da HeaderConverter propriedade).

Em proxies da Web personalizados, o certificado é passado como um cabeçalho de solicitação personalizado, por exemplo X-SSL-CERT. Para usar o certificado, configure o encaminhamento de certificados no ficheiro 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 NGINX for usado com a configuração proxy_set_header ssl-client-cert $ssl_client_escaped_cert para fazer proxy reverso da aplicação, ou se a aplicação for implementada no Kubernetes através do NGINX Ingress, o certificado do cliente é passado para a aplicação em forma codificada por URL. Para usar o certificado, decodifice-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 no ficheiro Program.cs . Ao método UseCertificateForwarding é chamado antes das chamadas aos métodos 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, certifique-se de que apenas o certificado possa ser usado. Valide se as impressões digitais tanto do certificado do cliente como do certificado do servidor correspondem. Caso contrário, qualquer certificado pode ser usado e ser suficiente para autenticação. O certificado é então utilizado dentro do AddCertificate método. Também pode validar o sujeito ou o emissor aqui, se usar certificados intermédios ou filhos.

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 com um certificado e a IHttpClientFactory

No exemplo seguinte, um certificado de cliente é adicionado a HttpClientHandler usando a ClientCertificates propriedade do handler. Este handler pode então ser usado numa instância nomeada de um HttpClient com o método ConfigurePrimaryHttpMessageHandler. Este cenário está configurado no ficheiro 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 então ser usado para obter a instância nomeada com o manipulador e o certificado. O CreateClient método com o nome do cliente definido no ficheiro Program.cs é usado para obter a instância. O pedido HTTP pode ser enviado utilizando 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 não for enviado nenhum certificado ou o certificado errado, o servidor devolve um código de estado HTTP 403.

Certificados em PowerShell

Criar os certificados é a parte mais difícil na configuração desse fluxo. Podes criar um certificado raiz usando o New-SelfSignedCertificate cmdlet PowerShell. Ao criar o certificado, use uma palavra-passe forte. É importante adicionar o KeyUsageProperty parâmetro e o KeyUsage parâmetro como mostrado nos exemplos.

Criar autoridade de certificação raiz

O seguinte código mostra como criar uma autoridade certificadora (CA) na 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 -DnsName valor do parâmetro deve corresponder ao destino de implantação do aplicativo. Por exemplo, "localhost" para desenvolvimento.

Instalar na raiz confiável

O certificado raiz deve ser confiável em seu sistema host. Somente certificados raiz criados por uma autoridade de certificação são confiáveis por padrão. Para informações sobre como confiar no certificado raiz no Windows, consulte a documentação Windows ou o cmdlet PowerShell Import-Certificate.

Use um certificado intermédio

Um certificado intermediário agora pode ser criado a partir do certificado raiz. Esta abordagem não é necessária para todos os casos de uso, mas pode ser necessário criar muitos certificados ou ativar ou desativar grupos de certificados. O TextExtension parâmetro é necessário para definir o comprimento do caminho nas restrições básicas do certificado.

O certificado intermediário pode então ser adicionado ao certificado intermediário confiável no sistema host 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 a partir de certificado intermediário

Um certificado de criança pode ser criado a partir do certificado intermediário. Este certificado filho é a entidade final. Não precisa de criar mais certidões de criança.

$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 a partir do certificado raiz

Um certificado filho também pode ser criado diretamente a partir 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

Exemplo: certificado raiz - certificado intermédio - certificado

O exemplo seguinte mostra a configuração da CA raiz, do certificado intermédio e do certificado filho:

$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

Quando utiliza os certificados raiz, intermédios ou de derivação, estes podem ser validados usando a impressão digital ou a chave pública 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);
}

Resultados da validação de certificados de cache

As versões .NET 5 e posteriores suportam a capacidade de permitir a cache dos resultados de validação. A cache melhora drasticamente o desempenho da autenticação de certificados porque a validação é uma operação dispendiosa.

Por padrão, a autenticação de certificado desabilita o cache. Para permitir a cache, chame o AddCertificateCache método no ficheiro 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 resultados em memória. Você pode fornecer seu próprio cache implementando-o ICertificateValidationCache e registrando-o com injeção de dependência. Por exemplo, services.AddSingleton<ICertificateValidationCache, YourCache>().

Utilizar certificados opcionais de cliente

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

  • Os certificados de cliente são uma funcionalidade TLS, não uma funcionalidade HTTP.
  • Os certificados do cliente são negociados por ligação e geralmente no início da ligação, antes de qualquer dado HTTP estar disponível.

Existem duas abordagens para implementar certificados opcionais de cliente:

  • Opção 1: Use nomes de host separados (SNI) e redirecionamento. Embora esta opção envolva mais trabalho de configuração, a abordagem é recomendada porque funciona na maioria dos ambientes e protocolos.
  • Opção 2: Renegociação durante um pedido HTTP. Esta abordagem tem várias limitações e não é recomendada.

Servidores separados (SNI)

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

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

Renegociação TLS

A renegociação do TLS é um processo pelo qual o cliente e o servidor podem reavaliar os requisitos de encriptação para uma ligação individual, incluindo o pedido de um certificado de cliente caso não tenha sido previamente fornecido. A renegociação TLS é um risco de segurança e não é recomendada porque:

  • No HTTP/1.1, o servidor deve primeiro armazenar ou consumir quaisquer dados HTTP em trânsito, como os corpos de requisição POST, para garantir que a ligação esteja livre para a renegociação. Caso contrário, a renegociação pode deixar de responder ou falhar.
  • HTTP/2 e HTTP/3 proíbem explicitamente a renegociação.
  • Existem 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 buffering e versões de protocolo HTTP.

A implementação e configuração desta funcionalidade variam consoante a versão do servidor e do framework, conforme descrito nas secções seguintes.

IIS

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

O IIS armazena automaticamente qualquer dado do corpo do pedido até um limite de tamanho configurado antes de renegociar. Os pedidos que excedam o limite são rejeitados com uma resposta 413. Este limite por defeito é de 48 KB e pode ser configurado definindo a propriedade uploadReadAheadSize .

HttpSys

O HttpSys tem duas definições que controlam a negociação do certificado do cliente e ambas devem estar definidas. A primeira está no netsh.exe ficheiro sob http add sslcert clientcertnegotiation=enable/disable. Esta bandeira indica se deve negociar o certificado do cliente no início de uma ligação. Defina o valor para disable para certificados de cliente opcionais. Para mais informações, consulte a http add sslcert utilização de parâmetros na documentação netsh.

O outro ajuste é a ClientCertificateMethod propriedade. Quando definido como AllowRenegotation, o certificado do cliente pode ser renegociado durante uma solicitação.

Observação

A aplicação deve armazenar ou consumir quaisquer dados do corpo do pedido antes de tentar a renegociação. Caso contrário, o pedido pode ficar sem resposta.

Um aplicativo pode primeiro verificar a ClientCertificate propriedade para ver se o certificado está disponível. Se não estiver disponível, certifique-se de que o corpo do pedido seja consumido antes de chamar o método GetClientCertificateAsync para negociar um. GetClientCertificateAsync pode devolver um certificado nulo se o cliente recusar fornecer um.

Observação

O comportamento da propriedade ClientCertificate mudou em .NET 6. Para mais informações, consulte GitHub edição #466.

Kestrel

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

.NET 6 e posteriores fornecem a opção DelayCertificate para a propriedade ClientCertificateMode. Quando esta opção está definida, uma aplicação pode verificar a ClientCertificate propriedade para ver se o certificado está disponível. Se não estiver disponível, certifique-se de que o corpo da solicitação é consumido antes de chamar o método GetClientCertificateAsync para negociar um. GetClientCertificateAsync pode devolver um certificado nulo se o cliente recusar fornecer um.

Observação

A aplicação deve armazenar ou processar quaisquer dados do corpo da solicitação antes de tentar a renegociação. Caso contrário, GetClientCertificateAsync pode lançar a exceção InvalidOperationException: O fluxo cliente precisa ser esvaziado antes da renegociação.

Se configurares programaticamente as definições TLS por cada nome de host SNI, chama a sobrecarga UseHttps (.NET 6 ou posterior) que aceita um objeto da classe TlsHandshakeCallbackOptions. Esta opção controla a renegociação de certificados do cliente através da AllowDelayedClientCertificateNegotation propriedade. Para mais informações, consulte o método ListenOptionsHttpsExtensions.UseHttps .

Microsoft.AspNetCore.Authentication.Certificate contém uma implementação semelhante à Autenticação de Certificado para ASP.NET Core. A autenticação de certificados ocorre ao nível do TLS, o que ocorre muito antes de chegar ao ASP.NET Core. Mais precisamente, este gestor de autenticação valida o certificado e fornece um evento que permite converter o certificado para um valor ClaimsPrincipal.

Configure 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, utilizado principalmente quando um proxy ou balanceador de carga não lida com 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 balanceador de carga:

  • Gere 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 sobre as informações de autenticação.

Uma alternativa à autenticação de certificado em ambientes onde proxies e balanceadores de carga são usados é o Ative Directory Federated Services (ADFS) com OpenID Connect (OIDC).

Introdução

Adquira um certificado HTTPS, aplique-o e configure seu 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 as suas opções, fornecendo um delegado para OnCertificateValidated fazer qualquer validação suplementar no certificado do cliente enviado com os pedidos. Transforme essa informação em um ClaimsPrincipal e defina-a na propriedade context.Principal.

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

Também adicione app.UseAuthentication(); no método Startup.Configure. Caso contrário, o HttpContext.User não será definido para ClaimsPrincipal, que foi 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 autenticação de certificado. O manipulador constrói um principal de utilizador usando as propriedades comuns do certificado.

Configurar validação de 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 = Encadeado, Autoassinado ou Todos (Encadeado | Autoassinado)

Valor predefinido: CertificateTypes.Chained

Essa verificação valida que apenas 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.

ValidarUsoDoCertificado

Valor predefinido: true

Esta verificação valida se o certificado apresentado pelo cliente tem o uso estendido de chave para Autenticação de Cliente (EKU) ou não tem nenhum EKU. Como dizem as especificações, se nenhum EKU for especificado, todos os EKUs serão considerados válidos.

ValidarPeriodoDeValidade

Valor predefinido: true

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

Indicador de Revogação

Valor predefinido: X509RevocationFlag.ExcludeRoot

Um indicador que especifica quais certificados na cadeia de certificação são verificados para revogação.

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

Modo de revogação

Valor predefinido: X509RevocationMode.Online

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

Especificar uma verificação online pode resultar num longo atraso enquanto a entidade emissora do certificado é contactada.

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

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

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

Eventos do manipulador

O manipulador tem dois eventos:

  • OnAuthenticationFailed: Chamado se uma exceção acontecer durante a autenticação, permitindo que você reaja.
  • OnCertificateValidated: Chamado depois que o certificado foi validado, foi aprovado na validação e uma instância padrão foi criada. Esse evento permite que você execute sua própria validação e aumente ou substitua o principal. Os exemplos incluem:
    • Verificar se o certificado é conhecido nos seus serviços.

    • Construindo o seu próprio princípio. Considere o seguinte exemplo 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ê achar que o certificado de entrada não atende à sua validação extra, ligue 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 armazenamento de usuário. Acesse seu serviço usando o contexto passado para seu delegado. Considere o seguinte exemplo 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;
            }
        };
    });

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

Configurar o servidor para exigir certificados

Kestrel

No Program.cs, configure Kestrel da seguinte forma:

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 ao chamar Listenantes de chamar 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 de cliente .

Configurações de certificado de cliente no IIS

Azure e proxies da Web personalizados

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

Usar autenticação de certificado nos Aplicativos Web do Azure

Nenhuma configuração de encaminhamento é necessária para o Azure. A configuração de encaminhamento é definida pelo Middleware de Encaminhamento de Certificados.

Observação

O middleware de encaminhamento de certificados é necessário para este cenário.

Para obter mais informações, consulte Usar um certificado TLS/SSL em seu código no Serviço de Aplicativo do Azure (documentação do Azure).

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

O AddCertificateForwarding método é utilizado para especificar:

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

Em proxies da 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 certificados 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 usado como proxy reverso pelo NGINX com a configuração proxy_set_header ssl-client-cert $ssl_client_escaped_cert ou implantado no Kubernetes usando NGINX Ingress, o certificado do cliente será entregue ao aplicativo no formato codificado por URL. Para usar o certificado, decodifice-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 então 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, certifique-se de que apenas o certificado possa 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 AddCertificate método. 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 poderia ser adicionado diretamente no construtor da HttpClient classe. Deve-se ter cuidado ao criar instâncias do HttpClient. O HttpClient irá então enviar o certificado com cada pedido.

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 de cliente é adicionado a um HttpClientHandler usando a propriedade ClientCertificates do manipulador. Este manipulador pode, então, ser usado numa instância nomeada de um HttpClient com o método ConfigurePrimaryHttpMessageHandler. Isto está 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 então ser usado para obter a instância nomeada com o manipulador e o certificado. O CreateClient método com o nome do cliente definido na Startup classe é 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 o certificado errado for enviado, um código de status HTTP 403 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 autoridade de certificação 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 -DnsName valor do parâmetro 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 intermédio

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

O certificado intermediário pode então ser adicionado ao certificado intermediário confiável no sistema host 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 a partir de certificado intermediário

Um certificado de criança pode ser criado a partir do certificado intermediário. Esta é 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 a partir do certificado raiz

Um certificado filho também pode ser criado diretamente a partir 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

Exemplo raiz - 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 o certificado raiz, certificado intermediário ou certificado filho, os certificados podem ser validados usando a impressão digital ou a chave pública, 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

O .NET 5 ou versões posteriores oferecem suporte à capacidade de habilitar o cache de resultados de validação. O cache melhora drasticamente o desempenho da autenticação de certificado, já que a validação é uma operação cara.

Por padrão, a autenticação de certificado desabilita o cache. Para habilitar cache, chame AddCertificateCache: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 resultados em memória. Você pode fornecer seu próprio cache implementando-o ICertificateValidationCache e registrando-o com injeção de dependência. Por exemplo, services.AddSingleton<ICertificateValidationCache, YourCache>().

Certificados de cliente opcionais

Esta seção fornece informações para aplicativos que devem proteger um subconjunto do aplicativo com um certificado. Por exemplo, uma Razor Página ou controlador no aplicativo pode exigir certificados de cliente. Isso apresenta desafios relacionados aos 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 qualquer dado HTTP esteja disponível.

Há duas abordagens para implementar certificados de cliente opcionais:

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

Anfitriões separados (SNI)

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

O .NET 5 ou posterior adiciona suporte mais conveniente para redirecionamento para adquirir certificados de cliente opcionais. Para obter mais informações, consulte o Exemplo de certificados opcionais.

  • Para solicitações ao aplicativo Web que exigem um certificado de cliente e não têm um:
    • Redirecionar para a mesma página usando o subdomínio protegido por certificado de cliente.
    • Por exemplo, redirecione para myClient.contoso.com/requestedPage. Como a solicitação para myClient.contoso.com/requestedPage é um nome de host diferente do 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.

† Server Name Indication (SNI) é uma extensão TLS para incluir um domínio virtual como parte da negociação SSL. Isso significa efetivamente 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 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 de cliente se não tiver sido fornecido anteriormente. A renegociação TLS é um risco de segurança e não é recomendada porque:

  • Em HTTP/1.1, o servidor deve primeiro armazenar em buffer ou consumir quaisquer dados HTTP que estejam em voo, como corpos de solicitação POST, para garantir que a conexão esteja clara para a renegociação. Caso contrário, a renegociação pode deixar de responder ou falhar.
  • HTTP/2 e HTTP/3 proíbem explicitamente a renegociação.
  • Existem 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 buffering e versões de protocolo HTTP.

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

IIS

O IIS gerencia a negociação de certificado de cliente em seu nome. Uma subseção do aplicativo pode habilitar a SslRequireCert opção 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 em buffer todos os dados do corpo da solicitação até um limite de tamanho configurado antes de renegociar. Os pedidos que excedam o limite são rejeitados com uma resposta 413. O padrão desse limite é de 48 KB e pode ser configurado definindo o uploadReadAheadSize.

HttpSys

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

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

OBSERVAÇÃO O aplicativo deve armazenar em buffer ou consumir quaisquer dados do corpo da solicitação antes de tentar a renegociação, caso contrário, a solicitação pode deixar de responder.

Há um problema conhecido em que a habilitação AllowRenegotation pode fazer com que a renegociação aconteça de forma síncrona ao acessar a ClientCertificate propriedade. Chame o GetClientCertificateAsync método para evitar isso. Isso foi abordado no .NET 6. Para obter mais informações, consulte este problema do GitHub. O Note GetClientCertificateAsync pode retornar um certificado nulo se o cliente se recusar a fornecer um.

Kestrel

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

Para .NET 5 ou anterior Kestrel não suporta a 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.Certificate contém uma implementação semelhante à Autenticação de Certificado para ASP.NET Core. A autenticação de certificado acontece no nível TLS, muito antes de chegar ao ASP.NET Core. Mais precisamente, este é um gestor de autenticação que valida o certificado e, em seguida, fornece um evento no qual é possível associar esse certificado a um ClaimsPrincipal.

Configure 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, utilizado principalmente quando um proxy ou balanceador de carga não lida com 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 balanceador de carga:

  • Gere 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 sobre as informações de autenticação.

Uma alternativa à autenticação de certificado em ambientes onde proxies e balanceadores de carga são usados é o Ative Directory Federated Services (ADFS) com OpenID Connect (OIDC).

Introdução

Adquira um certificado HTTPS, aplique-o e configure seu 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 as suas opções, fornecendo um delegado para OnCertificateValidated fazer qualquer validação suplementar no certificado do cliente enviado com os pedidos. Transforme essa informação em um ClaimsPrincipal e defina-a na propriedade context.Principal.

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

Também adicione app.UseAuthentication(); no método Startup.Configure. Caso contrário, o HttpContext.User não será definido para ClaimsPrincipal, que foi 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 autenticação de certificado. O manipulador constrói um principal de utilizador usando as propriedades comuns do certificado.

Configurar validação de 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 = Encadeado, Autoassinado ou Todos (Encadeado | Autoassinado)

Valor predefinido: CertificateTypes.Chained

Essa verificação valida que apenas 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.

ValidarUsoDoCertificado

Valor predefinido: true

Esta verificação valida se o certificado apresentado pelo cliente tem o uso estendido de chave para Autenticação de Cliente (EKU) ou não tem nenhum EKU. Como dizem as especificações, se nenhum EKU for especificado, todos os EKUs serão considerados válidos.

ValidarPeriodoDeValidade

Valor predefinido: true

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

Indicador de Revogação

Valor predefinido: X509RevocationFlag.ExcludeRoot

Um indicador que especifica quais certificados na cadeia de certificação são verificados para revogação.

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

Modo de revogação

Valor predefinido: X509RevocationMode.Online

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

Especificar uma verificação online pode resultar num longo atraso enquanto a entidade emissora do certificado é contactada.

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

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

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

Eventos do manipulador

O manipulador tem dois eventos:

  • OnAuthenticationFailed: Chamado se uma exceção acontecer durante a autenticação, permitindo que você reaja.
  • OnCertificateValidated: Chamado depois que o certificado foi validado, foi aprovado na validação e uma instância padrão foi criada. Esse evento permite que você execute sua própria validação e aumente ou substitua o principal. Os exemplos incluem:
    • Verificar se o certificado é conhecido nos seus serviços.

    • Construindo o seu próprio princípio. Considere o seguinte exemplo 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ê achar que o certificado de entrada não atende à sua validação extra, ligue 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 armazenamento de usuário. Acesse seu serviço usando o contexto passado para seu delegado. Considere o seguinte exemplo 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;
            }
        };
    });

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

Configurar o servidor para exigir certificados

Kestrel

No Program.cs, configure Kestrel da seguinte forma:

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 ao chamar Listenantes de chamar 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 de cliente .

Configurações de certificado de cliente no IIS

Azure e proxies da Web personalizados

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

Usar autenticação de certificado nos Aplicativos Web do Azure

Nenhuma configuração de encaminhamento é necessária para o Azure. A configuração de encaminhamento é definida pelo Middleware de Encaminhamento de Certificados.

Observação

O middleware de encaminhamento de certificados é necessário para este cenário.

Para obter mais informações, consulte Usar um certificado TLS/SSL em seu código no Serviço de Aplicativo do Azure (documentação do Azure).

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

O AddCertificateForwarding método é utilizado para especificar:

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

Em proxies da 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 certificados 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 usado como proxy reverso pelo NGINX com a configuração proxy_set_header ssl-client-cert $ssl_client_escaped_cert ou implantado no Kubernetes usando NGINX Ingress, o certificado do cliente será entregue ao aplicativo no formato codificado por URL. Para usar o certificado, decodifice-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 então 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, certifique-se de que apenas o certificado possa 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 AddCertificate método. 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 poderia ser adicionado diretamente no construtor da HttpClient classe. Deve-se ter cuidado ao criar instâncias do HttpClient. O HttpClient irá então enviar o certificado com cada pedido.

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 de cliente é adicionado a um HttpClientHandler usando a propriedade ClientCertificates do manipulador. Este manipulador pode, então, ser usado numa instância nomeada de um HttpClient com o método ConfigurePrimaryHttpMessageHandler. Isto está 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 então ser usado para obter a instância nomeada com o manipulador e o certificado. O CreateClient método com o nome do cliente definido na Startup classe é 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 o certificado errado for enviado, um código de status HTTP 403 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 autoridade de certificação 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 -DnsName valor do parâmetro 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 intermédio

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

O certificado intermediário pode então ser adicionado ao certificado intermediário confiável no sistema host 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 a partir de certificado intermediário

Um certificado de criança pode ser criado a partir do certificado intermediário. Esta é 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 a partir do certificado raiz

Um certificado filho também pode ser criado diretamente a partir 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

Exemplo raiz - 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 o certificado raiz, certificado intermediário ou certificado filho, os certificados podem ser validados usando a impressão digital ou a chave pública, 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 de cliente opcionais

Esta seção fornece informações para aplicativos que devem proteger um subconjunto do aplicativo com um certificado. Por exemplo, uma Razor Página ou controlador no aplicativo pode exigir certificados de cliente. Isso apresenta desafios relacionados aos 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 qualquer dado HTTP esteja disponível.

Há duas abordagens para implementar certificados de cliente opcionais:

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

Anfitriões separados (SNI)

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

O .NET 5 ou posterior adiciona suporte mais conveniente para redirecionamento para adquirir certificados de cliente opcionais. Para obter mais informações, consulte o Exemplo de certificados opcionais.

  • Para solicitações ao aplicativo Web que exigem um certificado de cliente e não têm um:
    • Redirecionar para a mesma página usando o subdomínio protegido por certificado de cliente.
    • Por exemplo, redirecione para myClient.contoso.com/requestedPage. Como a solicitação para myClient.contoso.com/requestedPage é um nome de host diferente do 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.

† Server Name Indication (SNI) é uma extensão TLS para incluir um domínio virtual como parte da negociação SSL. Isso significa efetivamente 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 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 de cliente se não tiver sido fornecido anteriormente. A renegociação TLS é um risco de segurança e não é recomendada porque:

  • Em HTTP/1.1, o servidor deve primeiro armazenar em buffer ou consumir quaisquer dados HTTP que estejam em voo, como corpos de solicitação POST, para garantir que a conexão esteja clara para a renegociação. Caso contrário, a renegociação pode deixar de responder ou falhar.
  • HTTP/2 e HTTP/3 proíbem explicitamente a renegociação.
  • Existem 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 buffering e versões de protocolo HTTP.

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

IIS

O IIS gerencia a negociação de certificado de cliente em seu nome. Uma subseção do aplicativo pode habilitar a SslRequireCert opção 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 em buffer todos os dados do corpo da solicitação até um limite de tamanho configurado antes de renegociar. Os pedidos que excedam o limite são rejeitados com uma resposta 413. O padrão desse limite é de 48 KB e pode ser configurado definindo o uploadReadAheadSize.

HttpSys

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

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

OBSERVAÇÃO O aplicativo deve armazenar em buffer ou consumir quaisquer dados do corpo da solicitação antes de tentar a renegociação, caso contrário, a solicitação pode deixar de responder.

Há um problema conhecido em que a habilitação AllowRenegotation pode fazer com que a renegociação aconteça de forma síncrona ao acessar a ClientCertificate propriedade. Chame o GetClientCertificateAsync método para evitar isso. Isso foi abordado no .NET 6. Para obter mais informações, consulte este problema do GitHub. O Note GetClientCertificateAsync pode retornar um certificado nulo se o cliente se recusar a fornecer um.

Kestrel

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

Para .NET 5 ou anterior Kestrel não suporta a 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 opcionais de clientes no tópico de discussão da edição GitHub #18720.