Configurar la autenticación de certificados en ASP.NET Core

Microsoft.AspNetCore.Authentication.Certificatecontiene una implementación similar a la Autenticación de certificados para ASP.NET Core. La autenticación de certificados se produce en el nivel tls antes de que llegue a ASP.NET Core. Con mayor precisión, esta funcionalidad es un controlador de autenticación que valida el certificado y, a continuación, le proporciona un evento donde puede resolver ese certificado en un ClaimsPrincipal.

Debe configurar su servidor para la autenticación de certificados con IIS, Kestrel, Azure Web Apps o su solución preferida.

En este artículo se describe cómo configurar la autenticación de certificados en ASP.NET Core para IIS y HTTP.sys, y se proporcionan ejemplos para llamar a varios métodos y trabajar con propiedades.

Revisión de escenarios de proxy y balanceador de carga

La autenticación de certificados es un escenario con estado que se usa principalmente cuando un proxy o equilibrador de carga no controla el tráfico entre clientes y servidores. Si se usa un proxy o equilibrador de carga, la autenticación de certificados solo funciona si el proxy o el equilibrador de carga:

  • Controla la autenticación.
  • Pasa la información de autenticación de usuario a la aplicación (por ejemplo, en un encabezado de solicitud), que actúa sobre la información de autenticación.

Una alternativa a la autenticación de certificados en entornos en los que se usan servidores proxy y equilibradores de carga es Active Directory Federated Services (ADFS) con OpenID Connect (OIDC).

Comenzar

Adquiera un certificado HTTPS, aplíquelo y configure el servidor para que requiera certificados.

En la aplicación web:

  • Agregue una referencia al paquete Microsoft.AspNetCore.Authentication.Certificate NuGet.

  • En el archivo Program.cs, llame al método builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...);. Proporcione un delegado para que el OnCertificateValidated controlador de eventos complete cualquier validación complementaria en el certificado de cliente enviado con solicitudes. Convierta esa información en un ClaimsPrincipal valor y establézcala en la context.Principal propiedad .

Si se produce un error en la autenticación, este controlador devuelve una 403 (Forbidden) respuesta en lugar de , 401 (Unauthorized)como podría esperar. El controlador devuelve una respuesta diferente porque espera que se produzca la autenticación durante la conexión TLS inicial. Cuando llega al controlador, es demasiado tarde. No hay ninguna manera de actualizar la conexión desde una conexión anónima a una con un certificado.

El UseAuthentication método es necesario para establecer HttpContext.User en un ClaimsPrincipal valor creado a partir del certificado. Por ejemplo:

var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

app.UseAuthentication();

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

app.Run();

En el ejemplo anterior se muestra la manera predeterminada de agregar la autenticación de certificados. El controlador construye un principal de usuario mediante las propiedades comunes del certificado.

Configurar la validación de certificados

El CertificateAuthenticationOptions controlador tiene algunas validaciones integradas que son las validaciones mínimas que debe realizar en un certificado. Cada una de estas opciones está habilitada de forma predeterminada. En las secciones siguientes se describe cómo trabajar con la configuración.

AllowedCertificateTypes = Encadenado, SelfSigned o All (encadenado | SelfSigned)

Valor predeterminado: CertificateTypes.Chained

Esta comprobación valida que solo se permite el tipo de certificado adecuado. Si la aplicación usa certificados autofirmados, esta opción debe establecerse en CertificateTypes.All o CertificateTypes.SelfSigned.

ChainTrustValidationMode

Valor predeterminado: X509ChainTrustMode.System

El certificado presentado por el cliente debe estar vinculado a un certificado raíz de confianza. Esta comprobación controla qué almacén de confianza contiene estos certificados raíz.

De forma predeterminada, el controlador usa el almacén de confianza del sistema. Si el certificado de cliente presentado necesita encadenar a un certificado raíz que no aparece en el almacén de confianza del sistema, puede establecer la opción en X509ChainTrustMode.CustomRootTrust para que el controlador use la CustomTrustStore propiedad.

CustomTrustStore

Valor predeterminado: elemento vacío (X509Certificate2Collection)

Si la propiedad ChainTrustValidationMode del controlador se establece en X509ChainTrustMode.CustomRootTrust, este objeto X509Certificate2Collection contiene todos los certificados utilizados para validar el certificado de cliente hasta una raíz de confianza, incluida la raíz de confianza.

Cuando el cliente presenta un certificado que forma parte de una cadena de certificados de varios niveles, la CustomTrustStore propiedad debe contener todos los certificados emisores de la cadena.

ValidateCertificateUse

Valor predeterminado: true

Esta comprobación valida que el certificado presentado por el cliente tenga el uso extendido de clave de autenticación de cliente (EKU) o ninguna EKU en absoluto. Como se indica en las especificaciones, si no se especifica ninguna EKU, todas las EKU se consideran válidas.

ValidarPeriodoDeValidez

Valor predeterminado: true

Esta comprobación valida que el certificado está dentro de su período de validez. En cada solicitud, el controlador garantiza que un certificado que era válido cuando se presentó no ha expirado durante su sesión actual.

Indicador de Revocación

Valor predeterminado: X509RevocationFlag.ExcludeRoot

Un indicador que especifica qué certificados de la cadena se comprueban para su revocación.

Las comprobaciones de revocación solo se realizan cuando el certificado está encadenado a un certificado raíz.

RevocationMode

Valor predeterminado: X509RevocationMode.Online

Marca que especifica cómo se realizan las comprobaciones de revocación.

La especificación de una comprobación en línea puede provocar un retraso largo mientras se contacta con la entidad de certificación.

Las comprobaciones de revocación solo se realizan cuando el certificado está encadenado a un certificado raíz.

Preguntas más frecuentes: ¿Puedo configurar mi aplicación para requerir un certificado solo en determinadas rutas de acceso?

Este enfoque no es posible. El intercambio de certificados se completa al principio de la conversación HTTPS. El servidor realiza la operación antes de que se reciba la primera solicitud en esa conexión, por lo que no es posible definir el ámbito en función de los campos de solicitud.

Eventos del controlador de procesos

El controlador tiene dos eventos:

  • OnAuthenticationFailed: Se llama si se produce una excepción durante la autenticación y le permite reaccionar.

  • OnCertificateValidated: Se invoca después de que el certificado se haya validado, haya superado la validación y se haya creado un principal predeterminado. Este evento le permite realizar su propia validación y aumentar o reemplazar la entidad de seguridad. Algunos ejemplos son:

    • Determinar si el certificado es conocido para los servicios.

    • Creación de su propio principal, como en el siguiente ejemplo:

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

Si el certificado de entrada no satisface la validación adicional, llame a context.Fail("failure reason") con un motivo de error.

