Konfigurieren der Zertifikatauthentifizierung in ASP.NET Core

Microsoft.AspNetCore.Authentication.Certificate enthält eine Implementierung ähnlich der Zertifikatauthentifizierung für ASP.NET Core. Die Zertifikatsauthentifizierung erfolgt auf der TLS-Ebene, lange bevor sie überhaupt zum ASP.NET Core gelangt. Genauer gesagt ist dies ein Authentifizierungshandler, der das Zertifikat überprüft und Ihnen dann ein Ereignis bereitstellt, bei dem Sie dieses Zertifikat in einen ClaimsPrincipal auflösen können.

Sie müssen Ihren Server für die Zertifikatauthentifizierung konfigurieren. Dabei können Sie IIS, Kestrel, Azure-Web-Apps oder etwas anderes verwenden.

Szenarien mit Proxy und Lastenausgleich

Die Zertifikatauthentifizierung ist ein zustandsbehaftetes Szenario, das hauptsächlich verwendet wird, in denen der Datenverkehr zwischen Clients und Servern nicht durch einen Proxy oder Lastenausgleich verarbeitet wird. Wenn ein Proxy oder Lastenausgleich verwendet wird, funktioniert die Zertifikatauthentifizierung nur, wenn Proxy oder Lastenausgleich folgende Funktionen übernehmen:

  • Verarbeiten der Authentifizierung
  • Übergeben von Benutzerauthentifizierungsinformationen an die App (z. B. in einem Anforderungsheader), die die Authentifizierungsinformationen nutzt

Eine Alternative zur Zertifikatauthentifizierung in Umgebungen, in denen Proxys und Lastenausgleichsmodule verwendet werden, bildet Active Directory-Verbunddienste (AD FS) mit OpenID Connect (OIDC).

Erste Schritte

Rufen Sie ein HTTPS-Zertifikat ab, wenden Sie es an, und konfigurieren Sie Ihren Server so, dass Zertifikate erforderlich sind.

In der Web-App:

  • Fügen Sie einen Verweis auf das NuGet-Paket Microsoft.AspNetCore.Authentication.Certificate hinzu.
  • Rufen Sie builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...); in Program.cs auf. Stellen Sie einen Delegaten für OnCertificateValidated bereit, um zusätzliche Überprüfungen für Clientzertifikate durchzuführen, die mit Anforderungen gesendet werden. Wandeln Sie diese Informationen in einen ClaimsPrincipal um, und legen Sie diesen in der context.Principal-Eigenschaft fest.

Bei einem Authentifizierungsfehler gibt dieser Handler wie erwartet eine 403 (Forbidden)-Antwort anstelle von 401 (Unauthorized) zurück. Der Grund hierfür ist, dass die Authentifizierung während der ersten TLS-Verbindung erfolgen sollte. Wenn der Handler erreicht wird, ist es zu spät. Es gibt keine Möglichkeit, die Verbindung von einer anonymen Verbindung in eine Verbindung mit einem Zertifikat zu ändern.

UseAuthentication ist erforderlich, um HttpContext.User auf einen ClaimsPrincipal festzulegen, der aus dem Zertifikat erstellt wurde. Beispiel:

var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

app.UseAuthentication();

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

app.Run();

Im obigen Beispiel wird das Standardverfahren zum Hinzufügen der Zertifikatauthentifizierung veranschaulicht. Der Handler erstellt einen Benutzerprinzipal mit den allgemeinen Zertifikateigenschaften.

Konfigurieren der Zertifikatüberprüfung

Der CertificateAuthenticationOptions-Handler verfügt über einige integrierte Validierungen. Dabei handelt es such um die mindestens erforderlichen Überprüfungen, die Sie für ein Zertifikat ausführen sollten. Jede dieser Einstellungen ist standardmäßig aktiviert.

AllowedCertificateTypes = Chained, SelfSigned oder All (Chained | SelfSigned)

Standardwert: CertificateTypes.Chained

Bei dieser Überprüfung wird getestet, ob nur der entsprechende Zertifikattyp zulässig ist. Wenn die App selbstsignierte Zertifikate verwendet, muss diese Option auf CertificateTypes.All oder CertificateTypes.SelfSigned festgelegt werden.

ChainTrustValidationMode

Standardwert: X509ChainTrustMode.System

Das vom Client angegebene Zertifikat muss mit einem vertrauenswürdigen Stammzertifikat verkettet sein. Diese Überprüfung kontrolliert, welcher Vertrauensspeicher diese Stammzertifikate enthält.

Standardmäßig verwendet der Handler den Systemvertrauensspeicher. Wenn das angegebene Clientzertifikat mit einem Stammzertifikat verkettet werden muss, das nicht im Systemvertrauensspeicher vorhanden ist, kann diese Option auf X509ChainTrustMode.CustomRootTrust festgelegt werden, damit der Handler den CustomTrustStore verwendet.

CustomTrustStore

Standardwert: leere X509Certificate2Collection

Wenn die Eigenschaft ChainTrustValidationMode des Handlers auf X509ChainTrustMode.CustomRootTrust festgelegt ist, enthält diese X509Certificate2Collection jedes Zertifikat, das verwendet wird, um das Clientzertifikat bis zu einem vertrauenswürdigen Stamm zu überprüfen, einschließlich des vertrauenswürdigen Stamms.

Wenn der Client ein Zertifikat angibt, das Teil einer mehrstufigen Zertifikatkette ist, muss CustomTrustStore jedes ausstellende Zertifikat in der Kette enthalten.

ValidateCertificateUse

Standardwert: true

Bei dieser Überprüfung wird getestet, ob das vom Client bereitgestellte Zertifikat über die erweiterte Schlüsselverwendung (EKU) der Clientauthentifizierung oder überhaupt keine EKUs verfügt. Wenn keine EKU angegeben wird, gelten laut Spezifikationen alle EKUs als gültig.

ValidateValidityPeriod

Standardwert: true

Bei dieser Überprüfung wird getestet, ob das Zertifikat innerhalb seines Gültigkeitszeitraums liegt. Bei jeder Anforderung stellt der Handler sicher, dass ein Zertifikat, das bei der Präsentation gültig war, während der aktuellen Sitzung nicht abgelaufen ist.

RevocationFlag

Standardwert: X509RevocationFlag.ExcludeRoot

Dieses Flag gibt an, welche Zertifikate in der Kette auf Sperrung überprüft werden sollen.

Sperrprüfungen werden nur ausgeführt, wenn das Zertifikat mit einem Stammzertifikat verkettet ist.

RevocationMode

Standardwert: X509RevocationMode.Online

Dieses Flag gibt an, wie Sperrüberprüfungen ausgeführt werden.

Die Angabe einer Onlineüberprüfung kann zu einer langen Verzögerung führen, während die Zertifizierungsstelle kontaktiert wird.

Sperrprüfungen werden nur ausgeführt, wenn das Zertifikat mit einem Stammzertifikat verkettet ist.

Kann ich meine App so konfigurieren, dass ein Zertifikat nur für bestimmte Pfade erforderlich ist?

Dies ist nicht möglich. Denken Sie daran, dass der Zertifikataustausch zu Beginn der HTTPS-Konversation erfolgt. Er wird vom Server ausgeführt, bevor die erste Anforderung für diese Verbindung empfangen wird, sodass es nicht möglich ist, einen Bereich basierend auf Anforderungsfeldern zu verwenden.

Handlerereignisse

Der Handler weist zwei Ereignisse auf:

  • OnAuthenticationFailed wird aufgerufen, wenn während der Authentifizierung eine Ausnahme auftritt und Sie reagieren können.
  • OnCertificateValidated wird aufgerufen, nachdem das Zertifikat überprüft, die Überprüfung bestanden und ein Standardprinzipal erstellt wurde. Mit diesem Ereignis können Sie Ihre eigene Validierung durchführen und den Prinzipal erweitern oder ersetzen. Beispiele dafür sind:
    • Ermitteln, ob das Zertifikat Ihrem Diensten bekannt ist

    • Erstellen eines eigenen Prinzipals Betrachten Sie das folgende Beispiel:

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

Wenn Sie feststellen, dass das eingehende Zertifikat Ihre zusätzliche Validierung nicht besteht, rufen Sie context.Fail("failure reason") mit einem Fehlergrund auf.

Rufen Sie zur Verbesserung der Funktionalität einen Dienst auf, der bei der Abhängigkeitsinjektion registriert ist und eine Verbindung mit einer Datenbank oder einem anderen Benutzerspeicher herstellt. Greifen Sie mithilfe des Kontexts, der an den Delegaten übergeben wurde, auf den Dienst zu. Betrachten Sie das folgende Beispiel:

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

