Configurare l'autenticazione del certificato in ASP.NET Core

Microsoft.AspNetCore.Authentication.Certificatecontiene un'implementazione simile all'autenticazione del certificato per ASP.NET Core. L'autenticazione del certificato avviene a livello di TLS, molto prima che arrivi a ASP.NET Core. Più accuratamente, si tratta di un gestore di autenticazione che convalida il certificato e quindi fornisce un evento in cui è possibile risolvere il certificato in un oggetto ClaimsPrincipal.

È necessarioconfigurare il server per l'autenticazione del certificato, ad esempio IIS, Kestrel, Azure App Web o qualsiasi altra operazione in uso.

Scenari di proxy e bilanciamento del carico

L'autenticazione del certificato è uno scenario con stato usato principalmente in cui un proxy o un servizio di bilanciamento del carico non gestisce il traffico tra client e server. Se si usa un proxy o un servizio di bilanciamento del carico, l'autenticazione del certificato funziona solo se il proxy o il servizio di bilanciamento del carico:

  • Gestisce l'autenticazione.
  • Passa le informazioni di autenticazione utente all'app (ad esempio, in un'intestazione di richiesta), che agisce sulle informazioni di autenticazione.

Un'alternativa all'autenticazione del certificato negli ambienti in cui vengono usati proxy e servizi di bilanciamento del carico è Active Directory Federated Services (ADFS) con OpenID Connessione (OIDC).

Attività iniziali

Acquisire un certificato HTTPS, applicarlo e configurare il server per richiedere i certificati.

Nell'applicazione Web:

  • Aggiungere un riferimento al pacchetto NuGet Microsoft.AspNetCore.Authentication.Certificate .
  • In Program.cschiamare builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...);. Specificare un delegato per OnCertificateValidated eseguire qualsiasi convalida supplementare sul certificato client inviato con le richieste. Trasformare tali informazioni in un ClaimsPrincipal oggetto e impostarlo sulla context.Principal proprietà .

Se l'autenticazione non riesce, questo gestore restituisce una 403 (Forbidden) risposta piuttosto che un 401 (Unauthorized)oggetto , come previsto. Il motivo è che l'autenticazione deve avvenire durante la connessione TLS iniziale. Quando raggiunge il gestore, è troppo tardi. Non è possibile aggiornare la connessione da una connessione anonima a una con un certificato.

UseAuthentication è necessario per impostare HttpContext.User su un ClaimsPrincipal oggetto creato dal certificato. Ad esempio:

var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

app.UseAuthentication();

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

app.Run();

L'esempio precedente illustra il modo predefinito per aggiungere l'autenticazione del certificato. Il gestore costruisce un'entità utente usando le proprietà comuni del certificato.

Configurare la convalida del certificato

Il CertificateAuthenticationOptions gestore include alcune convalide predefinite che rappresentano le convalide minime da eseguire su un certificato. Ognuna di queste impostazioni è abilitata per impostazione predefinita.

AllowedCertificateTypes = Concatenato, SelfSigned o Tutto (concatenato | SelfSigned)

Valore predefinito: CertificateTypes.Chained

Questo controllo verifica che sia consentito solo il tipo di certificato appropriato. Se l'app usa certificati autofirmato, questa opzione deve essere impostata su CertificateTypes.All o CertificateTypes.SelfSigned.

ChainTrustValidationMode

Valore predefinito: X509ChainTrustMode.System

Il certificato presentato dal client deve essere concatenato a un certificato radice attendibile. Questo controllo controlla quale archivio attendibilità contiene questi certificati radice.

Per impostazione predefinita, il gestore usa l'archivio attendibilità del sistema. Se il certificato client presentato deve essere concatenato a un certificato radice che non viene visualizzato nell'archivio attendibilità del sistema, questa opzione può essere impostata su X509ChainTrustMode.CustomRootTrust per fare in modo che il gestore usi .CustomTrustStore

CustomTrustStore

Valore predefinito: Vuoto X509Certificate2Collection

Se la proprietà del ChainTrustValidationMode gestore è impostata su X509ChainTrustMode.CustomRootTrust, contiene X509Certificate2Collection ogni certificato che verrà usato per convalidare il certificato client fino a una radice attendibile, inclusa la radice attendibile.

Quando il client presenta un certificato che fa parte di una catena di certificati multi-livello, CustomTrustStore deve contenere ogni certificato emittente nella catena.

ValidateCertificateUse

Valore predefinito: true

Questo controllo verifica che il certificato presentato dal client disponga dell'uso della chiave estesa per l'autenticazione client (EKU) o che non siano presenti EKU. Come indicato dalle specifiche, se non viene specificato alcun EKU, tutte le EKU vengono considerate valide.

ValidateValidityPeriod

Valore predefinito: true

Questo controllo verifica che il certificato sia compreso nel periodo di validità. In ogni richiesta, il gestore garantisce che un certificato valido quando è stato presentato non sia scaduto durante la sessione corrente.

RevocheFlag

Valore predefinito: X509RevocationFlag.ExcludeRoot

Flag che specifica quali certificati nella catena vengono controllati per la revoca.

I controlli di revoca vengono eseguiti solo quando il certificato viene concatenato a un certificato radice.

RevocationMode

Valore predefinito: X509RevocationMode.Online

Flag che specifica come vengono eseguiti i controlli di revoca.

La specifica di un controllo online può comportare un lungo ritardo mentre viene contattata l'autorità di certificazione.

I controlli di revoca vengono eseguiti solo quando il certificato viene concatenato a un certificato radice.

È possibile configurare l'app per richiedere un certificato solo in determinati percorsi?

Non è possibile. Tenere presente che lo scambio di certificati viene eseguito all'inizio della conversazione HTTPS, viene eseguito dal server prima che la prima richiesta venga ricevuta su tale connessione in modo che non sia possibile definire l'ambito in base a qualsiasi campo della richiesta.

Eventi del gestore

Il gestore ha due eventi:

  • OnAuthenticationFailed: Chiamato se si verifica un'eccezione durante l'autenticazione e consente di reagire.
  • OnCertificateValidated: Chiamato dopo che il certificato è stato convalidato, è stata superata la convalida e è stata creata un'entità predefinita. Questo evento consente di eseguire la propria convalida e di aumentare o sostituire l'entità. Ad esempio:
    • Determinare se il certificato è noto ai servizi.

    • Creazione di un'entità personalizzata. Si consideri l'esempio seguente:

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

Se il certificato in ingresso non soddisfa la convalida aggiuntiva, chiamare context.Fail("failure reason") con un motivo di errore.

Per una migliore funzionalità, chiamare un servizio registrato nell'inserimento delle dipendenze che si connette a un database o a un altro tipo di archivio utenti. Accedere al servizio usando il contesto passato al delegato. Si consideri l'esempio seguente:

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

Concettualmente, la convalida del certificato è un problema di autorizzazione. L'aggiunta di un controllo, ad esempio, un'autorità emittente o un'identificazione personale in un criterio di autorizzazione, anziché all'interno OnCertificateValidateddi , è perfettamente accettabile.

Configurare il server per richiedere i certificati

Kestrel

In Program.csconfigurare Kestrel come segue:

var builder = WebApplication.CreateBuilder(args);

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

Nota

Gli endpoint creati chiamando Listenprima della chiamata ConfigureHttpsDefaults non avranno le impostazioni predefinite applicate.