Para mejorar la funcionalidad, llame a un servicio registrado en la inyección de dependencias que se conecte a una base de datos u otro tipo de repositorio de usuarios. Acceda al servicio mediante el contexto pasado al delegado. Considere el ejemplo siguiente:

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, la validación del certificado es un problema de autorización. Por ejemplo, puede añadir una comprobación de un emisor o una huella digital en una directiva de autorización en lugar de dentro del controlador OnCertificateValidated.

Configuración del servidor para requerir certificados

En las secciones siguientes se describe cómo configurar el servidor para que requiera certificados para una solución específica, incluidos Kestrel, IIS, Azure, servidores proxy web personalizados y Azure Web Apps.

Kestrel

En el archivo Program.cs , configure Kestrel de la siguiente manera:

var builder = WebApplication.CreateBuilder(args);

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

Nota

Cuando se crea un punto de conexión llamando al Listen método antes de llamar al ConfigureHttpsDefaults método , el punto de conexión no tiene aplicados los valores predeterminados.

IIS

Complete los pasos siguientes en el Administrador de IIS:

  1. En la pestaña Conexiones , seleccione el sitio.
  2. En la ventana Vista características , haga doble clic en Configuración de SSL.
  3. Active la casilla Requerir SSL .
  4. En la opción Certificados de cliente , seleccione Requerir.

Captura de pantalla que muestra cómo configurar la configuración del certificado de cliente en IIS.

Azure y proxies web personalizados

Para obtener más información sobre cómo configurar el middleware de reenvío de certificados, consulte la documentación de hospedaje y despliegue.

Autenticación de certificados en Azure Web Apps

No se requiere ninguna configuración de reenvío para Azure. El middleware de reenvío de certificados configura la configuración.

Nota

El middleware de reenvío de certificados es necesario para este escenario.

Para obtener más información, consulte Use los certificados TLS/SSL en el código de la aplicación (documentación de Azure).

Autenticación de certificados en servidores proxy web personalizados

El AddCertificateForwarding método se usa para especificar:

  • Nombre del encabezado de cliente.
  • Cómo cargar el certificado (a través de la HeaderConverter propiedad ).

En servidores proxy web personalizados, el certificado se pasa como un encabezado de solicitud personalizado, por ejemplo X-SSL-CERT. Para usar el certificado, configure el reenvío de certificados en el archivo 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;
        }
    };
});

Si NGINX se usa con la configuración proxy_set_header ssl-client-cert $ssl_client_escaped_cert para invertir el proxy de la aplicación o la aplicación se implementa en Kubernetes mediante NGINX Ingress, el certificado de cliente se pasa a la aplicación en formato codificado en URL. Para usar el certificado, descodificarlo de la siguiente manera:

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

Agregue el middleware en el archivo Program.cs . El método UseCertificateForwarding se llama antes de las llamadas a los métodos UseAuthentication y UseAuthorization.

var app = builder.Build();

app.UseCertificateForwarding();

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

Se puede usar una clase independiente para implementar la lógica de validación. Dado que en este ejemplo se usa el mismo certificado autofirmado, asegúrese de que solo se puede usar el certificado. Valide que las huellas digitales del certificado de cliente y del certificado de servidor coincidan. De lo contrario, se puede usar cualquier certificado y ser suficiente para la autenticación. A continuación, el certificado se usa dentro del AddCertificate método . También puede validar el sujeto o el emisor aquí si utiliza certificados intermedios o secundarios.

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 un HttpClient con un certificado e IHttpClientFactory

En el ejemplo siguiente, se agrega un certificado de cliente a HttpClientHandler utilizando la propiedad ClientCertificates del controlador. A continuación, este controlador se puede usar en una instancia con nombre de HttpClient con el método ConfigurePrimaryHttpMessageHandler. Este escenario se configura en el archivo Program.cs :

