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 prima di ASP.NET Core. In modo più accurato, questa funzionalità è un gestore di autenticazione che convalida il certificato e quindi fornisce un evento in cui è possibile risolvere il certificato in un oggetto ClaimsPrincipal.

Devi configurare il tuo server per l'autenticazione dei certificati con IIS, Kestrel, Azure App Web o la soluzione preferita.

Questo articolo descrive come configurare l'autenticazione del certificato in ASP.NET Core per IIS e HTTP.syse fornisce esempi per chiamare vari metodi e usare le proprietà.

Esaminare gli scenari del proxy e del servizio di 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 Connect (OIDC).

Inizia

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 .

  • Nel file Program.cs chiamare il builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...); metodo . Specificare un delegato per il OnCertificateValidated gestore eventi per completare qualsiasi convalida supplementare sul certificato client inviato con le richieste. Trasformare tali informazioni in un ClaimsPrincipal valore e impostarlo sulla context.Principal proprietà .

Se l'autenticazione non riesce, questo gestore restituisce una 403 (Forbidden) risposta anziché un 401 (Unauthorized), come previsto. Il gestore restituisce una risposta diversa perché prevede che l'autenticazione venga eseguita 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.

Il UseAuthentication metodo è necessario per impostare su HttpContext.User un ClaimsPrincipal valore 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 principale 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. Le sezioni seguenti descrivono come usare le impostazioni.

AllowedCertificateTypes = A Catena, AutoFirmato o Tutti (A Catena | AutoFirmato)

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.

Modalità di Validazione della Fiducia della Catena

Valore predefinito: X509ChainTrustMode.System

Il certificato presentato dal client deve essere concatenato a un certificato radice attendibile. Questa verifica 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, è possibile impostare l'opzione su X509ChainTrustMode.CustomRootTrust in modo che il gestore usi la CustomTrustStore proprietà .

CustomTrustStore

Valore predefinito: Vuoto X509Certificate2Collection

Se la proprietà del ChainTrustValidationMode gestore è impostata su X509ChainTrustMode.CustomRootTrust, questo X509Certificate2Collection oggetto contiene ogni certificato 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 a più livelli, la CustomTrustStore proprietà deve contenere ogni certificato emittente nella catena.

ConvalidaUsoCertificato

Valore predefinito: true

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

ConvalidaPeriodoValidità

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.

RevocaFlag

Valore predefinito: X509RevocationFlag.ExcludeRoot

Un flag che specifica quali certificati nella catena vengono controllati per verificare la revoca.

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

Modalità di Revoca

Valore predefinito: X509RevocationMode.Online

Flag che specifica come vengono eseguiti i controlli di revoca.

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

Domande frequenti: è possibile configurare l'app per richiedere un certificato solo in determinati percorsi?

Questo approccio non è possibile. Lo scambio di certificati viene completato all'inizio della conversazione HTTPS. L'operazione viene eseguita dal server prima che la prima richiesta venga ricevuta su tale connessione, quindi non è possibile definire l'ambito in base ai campi della richiesta.

Eventi del gestore di processi

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 e viene creato un principale predefinito. Questo evento consente di eseguire la propria convalida e di integrare o sostituire il principale. Gli esempi includono:

    • Determinare se il certificato è noto ai tuoi servizi.

    • Creazione di un'entità personalizzata, ad esempio nell'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, chiama context.Fail("failure reason") con una ragione del fallimento.

Per una migliore funzionalità, chiamare un servizio registrato nell'iniezione delle dipendenze che si connette a un database o a un altro tipo di archivio di 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. Ad esempio, è possibile aggiungere un controllo su un'autorità emittente o un'impronta digitale in un criterio di autorizzazione anziché all'interno del gestore OnCertificateValidated.

Configurare il server per richiedere i certificati

Le sezioni seguenti descrivono come configurare il server per richiedere certificati per una soluzione specifica, tra cui Kestrel, IIS, Azure, proxy Web personalizzati e Azure App Web.

Kestrel

Nel file Program.cs configurare Kestrel come segue:

var builder = WebApplication.CreateBuilder(args);

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

Nota

Quando viene creato un endpoint chiamando il metodo Listen di chiamare il ConfigureHttpsDefaults metodo , l'endpoint non ha le impostazioni predefinite applicate.

IIS

Completare i passaggi seguenti in Gestione IIS:

  1. Nella scheda Connessioni selezionare il sito.
  2. Nella finestra Visualizzazione funzionalità fare doppio clic su Impostazioni SSL.
  3. Seleziona la casella di controllo Richiedi SSL.
  4. Per l'opzione Certificati client selezionare Richiedi.