Konzeptionell ist die Validierung des Zertifikats ein Autorisierungsschritt. Das Hinzufügen einer Überprüfung, z. B. eines Ausstellers oder Fingerabdrucks in einer Autorisierungsrichtlinie und nicht innerhalb von OnCertificateValidated, ist problemlos möglich.

Konfigurieren Ihres Servers für die Erzwingung von Zertifikaten

Kestrel

Konfigurieren Sie Kestrel in Program.cs wie folgt:

var builder = WebApplication.CreateBuilder(args);

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

Hinweis

Auf Endpunkte, die durch Aufrufen von Listen vor dem Aufrufen von ConfigureHttpsDefaults erstellt werden, werden die Standardwerte nicht angewendet.

IIS

Führen Sie im IIS-Manager die folgenden Schritte aus:

  1. Wählen Sie Ihre Website auf der Registerkarte Verbindungen aus.
  2. Doppelklicken Sie im Fenster Featureansicht auf die Option SSL-Einstellungen.
  3. Aktivieren Sie das Kontrollkästchen SSL anfordern und im Abschnitt Clientzertifikate das Optionsfeld Erforderlich.

Einstellungen für Clientzertifikate in IIS

Azure und benutzerdefinierte Webproxys

Informationen zum Konfigurieren der Middleware für die Zertifikatweiterleitung finden Sie in der Host- und Bereitstellungsdokumentation.

Verwenden der Zertifikatauthentifizierung in Azure-Web-Apps

Für Azure ist keine Weiterleitungskonfiguration erforderlich. Die Weiterleitungskonfiguration wird von der Middleware für die Zertifikatweiterleitung eingerichtet.

Hinweis

Für dieses Szenario ist Middleware für die Zertifikatweiterleitung erforderlich.

Weitere Informationen finden Sie unter Verwenden eines TLS-/SSL-Zertifikats in Ihrem Code in Azure App Service (Azure-Dokumentation).

Verwenden der Zertifikatauthentifizierung in benutzerdefinierten Webproxys

Mit der AddCertificateForwarding-Methode wird Folgendes angegeben:

  • Der Clientheadername
  • Wie das Zertifikat geladen werden soll (mit der HeaderConverter-Eigenschaft)

Bei benutzerdefinierten Webproxys wird das Zertifikat als benutzerdefinierter Anforderungsheader übergeben, z. B. X-SSL-CERT. Um es zu verwenden, konfigurieren Sie die Zertifikatweiterleitung in Program.cs:

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

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

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

        return clientCertificate!;

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

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

            return bytes;
        }
    };
});

Wenn für die App ein Reverseproxy von NGINX mit der Konfiguration proxy_set_header ssl-client-cert $ssl_client_escaped_cert verwendet wird oder wenn sie in Kubernetes mithilfe von NGINX Ingress bereitgestellt wird, wird das Clientzertifikat URL-codiert an die App übergeben. Um das Zertifikat zu verwenden, decodieren Sie es wie folgt:

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

Fügen Sie die Middleware in Program.cs hinzu. UseCertificateForwarding wird vor den Aufrufen von UseAuthentication und UseAuthorization aufgerufen:

var app = builder.Build();

app.UseCertificateForwarding();

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

Mithilfe einer separaten Klasse können Sie Validierungslogik implementieren. Da in diesem Beispiel dasselbe selbstsignierte Zertifikat verwendet wird, müssen Sie sicherstellen, dass nur Ihr Zertifikat verwendet werden kann. Überprüfen Sie, ob die Fingerabdrücke des Clientzertifikats und des Serverzertifikats übereinstimmen. Andernfalls kann jedes Zertifikat verwendet werden, und es reicht für die Authentifizierung aus. Dies wird innerhalb der AddCertificate-Methode verwendet. Sie können hier auch den Antragsteller oder den Aussteller überprüfen, wenn Sie zwischengeschaltete oder untergeordnete Zertifikate verwenden.

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

Implementieren eines HttpClient mithilfe eines Zertifikats und von IHttpClientFactory

Im folgenden Beispiel wird ein Clientzertifikat mithilfe der ClientCertificates-Eigenschaft des Handlers einem HttpClientHandler hinzugefügt. Dieser Handler kann dann in einer benannten Instanz eines HttpClient mithilfe der ConfigurePrimaryHttpMessageHandler-Methode verwendet werden. Dies wird in Program.cs eingerichtet:

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

Die IHttpClientFactory kann dann verwendet werden, um die benannte Instanz mit dem Handler und dem Zertifikat abzurufen. Die CreateClient-Methode mit dem Namen des in Program.cs definierten Clients wird verwendet, um die Instanz abzurufen. Die HTTP-Anforderung kann bei Bedarf über den Client gesendet werden:

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

Wenn das richtige Zertifikat an den Server gesendet wird, werden die Daten zurückgegeben. Wenn kein Zertifikat oder ein falsches Zertifikat übermittelt wurde, wird der HTTP-Statuscode 403 zurückgegeben.

Erstellen von Zertifikaten in PowerShell

Das Erstellen der Zertifikate ist der schwierigste Teil beim Einrichten dieses Flows. Ein Stammzertifikat kann mithilfe des PowerShell-Cmdlets New-SelfSignedCertificate erstellt werden. Verwenden Sie beim Erstellen des Zertifikats ein sicheres Kennwort. Es ist wichtig, den KeyUsageProperty-Parameter und den KeyUsage-Parameter wie gezeigt hinzuzufügen.

Erstellen einer Stammzertifizierungsstelle

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

Hinweis

Der Wert des -DnsName-Parameters muss mit dem Bereitstellungsziel der App übereinstimmen. Bei der Entwicklung ist dies z. B. „localhost“.

Installieren im vertrauenswürdigen Stamm

Das Stammzertifikat muss auf Ihrem Hostsystem als vertrauenswürdig gelten. Standardmäßig werden nur von einer Zertifizierungsstelle erstellte Stammzertifikate als vertrauenswürdig eingestuft. Informationen dazu. wie dem Stammzertifikat unter Windows vertraut werden kann, finden Sie in der Windows-Dokumentation oder im Import-Certificate-PowerShell-Cmdlet.

Zwischenzertifikat

Aus dem Stammzertifikat kann nun ein Zwischenzertifikat erstellt werden. Dies ist nicht für alle Anwendungsfälle erforderlich, aber möglicherweise müssen Sie viele Zertifikate erstellen oder Zertifikatgruppen aktivieren oder deaktivieren. Der TextExtension-Parameter ist erforderlich, um die Pfadlänge in den grundlegenden Einschränkungen des Zertifikats festzulegen.

Das Zwischenzertifikat kann dann dem vertrauenswürdigen Zwischenzertifikat im Windows-Hostsystem hinzugefügt werden.

$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

Erstellen eines untergeordneten Zertifikats aus einem Zwischenzertifikat

Aus dem Zwischenzertifikat kann nun ein untergeordnetes Zertifikat erstellt werden. Dies ist die letzte Entität, von der keine weiteren untergeordneten Zertifikate erstellt werden müssen.

$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

Erstellen eines untergeordneten Zertifikats aus einem Stammzertifikat

Ein untergeordnetes Zertifikat kann auch direkt aus dem Stammzertifikat erstellt werden.

$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

Beispielstamm > Zwischenzertifikat > Zertifikat

$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

Bei Verwendung der Stamm-, Zwischen- oder untergeordneten Zertifikate können die Zertifikate nach Bedarf mit dem Fingerabdruck oder dem öffentlichen Schlüssel überprüft werden:

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

Zwischenspeichern der Zertifikatvalidierung

ASP.NET Core 5.0 und höher unterstützt die Zwischenspeicherung von Validierungsergebnissen. Die Zwischenspeicherung verbessert die Leistung der Zertifikatauthentifizierung erheblich, da die Validierung ein aufwendiger Vorgang ist.

Standardmäßig deaktiviert die Zertifikatauthentifizierung die Zwischenspeicherung. Rufen Sie zum Aktivieren der Zwischenspeicherung AddCertificateCache in Program.cs auf:

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

Bei der Standardimplementierung der Zwischenspeicherung werden die Ergebnisse im Arbeitsspeicher gespeichert. Sie können einen eigenen Cache bereitstellen, indem Sie ICertificateValidationCache implementieren und bei der Abhängigkeitsinjektion registrieren. Beispiel: services.AddSingleton<ICertificateValidationCache, YourCache>().

Optionale Clientzertifikate