var clientCertificate =
    new X509Certificate2(
      Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234");

builder.Services.AddHttpClient("namedClient", c =>
{
}).ConfigurePrimaryHttpMessageHandler(() =>
{
    var handler = new HttpClientHandler();
    handler.ClientCertificates.Add(clientCertificate);
    return handler;
});

IHttpClientFactory A continuación, se puede usar para obtener la instancia con nombre con el controlador y el certificado. El CreateClient método con el nombre del cliente definido en el archivo Program.cs se usa para obtener la instancia. La solicitud HTTP se puede enviar mediante el cliente según sea necesario:

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

Si el certificado correcto se envía al servidor, se devuelven los datos. Si no se envía ningún certificado o el certificado incorrecto, el servidor devuelve un código de estado HTTP 403.

Certificados en PowerShell

La creación de certificados es la parte más difícil de configurar este flujo. Puede crear un certificado raíz mediante el cmdlet de New-SelfSignedCertificate PowerShell. Al crear el certificado, use una contraseña segura. Es importante agregar el KeyUsageProperty parámetro y el KeyUsage parámetro como se muestra en los ejemplos.

Creación de una entidad de certificación raíz

El código siguiente muestra cómo crear una entidad de certificación (CA) en la raíz:

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

Nota

El -DnsName valor del parámetro debe coincidir con el destino de implementación de la aplicación. Por ejemplo, "localhost" para el desarrollo.

Instalación en la raíz de confianza

El certificado raíz debe ser de confianza en tu sistema host. Solamente los certificados raíz creados por una entidad de certificación son de confianza de forma predeterminada. Para obtener información sobre cómo confiar en el certificado raíz en Windows, consulte la documentación de Windows o el cmdlet Import-Certificate PowerShell.

Uso de un certificado intermedio

Ahora se puede crear un certificado intermedio a partir del certificado raíz. Este enfoque no es necesario para todos los casos de uso, pero es posible que tenga que crear muchos certificados o tener que activar o deshabilitar grupos de certificados. El TextExtension parámetro es necesario para establecer la longitud de la ruta de acceso en las restricciones básicas del certificado.

A continuación, el certificado intermedio se puede agregar al certificado intermedio de confianza en el sistema host de 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

Creación de un certificado secundario a partir de un certificado intermedio

Se puede crear un certificado hijo a partir del certificado intermedio. Este certificado secundario es la entidad final. No es necesario crear más certificados secundarios.

$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

Creación de un certificado secundario a partir del certificado raíz

También se puede crear un certificado secundario a partir del certificado raíz directamente.

$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

Ejemplo: raíz - certificado intermedio - certificado

En el siguiente ejemplo se muestra la configuración de la autoridad certificadora raíz, el certificado intermedio y el certificado subordinado.

$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

Al usar los certificados raíz, intermedios o secundarios, los certificados se pueden validar mediante la huella digital o PublicKey según sea necesario:

using System.Security.Cryptography.X509Certificates;

namespace CertAuthSample.Snippets;

public class SampleCertificateThumbprintsValidationService : ICertificateValidationService
{
    private readonly string[] validThumbprints = new[]
    {
        "141594A0AE38CBBECED7AF680F7945CD51D8F28A",
        "0C89639E4E2998A93E423F919B36D4009A0F9991",
        "BA9BF91ED35538A01375EFC212A2F46104B33A44"
    };

    public bool ValidateCertificate(X509Certificate2 clientCertificate)
        => validThumbprints.Contains(clientCertificate.Thumbprint);
}

Resultados de validación de certificados de caché

.NET 5 y versiones posteriores admiten la capacidad de habilitar el almacenamiento en caché de los resultados de validación. El almacenamiento en caché mejora considerablemente el rendimiento de la autenticación de certificados porque la validación es una operación costosa.

De forma predeterminada, la autenticación de certificados deshabilita el almacenamiento en caché. Para habilitar el almacenamiento en caché, llame al AddCertificateCache método en el archivo Program.cs :

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

La implementación de almacenamiento en caché predeterminada almacena los resultados en la memoria. Puede proporcionar su propia memoria caché mediante la implementación de ICertificateValidationCache y registrarlo mediante la inyección de dependencias. Por ejemplo: services.AddSingleton<ICertificateValidationCache, YourCache>().

Uso de certificados de cliente opcionales

En esta sección se proporciona información sobre las aplicaciones que deben proteger un subconjunto de la aplicación con un certificado. Por ejemplo, una página o un Razor controlador en la aplicación puede requerir certificados de cliente. Este escenario presenta algunos desafíos:

  • Los certificados de cliente son una característica TLS, no una característica HTTP.
  • Los certificados de cliente se negocian por conexión y normalmente al principio de la conexión antes de que haya datos HTTP disponibles.

Hay dos enfoques para implementar certificados de cliente opcionales:

  • Opción 1: Usar nombres de host independientes (SNI) y redireccionamiento. Aunque esta opción implica más trabajo para configurar, se recomienda el enfoque porque funciona en la mayoría de entornos y protocolos.
  • Opción 2: Renegociación durante una solicitud HTTP. Este enfoque tiene varias limitaciones y no se recomienda.

Servidores separados (SNI)

Al principio de la conexión, solo se conoce la indicación de nombre de servidor (SNI) †. Los certificados de cliente se pueden configurar por nombre de host, por lo que un host requiere los certificados y otro no.

† indicación de nombre de servidor (SNI) es una extensión TLS que se usa para incluir un dominio virtual como parte de la negociación SSL. Este enfoque significa eficazmente que el nombre de dominio virtual o un nombre de host se pueden usar para identificar el punto de conexión de red.

Renegociación de TLS

La renegociación de TLS es un proceso por el que el cliente y el servidor pueden volver a evaluar los requisitos de cifrado de una conexión individual, incluida la solicitud de un certificado de cliente si no se proporcionó anteriormente. La renegociación de TLS es un riesgo de seguridad y no se recomienda porque:

  • En HTTP/1.1, el servidor debe primero almacenar en búfer o consumir cualquier dato HTTP en tránsito, como los cuerpos de las solicitudes POST, para asegurarse de que la conexión está despejada para la renegociación. De lo contrario, la renegociación puede dejar de responder o fallar.
  • HTTP/2 y HTTP/3 prohíben explícitamente la renegociación.
  • Hay riesgos de seguridad asociados a la renegociación. TLS 1.3 quitó la renegociación de toda la conexión y la reemplazó por una nueva extensión para solicitar solo el certificado de cliente después del inicio de la conexión. Este mecanismo se expone a través de las mismas API y sigue sujeto a las restricciones anteriores de almacenamiento en búfer y versiones del protocolo HTTP.

La implementación y configuración de esta característica varía según la versión del servidor y del marco, como se describe en las secciones siguientes.

IIS

IIS administra la negociación de certificados de cliente en su nombre. Una subsección de la aplicación puede habilitar la SslRequireCert opción para negociar el certificado de cliente para esas solicitudes. Para obtener más información, consulte Configuración en la documentación de IIS.

IIS almacena automáticamente en búfer los datos del cuerpo de la solicitud hasta un límite de tamaño configurado antes de renegociar. Las solicitudes que superan el límite se rechazan con una respuesta 413. Este límite tiene como valor predeterminado 48 KB y se puede configurar estableciendo la propiedad uploadReadAheadSize .

HttpSys

HttpSys tiene dos valores que controlan la negociación de certificados de cliente y ambos deben establecerse. El primero está en el archivo netsh.exe en http add sslcert clientcertnegotiation=enable/disable. Esta marca indica si se va a negociar el certificado de cliente al principio de una conexión. Establezca el valor disable en para los certificados de cliente opcionales. Para más información, consulte el uso del http add sslcert parámetro en la documentación de netsh.

La otra configuración es la propiedad ClientCertificateMethod. Cuando se establece en AllowRenegotation, el certificado de cliente se puede renegociar durante una solicitud.

Nota

La aplicación debe almacenar en búfer o consumir cualquier dato del cuerpo de la solicitud antes de intentar la renegociación. De lo contrario, la solicitud podría dejar de responder.

Una aplicación puede comprobar primero la ClientCertificate propiedad para ver si el certificado está disponible. Si no está disponible, asegúrese de que el cuerpo de la solicitud se consuma antes de llamar al método GetClientCertificateAsync para negociar uno. GetClientCertificateAsync puede devolver un certificado NULL si el cliente rechaza proporcionar uno.

Nota

El comportamiento de la propiedad ClientCertificate ha cambiado en .NET 6. Para obtener más información, vea GitHub problema #466.

Kestrel

Kestrel controla la negociación de certificados de cliente con la propiedad ClientCertificateMode.

.NET 6 y versiones posteriores proporcionan la DelayCertificate opción para la propiedad ClientCertificateMode. Cuando se establece esta opción, una aplicación puede comprobar la ClientCertificate propiedad para ver si el certificado está disponible. Si no está disponible, asegúrese de que el cuerpo de la solicitud se consuma antes de llamar al método GetClientCertificateAsync para negociar uno. GetClientCertificateAsync puede devolver un certificado NULL si el cliente rechaza proporcionar uno.

Nota

La aplicación debe almacenar en búfer o consumir cualquier dato del cuerpo de la solicitud antes de intentar la renegociación. De lo contrario, GetClientCertificateAsync podría lanzar la excepción InvalidOperationException: Es necesario vaciar el flujo del cliente antes de la renegociación.

Si configura los parámetros TLS mediante código para cada nombre de host SNI, llame a la sobrecarga UseHttps (.NET 6 o posterior) que admite un objeto de clase TlsHandshakeCallbackOptions. Esta opción controla la renegociación de certificados de cliente a través de la AllowDelayedClientCertificateNegotation propiedad . Para obtener más información, vea el método ListenOptionsHttpsExtensions.UseHttps .

Microsoft.AspNetCore.Authentication.Certificatecontiene una implementación similar a la Autenticación de certificados para ASP.NET Core. La autenticación de certificados se produce en el nivel TLS, que se produce mucho antes de que llegue a ASP.NET Core. De manera más precisa, este controlador de autenticación valida el certificado y le proporciona un evento en el que puede resolver el certificado a un valor de ClaimsPrincipal.

Configure el servidor para la autenticación de certificados, ya sea IIS, Kestrel, Azure Web Apps o cualquier otra cosa que esté usando.

Escenarios de proxy y equilibrador de carga

La autenticación de certificados es un escenario con retención de estado que se utiliza principalmente cuando un proxy o un equilibrador de carga no maneja el tráfico entre clientes y servidores. Si se usa un proxy o equilibrador de carga, la autenticación de certificados solo funciona si el proxy o el equilibrador de carga:

  • Controla la autenticación.
  • Pasa la información de autenticación de usuario a la aplicación (por ejemplo, en un encabezado de solicitud), que actúa sobre la información de autenticación.

Una alternativa a la autenticación de certificados en entornos en los que se usan servidores proxy y equilibradores de carga es Active Directory Federated Services (ADFS) con OpenID Connect (OIDC).

Comenzar

Adquiera un certificado HTTPS, aplíquelo y configure el servidor para que requiera certificados.

En la aplicación web, agregue una referencia al paquete Microsoft.AspNetCore.Authentication.Certificate . A continuación, en el método Startup.ConfigureServices, llame a services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...); con sus opciones, proporcionando un delegado para OnCertificateValidated realizar cualquier validación complementaria en el certificado de cliente que se envía con las solicitudes. Convierta esa información en un ClaimsPrincipal y establézcala en la propiedad context.Principal.