Screenshot che mostra come configurare le impostazioni del certificato client in IIS.

Azure e proxy Web personalizzati

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

Autenticazione del certificato in Azure App Web

Non è necessaria alcuna configurazione di inoltro per Azure. Il middleware di inoltro certificati configura la configurazione.

Nota

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

Per altre informazioni, vedere Usare i certificati TLS/SSL nel codice dell'app (documentazione Azure).

Autenticazione del certificato nei proxy Web personalizzati

Il AddCertificateForwarding metodo viene usato per specificare:

  • Nome dell'intestazione del client.
  • Come caricare il certificato (tramite la HeaderConverter proprietà ).

Nei proxy Web personalizzati il certificato viene passato come intestazione di richiesta personalizzata, ad esempio X-SSL-CERT. Per usare il certificato, configurare l'inoltro dei certificati nel file 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 NGINX viene usato con la configurazione proxy_set_header ssl-client-cert $ssl_client_escaped_cert per invertire il proxy dell'app o l'app viene distribuita in Kubernetes usando L'ingresso NGINX, 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 nel file Program.cs . Il UseCertificateForwarding metodo viene chiamato prima delle chiamate ai UseAuthentication metodi 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 impronte digitali del certificato client e del certificato server corrispondano. In caso contrario, qualsiasi certificato può essere usato e sufficiente per l'autenticazione. Il certificato viene quindi usato all'interno del AddCertificate metodo . È anche possibile convalidare il soggetto o l'emittente qui se si usano certificati intermedi o secondari.

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

Nell'esempio seguente un certificato client viene aggiunto a un HttpClientHandler oggetto usando la ClientCertificates proprietà del gestore. Questo gestore può quindi essere usato in un'istanza denominata di HttpClient utilizzando il metodo ConfigurePrimaryHttpMessageHandler. Questo scenario viene configurato nel file Program.cs :

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

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

IHttpClientFactory può quindi essere utilizzato per ottenere l'istanza denominata con il gestore e il certificato. Il CreateClient metodo con il nome del client definito nel file 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, il server restituisce un codice di stato HTTP 403.

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

Creare un'autorità di certificazione radice

Il codice seguente illustra come creare un'autorità di certificazione (CA) nella 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. Per impostazione predefinita, solo i certificati radice creati da un'autorità di certificazione sono considerati attendibili. Per informazioni su come considerare attendibile il certificato radice in Windows, vedere la documentazione Windows o il cmdlet di PowerShell Import-Certificate.

Usare un certificato intermedio

È ora possibile creare un certificato intermedio dal certificato radice. Questo approccio 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. Questo certificato figlio è l'entità finale. 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

Esempio: radice - certificato intermedio - certificato

L'esempio seguente illustra la configurazione della CA radice, il certificato intermedio e il certificato figlio:

$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 utilizzano i certificati radice, intermedio o secondario, i certificati possono essere convalidati usando l'impronta digitale o la chiave pubblica, a seconda delle necessità.

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

Risultati della convalida dei certificati della cache

.NET 5 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 il AddCertificateCache metodo nel file 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'iniezione delle dipendenze. Ad esempio: services.AddSingleton<ICertificateValidationCache, YourCache>().

Usare i certificati client opzionali

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. Questo scenario presenta alcune sfide:

  • I certificati client sono una funzionalità TLS, non una funzionalità HTTP.
  • I certificati client vengono negoziati per connessione e in genere all'inizio della connessione prima che siano disponibili dati HTTP.

Esistono due approcci per l'implementazione di certificati client facoltativi:

  • Opzione 1: usare nomi host separati (SNI) e reindirizzamento. Anche se questa opzione prevede più lavoro da configurare, l'approccio è consigliato perché funziona nella maggior parte degli ambienti e dei protocolli.
  • Opzione 2: Rinegoziazione durante una richiesta HTTP. Questo approccio 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 richieda i certificati e un altro non lo sia.

  • Configurare l'associazione per il dominio e il sottodominio.

    Ad esempio, configurare le associazioni in contoso.com e myClient.contoso.com. L'host contoso.com non richiede un certificato client, ma myClient.contoso.com lo fa.

    Per altre informazioni, vedere le risorse seguenti:

    .NET 5 e versioni successive offre 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 che non ne hanno una, reindirizzare 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.

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