Dieser Abschnitt enthält Informationen zu Apps, die einen Teil der App mit einem Zertifikat schützen müssen. Für eine Razor-Seite oder einen Controller in der App können beispielsweise Clientzertifikate erforderlich sein. Dies stellt Herausforderungen als Clientzertifikate dar:

  • sind ein TLS-Feature, kein HTTP-Feature.
  • werden pro Verbindung und in der Regel zu Beginn der Verbindung ausgehandelt, bevor HTTP-Daten verfügbar sind.

Es gibt zwei Ansätze zum Implementieren optionaler Clientzertifikate:

  1. Verwenden von separaten Hostnamen (SNI) und Umleitung. Auch wenn der Aufwand höher ist, wird dies empfohlen, da es in den meisten Umgebungen und mit den meisten Protokollen funktioniert.
  2. Neuverhandlung während einer HTTP-Anforderung. Dieser Ansatz weist einige Einschränkungen auf und wird nicht empfohlen.

Separate Hosts (SNI)

Zu Beginn der Verbindung ist nur die Servernamensanzeige (Server Name Indication, SNI†) bekannt. Clientzertifikate können pro Hostnamen konfiguriert werden, wenn sie z. B. nur von einzelnen Hosts benötigt werden.

ASP.NET Core 5 und höher bietet eine bequemere Unterstützung für die Umleitung zum Abrufen optionaler Clientzertifikate. Weitere Informationen finden Sie im Beispiel für optionale Zertifikate.

  • Für Anforderungen an die Web-App, die ein Clientzertifikat erfordern, aber über keins verfügen:
    • Leiten Sie die Anforderung an dieselbe Seite mit der durch ein Clientzertifikat geschützten Unterdomäne um.
    • Richten Sie z. B. eine Umleitung zu myClient.contoso.com/requestedPage ein. Da die Anforderung an myClient.contoso.com/requestedPage einen anderen Hostnamen als contoso.com/requestedPage verwendet, stellt der Client eine andere Verbindung her, und das Clientzertifikat wird bereitgestellt.
    • Weitere Informationen finden Sie unter Einführung in die Autorisierung in ASP.NET Core.

† Die Servernamensanzeige (SNI) ist eine TLS-Erweiterung zum Einbinden einer virtuellen Domäne als Teil der SSL-Aushandlung. Dies bedeutet, dass der Name der virtuellen Domäne oder eines Hosts zur Identifizierung des Netzwerkendpunkts verwendet werden kann.

Erneutes Aushandeln

Die erneute TLS-Aushandlung ist ein Prozess, bei dem Client und Server die Verschlüsselungsanforderungen für eine einzelne Verbindung neu bewerten können. Dies schließt auch die Anforderung eines Clientzertifikats ein, falls dieses zuvor nicht bereitgestellt wurde. Die erneute TLS-Aushandlung stellt ein Sicherheitsrisiko dar und wird aus folgenden Gründen nicht empfohlen:

  • In HTTP/1.1 muss der Server zunächst HTTP-Daten direkt puffern oder verarbeiten (z. B. POST-Anforderungstexte), um sicherzustellen, dass die Verbindung für die erneute Aushandlung geeignet ist. Andernfalls reagiert die erneute Aushandlung eventuell nicht mehr reagieren oder führt zu einem Fehler.
  • HTTP/2 und HTTP/3 verbieten explizit die erneute Aushandlung.
  • Die erneute Aushandlung ist mit Sicherheitsrisiken verbunden. In TLS 1.3 wurde die erneute Aushandlung für die gesamte Verbindung entfernt und durch eine neue Erweiterung für das Anfordern ausschließlich des Clientzertifikats nach dem Verbindungsstart ersetzt. Dieser Mechanismus wird über dieselben APIs verfügbar gemacht und unterliegt weiterhin den vorherigen Einschränkungen bei der Pufferung und den HTTP-Protokollversionen.

Die Implementierung und Konfiguration dieses Features variiert je nach Server- und Frameworkversion.

IIS

IIS verwaltet die Clientzertifikataushandlung in Ihrem Namen. Ein Unterabschnitt der Anwendung kann die SslRequireCert-Option zum Aushandeln des Clientzertifikats für diese Anforderungen aktivieren. Einzelheiten dazu finden Sie in der IIS-Dokumentation zur Konfiguration.

IIS puffert alle Daten im Anforderungstext vor der erneuten Aushandlung automatisch bis zu einem konfigurierten Größengrenzwert. Anforderungen, die den Grenzwert überschreiten, werden mit der Antwort 413 abgelehnt. Dieser Grenzwert ist standardmäßig auf 48 KB festgelegt und kann durch Festlegen von uploadReadAheadSize konfiguriert werden.

HttpSys

HttpSys verfügt über zwei Einstellungen zur Steuerung der Clientzertifikataushandlung, die beide festgelegt werden sollten. Die erste befindet sich in „netsh.exe“ unter http add sslcert clientcertnegotiation=enable/disable. Dieses Flag gibt an, ob das Clientzertifikat zu Beginn einer Verbindung ausgehandelt werden soll. Es sollte für optionale Clientzertifikate auf disable festgelegt werden. Weitere Informationen finden Sie in der netsh-Dokumentation.

Die andere Einstellung ist ClientCertificateMethod. Bei der Festlegung auf AllowRenegotation kann das Clientzertifikat während einer Anforderung neu ausgehandelt werden.

HINWEIS: Die Anwendung sollte alle Daten im Anforderungstext puffern oder verarbeiten, bevor eine erneute Aushandlung versucht wird. Andernfalls reagiert die Anforderung möglicherweise nicht mehr.

Eine Anwendung kann zunächst die ClientCertificate-Eigenschaft überprüfen, um festzustellen, ob das Zertifikat verfügbar ist. Wenn es nicht verfügbar ist, stellen Sie sicher, dass der Anforderungstext verarbeitet wurde, bevor Sie GetClientCertificateAsync aufrufen, um eines auszuhandeln. Hinweis: GetClientCertificateAsync kann ein NULL-Zertifikat zurückgeben, wenn der Client die Bereitstellung eines Zertifikats ablehnt.

HINWEIS: Das Verhalten der ClientCertificate-Eigenschaft wurde in .NET 6 geändert. Weitere Informationen finden Sie in diesem GitHub-Issue.

Kestrel

Kestrel steuert die Aushandlung von Clientzertifikaten mit der ClientCertificateMode-Option.

ClientCertificateMode.DelayCertificate ist eine neue Option, die ab .NET 6 verfügbar ist. Bei einer Festlegung kann eine App über die ClientCertificate-Eigenschaft überprüfen, ob das Zertifikat verfügbar ist. Wenn es nicht verfügbar ist, stellen Sie sicher, dass der Anforderungstext verarbeitet wurde, bevor Sie GetClientCertificateAsync aufrufen, um eines auszuhandeln. Hinweis: GetClientCertificateAsync kann ein NULL-Zertifikat zurückgeben, wenn der Client die Bereitstellung eines Zertifikats ablehnt.

HINWEIS: Die Anwendung sollte alle Daten im Anforderungstext puffern oder verarbeiten, bevor eine erneute Aushandlung versucht wird. Andernfalls könnte GetClientCertificateAsync die Ausnahme InvalidOperationException: Client stream needs to be drained before renegotiation. auslösen.

Wenn Sie die TLS-Einstellungen pro Host programmgesteuert konfigurieren, ist in .NET 6 und höher eine neue UseHttps-Überladung verfügbar, die TlsHandshakeCallbackOptions akzeptiert und das erneute Aushandeln von Clientzertifikaten über TlsHandshakeCallbackContext.AllowDelayedClientCertificateNegotation steuert.

Microsoft.AspNetCore.Authentication.Certificate enthält eine Implementierung ähnlich der Zertifikatauthentifizierung für ASP.NET Core. Die Zertifikatsauthentifizierung erfolgt auf der TLS-Ebene, lange bevor sie überhaupt zum ASP.NET Core gelangt. Genauer gesagt ist dies ein Authentifizierungshandler, der das Zertifikat überprüft und Ihnen dann ein Ereignis bereitstellt, bei dem Sie dieses Zertifikat in einen ClaimsPrincipal auflösen können.

Konfigurieren Sie Ihren Server für die Zertifikatauthentifizierung. Dabei können Sie IIS, Kestrel, Azure-Web-Apps oder etwas anderes verwenden.

Szenarien mit Proxy und Lastenausgleich

Die Zertifikatauthentifizierung ist ein zustandsbehaftetes Szenario, das hauptsächlich verwendet wird, in denen der Datenverkehr zwischen Clients und Servern nicht durch einen Proxy oder Lastenausgleich verarbeitet wird. Wenn ein Proxy oder Lastenausgleich verwendet wird, funktioniert die Zertifikatauthentifizierung nur, wenn Proxy oder Lastenausgleich folgende Funktionen übernehmen:

  • Verarbeiten der Authentifizierung
  • Übergeben von Benutzerauthentifizierungsinformationen an die App (z. B. in einem Anforderungsheader), die die Authentifizierungsinformationen nutzt