Si se produce un error en la autenticación, este controlador devuelve una 403 (Forbidden) respuesta en lugar de , 401 (Unauthorized)como podría esperar. El razonamiento es que la autenticación debe producirse durante la conexión TLS inicial. Cuando llega al controlador, es demasiado tarde. No hay ninguna manera de actualizar la conexión desde una conexión anónima a una con un certificado.

También agregue app.UseAuthentication(); en el método Startup.Configure. De lo contrario, HttpContext.Userno se establecerá enClaimsPrincipal creado a partir del certificado. Por ejemplo:

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
}

En el ejemplo anterior se muestra la manera predeterminada de agregar la autenticación de certificados. El manejador construye un principal de usuario mediante las propiedades comunes del certificado.

Configurar la validación de certificados

El CertificateAuthenticationOptions controlador tiene algunas validaciones integradas que son las validaciones mínimas que debe realizar en un certificado. Cada una de estas opciones está habilitada de forma predeterminada.

AllowedCertificateTypes = Encadenado, SelfSigned o All (encadenado | SelfSigned)

Valor predeterminado: CertificateTypes.Chained

Esta comprobación valida que solo se permite el tipo de certificado adecuado. Si la aplicación usa certificados autofirmados, esta opción debe establecerse en CertificateTypes.All o CertificateTypes.SelfSigned.

ValidateCertificateUse

Valor predeterminado: true

Esta comprobación valida que el certificado presentado por el cliente tenga el uso extendido de clave de autenticación de cliente (EKU) o ninguna EKU en absoluto. Como se indica en las especificaciones, si no se especifica ninguna EKU, todas las EKU se consideran válidas.

ValidarPeriodoDeValidez

Valor predeterminado: true

Esta comprobación valida que el certificado está dentro de su período de validez. En cada solicitud, el controlador garantiza que un certificado que era válido cuando se presentó no ha expirado durante su sesión actual.

Indicador de Revocación

Valor predeterminado: X509RevocationFlag.ExcludeRoot

Un indicador que especifica qué certificados de la cadena se comprueban para su revocación.

Las comprobaciones de revocación solo se realizan cuando el certificado está encadenado a un certificado raíz.

RevocationMode

Valor predeterminado: X509RevocationMode.Online

Marca que especifica cómo se realizan las comprobaciones de revocación.

La especificación de una comprobación en línea puede provocar un retraso largo mientras se contacta con la entidad de certificación.

Las comprobaciones de revocación solo se realizan cuando el certificado está encadenado a un certificado raíz.

¿Puedo configurar mi aplicación para que requiera un certificado solo en determinadas rutas de acceso?

Esto no es posible. Recuerde que el intercambio de certificados se realiza al principio de la conversación HTTPS, lo hace el servidor antes de que se reciba la primera solicitud en esa conexión, por lo que no es posible definir el ámbito en función de ningún campo de solicitud.

Eventos de controlador

El controlador tiene dos eventos:

  • OnAuthenticationFailed: Se llama si se produce una excepción durante la autenticación y le permite reaccionar.
  • OnCertificateValidated: Se invoca después de que el certificado haya sido validado, haya superado la validación y se haya creado un principal predeterminado. Este evento le permite realizar su propia validación y aumentar o reemplazar la entidad de seguridad. Entre los ejemplos se incluyen:
    • Determinar si el certificado es conocido para los servicios.

    • Construir su propia entidad de seguridad. Considere el ejemplo siguiente de 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;
                  }
              };
          });
      

Si encuentra que el certificado entrante no cumple la validación adicional, llame a context.Fail("failure reason") con un motivo de error.

Para una funcionalidad real, probablemente quiera llamar a un servicio registrado en la inserción de dependencias que se conecta a una base de datos u otro tipo de almacén de usuarios. Acceda a su servicio usando el contexto proporcionado a su delegado. Considere el ejemplo siguiente de 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, la validación del certificado es un problema de autorización. Agregar una comprobación sobre, por ejemplo, un emisor o una huella digital en una directiva de autorización, en lugar de hacerlo dentro de OnCertificateValidated, es perfectamente aceptable.

Configuración del servidor para requerir certificados

Kestrel

En Program.cs, configure Kestrel de la siguiente manera:

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

Nota

Los puntos de conexión que se crean mediante una llamada a Listenantes de llamar a ConfigureHttpsDefaults no tendrán aplicados los valores predeterminados.

IIS

Complete los pasos siguientes en el Administrador de IIS:

  1. Seleccione el sitio en la pestaña Conexiones .
  2. Haga doble clic en la opción Configuración de SSL en la ventana Vista de características .
  3. Active la casilla Requerir SSL y seleccione el botón de radio Requerir en la sección Certificados de cliente .

Configuración del certificado de cliente en IIS

Azure y proxies web personalizados

Vea la documentación de hospedaje e implementación para configurar el middleware de reenvío de certificados.

Uso de la autenticación de certificados en Azure Web Apps

No se requiere ninguna configuración de reenvío para Azure. El middleware de reenvío de certificados configura la configuración del reenvío.

Nota

El middleware de reenvío de certificados es necesario para este escenario.

Para obtener más información, vea Usar un certificado TLS/SSL en su código en Azure App Service (documentación de Azure).

Uso de la autenticación de certificados en servidores proxy web personalizados

El AddCertificateForwarding método se usa para especificar:

  • Nombre del encabezado de cliente.
  • Cómo se va a cargar el certificado (usando la propiedad HeaderConverter).