Rinegoziazione TLS

La rinegoziazione TLS è un processo in base al quale il client e il server possono rivalutare 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 consumare i dati HTTP in transito, come i corpi di richiesta POST, per assicurarsi che la connessione sia libera per la rinegoziazione. In caso contrario, la rinegoziazione può smettere di rispondere 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, come descritto nelle sezioni seguenti.

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 altre informazioni, 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. Questo limite è predefinito di 48 KB ed è configurabile impostando la proprietà uploadReadAheadSize .

HttpSys

HttpSys ha due impostazioni che controllano la negoziazione del certificato client ed entrambi devono essere impostati. Il primo si trova nel file netsh.exe in http add sslcert clientcertnegotiation=enable/disable. Questo flag indica se negoziare il certificato client all'inizio di una connessione. Impostare il valore su disable per i certificati client facoltativi. Per altre informazioni, vedere l'utilizzo dei http add sslcert parametri nella documentazione di netsh.

L'altra impostazione è la ClientCertificateMethod proprietà . 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. Altrimenti, la richiesta potrebbe diventare non operativa.

Un'applicazione può prima controllare la ClientCertificate proprietà per verificare se il certificato è disponibile. Se non è disponibile, assicurarsi che il corpo della richiesta venga consumato prima di chiamare il metodo GetClientCertificateAsync per negoziarne uno. GetClientCertificateAsync può restituire un certificato Null se il client rifiuta di specificarne uno.

Nota

Il comportamento della proprietà ClientCertificate modificato in .NET 6. Per altre informazioni, vedere GitHub problema #466.

Kestrel

Kestrel controlla la negoziazione del certificato client tramite la proprietà ClientCertificateMode.

.NET 6 e versioni successive fornisce l'opzione DelayCertificate per la proprietà ClientCertificateMode. Quando questa opzione è impostata, un'app può controllare la ClientCertificate proprietà per verificare se il certificato è disponibile. Se non è disponibile, assicurarsi che il corpo della richiesta venga consumato prima di chiamare il metodo GetClientCertificateAsync per negoziare uno. GetClientCertificateAsync può restituire un certificato Null se il client rifiuta di specificarne uno.

Nota

L'applicazione deve memorizzare nel buffer o consumare i dati del corpo della richiesta prima di tentare la rinegoziazione. In caso contrario, GetClientCertificateAsync potrebbe generare l'eccezione InvalidOperationException: il flusso client deve essere svuotato prima della rinegoziazione.

Se si configurano a livello di codice le impostazioni TLS per nome host SNI, chiamare l'overload UseHttps (.NET 6 o versione successiva) che accetta un oggetto classe TlsHandshakeCallbackOptions. Questa opzione controlla la rinegoziazione del certificato client tramite la AllowDelayedClientCertificateNegotation proprietà . Per altre informazioni, vedere il metodo ListenOptionsHttpsExtensions.UseHttps .

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 tempo prima che i dati arrivino ad ASP.NET Core. Più accuratamente, questo gestore di autenticazione convalida il certificato e fornisce un evento in cui è possibile risolvere il certificato in un ClaimsPrincipal valore.

Configura il server per l'autenticazione tramite certificato, sia esso IIS, Kestrel, Azure App Web o qualsiasi altra piattaforma tu stia utilizzando.

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 Connect (OIDC).

Inizia

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 metodo Startup.ConfigureServices chiamare services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...); con le opzioni, fornendo un delegato a OnCertificateValidated per 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 anziché una 401 (Unauthorized), come ci si potrebbe aspettare. 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.

Aggiungere anche nel 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 crea un principal 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, AutoFirmato o Tutto (Concatenato | AutoFirmato)

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.

ConvalidaUsoCertificato

Valore predefinito: true

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

ConvalidaPeriodoValidità

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

L'indicatore 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.

Modalità di Revoca

Valore predefinito: X509RevocationMode.Online

Flag che specifica come vengono eseguiti i controlli di revoca.

Specificare di eseguire un controllo online potrebbe causare 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?

Questa operazione non è attualmente 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 handler

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, dopo il superamento della convalida e la creazione di un principale predefinito. Questo evento consente di eseguire la propria convalida e di potenziare o sostituire il principale. Ad esempio:
    • Determinare se il certificato è noto ai tuoi servizi.

    • Creazione del proprio principale. 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 validazione supplementare, chiama context.Fail("failure reason") con un motivo di fallimento.