IIS

Completare i passaggi seguenti in Gestione IIS:

  1. Selezionare il sito nella scheda Connessione ions.
  2. Fare doppio clic sull'opzione SSL Impostazioni nella finestra Visualizzazione funzionalità.
  3. Selezionare la casella di controllo Richiedi SSL e selezionare il pulsante di opzione Richiedi certificati nella sezione Certificati client.

Client certificate settings in IIS

Azure e proxy Web personalizzati

Per informazioni su come configurare il middleware di inoltro dei certificati, vedere la documentazione relativa all'host e alla distribuzione.

Usare l'autenticazione del certificato in Azure App Web

Non è necessaria alcuna configurazione di inoltro per Azure. La configurazione di inoltro viene configurata dal middleware di inoltro dei certificati.

Nota

Il middleware di inoltro dei certificati è necessario per questo scenario.

Per altre informazioni, vedere Usare un certificato TLS/SSL nel codice in app Azure Service (documentazione di Azure).

Usare l'autenticazione del certificato nei proxy Web personalizzati

Il AddCertificateForwarding metodo viene usato per specificare:

  • Nome dell'intestazione del client.
  • Modalità di caricamento del certificato (tramite la HeaderConverter proprietà ).

Nei proxy Web personalizzati il certificato viene passato come intestazione di richiesta personalizzata, ad esempio X-SSL-CERT. Per usarlo, configurare l'inoltro dei certificati in Program.cs:

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

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

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

        return clientCertificate!;

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

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

            return bytes;
        }
    };
});

Se l'app è inversa tramite proxy da NGINX con la configurazione proxy_set_header ssl-client-cert $ssl_client_escaped_cert o distribuita in Kubernetes usando NGINX Ingress, il certificato client viene passato all'app in formato con codifica URL. Per usare il certificato, decodificarlo come segue:

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

Aggiungere il middleware in Program.cs. UseCertificateForwarding viene chiamato prima delle chiamate a UseAuthentication e UseAuthorization:

var app = builder.Build();

app.UseCertificateForwarding();

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

È possibile usare una classe separata per implementare la logica di convalida. Poiché in questo esempio viene usato lo stesso certificato autofirmato, assicurarsi che sia possibile usare solo il certificato. Verificare che le identificazioni personali del certificato client e del certificato server corrispondano, altrimenti è possibile usare qualsiasi certificato e sarà sufficiente per l'autenticazione. Questa operazione verrà utilizzata all'interno del AddCertificate metodo . È anche possibile convalidare l'oggetto o l'autorità emittente se si usano certificati intermedi o figlio.

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

Implementare un httpClient usando un certificato e IHttpClientFactory

Nell'esempio seguente viene aggiunto un certificato client a un HttpClientHandler oggetto usando la ClientCertificates proprietà del gestore. Questo gestore può quindi essere usato in un'istanza denominata di un HttpClient oggetto usando il ConfigurePrimaryHttpMessageHandler metodo . Questa è la configurazione in 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;
});

Può IHttpClientFactory quindi essere usato per ottenere l'istanza denominata con il gestore e il certificato. Il CreateClient metodo con il nome del client definito in Program.cs viene usato per ottenere l'istanza. La richiesta HTTP può essere inviata usando il client in base alle esigenze:

public class SampleHttpService
{
    private readonly IHttpClientFactory _httpClientFactory;

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

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

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

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

Se il certificato corretto viene inviato al server, vengono restituiti i dati. Se non viene inviato alcun certificato o certificato errato, viene restituito un codice di stato HTTP 403.

Creare certificati in PowerShell

La creazione dei certificati è la parte più difficile nella configurazione di questo flusso. È possibile creare un certificato radice usando il New-SelfSignedCertificate cmdlet di PowerShell. Quando si crea il certificato, usare una password complessa. È importante aggiungere il KeyUsageProperty parametro e il KeyUsage parametro come illustrato.

Creare una CA radice

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

Il valore del -DnsName parametro deve corrispondere alla destinazione di distribuzione dell'app. Ad esempio, "localhost" per lo sviluppo.

Eseguire l'installazione nella radice attendibile

Il certificato radice deve essere considerato attendibile nel sistema host. Un certificato radice che non è stato creato da un'autorità di certificazione non sarà considerato attendibile per impostazione predefinita. Per informazioni su come considerare attendibile il certificato radice in Windows, vedere questa domanda.

Certificato intermedio

È ora possibile creare un certificato intermedio dal certificato radice. Questo non è necessario per tutti i casi d'uso, ma potrebbe essere necessario creare molti certificati o dover attivare o disabilitare gruppi di certificati. Il TextExtension parametro è necessario per impostare la lunghezza del percorso nei vincoli di base del certificato.

Il certificato intermedio può quindi essere aggiunto al certificato intermedio attendibile nel sistema host Windows.

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

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint of the root..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "intermediate_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "intermediate_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")

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

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

Creare un certificato figlio dal certificato intermedio

È possibile creare un certificato figlio dal certificato intermedio. Si tratta dell'entità finale e non è necessario creare più certificati figlio.

$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

Creare un certificato figlio dal certificato radice

È anche possibile creare un certificato figlio direttamente dal certificato radice.

$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

Radice di esempio - Certificato intermedio - Certificato

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

New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "root_ca_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature

Get-ChildItem -Path cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwdroot

Export-Certificate -Cert cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 -FilePath root_ca_dev_damienbod.crt

$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\0C89639E4E2998A93E423F919B36D4009A0F9991 )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")

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

Export-Certificate -Cert cert:\localMachine\my\BA9BF91ED35538A01375EFC212A2F46104B33A44 -FilePath child_a_dev_damienbod.crt

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\BA9BF91ED35538A01375EFC212A2F46104B33A44 )

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

Get-ChildItem -Path cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_b_from_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A -FilePath child_b_from_a_dev_damienbod.crt

Quando si usano i certificati radice, intermedio o figlio, i certificati possono essere convalidati usando l'identificazione personale o PublicKey in base alle esigenze:

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

Memorizzazione nella cache della convalida dei certificati

ASP.NET Core 5.0 e versioni successive supportano la possibilità di abilitare la memorizzazione nella cache dei risultati della convalida. La memorizzazione nella cache migliora notevolmente le prestazioni dell'autenticazione del certificato, perché la convalida è un'operazione costosa.

Per impostazione predefinita, l'autenticazione del certificato disabilita la memorizzazione nella cache. Per abilitare la memorizzazione nella cache, chiamare AddCertificateCache in Program.cs:

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

L'implementazione di memorizzazione nella cache predefinita archivia i risultati in memoria. È possibile fornire la propria cache implementandola ICertificateValidationCache e registrandola con l'inserimento delle dipendenze. Ad esempio: services.AddSingleton<ICertificateValidationCache, YourCache>().

Certificati client facoltativi

Questa sezione fornisce informazioni per le app che devono proteggere un subset dell'app con un certificato. Ad esempio, un controller o una Razor pagina nell'app potrebbe richiedere certificati client. Ciò presenta problemi come certificati client:

  • Si tratta di una funzionalità TLS, non di una funzionalità HTTP.
  • Vengono negoziati per connessione e in genere all'inizio della connessione prima che siano disponibili dati HTTP.