Eine Alternative zur Zertifikatauthentifizierung in Umgebungen, in denen Proxys und Lastenausgleichsmodule verwendet werden, bildet Active Directory-Verbunddienste (AD FS) mit OpenID Connect (OIDC).

Erste Schritte

Rufen Sie ein HTTPS-Zertifikat ab, wenden Sie es an, und konfigurieren Sie Ihren Server so, dass Zertifikate erforderlich sind.

Fügen Sie in Ihre Web-App einen Verweis auf das Paket Microsoft.AspNetCore.Authentication.Certificate hinzu. Rufen Sie dann in der Startup.ConfigureServices-Methode services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...); mit Ihren Optionen auf, und geben Sie einen Delegaten für OnCertificateValidated an, um eine zusätzliche Überprüfung des Clientzertifikats durchzuführen, das mit den Anforderungen gesendet wird. Wandeln Sie diese Informationen in einen ClaimsPrincipal um, und legen Sie diesen in der context.Principal-Eigenschaft fest.

Bei einem Authentifizierungsfehler gibt dieser Handler wie erwartet eine 403 (Forbidden)-Antwort anstelle von 401 (Unauthorized) zurück. Der Grund hierfür ist, dass die Authentifizierung während der ersten TLS-Verbindung erfolgen sollte. Wenn der Handler erreicht wird, ist es zu spät. Es gibt keine Möglichkeit, die Verbindung von einer anonymen Verbindung in eine Verbindung mit einem Zertifikat zu ändern.

Fügen Sie auch app.UseAuthentication(); in der Startup.Configure-Methode hinzu. Andernfalls wird HttpContext.User nicht auf den ClaimsPrincipal festgelegt, der aus dem Zertifikat erstellt wurde. Beispiel:

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
}

Im obigen Beispiel wird das Standardverfahren zum Hinzufügen der Zertifikatauthentifizierung veranschaulicht. Der Handler erstellt einen Benutzerprinzipal mit den allgemeinen Zertifikateigenschaften.

Konfigurieren der Zertifikatüberprüfung

Der CertificateAuthenticationOptions-Handler verfügt über einige integrierte Validierungen. Dabei handelt es such um die mindestens erforderlichen Überprüfungen, die Sie für ein Zertifikat ausführen sollten. Jede dieser Einstellungen ist standardmäßig aktiviert.

AllowedCertificateTypes = Chained, SelfSigned oder All (Chained | SelfSigned)

Standardwert: CertificateTypes.Chained

Bei dieser Überprüfung wird getestet, ob nur der entsprechende Zertifikattyp zulässig ist. Wenn die App selbstsignierte Zertifikate verwendet, muss diese Option auf CertificateTypes.All oder CertificateTypes.SelfSigned festgelegt werden.

ValidateCertificateUse

Standardwert: true

Bei dieser Überprüfung wird getestet, ob das vom Client bereitgestellte Zertifikat über die erweiterte Schlüsselverwendung (EKU) der Clientauthentifizierung oder überhaupt keine EKUs verfügt. Wenn keine EKU angegeben wird, gelten laut Spezifikationen alle EKUs als gültig.

ValidateValidityPeriod

Standardwert: true

Bei dieser Überprüfung wird getestet, ob das Zertifikat innerhalb seines Gültigkeitszeitraums liegt. Bei jeder Anforderung stellt der Handler sicher, dass ein Zertifikat, das bei der Präsentation gültig war, während der aktuellen Sitzung nicht abgelaufen ist.

RevocationFlag

Standardwert: X509RevocationFlag.ExcludeRoot

Dieses Flag gibt an, welche Zertifikate in der Kette auf Sperrung überprüft werden sollen.

Sperrprüfungen werden nur ausgeführt, wenn das Zertifikat mit einem Stammzertifikat verkettet ist.

RevocationMode

Standardwert: X509RevocationMode.Online

Dieses Flag gibt an, wie Sperrüberprüfungen ausgeführt werden.

Die Angabe einer Onlineüberprüfung kann zu einer langen Verzögerung führen, während die Zertifizierungsstelle kontaktiert wird.

Sperrprüfungen werden nur ausgeführt, wenn das Zertifikat mit einem Stammzertifikat verkettet ist.

Kann ich meine App so konfigurieren, dass ein Zertifikat nur für bestimmte Pfade erforderlich ist?

Dies ist nicht möglich. Denken Sie daran, dass der Zertifikataustausch zu Beginn der HTTPS-Konversation erfolgt. Er wird vom Server ausgeführt, bevor die erste Anforderung für diese Verbindung empfangen wird, sodass es nicht möglich ist, einen Bereich basierend auf Anforderungsfeldern zu verwenden.

Handlerereignisse

Der Handler weist zwei Ereignisse auf:

  • OnAuthenticationFailed wird aufgerufen, wenn während der Authentifizierung eine Ausnahme auftritt und Sie reagieren können.
  • OnCertificateValidated wird aufgerufen, nachdem das Zertifikat überprüft, die Überprüfung bestanden und ein Standardprinzipal erstellt wurde. Mit diesem Ereignis können Sie Ihre eigene Validierung durchführen und den Prinzipal erweitern oder ersetzen. Beispiele dafür sind:
    • Ermitteln, ob das Zertifikat Ihrem Diensten bekannt ist

    • Erstellen eines eigenen Prinzipals Betrachten Sie das folgende Beispiel 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;
                  }
              };
          });
      

Wenn Sie feststellen, dass das eingehende Zertifikat Ihre zusätzliche Validierung nicht besteht, rufen Sie context.Fail("failure reason") mit einem Fehlergrund auf.

Bei der praktischen Anwendung rufen Sie wahrscheinlich einen Dienst auf, der bei der Abhängigkeitsinjektion registriert ist und eine Verbindung mit einer Datenbank oder einem anderen Benutzerspeicher herstellt. Greifen Sie mithilfe des Kontexts, der an den Delegaten übergeben wurde, auf Ihren Dienst zu. Betrachten Sie das folgende Beispiel 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;
            }
        };
    });

Konzeptionell ist die Validierung des Zertifikats ein Autorisierungsschritt. Das Hinzufügen einer Überprüfung, z. B. eines Ausstellers oder Fingerabdrucks in einer Autorisierungsrichtlinie und nicht innerhalb von OnCertificateValidated, ist problemlos möglich.

Konfigurieren Ihres Servers für die Erzwingung von Zertifikaten

Kestrel

Konfigurieren Sie Kestrel in Program.cs wie folgt:

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

Hinweis

Auf Endpunkte, die durch Aufrufen von Listen vor dem Aufrufen von ConfigureHttpsDefaults erstellt werden, werden die Standardwerte nicht angewendet.

IIS

Führen Sie im IIS-Manager die folgenden Schritte aus:

  1. Wählen Sie Ihre Website auf der Registerkarte Verbindungen aus.
  2. Doppelklicken Sie im Fenster Featureansicht auf die Option SSL-Einstellungen.
  3. Aktivieren Sie das Kontrollkästchen SSL anfordern und im Abschnitt Clientzertifikate das Optionsfeld Erforderlich.

Einstellungen für Clientzertifikate in IIS

Azure und benutzerdefinierte Webproxys

Informationen zum Konfigurieren der Middleware für die Zertifikatweiterleitung finden Sie in der Host- und Bereitstellungsdokumentation.

Verwenden der Zertifikatauthentifizierung in Azure-Web-Apps

Für Azure ist keine Weiterleitungskonfiguration erforderlich. Die Weiterleitungskonfiguration wird von der Middleware für die Zertifikatweiterleitung eingerichtet.

Hinweis

Für dieses Szenario ist Middleware für die Zertifikatweiterleitung erforderlich.

Weitere Informationen finden Sie unter Verwenden eines TLS-/SSL-Zertifikats in Ihrem Code in Azure App Service (Azure-Dokumentation).

Verwenden der Zertifikatauthentifizierung in benutzerdefinierten Webproxys

Mit der AddCertificateForwarding-Methode wird Folgendes angegeben:

  • Der Clientheadername
  • Wie das Zertifikat geladen werden soll (mit der HeaderConverter-Eigenschaft)

Bei benutzerdefinierten Webproxys wird das Zertifikat als benutzerdefinierter Anforderungsheader übergeben, z. B. X-SSL-CERT. Um es zu verwenden, konfigurieren Sie die Zertifikatweiterleitung 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;
}

