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

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

Introducción

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 Program.cs, llame a builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...);. Proporcione un delegado para OnCertificateValidated que realice cualquier validación complementaria en el certificado de cliente enviado con solicitudes. Convierta esa información en y ClaimsPrincipal 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 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.

UseAuthentication es necesario establecer HttpContext.User en un ClaimsPrincipal objeto 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 una entidad de seguridad 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.

ChainTrustValidationMode

Valor predeterminado: X509ChainTrustMode.System

El certificado presentado por el cliente debe encadenar 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 debe encadenar a un certificado raíz que no aparece en el almacén de confianza del sistema, esta opción se puede establecer en X509ChainTrustMode.CustomRootTrust para que el controlador use CustomTrustStore.

CustomTrustStore

Valor predeterminado: elemento vacío (X509Certificate2Collection)

Si la propiedad ChainTrustValidationMode del controlador está establecida en X509ChainTrustMode.CustomRootTrust, este X509Certificate2Collection contiene todos los certificados que se usarán 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, CustomTrustStore 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.

ValidateValidityPeriod

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.

RevocationFlag

Valor predeterminado: X509RevocationFlag.ExcludeRoot

Un indicador que especifica qué certificados de la cadena se comprueban para 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 llama después de validar el certificado, se ha superado la validación y se ha creado una entidad de seguridad predeterminada. 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:

      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 encuentra que el certificado entrante no cumple 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 inserción de dependencias que se conecte a una base de datos u otro tipo de almacén 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. Agregar una comprobación, por ejemplo, un emisor o huella digital en una directiva de autorización, en lugar de dentro OnCertificateValidated, es perfectamente aceptable.

Configuración del servidor para requerir certificados

Kestrel

En 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

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 .

Client certificate settings in IIS

Azure y proxies web personalizados

Vea la documentación de host e implementación para ver cómo 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 (mediante la HeaderConverter propiedad ).

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 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 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 la entrada NGINX, el certificado de cliente se pasa a la aplicación en formato con codificación 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 enProgram.cs. UseCertificateForwarding se llama a antes de las llamadas a 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. 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.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;
    }
}

Implementación de httpClient mediante un certificado e IHttpClientFactory

En el ejemplo siguiente, se agrega un certificado de cliente a HttpClientHandlermediante la ClientCertificates propiedad del controlador. A continuación, este controlador se puede usar en una instancia con nombre de HttpClientmediante el ConfigurePrimaryHttpMessageHandler método . Esto se configura en 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 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, 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 secundario 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

Raíz de ejemplo: 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.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);
}

Almacenamiento en caché de validación de certificados

ASP.NET Core 5.0 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, 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 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 ICertificateValidationCache y el registro con la inserció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 como 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 redireccionamiento. 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.

ASP.NET Core 5 y versiones posteriores agrega compatibilidad más cómoda para redirigir para adquirir 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 mediante el subdominio protegido por certificados 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 producir un error.
  • 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.

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 ha consumido antes de llamar GetClientCertificateAsync a para negociar uno. Nota GetClientCertificateAsync puede devolver un certificado null si el cliente rechaza proporcionar uno.

NOTA El comportamiento de la ClientCertificate propiedad ha cambiado en .NET 6. Para más información, consulte este problema de GitHub.

Kestrel

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

ClientCertificateMode.DelayCertificate es una nueva opción disponible en .NET 6 o posterior. Cuando se establece, 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 ha consumido antes de llamar GetClientCertificateAsync a para negociar uno. Nota GetClientCertificateAsync puede devolver un certificado null si el cliente rechaza proporcionar uno.

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 GetClientCertificateAsync, puede iniciar InvalidOperationException: Client stream needs to be drained before renegotiation..

Si configura mediante programación la configuración de TLS por host, hay una nueva sobrecarga UseHttps disponible en .NET 6 y versiones posteriores que toma TlsHandshakeCallbackOptions y controla la renegociación del certificado de cliente mediante TlsHandshakeCallbackContext.AllowDelayedClientCertificateNegotation.

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

Introducción

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 OnCertificateValidated realizar cualquier validación complementaria en el certificado de cliente enviado con solicitudes. Convierta esa información en y ClaimsPrincipal 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 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.

app.UseAuthentication(); Agregue también el Startup.Configure método . 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 controlador construye una entidad de seguridad 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.

ValidateValidityPeriod

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.

RevocationFlag

Valor predeterminado: X509RevocationFlag.ExcludeRoot

Un indicador que especifica qué certificados de la cadena se comprueban para 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 llama después de validar el certificado, se ha superado la validación y se ha creado una entidad de seguridad predeterminada. 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 al servicio mediante el contexto pasado al 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, por ejemplo, un emisor o huella digital en una directiva de autorización, en lugar de dentro 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 .

Client certificate settings in IIS

Azure y proxies web personalizados

Vea la documentación de host e implementación para ver cómo 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 (mediante la HeaderConverter propiedad ).

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 la entrada NGINX, el certificado de cliente se pasa a la aplicación en formato con codificación 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 llama a 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 HttpClient clase . 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 HttpClientHandlermediante la ClientCertificates propiedad del controlador. A continuación, este controlador se puede usar en una instancia con nombre de HttpClientmediante el ConfigurePrimaryHttpMessageHandler método . 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 secundario 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

Raíz de ejemplo: 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

ASP.NET Core 5.0 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, 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 ICertificateValidationCache y el registro con la inserció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 como 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 redireccionamiento. 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.

ASP.NET Core 5 y versiones posteriores agrega compatibilidad más cómoda para redirigir para adquirir 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 mediante el subdominio protegido por certificados 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 producir un error.
  • 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 y 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 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).

Introducción

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 OnCertificateValidated realizar cualquier validación complementaria en el certificado de cliente enviado con solicitudes. Convierta esa información en y ClaimsPrincipal 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 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.

app.UseAuthentication(); Agregue también el Startup.Configure método . 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 controlador construye una entidad de seguridad 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.

ValidateValidityPeriod

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.

RevocationFlag

Valor predeterminado: X509RevocationFlag.ExcludeRoot

Un indicador que especifica qué certificados de la cadena se comprueban para 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 llama después de validar el certificado, se ha superado la validación y se ha creado una entidad de seguridad predeterminada. 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 al servicio mediante el contexto pasado al 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, por ejemplo, un emisor o huella digital en una directiva de autorización, en lugar de dentro 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 .

Client certificate settings in IIS

Azure y proxies web personalizados

Vea la documentación de host e implementación para ver cómo 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 (mediante la HeaderConverter propiedad ).

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 la entrada NGINX, el certificado de cliente se pasa a la aplicación en formato con codificación 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 llama a 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 HttpClient clase . 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 HttpClientHandlermediante la ClientCertificates propiedad del controlador. A continuación, este controlador se puede usar en una instancia con nombre de HttpClientmediante el ConfigurePrimaryHttpMessageHandler método . 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 secundario 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

Raíz de ejemplo: 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 como 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 redireccionamiento. 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.

ASP.NET Core 5 y versiones posteriores agrega compatibilidad más cómoda para redirigir para adquirir 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 mediante el subdominio protegido por certificados 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 producir un error.
  • 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 y 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 otros comentarios sobre los certificados de cliente opcionales en este problema de discusión de GitHub .