Esistono due approcci per implementare i certificati client facoltativi:

  1. Uso di nomi host separati (SNI) e reindirizzamento. Anche se è consigliabile eseguire altre operazioni per la configurazione, è consigliabile perché funziona nella maggior parte degli ambienti e dei protocolli.
  2. Rinegoziazione durante una richiesta HTTP. Questo presenta diverse limitazioni e non è consigliato.

Host separati (SNI)

All'inizio della connessione è nota solo l'indicazione del nome server (SNI) †. I certificati client possono essere configurati per nome host in modo che un host li richieda e un altro no.

ASP.NET Core 5 e versioni successive aggiunge un supporto più pratico per il reindirizzamento per acquisire certificati client facoltativi. Per altre informazioni, vedere l'esempio di certificati facoltativi.

  • Per le richieste all'app Web che richiedono un certificato client e non ne hanno una:
    • Eseguire il reindirizzamento alla stessa pagina usando il sottodominio protetto dal certificato client.
    • Ad esempio, reindirizzare a myClient.contoso.com/requestedPage. Poiché la richiesta a myClient.contoso.com/requestedPage è un nome host diverso da contoso.com/requestedPage, il client stabilisce una connessione diversa e viene fornito il certificato client.
    • Per altre informazioni, vedere Introduzione all'autorizzazione in ASP.NET Core.

† Server Name Indication (SNI) è un'estensione TLS per includere un dominio virtuale come parte della negoziazione SSL. Ciò significa che il nome di dominio virtuale, o un nome host, può essere usato per identificare l'endpoint di rete.

Rinegoziazione

La rinegoziazione TLS è un processo in base al quale il client e il server possono valutare nuovamente i requisiti di crittografia per una singola connessione, inclusa la richiesta di un certificato client se non specificato in precedenza. La rinegoziazione TLS è un rischio per la sicurezza e non è consigliata perché:

  • In HTTP/1.1 il server deve prima memorizzare nel buffer o utilizzare tutti i dati HTTP in esecuzione, ad esempio i corpi di richiesta POST, per assicurarsi che la connessione sia chiara per la rinegoziazione. In caso contrario, la rinegoziazione può interrompere la risposta o non riuscire.
  • HTTP/2 e HTTP/3 vietano esplicitamente la rinegoziazione.
  • Esistono rischi per la sicurezza associati alla rinegoziazione. TLS 1.3 ha rimosso la rinegoziazione dell'intera connessione e l'ha sostituita con una nuova estensione per richiedere solo il certificato client dopo l'avvio della connessione. Questo meccanismo viene esposto tramite le stesse API ed è comunque soggetto ai vincoli precedenti delle versioni del protocollo HTTP e del buffering.

L'implementazione e la configurazione di questa funzionalità variano in base alla versione del server e del framework.

IIS

IIS gestisce la negoziazione del certificato client per conto dell'utente. Una sottosezione dell'applicazione può abilitare l'opzione SslRequireCert per negoziare il certificato client per tali richieste. Per informazioni dettagliate, vedere Configurazione nella documentazione di IIS.

IIS memorizza automaticamente nel buffer tutti i dati del corpo della richiesta fino a un limite di dimensioni configurato prima della rinegoziazione. Le richieste che superano il limite vengono rifiutate con una risposta 413. Per impostazione predefinita, questo limite è 48 KB ed è configurabile impostando uploadReadAheadSize.

HttpSys

HttpSys ha due impostazioni che controllano la negoziazione del certificato client ed entrambi devono essere impostati. Il primo si trova in netsh.exe in http add sslcert clientcertnegotiation=enable/disable. Questo flag indica se il certificato client deve essere negoziato all'inizio di una connessione e deve essere impostato su disable per i certificati client facoltativi. Per informazioni dettagliate, vedere la documentazione netsh.

L'altra impostazione è ClientCertificateMethod. Se impostato su AllowRenegotation, il certificato client può essere rinegoziato durante una richiesta.

NOTA L'applicazione deve memorizzare nel buffer o utilizzare i dati del corpo della richiesta prima di tentare la rinegoziazione; in caso contrario, la richiesta potrebbe non rispondere.

Un'applicazione può prima controllare la ClientCertificate proprietà per verificare se il certificato è disponibile. Se non è disponibile, verificare che il corpo della richiesta sia stato utilizzato prima di chiamare GetClientCertificateAsync per negoziare uno. Nota GetClientCertificateAsync può restituire un certificato Null se il client rifiuta di specificarne uno.

NOTA Il comportamento della ClientCertificate proprietà è stato modificato in .NET 6. Per altre informazioni, vedere questo problema in GitHub.

Kestrel

Kestrel controlla la negoziazione del certificato client con l'opzione ClientCertificateMode .

ClientCertificateMode.DelayCertificate è una nuova opzione disponibile in .NET 6 o versione successiva. Se impostata, un'app può controllare la ClientCertificate proprietà per verificare se il certificato è disponibile. Se non è disponibile, verificare che il corpo della richiesta sia stato utilizzato prima di chiamare GetClientCertificateAsync per negoziare uno. Nota GetClientCertificateAsync può restituire un certificato Null se il client rifiuta di specificarne uno.

NOTA L'applicazione deve memorizzare nel buffer o utilizzare i dati del corpo della richiesta prima di tentare la rinegoziazione. In caso contrario GetClientCertificateAsync , potrebbe generare un'eccezione InvalidOperationException: Client stream needs to be drained before renegotiation..

Se si configurano a livello di codice le impostazioni TLS per host, è disponibile un nuovo overload UseHttps in .NET 6 e versioni successive che accetta TlsHandshakeCallbackOptions e controlla la rinegoziazione dei certificati client tramite TlsHandshakeCallbackContext.AllowDelayedClientCertificateNegotation.

Microsoft.AspNetCore.Authentication.Certificatecontiene un'implementazione simile all'autenticazione del certificato per ASP.NET Core. L'autenticazione del certificato avviene a livello di TLS, molto prima che arrivi a ASP.NET Core. Più accuratamente, si tratta di un gestore di autenticazione che convalida il certificato e quindi fornisce un evento in cui è possibile risolvere il certificato in un oggetto ClaimsPrincipal.

Configurare il server per l'autenticazione del certificato, ad esempio IIS, Kestrel, Azure App Web o qualsiasi altra operazione in uso.

Scenari di proxy e bilanciamento del carico

L'autenticazione del certificato è uno scenario con stato usato principalmente in cui un proxy o un servizio di bilanciamento del carico non gestisce il traffico tra client e server. Se si usa un proxy o un servizio di bilanciamento del carico, l'autenticazione del certificato funziona solo se il proxy o il servizio di bilanciamento del carico:

  • Gestisce l'autenticazione.
  • Passa le informazioni di autenticazione utente all'app (ad esempio, in un'intestazione di richiesta), che agisce sulle informazioni di autenticazione.

Un'alternativa all'autenticazione del certificato negli ambienti in cui vengono usati proxy e servizi di bilanciamento del carico è Active Directory Federated Services (ADFS) con OpenID Connessione (OIDC).

Attività iniziali

Acquisire un certificato HTTPS, applicarlo e configurare il server per richiedere i certificati.

Nell'app Web aggiungere un riferimento al pacchetto Microsoft.AspNetCore.Authentication.Certificate . Quindi, nel Startup.ConfigureServices metodo chiamare services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...); con le opzioni, fornendo un delegato per OnCertificateValidated eseguire qualsiasi convalida supplementare sul certificato client inviato con le richieste. Trasformare tali informazioni in un ClaimsPrincipal oggetto e impostarlo sulla context.Principal proprietà .