Wenn für die App ein Reverseproxy von NGINX mit der Konfiguration proxy_set_header ssl-client-cert $ssl_client_escaped_cert verwendet wird oder wenn sie in Kubernetes mithilfe von NGINX Ingress bereitgestellt wird, wird das Clientzertifikat URL-codiert an die App übergeben. Um das Zertifikat zu verwenden, decodieren Sie es wie folgt:

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

Die Startup.Configure-Methode fügt dann die Middleware hinzu. UseCertificateForwarding wird vor den Aufrufen von UseAuthentication und UseAuthorization aufgerufen:

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

    app.UseRouting();

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

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

Mithilfe einer separaten Klasse können Sie Validierungslogik implementieren. Da in diesem Beispiel dasselbe selbstsignierte Zertifikat verwendet wird, müssen Sie sicherstellen, dass nur Ihr Zertifikat verwendet werden kann. Überprüfen Sie, ob die Fingerabdrücke des Clientzertifikats und des Serverzertifikats übereinstimmen. Andernfalls kann jedes Zertifikat verwendet werden, und es reicht für die Authentifizierung aus. Dies wird innerhalb der AddCertificate-Methode verwendet. Sie können hier auch den Antragsteller oder den Aussteller überprüfen, wenn Sie zwischengeschaltete oder untergeordnete Zertifikate verwenden.

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

Implementieren eines HttpClient mithilfe eines Zertifikats und von HttpClientHandler

Der HttpClientHandler kann direkt im Konstruktor der HttpClient-Klasse hinzugefügt werden. Beim Erstellen von Instanzen von HttpClient ist Vorsicht geboten. Der HttpClient sendet dann das Zertifikat mit jeder Anforderung.

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

Implementieren eines HttpClient mithilfe eines Zertifikats und eines benannten HttpClient aus IHttpClientFactory

Im folgenden Beispiel wird ein Clientzertifikat mithilfe der ClientCertificates-Eigenschaft des Handlers einem HttpClientHandler hinzugefügt. Dieser Handler kann dann in einer benannten Instanz eines HttpClient mithilfe der ConfigurePrimaryHttpMessageHandler-Methode verwendet werden. Dies wird in Startup.ConfigureServices eingerichtet:

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

Die IHttpClientFactory kann dann verwendet werden, um die benannte Instanz mit dem Handler und dem Zertifikat abzurufen. Die CreateClient-Methode mit dem Namen des in der Startup-Klasse definierten Clients wird verwendet, um die Instanz abzurufen. Die HTTP-Anforderung kann bei Bedarf über den Client gesendet werden.

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

Wenn das richtige Zertifikat an den Server gesendet wird, werden die Daten zurückgegeben. Wenn kein Zertifikat oder ein falsches Zertifikat übermittelt wurde, wird der HTTP-Statuscode 403 zurückgegeben.

Erstellen von Zertifikaten in PowerShell

Das Erstellen der Zertifikate ist der schwierigste Teil beim Einrichten dieses Flows. Ein Stammzertifikat kann mithilfe des PowerShell-Cmdlets New-SelfSignedCertificate erstellt werden. Verwenden Sie beim Erstellen des Zertifikats ein sicheres Kennwort. Es ist wichtig, den KeyUsageProperty-Parameter und den KeyUsage-Parameter wie gezeigt hinzuzufügen.

Erstellen einer Stammzertifizierungsstelle

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

Hinweis

Der Wert des -DnsName-Parameters muss mit dem Bereitstellungsziel der App übereinstimmen. Bei der Entwicklung ist dies z. B. „localhost“.

Installieren im vertrauenswürdigen Stamm

Das Stammzertifikat muss auf Ihrem Hostsystem als vertrauenswürdig gelten. Ein Stammzertifikat, das nicht von einer Zertifizierungsstelle ausgestellt wurde, gilt standardmäßig nicht als vertrauenswürdig. Weitere Informationen zu Vertrauensstellungen von Stammzertifikaten unter Windows finden Sie unter dieser Frage.

Zwischenzertifikat

Aus dem Stammzertifikat kann nun ein Zwischenzertifikat erstellt werden. Dies ist nicht für alle Anwendungsfälle erforderlich, aber möglicherweise müssen Sie viele Zertifikate erstellen oder Zertifikatgruppen aktivieren oder deaktivieren. Der TextExtension-Parameter ist erforderlich, um die Pfadlänge in den grundlegenden Einschränkungen des Zertifikats festzulegen.

Das Zwischenzertifikat kann dann dem vertrauenswürdigen Zwischenzertifikat im Windows-Hostsystem hinzugefügt werden.

$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

Erstellen eines untergeordneten Zertifikats aus einem Zwischenzertifikat

Aus dem Zwischenzertifikat kann nun ein untergeordnetes Zertifikat erstellt werden. Dies ist die letzte Entität, von der keine weiteren untergeordneten Zertifikate erstellt werden müssen.

$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

Erstellen eines untergeordneten Zertifikats aus einem Stammzertifikat

Ein untergeordnetes Zertifikat kann auch direkt aus dem Stammzertifikat erstellt werden.

$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

Beispielstamm > Zwischenzertifikat > Zertifikat

$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

Bei Verwendung der Stamm-, Zwischen- oder untergeordneten Zertifikate können die Zertifikate nach Bedarf mit dem Fingerabdruck oder dem öffentlichen Schlüssel überprüft werden.

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

Zwischenspeichern der Zertifikatvalidierung

ASP.NET Core 5.0 und höher unterstützt die Zwischenspeicherung von Validierungsergebnissen. Die Zwischenspeicherung verbessert die Leistung der Zertifikatauthentifizierung erheblich, da die Validierung ein aufwendiger Vorgang ist.

Standardmäßig deaktiviert die Zertifikatauthentifizierung die Zwischenspeicherung. Rufen Sie zum Aktivieren der Zwischenspeicherung AddCertificateCache in Startup.ConfigureServices auf:

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

Bei der Standardimplementierung der Zwischenspeicherung werden die Ergebnisse im Arbeitsspeicher gespeichert. Sie können einen eigenen Cache bereitstellen, indem Sie ICertificateValidationCache implementieren und bei der Abhängigkeitsinjektion registrieren. Beispiel: services.AddSingleton<ICertificateValidationCache, YourCache>().

Optionale Clientzertifikate

Dieser Abschnitt enthält Informationen zu Apps, die einen Teil der App mit einem Zertifikat schützen müssen. Für eine Razor-Seite oder einen Controller in der App können beispielsweise Clientzertifikate erforderlich sein. Dies stellt Herausforderungen als Clientzertifikate dar:

  • sind ein TLS-Feature, kein HTTP-Feature.
  • werden pro Verbindung und in der Regel zu Beginn der Verbindung ausgehandelt, bevor HTTP-Daten verfügbar sind.

Es gibt zwei Ansätze zum Implementieren optionaler Clientzertifikate:

  1. Verwenden von separaten Hostnamen (SNI) und Umleitung. Auch wenn der Aufwand höher ist, wird dies empfohlen, da es in den meisten Umgebungen und mit den meisten Protokollen funktioniert.
  2. Neuverhandlung während einer HTTP-Anforderung. Dieser Ansatz weist einige Einschränkungen auf und wird nicht empfohlen.

Separate Hosts (SNI)

Zu Beginn der Verbindung ist nur die Servernamensanzeige (Server Name Indication, SNI†) bekannt. Clientzertifikate können pro Hostnamen konfiguriert werden, wenn sie z. B. nur von einzelnen Hosts benötigt werden.

ASP.NET Core 5 und höher bietet eine bequemere Unterstützung für die Umleitung zum Abrufen optionaler Clientzertifikate. Weitere Informationen finden Sie im Beispiel für optionale Zertifikate.

  • Für Anforderungen an die Web-App, die ein Clientzertifikat erfordern, aber über keins verfügen:
    • Leiten Sie die Anforderung an dieselbe Seite mit der durch ein Clientzertifikat geschützten Unterdomäne um.
    • Richten Sie z. B. eine Umleitung zu myClient.contoso.com/requestedPage ein. Da die Anforderung an myClient.contoso.com/requestedPage einen anderen Hostnamen als contoso.com/requestedPage verwendet, stellt der Client eine andere Verbindung her, und das Clientzertifikat wird bereitgestellt.
    • Weitere Informationen finden Sie unter Einführung in die Autorisierung in ASP.NET Core.

† Die Servernamensanzeige (SNI) ist eine TLS-Erweiterung zum Einbinden einer virtuellen Domäne als Teil der SSL-Aushandlung. Dies bedeutet, dass der Name der virtuellen Domäne oder eines Hosts zur Identifizierung des Netzwerkendpunkts verwendet werden kann.

Erneutes Aushandeln