Per la funzionalità reale, è probabile che si voglia utilizzare un servizio registrato nell'iniezione delle dipendenze che si connette a un database o a un altro tipo di archivio di utenti. Accedi al tuo servizio utilizzando il contesto passato al tuo 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 su, ad esempio, un emittente o un'impronta digitale in un criterio di autorizzazione, anziché all'interno di OnCertificateValidated, è 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 Connessioni .
  2. Fare doppio clic sull'opzione Impostazioni SSL nella finestra Visualizzazione funzionalità.
  3. Selezionare la casella di controllo Richiedi SSL e il pulsante di opzione Richiedi nella sezione Certificati client.

Impostazioni del certificato client 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 tramite 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 è implementato come reverse proxy da NGINX con la configurazione proxy_set_header ssl-client-cert $ssl_client_escaped_cert o distribuita su Kubernetes usando NGINX Ingress, il certificato client viene passato all'app in formato codificato 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 il soggetto o l'autorità emittente se si usano certificati intermedi o subordinati.

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ò essere aggiunto HttpClientHandler direttamente nel costruttore della classe HttpClient. 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 con nome creato da IHttpClientFactory

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

IHttpClientFactory può quindi essere utilizzato 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, intermedi o figli, i certificati possono essere convalidati usando l'impronta digitale 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

.NET 5 o 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'iniezione 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 sfide come i 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.

.NET 5 o versione successiva 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:
    • Reindirizza alla stessa pagina utilizzando il sottodominio protetto dal certificato cliente.
    • 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 i documenti 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 o 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.

Configura il server per l'autenticazione tramite certificato, sia esso IIS, Kestrel, Azure App Web o qualsiasi altra piattaforma tu stia utilizzando.

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 Connect (OIDC).

Inizia

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 metodo Startup.ConfigureServices chiamare services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...); con le opzioni, fornendo un delegato a OnCertificateValidated per 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 anziché una 401 (Unauthorized), come ci si potrebbe aspettare. 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.

Aggiungere anche nel 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 crea un principal 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, AutoFirmato o Tutto (Concatenato | AutoFirmato)

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.

ConvalidaUsoCertificato

Valore predefinito: true

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

ConvalidaPeriodoValidità

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

L'indicatore 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.

Modalità di Revoca

Valore predefinito: X509RevocationMode.Online

Flag che specifica come vengono eseguiti i controlli di revoca.

Specificare di eseguire un controllo online potrebbe causare 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?

Questa operazione non è attualmente 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 handler

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, dopo il superamento della convalida e la creazione di un principale predefinito. Questo evento consente di eseguire la propria convalida e di potenziare o sostituire il principale. Ad esempio:
    • Determinare se il certificato è noto ai tuoi servizi.

    • Creazione del proprio principale. 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 validazione supplementare, chiama context.Fail("failure reason") con un motivo di fallimento.

Per la funzionalità reale, è probabile che si voglia utilizzare un servizio registrato nell'iniezione delle dipendenze che si connette a un database o a un altro tipo di archivio di utenti. Accedi al tuo servizio utilizzando il contesto passato al tuo 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 su, ad esempio, un emittente o un'impronta digitale in un criterio di autorizzazione, anziché all'interno di OnCertificateValidated, è 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 Connessioni .
  2. Fare doppio clic sull'opzione Impostazioni SSL nella finestra Visualizzazione funzionalità.
  3. Selezionare la casella di controllo Richiedi SSL e il pulsante di opzione Richiedi nella sezione Certificati client.

Impostazioni del certificato client 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 tramite 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 è implementato come reverse proxy da NGINX con la configurazione proxy_set_header ssl-client-cert $ssl_client_escaped_cert o distribuita su Kubernetes usando NGINX Ingress, il certificato client viene passato all'app in formato codificato 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 il soggetto o l'autorità emittente se si usano certificati intermedi o subordinati.

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ò essere aggiunto HttpClientHandler direttamente nel costruttore della classe HttpClient. 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 con nome creato da IHttpClientFactory

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

IHttpClientFactory può quindi essere utilizzato 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, intermedi o subordinati, i certificati possono essere convalidati usando l'impronta digitale o la chiave pubblica secondo necessità.

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 sfide in quanto riguarda i 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.

.NET 5 o versione successiva 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:
    • Reindirizza alla stessa pagina utilizzando il sottodominio protetto dal certificato cliente.
    • 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 i documenti 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 o 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.

Lascia domande, commenti e altri feedback sui certificati client facoltativi nel thread di discussione su GitHub issue #18720.