Se l'autenticazione non riesce, questo gestore restituisce una 403 (Forbidden) risposta piuttosto che un 401 (Unauthorized)oggetto , come previsto. Il motivo è che l'autenticazione deve avvenire durante la connessione TLS iniziale. Quando raggiunge il gestore, è troppo tardi. Non è possibile aggiornare la connessione da una connessione anonima a una con un certificato.

app.UseAuthentication(); Aggiungere anche nel Startup.Configure metodo . In caso contrario, HttpContext.User non verrà impostato su ClaimsPrincipal creato dal certificato. Ad esempio:

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
}

L'esempio precedente illustra il modo predefinito per aggiungere l'autenticazione del certificato. Il gestore costruisce un'entità utente usando le proprietà comuni del certificato.

Configurare la convalida del certificato

Il CertificateAuthenticationOptions gestore include alcune convalide predefinite che rappresentano le convalide minime da eseguire su un certificato. Ognuna di queste impostazioni è abilitata per impostazione predefinita.

AllowedCertificateTypes = Concatenato, SelfSigned o Tutto (concatenato | SelfSigned)

Valore predefinito: CertificateTypes.Chained

Questo controllo verifica che sia consentito solo il tipo di certificato appropriato. Se l'app usa certificati autofirmato, questa opzione deve essere impostata su CertificateTypes.All o CertificateTypes.SelfSigned.

ValidateCertificateUse

Valore predefinito: true

Questo controllo verifica che il certificato presentato dal client disponga dell'uso della chiave estesa per l'autenticazione client (EKU) o che non siano presenti EKU. Come indicato dalle specifiche, se non viene specificato alcun EKU, tutte le EKU vengono considerate valide.

ValidateValidityPeriod

Valore predefinito: true

Questo controllo verifica che il certificato sia compreso nel periodo di validità. In ogni richiesta, il gestore garantisce che un certificato valido quando è stato presentato non sia scaduto durante la sessione corrente.

RevocheFlag

Valore predefinito: X509RevocationFlag.ExcludeRoot

Flag che specifica quali certificati nella catena vengono controllati per la revoca.

I controlli di revoca vengono eseguiti solo quando il certificato viene concatenato a un certificato radice.

RevocationMode

Valore predefinito: X509RevocationMode.Online

Flag che specifica come vengono eseguiti i controlli di revoca.

La specifica di un controllo online può comportare un lungo ritardo mentre viene contattata l'autorità di certificazione.

I controlli di revoca vengono eseguiti solo quando il certificato viene concatenato a un certificato radice.

È possibile configurare l'app per richiedere un certificato solo in determinati percorsi?

Non è possibile. Tenere presente che lo scambio di certificati viene eseguito all'inizio della conversazione HTTPS, viene eseguito dal server prima che la prima richiesta venga ricevuta su tale connessione in modo che non sia possibile definire l'ambito in base a qualsiasi campo della richiesta.

Eventi del gestore

Il gestore ha due eventi:

  • OnAuthenticationFailed: Chiamato se si verifica un'eccezione durante l'autenticazione e consente di reagire.
  • OnCertificateValidated: Chiamato dopo che il certificato è stato convalidato, è stata superata la convalida e è stata creata un'entità predefinita. Questo evento consente di eseguire la propria convalida e di aumentare o sostituire l'entità. Ad esempio:
    • Determinare se il certificato è noto ai servizi.

    • Creazione di un'entità personalizzata. Si consideri l'esempio seguente in Startup.ConfigureServices:

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

Se il certificato in ingresso non soddisfa la convalida aggiuntiva, chiamare context.Fail("failure reason") con un motivo di errore.

Per le funzionalità reali, è probabile che si voglia chiamare un servizio registrato nell'inserimento delle dipendenze che si connette a un database o a un altro tipo di archivio utenti. Accedere al servizio usando il contesto passato al delegato. Si consideri l'esempio seguente in 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;
            }
        };
    });

Concettualmente, la convalida del certificato è un problema di autorizzazione. L'aggiunta di un controllo, ad esempio, un'autorità emittente o un'identificazione personale in un criterio di autorizzazione, anziché all'interno OnCertificateValidateddi , è perfettamente accettabile.

Configurare il server per richiedere i certificati

Kestrel

In Program.csconfigurare Kestrel come segue:

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

Gli endpoint creati chiamando Listenprima della chiamata ConfigureHttpsDefaults non avranno le impostazioni predefinite applicate.

IIS

Completare i passaggi seguenti in Gestione IIS:

  1. Selezionare il sito nella scheda Connessione ions.
  2. Fare doppio clic sull'opzione SSL Impostazioni nella finestra Visualizzazione funzionalità.
  3. Selezionare la casella di controllo Richiedi SSL e selezionare il pulsante di opzione Richiedi certificati nella sezione Certificati client.

Client certificate settings in IIS

Azure e proxy Web personalizzati

Per informazioni su come configurare il middleware di inoltro dei certificati, vedere la documentazione relativa all'host e alla distribuzione.

Usare l'autenticazione del certificato in Azure App Web

Non è necessaria alcuna configurazione di inoltro per Azure. La configurazione di inoltro viene configurata dal middleware di inoltro dei certificati.

Nota

Il middleware di inoltro dei certificati è necessario per questo scenario.

Per altre informazioni, vedere Usare un certificato TLS/SSL nel codice in app Azure Service (documentazione di Azure).

Usare l'autenticazione del certificato nei proxy Web personalizzati

Il AddCertificateForwarding metodo viene usato per specificare:

  • Nome dell'intestazione del client.
  • Modalità di caricamento del certificato (tramite la HeaderConverter proprietà ).

Nei proxy Web personalizzati il certificato viene passato come intestazione di richiesta personalizzata, ad esempio X-SSL-CERT. Per usarlo, configurare l'inoltro dei certificati in Startup.ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.AddCertificateForwarding(options =>
    {
        options.CertificateHeader = "X-SSL-CERT";
        options.HeaderConverter = (headerValue) =>
        {
            X509Certificate2 clientCertificate = null;

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

            return clientCertificate;
        };
    });
}

private static byte[] StringToByteArray(string hex)
{
    int NumberChars = hex.Length;
    byte[] bytes = new byte[NumberChars / 2];

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

    return bytes;
}

Se l'app è inversa tramite proxy da NGINX con la configurazione proxy_set_header ssl-client-cert $ssl_client_escaped_cert o distribuita in Kubernetes usando NGINX Ingress, il certificato client viene passato all'app in formato con codifica URL. Per usare il certificato, decodificarlo come segue:

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

Il Startup.Configure metodo aggiunge quindi il middleware. UseCertificateForwarding viene chiamato prima delle chiamate a UseAuthentication e UseAuthorization:

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

    app.UseRouting();

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

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

È possibile usare una classe separata per implementare la logica di convalida. Poiché in questo esempio viene usato lo stesso certificato autofirmato, assicurarsi che sia possibile usare solo il certificato. Verificare che le identificazioni personali del certificato client e del certificato server corrispondano, altrimenti è possibile usare qualsiasi certificato e sarà sufficiente per l'autenticazione. Questa operazione verrà utilizzata all'interno del AddCertificate metodo . È anche possibile convalidare l'oggetto o l'autorità emittente se si usano certificati intermedi o figlio.

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