Die erneute TLS-Aushandlung ist ein Prozess, bei dem Client und Server die Verschlüsselungsanforderungen für eine einzelne Verbindung neu bewerten können. Dies schließt auch die Anforderung eines Clientzertifikats ein, falls dieses zuvor nicht bereitgestellt wurde. Die erneute TLS-Aushandlung stellt ein Sicherheitsrisiko dar und wird aus folgenden Gründen nicht empfohlen:

  • In HTTP/1.1 muss der Server zunächst HTTP-Daten direkt puffern oder verarbeiten (z. B. POST-Anforderungstexte), um sicherzustellen, dass die Verbindung für die erneute Aushandlung geeignet ist. Andernfalls reagiert die erneute Aushandlung eventuell nicht mehr reagieren oder führt zu einem Fehler.
  • HTTP/2 und HTTP/3 verbieten explizit die erneute Aushandlung.
  • Die erneute Aushandlung ist mit Sicherheitsrisiken verbunden. In TLS 1.3 wurde die erneute Aushandlung für die gesamte Verbindung entfernt und durch eine neue Erweiterung für das Anfordern ausschließlich des Clientzertifikats nach dem Verbindungsstart ersetzt. Dieser Mechanismus wird über dieselben APIs verfügbar gemacht und unterliegt weiterhin den vorherigen Einschränkungen bei der Pufferung und den HTTP-Protokollversionen.

Die Implementierung und Konfiguration dieses Features variiert je nach Server- und Frameworkversion.

IIS

IIS verwaltet die Clientzertifikataushandlung in Ihrem Namen. Ein Unterabschnitt der Anwendung kann die SslRequireCert-Option zum Aushandeln des Clientzertifikats für diese Anforderungen aktivieren. Einzelheiten dazu finden Sie in der IIS-Dokumentation zur Konfiguration.

IIS puffert alle Daten im Anforderungstext vor der erneuten Aushandlung automatisch bis zu einem konfigurierten Größengrenzwert. Anforderungen, die den Grenzwert überschreiten, werden mit der Antwort 413 abgelehnt. Dieser Grenzwert ist standardmäßig auf 48 KB festgelegt und kann durch Festlegen von uploadReadAheadSize konfiguriert werden.

HttpSys

HttpSys verfügt über zwei Einstellungen zur Steuerung der Clientzertifikataushandlung, die beide festgelegt werden sollten. Die erste befindet sich in „netsh.exe“ unter http add sslcert clientcertnegotiation=enable/disable. Dieses Flag gibt an, ob das Clientzertifikat zu Beginn einer Verbindung ausgehandelt werden soll. Es sollte für optionale Clientzertifikate auf disable festgelegt werden. Weitere Informationen finden Sie in der netsh-Dokumentation.

Die andere Einstellung ist ClientCertificateMethod. Bei der Festlegung auf AllowRenegotation kann das Clientzertifikat während einer Anforderung neu ausgehandelt werden.

HINWEIS: Die Anwendung sollte alle Daten im Anforderungstext puffern oder verarbeiten, bevor eine erneute Aushandlung versucht wird. Andernfalls reagiert die Anforderung möglicherweise nicht mehr.

Es gibt ein bekanntes Problem, durch das bei der Aktivierung von AllowRenegotation die erneute Aushandlung synchron erfolgen kann, wenn auf die ClientCertificate-Eigenschaft zugegriffen wird. Rufen Sie die GetClientCertificateAsync-Methode auf, um dies zu verhindern. Dieses Problem wurde in .NET 6 behoben. Weitere Informationen finden Sie in diesem GitHub-Issue. Hinweis: GetClientCertificateAsync kann ein NULL-Zertifikat zurückgeben, wenn der Client die Bereitstellung eines Zertifikats ablehnt.

Kestrel

Kestrel steuert die Aushandlung von Clientzertifikaten mit der ClientCertificateMode-Option.

Für .NET 5 und früher unterstützt Kestrel die erneute Aushandlung nach dem Start einer Verbindung zum Abrufen eines Clientzertifikats nicht. Dieses Feature wurde in .NET 6 hinzugefügt.

Microsoft.AspNetCore.Authentication.Certificate enthält eine Implementierung ähnlich der Zertifikatauthentifizierung für ASP.NET Core. Die Zertifikatsauthentifizierung erfolgt auf der TLS-Ebene, lange bevor sie überhaupt zum ASP.NET Core gelangt. Genauer gesagt ist dies ein Authentifizierungshandler, der das Zertifikat überprüft und Ihnen dann ein Ereignis bereitstellt, bei dem Sie dieses Zertifikat in einen ClaimsPrincipal auflösen können.

Konfigurieren Sie Ihren Server für die Zertifikatauthentifizierung. Dabei können Sie IIS, Kestrel, Azure-Web-Apps oder etwas anderes verwenden.

Szenarien mit Proxy und Lastenausgleich

Die Zertifikatauthentifizierung ist ein zustandsbehaftetes Szenario, das hauptsächlich verwendet wird, in denen der Datenverkehr zwischen Clients und Servern nicht durch einen Proxy oder Lastenausgleich verarbeitet wird. Wenn ein Proxy oder Lastenausgleich verwendet wird, funktioniert die Zertifikatauthentifizierung nur, wenn Proxy oder Lastenausgleich folgende Funktionen übernehmen:

  • Verarbeiten der Authentifizierung
  • Übergeben von Benutzerauthentifizierungsinformationen an die App (z. B. in einem Anforderungsheader), die die Authentifizierungsinformationen nutzt

Eine Alternative zur Zertifikatauthentifizierung in Umgebungen, in denen Proxys und Lastenausgleichsmodule verwendet werden, bildet Active Directory-Verbunddienste (AD FS) mit OpenID Connect (OIDC).

Erste Schritte

Rufen Sie ein HTTPS-Zertifikat ab, wenden Sie es an, und konfigurieren Sie Ihren Server so, dass Zertifikate erforderlich sind.

Fügen Sie in Ihre Web-App einen Verweis auf das Paket Microsoft.AspNetCore.Authentication.Certificate hinzu. Rufen Sie dann in der Startup.ConfigureServices-Methode services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...); mit Ihren Optionen auf, und geben Sie einen Delegaten für OnCertificateValidated an, um eine zusätzliche Überprüfung des Clientzertifikats durchzuführen, das mit den Anforderungen gesendet wird. Wandeln Sie diese Informationen in einen ClaimsPrincipal um, und legen Sie diesen in der context.Principal-Eigenschaft fest.

Bei einem Authentifizierungsfehler gibt dieser Handler wie erwartet eine 403 (Forbidden)-Antwort anstelle von 401 (Unauthorized) zurück. Der Grund hierfür ist, dass die Authentifizierung während der ersten TLS-Verbindung erfolgen sollte. Wenn der Handler erreicht wird, ist es zu spät. Es gibt keine Möglichkeit, die Verbindung von einer anonymen Verbindung in eine Verbindung mit einem Zertifikat zu ändern.

Fügen Sie auch app.UseAuthentication(); in der Startup.Configure-Methode hinzu. Andernfalls wird HttpContext.User nicht auf den ClaimsPrincipal festgelegt, der aus dem Zertifikat erstellt wurde. Beispiel:

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
}

Im obigen Beispiel wird das Standardverfahren zum Hinzufügen der Zertifikatauthentifizierung veranschaulicht. Der Handler erstellt einen Benutzerprinzipal mit den allgemeinen Zertifikateigenschaften.

Konfigurieren der Zertifikatüberprüfung

Der CertificateAuthenticationOptions-Handler verfügt über einige integrierte Validierungen. Dabei handelt es such um die mindestens erforderlichen Überprüfungen, die Sie für ein Zertifikat ausführen sollten. Jede dieser Einstellungen ist standardmäßig aktiviert.

AllowedCertificateTypes = Chained, SelfSigned oder All (Chained | SelfSigned)

Standardwert: CertificateTypes.Chained

Bei dieser Überprüfung wird getestet, ob nur der entsprechende Zertifikattyp zulässig ist. Wenn die App selbstsignierte Zertifikate verwendet, muss diese Option auf CertificateTypes.All oder CertificateTypes.SelfSigned festgelegt werden.

ValidateCertificateUse

Standardwert: true

Bei dieser Überprüfung wird getestet, ob das vom Client bereitgestellte Zertifikat über die erweiterte Schlüsselverwendung (EKU) der Clientauthentifizierung oder überhaupt keine EKUs verfügt. Wenn keine EKU angegeben wird, gelten laut Spezifikationen alle EKUs als gültig.

ValidateValidityPeriod

Standardwert: true