En servidores proxy web personalizados, el certificado se pasa como un encabezado de solicitud personalizado, por ejemplo X-SSL-CERT. Para usarlo, configure el reenvío de certificados en 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;
}

Si NGINX realiza el proxy inverso de la aplicación con la configuración proxy_set_header ssl-client-cert $ssl_client_escaped_cert o se implementa en Kubernetes mediante NGINX Ingress, el certificado de cliente se pasa a la aplicación en formato codificado en URL. Para usar el certificado, descodificarlo de la siguiente manera:

En Startup.ConfigureServices (Startup.cs):

services.AddCertificateForwarding(options =>
{
    options.CertificateHeader = "ssl-client-cert";
    options.HeaderConverter = (headerValue) =>
    {
        X509Certificate2 clientCertificate = null;

        if (!string.IsNullOrWhiteSpace(headerValue))
        {
            string certPem = WebUtility.UrlDecode(headerValue);
            clientCertificate = X509Certificate2.CreateFromPem(certPem);
        }

        return clientCertificate;
    };
});

Startup.ConfigureA continuación, el método agrega el middleware. UseCertificateForwarding se invoca antes de las llamadas a UseAuthentication y UseAuthorization:

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

    app.UseRouting();

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

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

Se puede usar una clase independiente para implementar la lógica de validación. Dado que en este ejemplo se usa el mismo certificado autofirmado, asegúrese de que solo se puede usar el certificado. Compruebe que las huellas digitales del certificado de cliente y del certificado de servidor coinciden; de lo contrario, se puede usar cualquier certificado y será suficiente para autenticarse. Esto se usaría dentro del AddCertificate método . También puede validar el asunto o el emisor aquí si usa certificados intermedios o secundarios.

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

Implementación de httpClient mediante un certificado y HttpClientHandler

HttpClientHandler se podría agregar directamente en el constructor de la clase HttpClient. Se debe tener cuidado al crear instancias de HttpClient. HttpClient A continuación, enviará el certificado con cada solicitud.

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

Implementación de HttpClient mediante un certificado y un HttpClient con nombre de IHttpClientFactory

En el ejemplo siguiente, se agrega un certificado de cliente a HttpClientHandler mediante la propiedad ClientCertificates del manejador. A continuación, este controlador se puede usar en una instancia con nombre de HttpClient mediante el método ConfigurePrimaryHttpMessageHandler. Esto se configura en Startup.ConfigureServices:

var clientCertificate = 
    new X509Certificate2(
      Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234");

services.AddHttpClient("namedClient", c =>
{
}).ConfigurePrimaryHttpMessageHandler(() =>
{
    var handler = new HttpClientHandler();
    handler.ClientCertificates.Add(clientCertificate);
    return handler;
});

IHttpClientFactory A continuación, se puede usar para obtener la instancia con nombre con el controlador y el certificado. El CreateClient método con el nombre del cliente definido en la Startup clase se usa para obtener la instancia. La solicitud HTTP se puede enviar mediante el cliente según sea necesario.

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

Si el certificado correcto se envía al servidor, se devuelven los datos. Si no se envía ningún certificado o el certificado incorrecto, se devuelve un código de estado HTTP 403.

Crear certificados en PowerShell

La creación de certificados es la parte más difícil de configurar este flujo. Se puede crear un certificado raíz mediante el New-SelfSignedCertificate cmdlet de PowerShell. Al crear el certificado, use una contraseña segura. Es importante agregar el KeyUsageProperty parámetro y el KeyUsage parámetro como se muestra.

Crear CA raíz

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

Nota

El -DnsName valor del parámetro debe coincidir con el destino de implementación de la aplicación. Por ejemplo, "localhost" para el desarrollo.

Instalación en la raíz de confianza

El certificado raíz debe ser de confianza en el sistema host. Un certificado raíz que no creó una entidad de certificación no será de confianza de forma predeterminada. Para obtener información sobre cómo confiar en el certificado raíz en Windows, vea esta pregunta.

Certificado intermedio

Ahora se puede crear un certificado intermedio a partir del certificado raíz. Esto no es necesario para todos los casos de uso, pero es posible que tenga que crear muchos certificados o tener que activar o deshabilitar grupos de certificados. El TextExtension parámetro es necesario para establecer la longitud de la ruta de acceso en las restricciones básicas del certificado.

A continuación, el certificado intermedio se puede agregar al certificado intermedio de confianza en el sistema host de 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

Creación de un certificado secundario a partir de un certificado intermedio

Se puede crear un certificado hijo a partir del certificado intermedio. Esta es la entidad final y no necesita crear más certificados secundarios.

$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

Creación de un certificado secundario a partir del certificado raíz

También se puede crear un certificado secundario a partir del certificado raíz directamente.

$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

Ejemplo raíz - certificado intermedio - 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

Al usar los certificados raíz, intermedio o secundario, los certificados se pueden validar mediante la huella digital o PublicKey según sea necesario.

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

Almacenamiento en caché de validación de certificados

.NET 5 o versiones posteriores admiten la capacidad de habilitar el almacenamiento en caché de los resultados de validación. El almacenamiento en caché mejora considerablemente el rendimiento de la autenticación de certificados, ya que la validación es una operación costosa.

De forma predeterminada, la autenticación de certificados deshabilita el almacenamiento en caché. Para habilitar el almacenamiento en caché, llame a AddCertificateCache en Startup.ConfigureServices:

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

La implementación de almacenamiento en caché predeterminada almacena los resultados en la memoria. Puede proporcionar su propia memoria caché mediante la implementación de ICertificateValidationCache y registrarlo mediante la inyección de dependencias. Por ejemplo: services.AddSingleton<ICertificateValidationCache, YourCache>().

Certificados de cliente opcionales

En esta sección se proporciona información sobre las aplicaciones que deben proteger un subconjunto de la aplicación con un certificado. Por ejemplo, una página o un Razor controlador en la aplicación puede requerir certificados de cliente. Esto presenta desafíos relacionados con los certificados de cliente.

  • Son una característica TLS, no una característica HTTP.
  • Se negocian por conexión y normalmente al principio de la conexión antes de que haya datos HTTP disponibles.

Hay dos enfoques para implementar certificados de cliente opcionales:

  1. Usar nombres de host independientes (SNI) y llevar a cabo la re-dirección. Aunque hay más trabajo para configurar, esto se recomienda porque funciona en la mayoría de los entornos y protocolos.
  2. Renegociación durante una solicitud HTTP. Esto tiene varias limitaciones y no se recomienda.

Hosts independientes (SNI)

Al principio de la conexión, solo se conoce la indicación de nombre de servidor (SNI) †. Los certificados de cliente se pueden configurar por nombre de host para que un host los requiera y otro no.

.NET 5 o posterior agrega un soporte más conveniente para redirigir y obtener certificados de cliente opcionales. Para obtener más información, vea el ejemplo de certificados opcionales.

  • Para las solicitudes a la aplicación web que requieren un certificado de cliente y no tienen uno:
    • Redirija a la misma página utilizando el subdominio protegido por certificado de cliente.
    • Por ejemplo, redirija amyClient.contoso.com/requestedPage. Dado que la solicitud amyClient.contoso.com/requestedPage es un nombre de host diferente que contoso.com/requestedPage, el cliente establece una conexión diferente y se proporciona el certificado de cliente.
    • Para obtener más información, vea Introducción a la autorización en ASP.NET Core.

Indicación de nombre de servidor (SNI) es una extensión de TLS para incluir un dominio virtual como parte de la negociación SSL. Esto significa efectivamente que el nombre de dominio virtual, o un nombre de host, se puede usar para identificar el punto de conexión de la red.

Renegociación

La renegociación de TLS es un proceso por el que el cliente y el servidor pueden volver a evaluar los requisitos de cifrado de una conexión individual, incluida la solicitud de un certificado de cliente si no se proporcionó anteriormente. La renegociación de TLS es un riesgo de seguridad y no se recomienda porque:

  • En HTTP/1.1, el servidor debe almacenar primero en búfer o consumir cualquier dato HTTP que esté en curso, como cuerpos de solicitud POST, para asegurarse de que la conexión está clara para la renegociación. De lo contrario, la renegociación puede dejar de responder o fallar.
  • HTTP/2 y HTTP/3 prohíben explícitamente la renegociación.
  • Hay riesgos de seguridad asociados a la renegociación. TLS 1.3 quitó la renegociación de toda la conexión y la reemplazó por una nueva extensión para solicitar solo el certificado de cliente después del inicio de la conexión. Este mecanismo se expone a través de las mismas API y sigue sujeto a las restricciones anteriores de almacenamiento en búfer y versiones del protocolo HTTP.

La implementación y configuración de esta característica varía según la versión del servidor y del marco.

IIS

IIS administra la negociación de certificados de cliente en su nombre. Una subsección de la aplicación puede habilitar la SslRequireCert opción para negociar el certificado de cliente para esas solicitudes. Vea Configuración en la documentación de IIS para más detalles.

IIS almacenará automáticamente en búfer los datos del cuerpo de la solicitud hasta un límite de tamaño configurado antes de renegociar. Las solicitudes que superan el límite se rechazan con una respuesta 413. Este límite tiene como valor predeterminado 48 KB y se puede configurar estableciendo uploadReadAheadSize.

HttpSys

HttpSys tiene dos valores que controlan la negociación de certificados de cliente y ambos deben establecerse. El primero está en netsh.exe en http add sslcert clientcertnegotiation=enable/disable. Esta marca indica si el certificado de cliente se debe negociar al principio de una conexión y debe establecerse disable en para los certificados de cliente opcionales. Vea los documentos de netsh para más detalles.

El otro parámetro es ClientCertificateMethod. Cuando se establece en AllowRenegotation, el certificado de cliente se puede renegociar durante una solicitud.

NOTA La aplicación debe almacenar en búfer o consumir los datos del cuerpo de la solicitud antes de intentar la renegociación; de lo contrario, la solicitud puede dejar de responder.

Existe un problema conocido en el que la habilitaciónAllowRenegotation puede provocar que la renegociación se produzca de forma sincrónica al acceder a la propiedadClientCertificate. Llame alGetClientCertificateAsync método para evitarlo. Esto se ha corregido en .NET 6. Para más información, consulte este problema de GitHub. Nota GetClientCertificateAsync puede devolver un certificado null si el cliente rechaza proporcionar uno.

Kestrel

Kestrel controla la negociación de certificados de cliente con la ClientCertificateMode opción.

Para .NET 5 o versiones anteriores Kestrel no admite la renegociación después del inicio de una conexión para adquirir un certificado de cliente. Esta característica se ha agregado en .NET 6.

Microsoft.AspNetCore.Authentication.Certificatecontiene una implementación similar a la Autenticación de certificados para ASP.NET Core. La autenticación de certificados se realiza en el nivel de TLS, mucho antes de que llegue a ASP.NET Core. Con más precisión, se trata de un controlador de autenticación que valida el certificado y, a continuación, le proporciona un evento donde puede resolver ese certificado en un ClaimsPrincipal.

Configure el servidor para la autenticación de certificados, ya sea IIS, Kestrel, Azure Web Apps o cualquier otra cosa que esté usando.

Escenarios de proxy y equilibrador de carga

La autenticación de certificados es un escenario con retención de estado que se utiliza principalmente cuando un proxy o un equilibrador de carga no maneja el tráfico entre clientes y servidores. Si se usa un proxy o equilibrador de carga, la autenticación de certificados solo funciona si el proxy o el equilibrador de carga:

  • Controla la autenticación.
  • Pasa la información de autenticación de usuario a la aplicación (por ejemplo, en un encabezado de solicitud), que actúa sobre la información de autenticación.

Una alternativa a la autenticación de certificados en entornos en los que se usan servidores proxy y equilibradores de carga es Active Directory Federated Services (ADFS) con OpenID Connect (OIDC).

Comenzar

Adquiera un certificado HTTPS, aplíquelo y configure el servidor para que requiera certificados.

En la aplicación web, agregue una referencia al paquete Microsoft.AspNetCore.Authentication.Certificate . A continuación, en el Startup.ConfigureServices método, llame a services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...); con sus opciones, proporcionando un delegado para que OnCertificateValidated realice cualquier validación complementaria en el certificado de cliente enviado con solicitudes. Convierta esa información en un ClaimsPrincipal y establézcalo en la propiedad context.Principal.

Si se produce un error en la autenticación, este controlador devuelve una 403 (Forbidden) respuesta en lugar de , 401 (Unauthorized)como podría esperar. El razonamiento es que la autenticación debe producirse durante la conexión TLS inicial. Cuando llega al controlador, es demasiado tarde. No hay ninguna manera de actualizar la conexión desde una conexión anónima a una con un certificado.

También agregue app.UseAuthentication(); en el método Startup.Configure. De lo contrario, HttpContext.Userno se establecerá enClaimsPrincipal creado a partir del certificado. Por ejemplo:

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
}

En el ejemplo anterior se muestra la manera predeterminada de agregar la autenticación de certificados. El manejador construye un principal de usuario mediante las propiedades comunes del certificado.

Configurar la validación de certificados

El CertificateAuthenticationOptions controlador tiene algunas validaciones integradas que son las validaciones mínimas que debe realizar en un certificado. Cada una de estas opciones está habilitada de forma predeterminada.

AllowedCertificateTypes = Encadenado, SelfSigned o All (encadenado | SelfSigned)

Valor predeterminado: CertificateTypes.Chained

Esta comprobación valida que solo se permite el tipo de certificado adecuado. Si la aplicación usa certificados autofirmados, esta opción debe establecerse en CertificateTypes.All o CertificateTypes.SelfSigned.