Implementare un httpClient usando un certificato e HttpClientHandler

Può HttpClientHandler essere aggiunto direttamente nel costruttore della HttpClient classe . Prestare attenzione quando si creano istanze di HttpClient. HttpClient invierà quindi il certificato con ogni richiesta.

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

Implementare un httpClient usando un certificato e un httpClient denominato da IHttpClientFactory

Nell'esempio seguente viene aggiunto un certificato client a un HttpClientHandler oggetto usando la ClientCertificates proprietà del gestore. Questo gestore può quindi essere usato in un'istanza denominata di un HttpClient oggetto usando il ConfigurePrimaryHttpMessageHandler metodo . Questa è la configurazione in 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;
});

Può IHttpClientFactory quindi essere usato per ottenere l'istanza denominata con il gestore e il certificato. Il CreateClient metodo con il nome del client definito nella Startup classe viene usato per ottenere l'istanza. La richiesta HTTP può essere inviata usando il client in base alle esigenze.

private readonly IHttpClientFactory _clientFactory;

public ApiService(IHttpClientFactory clientFactory)
{
    _clientFactory = clientFactory;
}

private async Task<JsonDocument> GetApiDataWithNamedClient()
{
    var client = _clientFactory.CreateClient("namedClient");

    var request = new HttpRequestMessage()
    {
        RequestUri = new Uri("https://localhost:44379/api/values"),
        Method = HttpMethod.Get,
    };
    var response = await client.SendAsync(request);
    if (response.IsSuccessStatusCode)
    {
        var responseContent = await response.Content.ReadAsStringAsync();
        var data = JsonDocument.Parse(responseContent);
        return data;
    }

    throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
}

Se il certificato corretto viene inviato al server, vengono restituiti i dati. Se non viene inviato alcun certificato o certificato errato, viene restituito un codice di stato HTTP 403.

Creare certificati in PowerShell

La creazione dei certificati è la parte più difficile nella configurazione di questo flusso. È possibile creare un certificato radice usando il New-SelfSignedCertificate cmdlet di PowerShell. Quando si crea il certificato, usare una password complessa. È importante aggiungere il KeyUsageProperty parametro e il KeyUsage parametro come illustrato.

Creare una CA radice

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

Il valore del -DnsName parametro deve corrispondere alla destinazione di distribuzione dell'app. Ad esempio, "localhost" per lo sviluppo.

Eseguire l'installazione nella radice attendibile

Il certificato radice deve essere considerato attendibile nel sistema host. Un certificato radice che non è stato creato da un'autorità di certificazione non sarà considerato attendibile per impostazione predefinita. Per informazioni su come considerare attendibile il certificato radice in Windows, vedere questa domanda.

Certificato intermedio

È ora possibile creare un certificato intermedio dal certificato radice. Questo non è necessario per tutti i casi d'uso, ma potrebbe essere necessario creare molti certificati o dover attivare o disabilitare gruppi di certificati. Il TextExtension parametro è necessario per impostare la lunghezza del percorso nei vincoli di base del certificato.

Il certificato intermedio può quindi essere aggiunto al certificato intermedio attendibile nel sistema host Windows.

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

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint of the root..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "intermediate_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "intermediate_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")

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

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

Creare un certificato figlio dal certificato intermedio

È possibile creare un certificato figlio dal certificato intermedio. Si tratta dell'entità finale e non è necessario creare più certificati figlio.

$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

Creare un certificato figlio dal certificato radice

È anche possibile creare un certificato figlio direttamente dal certificato radice.

$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

Radice di esempio - Certificato intermedio - Certificato

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

New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "root_ca_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature

Get-ChildItem -Path cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwdroot

Export-Certificate -Cert cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 -FilePath root_ca_dev_damienbod.crt

$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\0C89639E4E2998A93E423F919B36D4009A0F9991 )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")

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

Export-Certificate -Cert cert:\localMachine\my\BA9BF91ED35538A01375EFC212A2F46104B33A44 -FilePath child_a_dev_damienbod.crt

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\BA9BF91ED35538A01375EFC212A2F46104B33A44 )

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

Get-ChildItem -Path cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_b_from_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A -FilePath child_b_from_a_dev_damienbod.crt

Quando si usano i certificati radice, intermedio o figlio, i certificati possono essere convalidati usando l'identificazione personale o PublicKey in base alle esigenze.

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

Memorizzazione nella cache della convalida dei certificati

ASP.NET Core 5.0 e versioni successive supportano la possibilità di abilitare la memorizzazione nella cache dei risultati della convalida. La memorizzazione nella cache migliora notevolmente le prestazioni dell'autenticazione del certificato, perché la convalida è un'operazione costosa.

Per impostazione predefinita, l'autenticazione del certificato disabilita la memorizzazione nella cache. Per abilitare la memorizzazione nella cache, chiamare AddCertificateCache in Startup.ConfigureServices:

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

L'implementazione di memorizzazione nella cache predefinita archivia i risultati in memoria. È possibile fornire la propria cache implementandola ICertificateValidationCache e registrandola con l'inserimento delle dipendenze. Ad esempio: services.AddSingleton<ICertificateValidationCache, YourCache>().

Certificati client facoltativi

Questa sezione fornisce informazioni per le app che devono proteggere un subset dell'app con un certificato. Ad esempio, un controller o una Razor pagina nell'app potrebbe richiedere certificati client. Ciò presenta problemi come certificati client:

  • Si tratta di una funzionalità TLS, non di una funzionalità HTTP.
  • Vengono negoziati per connessione e in genere all'inizio della connessione prima che siano disponibili dati HTTP.

Esistono due approcci per implementare i certificati client facoltativi:

  1. Uso di nomi host separati (SNI) e reindirizzamento. Anche se è consigliabile eseguire altre operazioni per la configurazione, è consigliabile perché funziona nella maggior parte degli ambienti e dei protocolli.
  2. Rinegoziazione durante una richiesta HTTP. Questo presenta diverse limitazioni e non è consigliato.

Host separati (SNI)

All'inizio della connessione è nota solo l'indicazione del nome server (SNI) †. I certificati client possono essere configurati per nome host in modo che un host li richieda e un altro no.

ASP.NET Core 5 e versioni successive aggiunge un supporto più pratico per il reindirizzamento per acquisire certificati client facoltativi. Per altre informazioni, vedere l'esempio di certificati facoltativi.

  • Per le richieste all'app Web che richiedono un certificato client e non ne hanno una:
    • Eseguire il reindirizzamento alla stessa pagina usando il sottodominio protetto dal certificato client.
    • Ad esempio, reindirizzare a myClient.contoso.com/requestedPage. Poiché la richiesta a myClient.contoso.com/requestedPage è un nome host diverso da contoso.com/requestedPage, il client stabilisce una connessione diversa e viene fornito il certificato client.
    • Per altre informazioni, vedere Introduzione all'autorizzazione in ASP.NET Core.

† Server Name Indication (SNI) è un'estensione TLS per includere un dominio virtuale come parte della negoziazione SSL. Ciò significa che il nome di dominio virtuale, o un nome host, può essere usato per identificare l'endpoint di rete.

Rinegoziazione