Bei dieser Überprüfung wird getestet, ob das Zertifikat innerhalb seines Gültigkeitszeitraums liegt. Bei jeder Anforderung stellt der Handler sicher, dass ein Zertifikat, das bei der Präsentation gültig war, während der aktuellen Sitzung nicht abgelaufen ist.

RevocationFlag

Standardwert: X509RevocationFlag.ExcludeRoot

Dieses Flag gibt an, welche Zertifikate in der Kette auf Sperrung überprüft werden sollen.

Sperrprüfungen werden nur ausgeführt, wenn das Zertifikat mit einem Stammzertifikat verkettet ist.

RevocationMode

Standardwert: X509RevocationMode.Online

Dieses Flag gibt an, wie Sperrüberprüfungen ausgeführt werden.

Die Angabe einer Onlineüberprüfung kann zu einer langen Verzögerung führen, während die Zertifizierungsstelle kontaktiert wird.

Sperrprüfungen werden nur ausgeführt, wenn das Zertifikat mit einem Stammzertifikat verkettet ist.

Kann ich meine App so konfigurieren, dass ein Zertifikat nur für bestimmte Pfade erforderlich ist?

Dies ist nicht möglich. Denken Sie daran, dass der Zertifikataustausch zu Beginn der HTTPS-Konversation erfolgt. Er wird vom Server ausgeführt, bevor die erste Anforderung für diese Verbindung empfangen wird, sodass es nicht möglich ist, einen Bereich basierend auf Anforderungsfeldern zu verwenden.

Handlerereignisse

Der Handler weist zwei Ereignisse auf:

  • OnAuthenticationFailed wird aufgerufen, wenn während der Authentifizierung eine Ausnahme auftritt und Sie reagieren können.
  • OnCertificateValidated wird aufgerufen, nachdem das Zertifikat überprüft, die Überprüfung bestanden und ein Standardprinzipal erstellt wurde. Mit diesem Ereignis können Sie Ihre eigene Validierung durchführen und den Prinzipal erweitern oder ersetzen. Beispiele dafür sind:
    • Ermitteln, ob das Zertifikat Ihrem Diensten bekannt ist

    • Erstellen eines eigenen Prinzipals Betrachten Sie das folgende Beispiel 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;
                  }
              };
          });
      

Wenn Sie feststellen, dass das eingehende Zertifikat Ihre zusätzliche Validierung nicht besteht, rufen Sie context.Fail("failure reason") mit einem Fehlergrund auf.

Bei der praktischen Anwendung rufen Sie wahrscheinlich einen Dienst auf, der bei der Abhängigkeitsinjektion registriert ist und eine Verbindung mit einer Datenbank oder einem anderen Benutzerspeicher herstellt. Greifen Sie mithilfe des Kontexts, der an den Delegaten übergeben wurde, auf Ihren Dienst zu. Betrachten Sie das folgende Beispiel 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;
            }
        };
    });

Konzeptionell ist die Validierung des Zertifikats ein Autorisierungsschritt. Das Hinzufügen einer Überprüfung, z. B. eines Ausstellers oder Fingerabdrucks in einer Autorisierungsrichtlinie und nicht innerhalb von OnCertificateValidated, ist problemlos möglich.

Konfigurieren Ihres Servers für die Erzwingung von Zertifikaten

Kestrel

Konfigurieren Sie Kestrel in Program.cs wie folgt:

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

Hinweis

Auf Endpunkte, die durch Aufrufen von Listen vor dem Aufrufen von ConfigureHttpsDefaults erstellt werden, werden die Standardwerte nicht angewendet.

IIS

Führen Sie im IIS-Manager die folgenden Schritte aus:

  1. Wählen Sie Ihre Website auf der Registerkarte Verbindungen aus.
  2. Doppelklicken Sie im Fenster Featureansicht auf die Option SSL-Einstellungen.
  3. Aktivieren Sie das Kontrollkästchen SSL anfordern und im Abschnitt Clientzertifikate das Optionsfeld Erforderlich.

Einstellungen für Clientzertifikate in IIS

Azure und benutzerdefinierte Webproxys

Informationen zum Konfigurieren der Middleware für die Zertifikatweiterleitung finden Sie in der Host- und Bereitstellungsdokumentation.

Verwenden der Zertifikatauthentifizierung in Azure-Web-Apps

Für Azure ist keine Weiterleitungskonfiguration erforderlich. Die Weiterleitungskonfiguration wird von der Middleware für die Zertifikatweiterleitung eingerichtet.

Hinweis

Für dieses Szenario ist Middleware für die Zertifikatweiterleitung erforderlich.

Weitere Informationen finden Sie unter Verwenden eines TLS-/SSL-Zertifikats in Ihrem Code in Azure App Service (Azure-Dokumentation).

Verwenden der Zertifikatauthentifizierung in benutzerdefinierten Webproxys

Mit der AddCertificateForwarding-Methode wird Folgendes angegeben:

  • Der Clientheadername
  • Wie das Zertifikat geladen werden soll (mit der HeaderConverter-Eigenschaft)

Bei benutzerdefinierten Webproxys wird das Zertifikat als benutzerdefinierter Anforderungsheader übergeben, z. B. X-SSL-CERT. Um es zu verwenden, konfigurieren Sie die Zertifikatweiterleitung 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;
}

Wenn für die App ein Reverseproxy von NGINX mit der Konfiguration proxy_set_header ssl-client-cert $ssl_client_escaped_cert verwendet wird oder wenn sie in Kubernetes mithilfe von NGINX Ingress bereitgestellt wird, wird das Clientzertifikat URL-codiert an die App übergeben. Um das Zertifikat zu verwenden, decodieren Sie es wie folgt:

Fügen Sie den Namespace für System.Net am Anfang der Datei Startup.cs hinzu:

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

Fügen Sie die UrlEncodedPemToByteArray-Methode hinzu:

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

Die Startup.Configure-Methode fügt dann die Middleware hinzu. UseCertificateForwarding wird vor den Aufrufen von UseAuthentication und UseAuthorization aufgerufen:

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

    app.UseRouting();

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

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

Mithilfe einer separaten Klasse können Sie Validierungslogik implementieren. Da in diesem Beispiel dasselbe selbstsignierte Zertifikat verwendet wird, müssen Sie sicherstellen, dass nur Ihr Zertifikat verwendet werden kann. Überprüfen Sie, ob die Fingerabdrücke des Clientzertifikats und des Serverzertifikats übereinstimmen. Andernfalls kann jedes Zertifikat verwendet werden, und es reicht für die Authentifizierung aus. Dies wird innerhalb der AddCertificate-Methode verwendet. Sie können hier auch den Antragsteller oder den Aussteller überprüfen, wenn Sie zwischengeschaltete oder untergeordnete Zertifikate verwenden.

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

Implementieren eines HttpClient mithilfe eines Zertifikats und von HttpClientHandler

Der HttpClientHandler kann direkt im Konstruktor der HttpClient-Klasse hinzugefügt werden. Beim Erstellen von Instanzen von HttpClient ist Vorsicht geboten. Der HttpClient sendet dann das Zertifikat mit jeder Anforderung.

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

Implementieren eines HttpClient mithilfe eines Zertifikats und eines benannten HttpClient aus IHttpClientFactory

Im folgenden Beispiel wird ein Clientzertifikat mithilfe der ClientCertificates-Eigenschaft des Handlers einem HttpClientHandler hinzugefügt. Dieser Handler kann dann in einer benannten Instanz eines HttpClient mithilfe der ConfigurePrimaryHttpMessageHandler-Methode verwendet werden. Dies wird in Startup.ConfigureServices eingerichtet:

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

Die IHttpClientFactory kann dann verwendet werden, um die benannte Instanz mit dem Handler und dem Zertifikat abzurufen. Die CreateClient-Methode mit dem Namen des in der Startup-Klasse definierten Clients wird verwendet, um die Instanz abzurufen. Die HTTP-Anforderung kann bei Bedarf über den Client gesendet werden.

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

Wenn das richtige Zertifikat an den Server gesendet wird, werden die Daten zurückgegeben. Wenn kein Zertifikat oder ein falsches Zertifikat übermittelt wurde, wird der HTTP-Statuscode 403 zurückgegeben.

Erstellen von Zertifikaten in PowerShell

Das Erstellen der Zertifikate ist der schwierigste Teil beim Einrichten dieses Flows. Ein Stammzertifikat kann mithilfe des PowerShell-Cmdlets New-SelfSignedCertificate erstellt werden. Verwenden Sie beim Erstellen des Zertifikats ein sicheres Kennwort. Es ist wichtig, den KeyUsageProperty-Parameter und den KeyUsage-Parameter wie gezeigt hinzuzufügen.

Erstellen einer Stammzertifizierungsstelle

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

Hinweis