ValidateCertificateUse

Valor predeterminado: true

Esta comprobación valida que el certificado presentado por el cliente tenga el uso extendido de clave de autenticación de cliente (EKU) o ninguna EKU en absoluto. Como se indica en las especificaciones, si no se especifica ninguna EKU, todas las EKU se consideran válidas.

ValidarPeriodoDeValidez

Valor predeterminado: true

Esta comprobación valida que el certificado está dentro de su período de validez. En cada solicitud, el controlador garantiza que un certificado que era válido cuando se presentó no ha expirado durante su sesión actual.

Indicador de Revocación

Valor predeterminado: X509RevocationFlag.ExcludeRoot

Un indicador que especifica qué certificados de la cadena se comprueban para su revocación.

Las comprobaciones de revocación solo se realizan cuando el certificado está encadenado a un certificado raíz.

RevocationMode

Valor predeterminado: X509RevocationMode.Online

Marca que especifica cómo se realizan las comprobaciones de revocación.

La especificación de una comprobación en línea puede provocar un retraso largo mientras se contacta con la entidad de certificación.

Las comprobaciones de revocación solo se realizan cuando el certificado está encadenado a un certificado raíz.

¿Puedo configurar mi aplicación para que requiera un certificado solo en determinadas rutas de acceso?

Esto no es posible. Recuerde que el intercambio de certificados se realiza al principio de la conversación HTTPS, lo hace el servidor antes de que se reciba la primera solicitud en esa conexión, por lo que no es posible definir el ámbito en función de ningún campo de solicitud.

Eventos de controlador

El controlador tiene dos eventos:

  • OnAuthenticationFailed: Se llama si se produce una excepción durante la autenticación y le permite reaccionar.
  • OnCertificateValidated: Se invoca después de que el certificado haya sido validado, haya superado la validación y se haya creado un principal predeterminado. Este evento le permite realizar su propia validación y aumentar o reemplazar la entidad de seguridad. Entre los ejemplos se incluyen:
    • Determinar si el certificado es conocido para los servicios.

    • Construir su propia entidad de seguridad. Considere el ejemplo siguiente de 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;
                  }
              };
          });
      

Si encuentra que el certificado entrante no cumple la validación adicional, llame a context.Fail("failure reason") con un motivo de error.

Para una funcionalidad real, probablemente quiera llamar a un servicio registrado en la inserción de dependencias que se conecta a una base de datos u otro tipo de almacén de usuarios. Acceda a su servicio usando el contexto proporcionado a su delegado. Considere el ejemplo siguiente de 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, la validación del certificado es un problema de autorización. Agregar una comprobación sobre, por ejemplo, un emisor o una huella digital en una directiva de autorización, en lugar de hacerlo dentro de OnCertificateValidated, es perfectamente aceptable.

Configuración del servidor para requerir certificados

Kestrel

En Program.cs, configure Kestrel de la siguiente manera:

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

Nota

Los puntos de conexión que se crean mediante una llamada a Listenantes de llamar a ConfigureHttpsDefaults no tendrán aplicados los valores predeterminados.

IIS

Complete los pasos siguientes en el Administrador de IIS:

  1. Seleccione el sitio en la pestaña Conexiones .
  2. Haga doble clic en la opción Configuración de SSL en la ventana Vista de características .
  3. Active la casilla Requerir SSL y seleccione el botón de radio Requerir en la sección Certificados de cliente .

Configuración del certificado de cliente en IIS

Azure y proxies web personalizados

Vea la documentación de hospedaje e implementación para configurar el middleware de reenvío de certificados.

Uso de la autenticación de certificados en Azure Web Apps

No se requiere ninguna configuración de reenvío para Azure. El middleware de reenvío de certificados configura la configuración del reenvío.

Nota

El middleware de reenvío de certificados es necesario para este escenario.

Para obtener más información, vea Usar un certificado TLS/SSL en su código en Azure App Service (documentación de Azure).

Uso de la autenticación de certificados en servidores proxy web personalizados

El AddCertificateForwarding método se usa para especificar:

  • Nombre del encabezado de cliente.
  • Cómo se va a cargar el certificado (usando la propiedad HeaderConverter).

En servidores proxy web personalizados, el certificado se pasa como un encabezado de solicitud personalizado, por ejemplo X-SSL-CERT. Para usarlo, configure el reenvío de certificados en 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;
}

Si NGINX realiza el proxy inverso de la aplicación con la configuración proxy_set_header ssl-client-cert $ssl_client_escaped_cert o se implementa en Kubernetes mediante NGINX Ingress, el certificado de cliente se pasa a la aplicación en formato codificado en URL. Para usar el certificado, descodificarlo de la siguiente manera:

Agregue el espacio de nombres paraSystem.Net en la parte superior deStartup.cs:

using System.Net;

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

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

Startup.ConfigureA continuación, el método agrega el middleware. UseCertificateForwarding se invoca antes de las llamadas a UseAuthentication y UseAuthorization:

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

    app.UseRouting();

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

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

Se puede usar una clase independiente para implementar la lógica de validación. Dado que en este ejemplo se usa el mismo certificado autofirmado, asegúrese de que solo se puede usar el certificado. Compruebe que las huellas digitales del certificado de cliente y del certificado de servidor coinciden; de lo contrario, se puede usar cualquier certificado y será suficiente para autenticarse. Esto se usaría dentro del AddCertificate método . También puede validar el asunto o el emisor aquí si usa certificados intermedios o secundarios.

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

Implementación de httpClient mediante un certificado y HttpClientHandler

HttpClientHandler se podría agregar directamente en el constructor de la clase HttpClient. Se debe tener cuidado al crear instancias de HttpClient. HttpClient A continuación, enviará el certificado con cada solicitud.

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

Implementación de HttpClient mediante un certificado y un HttpClient con nombre de IHttpClientFactory

En el ejemplo siguiente, se agrega un certificado de cliente a HttpClientHandler mediante la propiedad ClientCertificates del manejador. A continuación, este controlador se puede usar en una instancia con nombre de HttpClient mediante el método ConfigurePrimaryHttpMessageHandler. Esto se configura en Startup.ConfigureServices:

var clientCertificate = 
    new X509Certificate2(
      Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234");

services.AddHttpClient("namedClient", c =>
{
}).ConfigurePrimaryHttpMessageHandler(() =>
{
    var handler = new HttpClientHandler();
    handler.ClientCertificates.Add(clientCertificate);
    return handler;
});

IHttpClientFactory A continuación, se puede usar para obtener la instancia con nombre con el controlador y el certificado. El CreateClient método con el nombre del cliente definido en la Startup clase se usa para obtener la instancia. La solicitud HTTP se puede enviar mediante el cliente según sea necesario.

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

Si el certificado correcto se envía al servidor, se devuelven los datos. Si no se envía ningún certificado o el certificado incorrecto, se devuelve un código de estado HTTP 403.

Crear certificados en PowerShell

La creación de certificados es la parte más difícil de configurar este flujo. Se puede crear un certificado raíz mediante el New-SelfSignedCertificate cmdlet de PowerShell. Al crear el certificado, use una contraseña segura. Es importante agregar el KeyUsageProperty parámetro y el KeyUsage parámetro como se muestra.

Crear CA raíz

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

Nota

El -DnsName valor del parámetro debe coincidir con el destino de implementación de la aplicación. Por ejemplo, "localhost" para el desarrollo.

Instalación en la raíz de confianza

El certificado raíz debe ser de confianza en el sistema host. Un certificado raíz que no creó una entidad de certificación no será de confianza de forma predeterminada. Para obtener información sobre cómo confiar en el certificado raíz en Windows, vea esta pregunta.

Certificado intermedio

Ahora se puede crear un certificado intermedio a partir del certificado raíz. Esto no es necesario para todos los casos de uso, pero es posible que tenga que crear muchos certificados o tener que activar o deshabilitar grupos de certificados. El TextExtension parámetro es necesario para establecer la longitud de la ruta de acceso en las restricciones básicas del certificado.

A continuación, el certificado intermedio se puede agregar al certificado intermedio de confianza en el sistema host de 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

Creación de un certificado secundario a partir de un certificado intermedio

Se puede crear un certificado hijo a partir del certificado intermedio. Esta es la entidad final y no necesita crear más certificados secundarios.

$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

Creación de un certificado secundario a partir del certificado raíz

También se puede crear un certificado secundario a partir del certificado raíz directamente.

$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

Ejemplo raíz - certificado intermedio - 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

Al usar los certificados raíz, intermedio o secundario, los certificados se pueden validar mediante la huella digital o PublicKey según sea necesario.

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 opcionales

En esta sección se proporciona información sobre las aplicaciones que deben proteger un subconjunto de la aplicación con un certificado. Por ejemplo, una página o un Razor controlador en la aplicación puede requerir certificados de cliente. Esto presenta desafíos relacionados con los certificados de cliente.

  • Son una característica TLS, no una característica HTTP.
  • Se negocian por conexión y normalmente al principio de la conexión antes de que haya datos HTTP disponibles.

Hay dos enfoques para implementar certificados de cliente opcionales:

  1. Usar nombres de host independientes (SNI) y llevar a cabo la re-dirección. Aunque hay más trabajo para configurar, esto se recomienda porque funciona en la mayoría de los entornos y protocolos.
  2. Renegociación durante una solicitud HTTP. Esto tiene varias limitaciones y no se recomienda.

Hosts independientes (SNI)

Al principio de la conexión, solo se conoce la indicación de nombre de servidor (SNI) †. Los certificados de cliente se pueden configurar por nombre de host para que un host los requiera y otro no.

.NET 5 o posterior agrega un soporte más conveniente para redirigir y obtener certificados de cliente opcionales. Para obtener más información, vea el ejemplo de certificados opcionales.

  • Para las solicitudes a la aplicación web que requieren un certificado de cliente y no tienen uno:
    • Redirija a la misma página utilizando el subdominio protegido por certificado de cliente.
    • Por ejemplo, redirija amyClient.contoso.com/requestedPage. Dado que la solicitud amyClient.contoso.com/requestedPage es un nombre de host diferente que contoso.com/requestedPage, el cliente establece una conexión diferente y se proporciona el certificado de cliente.
    • Para obtener más información, vea Introducción a la autorización en ASP.NET Core.

Indicación de nombre de servidor (SNI) es una extensión de TLS para incluir un dominio virtual como parte de la negociación SSL. Esto significa efectivamente que el nombre de dominio virtual, o un nombre de host, se puede usar para identificar el punto de conexión de la red.

Renegociación

La renegociación de TLS es un proceso por el que el cliente y el servidor pueden volver a evaluar los requisitos de cifrado de una conexión individual, incluida la solicitud de un certificado de cliente si no se proporcionó anteriormente. La renegociación de TLS es un riesgo de seguridad y no se recomienda porque:

  • En HTTP/1.1, el servidor debe almacenar primero en búfer o consumir cualquier dato HTTP que esté en curso, como cuerpos de solicitud POST, para asegurarse de que la conexión está clara para la renegociación. De lo contrario, la renegociación puede dejar de responder o fallar.
  • HTTP/2 y HTTP/3 prohíben explícitamente la renegociación.
  • Hay riesgos de seguridad asociados a la renegociación. TLS 1.3 quitó la renegociación de toda la conexión y la reemplazó por una nueva extensión para solicitar solo el certificado de cliente después del inicio de la conexión. Este mecanismo se expone a través de las mismas API y sigue sujeto a las restricciones anteriores de almacenamiento en búfer y versiones del protocolo HTTP.

La implementación y configuración de esta característica varía según la versión del servidor y del marco.

IIS

IIS administra la negociación de certificados de cliente en su nombre. Una subsección de la aplicación puede habilitar la SslRequireCert opción para negociar el certificado de cliente para esas solicitudes. Vea Configuración en la documentación de IIS para más detalles.

IIS almacenará automáticamente en búfer los datos del cuerpo de la solicitud hasta un límite de tamaño configurado antes de renegociar. Las solicitudes que superan el límite se rechazan con una respuesta 413. Este límite tiene como valor predeterminado 48 KB y se puede configurar estableciendo uploadReadAheadSize.

HttpSys

HttpSys tiene dos valores que controlan la negociación de certificados de cliente y ambos deben establecerse. El primero está en netsh.exe en http add sslcert clientcertnegotiation=enable/disable. Esta marca indica si el certificado de cliente se debe negociar al principio de una conexión y debe establecerse disable en para los certificados de cliente opcionales. Vea los documentos de netsh para más detalles.

El otro parámetro es ClientCertificateMethod. Cuando se establece en AllowRenegotation, el certificado de cliente se puede renegociar durante una solicitud.

NOTA La aplicación debe almacenar en búfer o consumir los datos del cuerpo de la solicitud antes de intentar la renegociación; de lo contrario, la solicitud puede dejar de responder.

Existe un problema conocido en el que la habilitaciónAllowRenegotation puede provocar que la renegociación se produzca de forma sincrónica al acceder a la propiedadClientCertificate. Llame alGetClientCertificateAsync método para evitarlo. Esto se ha corregido en .NET 6. Para más información, consulte este problema de GitHub. Nota GetClientCertificateAsync puede devolver un certificado null si el cliente rechaza proporcionar uno.

Kestrel

Kestrel controla la negociación de certificados de cliente con la ClientCertificateMode opción.

Para .NET 5 o versiones anteriores Kestrel no admite la renegociación después del inicio de una conexión para adquirir un certificado de cliente. Esta característica se ha agregado en .NET 6.

Deje preguntas, comentarios y otras opiniones sobre los certificados de cliente opcionales en el subproceso de discusión de la incidencia de GitHub #18720.