La rinegoziazione TLS è un processo in base al quale il client e il server possono valutare nuovamente i requisiti di crittografia per una singola connessione, inclusa la richiesta di un certificato client se non specificato in precedenza. La rinegoziazione TLS è un rischio per la sicurezza e non è consigliata perché:

  • In HTTP/1.1 il server deve prima memorizzare nel buffer o utilizzare tutti i dati HTTP in esecuzione, ad esempio i corpi di richiesta POST, per assicurarsi che la connessione sia chiara per la rinegoziazione. In caso contrario, la rinegoziazione può interrompere la risposta o non riuscire.
  • HTTP/2 e HTTP/3 vietano esplicitamente la rinegoziazione.
  • Esistono rischi per la sicurezza associati alla rinegoziazione. TLS 1.3 ha rimosso la rinegoziazione dell'intera connessione e l'ha sostituita con una nuova estensione per richiedere solo il certificato client dopo l'avvio della connessione. Questo meccanismo viene esposto tramite le stesse API ed è comunque soggetto ai vincoli precedenti delle versioni del protocollo HTTP e del buffering.

L'implementazione e la configurazione di questa funzionalità variano in base alla versione del server e del framework.

IIS

IIS gestisce la negoziazione del certificato client per conto dell'utente. Una sottosezione dell'applicazione può abilitare l'opzione SslRequireCert per negoziare il certificato client per tali richieste. Per informazioni dettagliate, vedere Configurazione nella documentazione di IIS.

IIS memorizza automaticamente nel buffer tutti i dati del corpo della richiesta fino a un limite di dimensioni configurato prima della rinegoziazione. Le richieste che superano il limite vengono rifiutate con una risposta 413. Per impostazione predefinita, questo limite è 48 KB ed è configurabile impostando uploadReadAheadSize.

HttpSys

HttpSys ha due impostazioni che controllano la negoziazione del certificato client ed entrambi devono essere impostati. Il primo si trova in netsh.exe in http add sslcert clientcertnegotiation=enable/disable. Questo flag indica se il certificato client deve essere negoziato all'inizio di una connessione e deve essere impostato su disable per i certificati client facoltativi. Per informazioni dettagliate, vedere la documentazione netsh.

L'altra impostazione è ClientCertificateMethod. Se impostato su AllowRenegotation, il certificato client può essere rinegoziato durante una richiesta.

NOTA L'applicazione deve memorizzare nel buffer o utilizzare i dati del corpo della richiesta prima di tentare la rinegoziazione; in caso contrario, la richiesta potrebbe non rispondere.

Esiste un problema noto in cui l'abilitazione AllowRenegotation può causare la rinegoziazione in modo sincrono durante l'accesso alla ClientCertificate proprietà. Chiamare il GetClientCertificateAsync metodo per evitare questo problema. Questo problema è stato risolto in .NET 6. Per altre informazioni, vedere questo problema in GitHub. Nota GetClientCertificateAsync può restituire un certificato Null se il client rifiuta di specificarne uno.

Kestrel

Kestrel controlla la negoziazione del certificato client con l'opzione ClientCertificateMode .

Per .NET 5 e versioni precedenti Kestrel non supporta la rinegoziazione dopo l'avvio di una connessione per acquisire un certificato client. Questa funzionalità è stata aggiunta in .NET 6.

Microsoft.AspNetCore.Authentication.Certificatecontiene un'implementazione simile all'autenticazione del certificato per ASP.NET Core. L'autenticazione del certificato avviene a livello di TLS, molto prima che arrivi a ASP.NET Core. Più accuratamente, si tratta di un gestore di autenticazione che convalida il certificato e quindi fornisce un evento in cui è possibile risolvere il certificato in un oggetto ClaimsPrincipal.

Configurare il server per l'autenticazione del certificato, ad esempio IIS, Kestrel, Azure App Web o qualsiasi altra operazione in uso.

Scenari di proxy e bilanciamento del carico

L'autenticazione del certificato è uno scenario con stato usato principalmente in cui un proxy o un servizio di bilanciamento del carico non gestisce il traffico tra client e server. Se si usa un proxy o un servizio di bilanciamento del carico, l'autenticazione del certificato funziona solo se il proxy o il servizio di bilanciamento del carico:

  • Gestisce l'autenticazione.
  • Passa le informazioni di autenticazione utente all'app (ad esempio, in un'intestazione di richiesta), che agisce sulle informazioni di autenticazione.

Un'alternativa all'autenticazione del certificato negli ambienti in cui vengono usati proxy e servizi di bilanciamento del carico è Active Directory Federated Services (ADFS) con OpenID Connessione (OIDC).

Attività iniziali

Acquisire un certificato HTTPS, applicarlo e configurare il server per richiedere i certificati.

Nell'app Web aggiungere un riferimento al pacchetto Microsoft.AspNetCore.Authentication.Certificate . Quindi, nel Startup.ConfigureServices metodo chiamare services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...); con le opzioni, fornendo un delegato per OnCertificateValidated eseguire qualsiasi convalida supplementare sul certificato client inviato con le richieste. Trasformare tali informazioni in un ClaimsPrincipal oggetto e impostarlo sulla context.Principal proprietà .

Se l'autenticazione non riesce, questo gestore restituisce una 403 (Forbidden) risposta piuttosto che un 401 (Unauthorized)oggetto , come previsto. Il motivo è che l'autenticazione deve avvenire durante la connessione TLS iniziale. Quando raggiunge il gestore, è troppo tardi. Non è possibile aggiornare la connessione da una connessione anonima a una con un certificato.

app.UseAuthentication(); Aggiungere anche nel Startup.Configure metodo . In caso contrario, HttpContext.User non verrà impostato su ClaimsPrincipal creato dal certificato. Ad esempio:

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
}

L'esempio precedente illustra il modo predefinito per aggiungere l'autenticazione del certificato. Il gestore costruisce un'entità utente usando le proprietà comuni del certificato.

Configurare la convalida del certificato

Il CertificateAuthenticationOptions gestore include alcune convalide predefinite che rappresentano le convalide minime da eseguire su un certificato. Ognuna di queste impostazioni è abilitata per impostazione predefinita.

AllowedCertificateTypes = Concatenato, SelfSigned o Tutto (concatenato | SelfSigned)

Valore predefinito: CertificateTypes.Chained

Questo controllo verifica che sia consentito solo il tipo di certificato appropriato. Se l'app usa certificati autofirmato, questa opzione deve essere impostata su CertificateTypes.All o CertificateTypes.SelfSigned.

ValidateCertificateUse

Valore predefinito: true

Questo controllo verifica che il certificato presentato dal client disponga dell'uso della chiave estesa per l'autenticazione client (EKU) o che non siano presenti EKU. Come indicato dalle specifiche, se non viene specificato alcun EKU, tutte le EKU vengono considerate valide.

ValidateValidityPeriod

Valore predefinito: true

Questo controllo verifica che il certificato sia compreso nel periodo di validità. In ogni richiesta, il gestore garantisce che un certificato valido quando è stato presentato non sia scaduto durante la sessione corrente.

RevocheFlag

Valore predefinito: X509RevocationFlag.ExcludeRoot

Flag che specifica quali certificati nella catena vengono controllati per la revoca.

I controlli di revoca vengono eseguiti solo quando il certificato viene concatenato a un certificato radice.

RevocationMode

Valore predefinito: X509RevocationMode.Online

Flag che specifica come vengono eseguiti i controlli di revoca.

La specifica di un controllo online può comportare un lungo ritardo mentre viene contattata l'autorità di certificazione.

