Partilhar via


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 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.

Você deveconfigurar seu servidor para autenticação de certificado, seja IIS, Kestrel, Aplicativos Web do Azure ou qualquer outra coisa que 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.

Na aplicação Web:

  • Adicione uma referência ao pacote NuGet Microsoft.AspNetCore.Authentication.Certificate .
  • Em Program.cs, ligue para builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...);. Forneça um delegado para OnCertificateValidated fazer qualquer validação suplementar no certificado de 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.

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

var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

app.UseAuthentication();

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

app.Run();

O exemplo anterior demonstra a maneira padrão de adicionar 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.

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 de cliente apresentado precisar ser encadeado para um certificado raiz que não aparece no armazenamento confiável do sistema, essa opção pode ser definida como X509ChainTrustMode.CustomRootTrust para fazer com que o manipulador use o CustomTrustStore.

CustomTrustStore

Valor padrão: Vazio X509Certificate2Collection

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

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

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:

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

Se você achar que o certificado de entrada não atende à sua validação extra, ligue context.Fail("failure reason") com um motivo 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. 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:

var builder = WebApplication.CreateBuilder(args);

builder.Services.Configure<KestrelServerOptions>(options =>
{
    options.ConfigureHttpsDefaults(options =>
        options.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 Program.cs:

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

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

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

        return clientCertificate!;

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

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

            return bytes;
        }
    };
});

Se o aplicativo for 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:

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 ao Program.cs. UseCertificateForwarding é chamado antes das chamadas para UseAuthentication e UseAuthorization:

var app = builder.Build();

app.UseCertificateForwarding();

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

Uma classe separada pode ser usada para implementar a lógica de validação. Como o mesmo certificado autoassinado é usado neste exemplo, 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.Security.Cryptography.X509Certificates;

namespace CertAuthSample.Snippets;

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

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

Implementar um HttpClient usando um certificado e IHttpClientFactory

No exemplo a seguir, um certificado 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 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 em Program.cs é usado para obter a instância. A solicitação HTTP pode ser enviada usando o cliente, conforme necessário:

public class SampleHttpService
{
    private readonly IHttpClientFactory _httpClientFactory;

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

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

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

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

Se o certificado correto for enviado ao servidor, os dados serão retornados. Se nenhum certificado ou 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 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 obter informações sobre como confiar no certificado raiz no Windows, consulte a documentação do Windows ou o cmdlet do Import-Certificate PowerShell.

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 os certificados raiz, intermediário ou filho, os certificados 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);
}

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: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>().

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.

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 foi consumido antes de ligar GetClientCertificateAsync para negociar um. O Note GetClientCertificateAsync pode retornar um certificado nulo se o cliente se recusar a fornecer um.

OBSERVAÇÃO O comportamento da ClientCertificate propriedade foi alterado no .NET 6. Para obter mais informações, consulte este problema do GitHub.

Kestrel

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

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

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

Se estiver a configurar programaticamente as definições de TLS por nome de host SNI, chame a sobrecarga (.NET 6 ou posterior) que aceita UseHttps e controla a renegociação de certificados do cliente através de TlsHandshakeCallbackOptions.

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()
        // 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 outras opiniões sobre certificados de cliente opcionais nesta discussão do GitHub.