Der Wert des -DnsName-Parameters muss mit dem Bereitstellungsziel der App übereinstimmen. Bei der Entwicklung ist dies z. B. „localhost“.

Installieren im vertrauenswürdigen Stamm

Das Stammzertifikat muss auf Ihrem Hostsystem als vertrauenswürdig gelten. Ein Stammzertifikat, das nicht von einer Zertifizierungsstelle ausgestellt wurde, gilt standardmäßig nicht als vertrauenswürdig. Weitere Informationen zu Vertrauensstellungen von Stammzertifikaten unter Windows finden Sie unter dieser Frage.

Zwischenzertifikat

Aus dem Stammzertifikat kann nun ein Zwischenzertifikat erstellt werden. Dies ist nicht für alle Anwendungsfälle erforderlich, aber möglicherweise müssen Sie viele Zertifikate erstellen oder Zertifikatgruppen aktivieren oder deaktivieren. Der TextExtension-Parameter ist erforderlich, um die Pfadlänge in den grundlegenden Einschränkungen des Zertifikats festzulegen.

Das Zwischenzertifikat kann dann dem vertrauenswürdigen Zwischenzertifikat im Windows-Hostsystem hinzugefügt werden.

$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

Erstellen eines untergeordneten Zertifikats aus einem Zwischenzertifikat

Aus dem Zwischenzertifikat kann nun ein untergeordnetes Zertifikat erstellt werden. Dies ist die letzte Entität, von der keine weiteren untergeordneten Zertifikate erstellt werden müssen.

$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

Erstellen eines untergeordneten Zertifikats aus einem Stammzertifikat

Ein untergeordnetes Zertifikat kann auch direkt aus dem Stammzertifikat erstellt werden.

$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

Beispielstamm > Zwischenzertifikat > Zertifikat

$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

Bei Verwendung der Stamm-, Zwischen- oder untergeordneten Zertifikate können die Zertifikate nach Bedarf mit dem Fingerabdruck oder dem öffentlichen Schlüssel überprüft werden.

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

Optionale Clientzertifikate

Dieser Abschnitt enthält Informationen zu Apps, die einen Teil der App mit einem Zertifikat schützen müssen. Für eine Razor-Seite oder einen Controller in der App können beispielsweise Clientzertifikate erforderlich sein. Dies stellt Herausforderungen als Clientzertifikate dar:

  • sind ein TLS-Feature, kein HTTP-Feature.
  • werden pro Verbindung und in der Regel zu Beginn der Verbindung ausgehandelt, bevor HTTP-Daten verfügbar sind.

Es gibt zwei Ansätze zum Implementieren optionaler Clientzertifikate:

  1. Verwenden von separaten Hostnamen (SNI) und Umleitung. Auch wenn der Aufwand höher ist, wird dies empfohlen, da es in den meisten Umgebungen und mit den meisten Protokollen funktioniert.
  2. Neuverhandlung während einer HTTP-Anforderung. Dieser Ansatz weist einige Einschränkungen auf und wird nicht empfohlen.

Separate Hosts (SNI)

Zu Beginn der Verbindung ist nur die Servernamensanzeige (Server Name Indication, SNI†) bekannt. Clientzertifikate können pro Hostnamen konfiguriert werden, wenn sie z. B. nur von einzelnen Hosts benötigt werden.

ASP.NET Core 5 und höher bietet eine bequemere Unterstützung für die Umleitung zum Abrufen optionaler Clientzertifikate. Weitere Informationen finden Sie im Beispiel für optionale Zertifikate.

  • Für Anforderungen an die Web-App, die ein Clientzertifikat erfordern, aber über keins verfügen:
    • Leiten Sie die Anforderung an dieselbe Seite mit der durch ein Clientzertifikat geschützten Unterdomäne um.
    • Richten Sie z. B. eine Umleitung zu myClient.contoso.com/requestedPage ein. Da die Anforderung an myClient.contoso.com/requestedPage einen anderen Hostnamen als contoso.com/requestedPage verwendet, stellt der Client eine andere Verbindung her, und das Clientzertifikat wird bereitgestellt.
    • Weitere Informationen finden Sie unter Einführung in die Autorisierung in ASP.NET Core.

† Die Servernamensanzeige (SNI) ist eine TLS-Erweiterung zum Einbinden einer virtuellen Domäne als Teil der SSL-Aushandlung. Dies bedeutet, dass der Name der virtuellen Domäne oder eines Hosts zur Identifizierung des Netzwerkendpunkts verwendet werden kann.

Erneutes Aushandeln

Die erneute TLS-Aushandlung ist ein Prozess, bei dem Client und Server die Verschlüsselungsanforderungen für eine einzelne Verbindung neu bewerten können. Dies schließt auch die Anforderung eines Clientzertifikats ein, falls dieses zuvor nicht bereitgestellt wurde. Die erneute TLS-Aushandlung stellt ein Sicherheitsrisiko dar und wird aus folgenden Gründen nicht empfohlen:

  • In HTTP/1.1 muss der Server zunächst HTTP-Daten direkt puffern oder verarbeiten (z. B. POST-Anforderungstexte), um sicherzustellen, dass die Verbindung für die erneute Aushandlung geeignet ist. Andernfalls reagiert die erneute Aushandlung eventuell nicht mehr reagieren oder führt zu einem Fehler.
  • HTTP/2 und HTTP/3 verbieten explizit die erneute Aushandlung.
  • Die erneute Aushandlung ist mit Sicherheitsrisiken verbunden. In TLS 1.3 wurde die erneute Aushandlung für die gesamte Verbindung entfernt und durch eine neue Erweiterung für das Anfordern ausschließlich des Clientzertifikats nach dem Verbindungsstart ersetzt. Dieser Mechanismus wird über dieselben APIs verfügbar gemacht und unterliegt weiterhin den vorherigen Einschränkungen bei der Pufferung und den HTTP-Protokollversionen.

Die Implementierung und Konfiguration dieses Features variiert je nach Server- und Frameworkversion.

IIS

IIS verwaltet die Clientzertifikataushandlung in Ihrem Namen. Ein Unterabschnitt der Anwendung kann die SslRequireCert-Option zum Aushandeln des Clientzertifikats für diese Anforderungen aktivieren. Einzelheiten dazu finden Sie in der IIS-Dokumentation zur Konfiguration.

IIS puffert alle Daten im Anforderungstext vor der erneuten Aushandlung automatisch bis zu einem konfigurierten Größengrenzwert. Anforderungen, die den Grenzwert überschreiten, werden mit der Antwort 413 abgelehnt. Dieser Grenzwert ist standardmäßig auf 48 KB festgelegt und kann durch Festlegen von uploadReadAheadSize konfiguriert werden.

HttpSys

HttpSys verfügt über zwei Einstellungen zur Steuerung der Clientzertifikataushandlung, die beide festgelegt werden sollten. Die erste befindet sich in „netsh.exe“ unter http add sslcert clientcertnegotiation=enable/disable. Dieses Flag gibt an, ob das Clientzertifikat zu Beginn einer Verbindung ausgehandelt werden soll. Es sollte für optionale Clientzertifikate auf disable festgelegt werden. Weitere Informationen finden Sie in der netsh-Dokumentation.

Die andere Einstellung ist ClientCertificateMethod. Bei der Festlegung auf AllowRenegotation kann das Clientzertifikat während einer Anforderung neu ausgehandelt werden.

HINWEIS: Die Anwendung sollte alle Daten im Anforderungstext puffern oder verarbeiten, bevor eine erneute Aushandlung versucht wird. Andernfalls reagiert die Anforderung möglicherweise nicht mehr.

Es gibt ein bekanntes Problem, durch das bei der Aktivierung von AllowRenegotation die erneute Aushandlung synchron erfolgen kann, wenn auf die ClientCertificate-Eigenschaft zugegriffen wird. Rufen Sie die GetClientCertificateAsync-Methode auf, um dies zu verhindern. Dieses Problem wurde in .NET 6 behoben. Weitere Informationen finden Sie in diesem GitHub-Issue. Hinweis: GetClientCertificateAsync kann ein NULL-Zertifikat zurückgeben, wenn der Client die Bereitstellung eines Zertifikats ablehnt.

Kestrel

Kestrel steuert die Aushandlung von Clientzertifikaten mit der ClientCertificateMode-Option.

Für .NET 5 und früher unterstützt Kestrel die erneute Aushandlung nach dem Start einer Verbindung zum Abrufen eines Clientzertifikats nicht. Dieses Feature wurde in .NET 6 hinzugefügt.

Sie können Fragen, Kommentare und anderes Feedback zu optionalen Clientzertifikaten in dieser GitHub-Diskussion hinterlassen.