I controlli di revoca vengono eseguiti solo quando il certificato viene concatenato a un certificato radice.

È possibile configurare l'app per richiedere un certificato solo in determinati percorsi?

Non è possibile. Tenere presente che lo scambio di certificati viene eseguito all'inizio della conversazione HTTPS, viene eseguito dal server prima che la prima richiesta venga ricevuta su tale connessione in modo che non sia possibile definire l'ambito in base a qualsiasi campo della richiesta.

Eventi del gestore

Il gestore ha due eventi:

  • OnAuthenticationFailed: Chiamato se si verifica un'eccezione durante l'autenticazione e consente di reagire.
  • OnCertificateValidated: Chiamato dopo che il certificato è stato convalidato, è stata superata la convalida e è stata creata un'entità predefinita. Questo evento consente di eseguire la propria convalida e di aumentare o sostituire l'entità. Ad esempio:
    • Determinare se il certificato è noto ai servizi.

    • Creazione di un'entità personalizzata. Si consideri l'esempio seguente in Startup.ConfigureServices:

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

Se il certificato in ingresso non soddisfa la convalida aggiuntiva, chiamare context.Fail("failure reason") con un motivo di errore.

Per le funzionalità reali, è probabile che si voglia chiamare un servizio registrato nell'inserimento delle dipendenze che si connette a un database o a un altro tipo di archivio utenti. Accedere al servizio usando il contesto passato al delegato. Si consideri l'esempio seguente in 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;
            }
        };
    });

Concettualmente, la convalida del certificato è un problema di autorizzazione. L'aggiunta di un controllo, ad esempio, un'autorità emittente o un'identificazione personale in un criterio di autorizzazione, anziché all'interno OnCertificateValidateddi , è perfettamente accettabile.

Configurare il server per richiedere i certificati

Kestrel

In Program.csconfigurare Kestrel come segue:

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

Gli endpoint creati chiamando Listenprima della chiamata ConfigureHttpsDefaults non avranno le impostazioni predefinite applicate.

IIS

Completare i passaggi seguenti in Gestione IIS:

  1. Selezionare il sito nella scheda Connessione ions.
  2. Fare doppio clic sull'opzione SSL Impostazioni nella finestra Visualizzazione funzionalità.
  3. Selezionare la casella di controllo Richiedi SSL e selezionare il pulsante di opzione Richiedi certificati nella sezione Certificati client.

Client certificate settings in IIS

Azure e proxy Web personalizzati

Per informazioni su come configurare il middleware di inoltro dei certificati, vedere la documentazione relativa all'host e alla distribuzione.

Usare l'autenticazione del certificato in Azure App Web

Non è necessaria alcuna configurazione di inoltro per Azure. La configurazione di inoltro viene configurata dal middleware di inoltro dei certificati.

Nota

Il middleware di inoltro dei certificati è necessario per questo scenario.

Per altre informazioni, vedere Usare un certificato TLS/SSL nel codice in app Azure Service (documentazione di Azure).

Usare l'autenticazione del certificato nei proxy Web personalizzati

Il AddCertificateForwarding metodo viene usato per specificare:

  • Nome dell'intestazione del client.
  • Modalità di caricamento del certificato (tramite la HeaderConverter proprietà ).

Nei proxy Web personalizzati il certificato viene passato come intestazione di richiesta personalizzata, ad esempio X-SSL-CERT. Per usarlo, configurare l'inoltro dei certificati in Startup.ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.AddCertificateForwarding(options =>
    {
        options.CertificateHeader = "X-SSL-CERT";
        options.HeaderConverter = (headerValue) =>
        {
            X509Certificate2 clientCertificate = null;

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

            return clientCertificate;
        };
    });
}

private static byte[] StringToByteArray(string hex)
{
    int NumberChars = hex.Length;
    byte[] bytes = new byte[NumberChars / 2];

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

    return bytes;
}

Se l'app è inversa tramite proxy da NGINX con la configurazione proxy_set_header ssl-client-cert $ssl_client_escaped_cert o distribuita in Kubernetes usando NGINX Ingress, il certificato client viene passato all'app in formato con codifica URL. Per usare il certificato, decodificarlo come segue:

Aggiungere lo spazio dei nomi per System.Net all'inizio di Startup.cs:

using System.Net;

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

Aggiungere il metodo 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);
}

Il Startup.Configure metodo aggiunge quindi il middleware. UseCertificateForwarding viene chiamato prima delle chiamate a UseAuthentication e UseAuthorization:

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

    app.UseRouting();

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

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

È possibile usare una classe separata per implementare la logica di convalida. Poiché in questo esempio viene usato lo stesso certificato autofirmato, assicurarsi che sia possibile usare solo il certificato. Verificare che le identificazioni personali del certificato client e del certificato server corrispondano, altrimenti è possibile usare qualsiasi certificato e sarà sufficiente per l'autenticazione. Questa operazione verrà utilizzata all'interno del AddCertificate metodo . È anche possibile convalidare l'oggetto o l'autorità emittente se si usano certificati intermedi o figlio.

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

Implementare un httpClient usando un certificato e HttpClientHandler

Può HttpClientHandler essere aggiunto direttamente nel costruttore della HttpClient classe . Prestare attenzione quando si creano istanze di HttpClient. HttpClient invierà quindi il certificato con ogni richiesta.

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

Implementare un httpClient usando un certificato e un httpClient denominato da IHttpClientFactory

Nell'esempio seguente viene aggiunto un certificato client a un HttpClientHandler oggetto usando la ClientCertificates proprietà del gestore. Questo gestore può quindi essere usato in un'istanza denominata di un HttpClient oggetto usando il ConfigurePrimaryHttpMessageHandler metodo . Questa è la configurazione in 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;
});

Può IHttpClientFactory quindi essere usato per ottenere l'istanza denominata con il gestore e il certificato. Il CreateClient metodo con il nome del client definito nella Startup classe viene usato per ottenere l'istanza. La richiesta HTTP può essere inviata usando il client in base alle esigenze.

private readonly IHttpClientFactory _clientFactory;

public ApiService(IHttpClientFactory clientFactory)
{
    _clientFactory = clientFactory;
}

private async Task<JsonDocument> GetApiDataWithNamedClient()
{
    var client = _clientFactory.CreateClient("namedClient");

    var request = new HttpRequestMessage()
    {
        RequestUri = new Uri("https://localhost:44379/api/values"),
        Method = HttpMethod.Get,
    };
    var response = await client.SendAsync(request);
    if (response.IsSuccessStatusCode)
    {
        var responseContent = await response.Content.ReadAsStringAsync();
        var data = JsonDocument.Parse(responseContent);
        return data;
    }

    throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
}

Se il certificato corretto viene inviato al server, vengono restituiti i dati. Se non viene inviato alcun certificato o certificato errato, viene restituito un codice di stato HTTP 403.

Creare certificati in PowerShell

La creazione dei certificati è la parte più difficile nella configurazione di questo flusso. È possibile creare un certificato radice usando il New-SelfSignedCertificate cmdlet di PowerShell. Quando si crea il certificato, usare una password complessa. È importante aggiungere il KeyUsageProperty parametro e il KeyUsage parametro come illustrato.

Creare una CA radice

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

Il valore del -DnsName parametro deve corrispondere alla destinazione di distribuzione dell'app. Ad esempio, "localhost" per lo sviluppo.

