Configuración de 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 ClaimsPrincipal .

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

Escenarios de proxy y equilibrador de carga

La autenticación de certificados es un escenario con estado que se usa principalmente donde un proxy o equilibrador de carga no controla el tráfico entre clientes y servidores. Si se usa un proxy o un 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 donde se usan servidores proxy y equilibradores de carga es Active Directory Federated Services (ADFS) con OpenID Conectar (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 método , llame a con sus opciones, proporcionando un delegado para para realizar cualquier validación adicional en el certificado de Startup.ConfigureServices services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...); cliente enviado con OnCertificateValidated solicitudes. Convierta esa información en un ClaimsPrincipal y esta establezca en la propiedad context.Principal .

Si se produce un error en la autenticación, este 403 (Forbidden) controlador devuelve una respuesta en lugar de , como podría 401 (Unauthorized) 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 de una conexión anónima a una con un certificado.

Agregue también app.UseAuthentication(); en el Startup.Configure método . De lo contrario, HttpContext.User no se establecerá en ClaimsPrincipal 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
}
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 autenticación de certificado. El controlador construye una entidad de seguridad de usuario mediante las propiedades comunes del certificado.

Configuración de 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 = Chained, SelfSigned o All (Chained | 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 tiene el uso de clave extendida de autenticación de cliente (EKU) o ningún EKUs en absoluto. Como se indica en las especificaciones, si no se especifica ningún 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

Marca que especifica qué certificados de la cadena se comprueban para la 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 realiza el servidor antes de que se reciba la primera solicitud en esa conexión, por lo que no es posible el ámbito en función de los campos de solicitud.

Eventos de controlador

El controlador tiene dos eventos:

  • OnAuthenticationFailed: se llama si se produce una excepción durante la autenticación y permite reaccionar.
  • OnCertificateValidated: se llama una vez validado el certificado, se pasa la validación y se crea 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 los servicios conocen el certificado.

    • 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 descubre que el certificado de entrada no cumple la validación adicional, llame context.Fail("failure reason") a por 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 una huella digital en una directiva de autorización, en lugar de dentro OnCertificateValidated de , 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 Listen antes 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 Configuración SSL en la ventana Vista características.
  3. Active la casilla Requerir SSL y seleccione el botón de radio Requerir en la sección Certificados de cliente.

Configuración del certificado de cliente en IIS

Azure y servidores proxy web personalizados

Consulte la documentación de host e implementación para obtener información sobre 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 de reenvío.

Nota

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

Para más información, consulte Uso de un certificado TLS/SSLen el 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 ejecuta el proxy inverso de la aplicación con la configuración o se implementa en Kubernetes mediante la entrada de NGINX, el certificado de cliente se pasa a la aplicación en formato codificado como proxy_set_header ssl-client-cert $ssl_client_escaped_cert URL. Para usar el certificado, descodifique el certificado como se muestra a continuación:

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

Agregue el espacio de nombres System.Net de a la parte superior de Startup.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);
}

A Startup.Configure continuación, el método agrega el middleware. UseCertificateForwarding Se llama a antes de las llamadas UseAuthentication a 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 pueda 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. Se usaría dentro del AddCertificate método . También puede validar el firmante 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

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