Eseguire l'installazione nella radice attendibile

Il certificato radice deve essere considerato attendibile nel sistema host. Un certificato radice che non è stato creato da un'autorità di certificazione non sarà considerato attendibile per impostazione predefinita. Per informazioni su come considerare attendibile il certificato radice in Windows, vedere questa domanda.

Certificato intermedio

È ora possibile creare un certificato intermedio dal certificato radice. Questo non è necessario per tutti i casi d'uso, ma potrebbe essere necessario creare molti certificati o dover attivare o disabilitare gruppi di certificati. Il TextExtension parametro è necessario per impostare la lunghezza del percorso nei vincoli di base del certificato.

Il certificato intermedio può quindi essere aggiunto al certificato intermedio attendibile nel sistema host Windows.

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

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint of the root..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "intermediate_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "intermediate_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")

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

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

Creare un certificato figlio dal certificato intermedio

È possibile creare un certificato figlio dal certificato intermedio. Si tratta dell'entità finale e non è necessario creare più certificati figlio.

$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

Creare un certificato figlio dal certificato radice

È anche possibile creare un certificato figlio direttamente dal certificato radice.

$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

Radice di esempio - Certificato intermedio - Certificato

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

New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "root_ca_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature

Get-ChildItem -Path cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwdroot

Export-Certificate -Cert cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 -FilePath root_ca_dev_damienbod.crt

$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\0C89639E4E2998A93E423F919B36D4009A0F9991 )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")

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

Export-Certificate -Cert cert:\localMachine\my\BA9BF91ED35538A01375EFC212A2F46104B33A44 -FilePath child_a_dev_damienbod.crt

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\BA9BF91ED35538A01375EFC212A2F46104B33A44 )

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

Get-ChildItem -Path cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_b_from_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A -FilePath child_b_from_a_dev_damienbod.crt

Quando si usano i certificati radice, intermedio o figlio, i certificati possono essere convalidati usando l'identificazione personale o PublicKey in base alle esigenze.

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

Certificati client facoltativi

Questa sezione fornisce informazioni per le app che devono proteggere un subset dell'app con un certificato. Ad esempio, un controller o una Razor pagina nell'app potrebbe richiedere certificati client. Ciò presenta problemi come certificati client:

  • Si tratta di una funzionalità TLS, non di una funzionalità HTTP.
  • Vengono negoziati per connessione e in genere all'inizio della connessione prima che siano disponibili dati HTTP.

Esistono due approcci per implementare i certificati client facoltativi:

  1. Uso di nomi host separati (SNI) e reindirizzamento. Anche se è consigliabile eseguire altre operazioni per la configurazione, è consigliabile perché funziona nella maggior parte degli ambienti e dei protocolli.
  2. Rinegoziazione durante una richiesta HTTP. Questo presenta diverse limitazioni e non è consigliato.

Host separati (SNI)

All'inizio della connessione è nota solo l'indicazione del nome server (SNI) †. I certificati client possono essere configurati per nome host in modo che un host li richieda e un altro no.

ASP.NET Core 5 e versioni successive aggiunge un supporto più pratico per il reindirizzamento per acquisire certificati client facoltativi. Per altre informazioni, vedere l'esempio di certificati facoltativi.

  • Per le richieste all'app Web che richiedono un certificato client e non ne hanno una:
    • Eseguire il reindirizzamento alla stessa pagina usando il sottodominio protetto dal certificato client.
    • Ad esempio, reindirizzare a myClient.contoso.com/requestedPage. Poiché la richiesta a myClient.contoso.com/requestedPage è un nome host diverso da contoso.com/requestedPage, il client stabilisce una connessione diversa e viene fornito il certificato client.
    • Per altre informazioni, vedere Introduzione all'autorizzazione in ASP.NET Core.

† Server Name Indication (SNI) è un'estensione TLS per includere un dominio virtuale come parte della negoziazione SSL. Ciò significa che il nome di dominio virtuale, o un nome host, può essere usato per identificare l'endpoint di rete.

Rinegoziazione

La rinegoziazione TLS è un processo in base al quale il client e il server possono valutare nuovamente i requisiti di crittografia per una singola connessione, inclusa la richiesta di un certificato client se non specificato in precedenza. La rinegoziazione TLS è un rischio per la sicurezza e non è consigliata perché:

  • In HTTP/1.1 il server deve prima memorizzare nel buffer o utilizzare tutti i dati HTTP in esecuzione, ad esempio i corpi di richiesta POST, per assicurarsi che la connessione sia chiara per la rinegoziazione. In caso contrario, la rinegoziazione può interrompere la risposta o non riuscire.
  • HTTP/2 e HTTP/3 vietano esplicitamente la rinegoziazione.
  • Esistono rischi per la sicurezza associati alla rinegoziazione. TLS 1.3 ha rimosso la rinegoziazione dell'intera connessione e l'ha sostituita con una nuova estensione per richiedere solo il certificato client dopo l'avvio della connessione. Questo meccanismo viene esposto tramite le stesse API ed è comunque soggetto ai vincoli precedenti delle versioni del protocollo HTTP e del buffering.

L'implementazione e la configurazione di questa funzionalità variano in base alla versione del server e del framework.

IIS

IIS gestisce la negoziazione del certificato client per conto dell'utente. Una sottosezione dell'applicazione può abilitare l'opzione SslRequireCert per negoziare il certificato client per tali richieste. Per informazioni dettagliate, vedere Configurazione nella documentazione di IIS.

IIS memorizza automaticamente nel buffer tutti i dati del corpo della richiesta fino a un limite di dimensioni configurato prima della rinegoziazione. Le richieste che superano il limite vengono rifiutate con una risposta 413. Per impostazione predefinita, questo limite è 48 KB ed è configurabile impostando uploadReadAheadSize.

HttpSys

HttpSys ha due impostazioni che controllano la negoziazione del certificato client ed entrambi devono essere impostati. Il primo si trova in netsh.exe in http add sslcert clientcertnegotiation=enable/disable. Questo flag indica se il certificato client deve essere negoziato all'inizio di una connessione e deve essere impostato su disable per i certificati client facoltativi. Per informazioni dettagliate, vedere la documentazione netsh.

L'altra impostazione è ClientCertificateMethod. Se impostato su AllowRenegotation, il certificato client può essere rinegoziato durante una richiesta.

NOTA L'applicazione deve memorizzare nel buffer o utilizzare i dati del corpo della richiesta prima di tentare la rinegoziazione; in caso contrario, la richiesta potrebbe non rispondere.

Esiste un problema noto in cui l'abilitazione AllowRenegotation può causare la rinegoziazione in modo sincrono durante l'accesso alla ClientCertificate proprietà. Chiamare il GetClientCertificateAsync metodo per evitare questo problema. Questo problema è stato risolto in .NET 6. Per altre informazioni, vedere questo problema in GitHub. Nota GetClientCertificateAsync può restituire un certificato Null se il client rifiuta di specificarne uno.

Kestrel

Kestrel controlla la negoziazione del certificato client con l'opzione ClientCertificateMode .

Per .NET 5 e versioni precedenti Kestrel non supporta la rinegoziazione dopo l'avvio di una connessione per acquisire un certificato client. Questa funzionalità è stata aggiunta in .NET 6.

Lasciare domande, commenti e altri commenti sui certificati client facoltativi in questo problema di discussione su GitHub.