private async Task<JsonDocument> GetApiDataUsingHttpClientHandler()
{
    var cert = new X509Certificate2(Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234");
    var handler = new HttpClientHandler();
    handler.ClientCertificates.Add(cert);
    var client = new HttpClient(handler);
     
    var request = new HttpRequestMessage()
    {
        RequestUri = new Uri("https://localhost:44379/api/values"),
        Method = HttpMethod.Get,
    };
    var response = await client.SendAsync(request);
    if (response.IsSuccessStatusCode)
    {
        var responseContent = await response.Content.ReadAsStringAsync();
        var data = JsonDocument.Parse(responseContent);
        return data;
    }
 
    throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
}

Implementación de httpClient mediante un certificado y un httpclient con nombre de IHttpClientFactory

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

var clientCertificate = 
    new X509Certificate2(
      Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234");
 
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(clientCertificate);
 
services.AddHttpClient("namedClient", c =>
{
}).ConfigurePrimaryHttpMessageHandler(() => handler);

A IHttpClientFactory 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 clase se usa para obtener la instancia de Startup . 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 se envía el certificado correcto al servidor, se devuelven los datos. Si no se envía ningún certificado o un certificado incorrecto, se devuelve un código de estado HTTP 403.

Creación de certificados en PowerShell

La creación de los 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 parámetro y el parámetro como KeyUsageProperty KeyUsage se muestra.

Creación de una entidad de certificación 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 no creado por una entidad de certificación no será de confianza de forma predeterminada. En el vínculo siguiente se explica cómo se puede lograr esto Windows:

https://social.msdn.microsoft.com/Forums/SqlServer/5ed119ef-1704-4be4-8a4f-ef11de7c8f34/a-certificate-chain-processed-but-terminated-in-a-root-certificate-which-is-not-trusted-by-the

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

$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. Se trata de la entidad final y no es necesario crear más certificados secundarios.

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the Intermediate certificate..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com"

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt

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

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

$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

Cuando se usan los certificados raíz, intermedio o secundario, los certificados se pueden validar mediante la huella digital o la clave pública 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 AddCertificateCache a 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 memoria. Puede proporcionar su propia memoria caché implementando ICertificateValidationCache y registrándose con la inserción de dependencias. Por ejemplo, services.AddSingleton<ICertificateValidationCache, YourCache>().

Certificados de cliente opcionales

En esta sección se proporciona información para las aplicaciones que deben proteger un subconjunto de la aplicación con un certificado. Por ejemplo, una Razor página o un controlador de 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 los datos HTTP estén disponibles.

Hay dos enfoques para implementar certificados de cliente opcionales:

  1. Uso de nombres de host independientes (SNI) y redireccionamiento. Aunque hay más trabajo para configurar, 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 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 del certificado de cliente.
    • Por ejemplo, redirija a myClient.contoso.com/requestedPage . Dado que la solicitud a es un nombre de host diferente de , el cliente establece una conexión myClient.contoso.com/requestedPage diferente y se proporciona el certificado de contoso.com/requestedPage 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 TLS para incluir un dominio virtual como parte de la negociación SSL. Esto significa que el nombre de dominio virtual, o un nombre de host, se puede usar para identificar el punto de conexión de red.

Renegociación

La renegociación de TLS es un proceso mediante el cual el cliente y el servidor pueden volver a evaluar los requisitos de cifrado para una conexión individual, incluida la solicitud de un certificado de cliente si no se ha proporcionado previamente. La renegociación de TLS es un riesgo de seguridad y no se recomienda porque:

  • En HTTP/1.1, el servidor primero debe almacenar en búfer o consumir los datos HTTP que están en proceso, como los 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 de negociar el certificado de cliente para esas solicitudes. Consulte Configuración en la documentación de IIS para obtener más información.

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

HttpSys

HttpSys tiene dos configuraciones que controlan la neentación del certificado de cliente y ambas deben establecerse. La primera está en netsh.exe en http add sslcert clientcertnegotation=enable/disable . Esta marca indica si el certificado de cliente debe ser insignificante al principio de una conexión y debe establecerse en para los certificados de disable cliente opcionales. Consulte los documentos de netsh para obtener más información.

El otro valor es ClientCertificateMethod . Cuando se establece en AllowRenegotation , el certificado de cliente se puede volver a negociar 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 ClientCertificate la 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. Tenga GetClientCertificateAsync en cuenta que puede devolver un certificado nulo si el cliente rechaza proporcionar uno.

NOTA El comportamiento de la ClientCertificate propiedad ha cambiado en .NET 6; vea si trabaja con versiones https://github.com/aspnet/Announcements/issues/466 anteriores.

Hay un problema conocido por el que la habilitación puede hacer que la renegociación se ejecute sincrónicamente al acceder a la propiedad AllowRenegotation ClientCertificate . Llame al GetClientCertificateAsync método para evitarlo. Esto se ha solucionado en .NET 6, vea https://github.com/aspnet/Announcements/issues/466 . Tenga GetClientCertificateAsync en cuenta que puede devolver un certificado nulo si el cliente rechaza proporcionar uno.

Kestrel

Kestrel controla la neentación del certificado de cliente con la ClientCertificateMode opción .

Kestrel.Https.ClientCertificateMode.DelayCertificate es una nueva opción disponible en .NET 6 o posterior. Cuando se establece, una aplicación puede comprobar ClientCertificate la 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. Tenga GetClientCertificateAsync en cuenta que puede devolver un certificado nulo 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 producir InvalidOperationExeption: Client stream needs to be drained before renegotiation. .

Si va a configurar mediante programación los valores de TLS por host, hay una nueva sobrecarga UseHttps disponible en .NET 6 y versiones posteriores que toma y controla la renegociación de certificados de cliente a través de Server.Kestrel.Https.TlsHandshakeCallbackOptions Kestrel.Https.TlsHandshakeCallbackContext.AllowDelayedClientCertificateNegotation .

Para .NET 5 y versiones anteriores no admite la renegociación después del inicio Kestrel 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 GitHub de discusión.