Configurer l’authentification par certificat dans ASP.NET Core

Microsoft.AspNetCore.Authentication.Certificatecontient une implémentation similaire à l’authentification par certificat pour ASP.NET Core. L’authentification par certificat se produit au niveau TLS, bien avant qu’elle n’arrive à ASP.NET Core. Plus précisément, il s’agit d’un gestionnaire d’authentification qui valide le certificat, puis vous donne un événement dans lequel vous pouvez résoudre ce certificat en un ClaimsPrincipal.

Vous devez configurer votre serveur pour l’authentification par certificat, que ce soit IIS, Kestrel, Azure Web Apps ou autre chose que vous utilisez.

Scénarios avec un serveur et un équilibreur de charge

L’authentification par certificat est un scénario avec état principalement utilisé dans lequel un proxy ou un équilibreur de charge ne gère pas le trafic entre les clients et les serveurs. Si un proxy ou un équilibreur de charge est utilisé, l’authentification par certificat fonctionne uniquement si le proxy ou l’équilibreur de charge :

  • Gère l’authentification.
  • Transmet les informations d’authentification utilisateur à l’application (par exemple, dans un en-tête de demande), qui agit sur les informations d’authentification.

Une alternative à l’authentification par certificat dans les environnements où les proxys et les équilibreurs de charge sont utilisés est active Directory Federated Services (ADFS) avec OpenID Connect (OIDC).

Bien démarrer

Acquérir un certificat HTTPS, l’appliquer et configurer votre serveur pour exiger des certificats.

Dans l’application web :

  • Ajouter une référence au package NuGet Microsoft.AspNetCore.Authentication.Certificate.
  • Dans Program.cs, appelez builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...);. Fournissez un délégué pour queOnCertificateValidated effectue toute validation supplémentaire sur le certificat client envoyé avec des demandes. Transformez ces informations en un ClaimsPrincipal et définissez-les sur la propriété context.Principal.

Si l’authentification échoue, ce vendeur renvoie une réponse 403 (Forbidden) plutôt qu’un 401 (Unauthorized), comme vous pouvez vous y attendre. Le raisonnement est que l’authentification doit se produire pendant la connexion TLS initiale. Au moment où il atteint le gestionnaire, il est trop tard. Il n’existe aucun moyen de mettre à niveau la connexion d’une connexion anonyme vers une connexion avec un certificat.

UseAuthentication est nécessaire pour définir HttpContext.User sur un ClaimsPrincipal créé à partir du certificat. Par exemple :

var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

app.UseAuthentication();

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

app.Run();

L’exemple précédent illustre la façon par défaut d’ajouter l’authentification par certificat. Le gestionnaire construit un principal d’utilisateur à l’aide des propriétés de certificat communes.

Configurer la validation de certificat

Le gestionnaire CertificateAuthenticationOptions a des validations intégrées qui sont les validations minimales que vous devez effectuer sur un certificat. Chacun de ces paramètres est activé par défaut.

AllowedCertificateTypes = Chaîné, SelfSigned ou All (Chaîné | SelfSigned)

Valeur par défaut : CertificateTypes.Chained

Cette validation valide que seul le type de certificat approprié est autorisé. Si l’application utilise des certificats auto-signés, cette option doit être définie sur CertificateTypes.All ou CertificateTypes.SelfSigned.

ChainTrustValidationMode

Valeur par défaut : X509ChainTrustMode.System

Le certificat présenté par le client doit être chaîné à un certificat racine approuvé. Ce contrôle permet de déterminer quel magasin de confiance contient ces certificats racine.

Par défaut, le gestionnaire utilise le magasin de confiance système. Si le certificat client présenté doit être chaîné à un certificat racine qui n’apparaît pas dans le magasin de confiance système, cette option peut être définie sur X509ChainTrustMode.CustomRootTrust pour que le gestionnaire utilise le CustomTrustStore.

CustomTrustStore

Valeur par défaut : X509Certificate2Collection vide

Si la propriété ChainTrustValidationMode du gestionnaire est définie sur X509ChainTrustMode.CustomRootTrust, cette X509Certificate2Collection contient chaque certificat qui sera utilisé pour valider le certificat client jusqu’à une racine approuvée, y compris celle-ci.

Quand le client présente un certificat qui fait partie d’une chaîne de certificats multiniveaux, CustomTrustStore doit contenir chaque certificat émetteur dans la chaîne.

ValidateCertificateUse

Valeur par défaut : true

Cette case activée vérifie que le certificat présenté par le client a l’utilisation de clé étendue (EKU) de l’authentification client ou qu’il n’y a pas d’EKU du tout. Comme le disent les spécifications, si aucune référence EKU n’est spécifiée, toutes les EKU sont considérées comme valides.

ValidateValidityPeriod

Valeur par défaut : true

Cette validation vérifie que le certificat est dans sa période de validité. À chaque demande, le gestionnaire s’assure qu’un certificat valide lors de sa présentation n’a pas expiré pendant sa session active.

RevocationFlag

Valeur par défaut : X509RevocationFlag.ExcludeRoot

Une des valeurs d'énumération qui spécifie les certificats de la chaîne qui doivent être vérifiés pour révocation.

Les vérifications de révocation sont effectuées uniquement lorsque le certificat est chaîné à un certificat racine.

RevocationMode

Valeur par défaut : X509RevocationMode.Online

Indicateur qui spécifie la façon dont les vérifications de révocation sont effectuées.

La spécification d’une validation en ligne peut entraîner un long délai pendant que l’autorité de certification est contactée.

Les vérifications de révocation sont effectuées uniquement lorsque le certificat est chaîné à un certificat racine.

Puis-je configurer mon application pour exiger un certificat uniquement sur certains chemins d’accès ?

Ce n’est pas possible. N’oubliez pas que l’échange de certificats est effectué au début de la conversation HTTPS. Il est effectué par le serveur avant que la première demande ne soit reçue sur cette connexion, de sorte qu’il n’est pas possible d’étendre en fonction des champs de demande.

Événements de gestionnaire

Le gestionnaire a deux événements :

  • OnAuthenticationFailed: appelé si une exception se produit pendant l’authentification et vous permet de réagir.
  • OnCertificateValidated: appelé après la validation du certificat, la validation réussie et la création d’un principal par défaut. Cet événement vous permet d’effectuer votre propre validation et d’augmenter ou de remplacer le principal. Voici quelques exemples :
    • Déterminer si le certificat est connu de vos services.

    • Construction de votre propre principal. Prenons l’exemple suivant :

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

Si vous constatez que le certificat entrant ne répond pas à votre validation supplémentaire, appelez context.Fail("failure reason") avec une raison d’échec.

Pour de meilleures fonctionnalités, appelez un service inscrit dans l’injection de dépendances qui se connecte à une base de données ou à un autre type de magasin d’utilisateurs. Accédez au service à l’aide du contexte passé au délégué. Prenons l’exemple suivant :

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

D’un point de vue conceptuel, la validation du certificat est un problème d’autorisation. L’ajout d’un case activée sur, par exemple, un émetteur ou une empreinte numérique dans une stratégie d’autorisation, plutôt qu’à l’intérieur de OnCertificateValidated, est parfaitement acceptable.

Configurer votre serveur pour exiger des certificats

Kestrel

Dans Program.cs, configurez Kestrel comme suit :

var builder = WebApplication.CreateBuilder(args);

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

Notes

Pour les points de terminaison créés en appelant Listen avant l’appel de ConfigureHttpsDefaults, les valeurs par défaut ne sont pas appliquées.

IIS

Effectuez les étapes suivantes dans le Manager IIS :

  1. Sélectionnez votre site sous l’onglet Connexions.
  2. Double-cliquez sur l’option Paramètres SSL dans la fenêtre Affichage des fonctionnalités.
  3. Cochez la case Exiger SSL, puis sélectionnez la case d’option Exiger dans la section Certificats clients.

Paramètres de certificat client dans IIS

Azure et proxys web personnalisés

Consultez la documentation sur l’hôte et le déploiement pour savoir comment configurer l’intergiciel de transfert de certificat.

Utiliser l’authentification par certificat dans Azure Web Apps

Aucune configuration de transfert n’est requise pour Azure. La configuration du transfert est configurée par l’intergiciel de transfert de certificat.

Notes

L’intergiciel de transfert de certificat est requis pour ce scénario.

Pour plus d’informations, consultez Utiliser un certificat TLS/SSL dans votre code dans Azure App Service (documentation Azure).

Utiliser l’authentification par certificat dans les proxys web personnalisés

La méthode AddCertificateForwarding est utilisée pour spécifier :

  • Nom de l’en-tête du client.
  • Comment le certificat doit être chargé (à l’aide de la propriété HeaderConverter).

Dans les proxys web personnalisés, le certificat est passé en tant qu’en-tête de requête personnalisé, par exemple X-SSL-CERT. Pour l’utiliser, configurez le transfert de certificat dans Program.cs :

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

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

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

        return clientCertificate!;

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

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

            return bytes;
        }
    };
});

Si l’application est en proxy inversé par NGINX avec la configuration proxy_set_header ssl-client-cert $ssl_client_escaped_cert ou déployée sur Kubernetes à l’aide de l’entrée NGINX, le certificat client est transmis à l’application sous forme encodée en URL. Pour utiliser le certificat, décodez-le comme suit :

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

Ajoutez le middleware dans Program.cs. UseCertificateForwarding est appelé avant les appels à UseAuthentication et UseAuthorization :

var app = builder.Build();

app.UseCertificateForwarding();

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

Une classe distincte peut être utilisée pour implémenter la logique de validation. Étant donné que le même certificat auto-signé est utilisé dans cet exemple, vérifiez que seul votre certificat peut être utilisé. Vérifiez que les empreintes du certificat client et du certificat serveur correspondent. Sinon, tout certificat peut être utilisé et suffit pour s’authentifier. Cela serait utilisé à l’intérieur de la méthode AddCertificate. Vous pouvez également valider l’objet ou l’émetteur ici si vous utilisez des certificats intermédiaires ou enfants.

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

Implémenter un HttpClient à l’aide d’un certificat et d’IHttpClientFactory

Dans l’exemple suivant, un certificat client est ajouté à un HttpClientHandler à l’aide de la propriété ClientCertificates du gestionnaire. Ce gestionnaire peut ensuite être utilisé dans un instance nommé d’un HttpClient à l’aide de la méthode ConfigurePrimaryHttpMessageHandler. Ceci est configuré dans Program.cs :

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

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

Le IHttpClientFactory peut ensuite être utilisé pour obtenir le instance nommé avec le gestionnaire et le certificat. La méthode CreateClient avec le nom du client défini dans Program.cs est utilisée pour obtenir le instance. La requête HTTP peut être envoyée à l’aide du client en fonction des besoins :

public class SampleHttpService
{
    private readonly IHttpClientFactory _httpClientFactory;

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

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

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

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

Si le certificat correct est envoyé au serveur, les données sont retournées. Si aucun certificat ou certificat incorrect n’est envoyé, un code d’état http 403 est retourné.

Création de certificats dans PowerShell

La création des certificats est la partie la plus difficile dans la configuration de ce flux. Un certificat racine peut être créé à l’aide de l’applet de commande New-SelfSignedCertificate PowerShell. Lorsque vous créez le certificat, utilisez un mot de passe fort. Il est important d’ajouter le paramètre KeyUsageProperty et le paramètre KeyUsage comme indiqué.

Créer l’autorité de certification racine

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

Notes

La valeur du paramètre -DnsName doit correspondre à la cible de déploiement de l’application. Par exemple, « localhost » pour le développement.

Installer dans la racine approuvée

Le certificat racine doit être approuvé sur votre système hôte. Seuls les certificats racines créés par une autorité de certification sont approuvés par défaut. Pour plus d’informations sur la façon d’approuver le certificat racine sur Windows, consultez la documentation Windows ou le cmdlet Import-Certificate PowerShell.

Certificat intermédiaire

Un certificat intermédiaire peut maintenant être créé à partir du certificat racine. Cela n’est pas obligatoire pour tous les cas d’usage, mais vous devrez peut-être créer de nombreux certificats ou activer ou désactiver des groupes de certificats. Le paramètre TextExtension est requis pour définir la longueur du chemin dans les contraintes de base du certificat.

Le certificat intermédiaire peut ensuite être ajouté au certificat intermédiaire approuvé dans le système hôte Windows.

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

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

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

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

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

Créer un certificat enfant à partir d’un certificat intermédiaire

Un certificat enfant peut être créé à partir du certificat intermédiaire. Il s’agit de l’entité de fin et n’a pas besoin de créer d’autres certificats enfants.

$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

Créer un certificat enfant à partir d’un certificat racine

Un certificat enfant peut également être créé directement à partir du certificat racine.

$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

Exemple de racine - certificat intermédiaire - certificat

$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

Lors de l’utilisation des certificats racine, intermédiaire ou enfant, les certificats peuvent être validés à l’aide de l’empreinte numérique ou de la clé publique, selon les besoins :

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

Mise en cache de la validation des certificats

ASP.NET Core 5.0 et versions ultérieures prennent en charge la possibilité d’activer la mise en cache des résultats de validation. La mise en cache améliore considérablement les performances de l’authentification par certificat, car la validation est une opération coûteuse.

Par défaut, l’authentification par certificat désactive la mise en cache. Pour activer la mise en cache, appelez AddCertificateCache dans Program.cs :

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

L’implémentation de mise en cache par défaut stocke les résultats en mémoire. Vous pouvez fournir votre propre cache en l’implémentant ICertificateValidationCache et en l’inscrivant avec l’injection de dépendances. Par exemple : services.AddSingleton<ICertificateValidationCache, YourCache>().

Certificats clients facultatifs

Cette section fournit des informations sur les applications qui doivent protéger un sous-ensemble de l’application avec un certificat. Par exemple, une page ou un contrôleur Razor dans l’application peut nécessiter des certificats clients. Cela présente des défis en tant que certificats clients :

  • Il s’agit d’une fonctionnalité TLS, et non d’une fonctionnalité HTTP.
  • Sont négociés par connexion et généralement au début de la connexion avant la disponibilité des données HTTP.

Il existe deux approches pour implémenter des certificats clients facultatifs :

  1. Utilisation de noms d’hôte distincts (SNI) et redirection. Bien qu’il y ait davantage de travail à configurer, cela est recommandé, car il fonctionne dans la plupart des environnements et des protocoles.
  2. Renégociation lors d’une requête HTTP. Cela présente plusieurs limitations et n’est pas recommandé.

Hôtes distincts (SNI)

Au début de la connexion, seule l’indication du nom du serveur (SNI) † est connue. Les certificats clients peuvent être configurés par nom d’hôte afin qu’un hôte en ait besoin et qu’un autre n’en ait pas besoin.

ASP.NET Core 5 et versions ultérieures ajoute une prise en charge plus pratique de la redirection vers l’acquisition de certificats clients facultatifs. Pour plus d’informations, consultez l’exemple Certificats facultatifs.

  • Pour les demandes adressées à l’application web qui nécessitent un certificat client et qui n’en ont pas :
    • Redirigez vers la même page à l’aide du sous-domaine protégé par certificat client.
    • Par exemple, redirigez vers myClient.contoso.com/requestedPage. Étant donné que la demande à myClient.contoso.com/requestedPage est un nom d’hôte différent de contoso.com/requestedPage, le client établit une connexion différente et le certificat client est fourni.
    • Pour plus d’informations, consultez Introduction aux autorisations dans ASP.NET Core.

† SNI (Server Name Indication) est une extension TLS permettant d’inclure un domaine virtuel dans le cadre de la négociation SSL. Cela signifie en fait que le nom de domaine virtuel, ou un nom d’hôte, peut être utilisé pour identifier le point de terminaison du réseau.

Renégociation

La renégociation TLS est un processus par lequel le client et le serveur peuvent réévaluer les exigences de chiffrement pour une connexion individuelle, y compris demander un certificat client s’il n’est pas fourni précédemment. La renégociation TLS est un risque de sécurité et n’est pas recommandée pour les raisons suivantes :

  • Dans HTTP/1.1, le serveur doit d’abord mettre en mémoire tampon ou consommer toutes les données HTTP en cours de vol, telles que les corps de requête POST, pour s’assurer que la connexion est claire pour la renégociation. Sinon, la renégociation peut cesser de répondre ou échouer.
  • HTTP/2 et HTTP/3 interdisent explicitement la renégociation.
  • Il existe des risques de sécurité associés à la renégociation. TLS 1.3 a supprimé la renégociation de l’ensemble de la connexion et l’a remplacée par une nouvelle extension pour demander uniquement le certificat client après le début de la connexion. Ce mécanisme est exposé via les mêmes API et est toujours soumis aux contraintes antérieures de la mise en mémoire tampon et des versions de protocole HTTP.

L’implémentation et la configuration de cette fonctionnalité varient selon la version du serveur et du framework.

IIS

IIS gère la négociation du certificat client en votre nom. Une sous-section de l’application peut activer l’option SslRequireCert de négociation du certificat client pour ces demandes. Pour plus d'informations, consultez Configuration dans la documentation IIS.

IIS met automatiquement en mémoire tampon toutes les données du corps de la demande jusqu’à une limite de taille configurée avant la renégociation. Les demandes qui dépassent la limite sont rejetées avec une réponse 413. Cette limite est par défaut de 48 Ko et est configurable en définissant la uploadReadAheadSize.

HttpSys

HttpSys a deux paramètres qui contrôlent la négociation du certificat client. Les deux doivent être définis. Le premier se trouve dans netsh.exe sous http add sslcert clientcertnegotiation=enable/disable. Cet indicateur indique si le certificat client doit être négocié au début d’une connexion et s’il doit être défini sur disable pour les certificats clients facultatifs. Pour plus d’informations, consultez la documentation netsh .

L’autre paramètre est ClientCertificateMethod. Lorsqu’il est défini sur AllowRenegotation, le certificat client peut être renégocié lors d’une demande.

NOTE L’application doit mettre en mémoire tampon ou consommer les données du corps de la demande avant de tenter la renégociation, sinon la demande risque de ne plus répondre.

Une application peut d’abord vérifier la propriété ClientCertificate pour voir si le certificat est disponible. S’il n’est pas disponible, vérifiez que le corps de la demande a été consommé avant d’appeler GetClientCertificateAsync pour en négocier un. La remarque GetClientCertificateAsync peut retourner un certificat Null si le client refuse d’en fournir un.

NOTE Le comportement de la ClientCertificate propriété a changé dans .NET 6. Pour plus d’informations, consultez ce problème GitHub.

Kestrel

Kestrel contrôle la négociation du certificat client avec l’option ClientCertificateMode.

ClientCertificateMode.DelayCertificate est une nouvelle option disponible dans .NET 6 ou version ultérieure. Lorsqu’elle est définie, une application peut vérifier la propriété ClientCertificate pour voir si le certificat est disponible. S’il n’est pas disponible, vérifiez que le corps de la demande a été consommé avant d’appeler GetClientCertificateAsync pour en négocier un. La remarque GetClientCertificateAsync peut retourner un certificat Null si le client refuse d’en fournir un.

NOTE L’application doit mettre en mémoire tampon ou consommer les données du corps de la demande avant de tenter la renégociation. Sinon GetClientCertificateAsync peut lever InvalidOperationException: Client stream needs to be drained before renegotiation..

Si vous configurez par programme les paramètres TLS par nom d’hôte SNI, appelez la surcharge UseHttps (.NET 6 et versions ultérieures), qui prend TlsHandshakeCallbackOptions et contrôle la renégociation du certificat client via TlsHandshakeCallbackContext.AllowDelayedClientCertificateNegotation.

Microsoft.AspNetCore.Authentication.Certificatecontient une implémentation similaire à l’authentification par certificat pour ASP.NET Core. L’authentification par certificat se produit au niveau TLS, bien avant qu’elle n’arrive à ASP.NET Core. Plus précisément, il s’agit d’un gestionnaire d’authentification qui valide le certificat, puis vous donne un événement dans lequel vous pouvez résoudre ce certificat en un ClaimsPrincipal.

Configurez votre serveur pour l’authentification par certificat, qu’il s’agisse d’IIS, Kestrel, d’Azure Web Apps ou de tout autre élément que vous utilisez.

Scénarios avec un serveur et un équilibreur de charge

L’authentification par certificat est un scénario avec état principalement utilisé dans lequel un proxy ou un équilibreur de charge ne gère pas le trafic entre les clients et les serveurs. Si un proxy ou un équilibreur de charge est utilisé, l’authentification par certificat fonctionne uniquement si le proxy ou l’équilibreur de charge :

  • Gère l’authentification.
  • Transmet les informations d’authentification utilisateur à l’application (par exemple, dans un en-tête de demande), qui agit sur les informations d’authentification.

Une alternative à l’authentification par certificat dans les environnements où les proxys et les équilibreurs de charge sont utilisés est active Directory Federated Services (ADFS) avec OpenID Connect (OIDC).

Bien démarrer

Acquérir un certificat HTTPS, l’appliquer et configurer votre serveur pour exiger des certificats.

Dans votre application web, ajoutez une référence au package Microsoft.AspNetCore.Authentication.Certificate. Ensuite, dans la méthode Startup.ConfigureServices, appelez services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...); avec vos options, en fournissant un délégué pour queOnCertificateValidated effectue toute validation supplémentaire sur le certificat client envoyé avec les demandes. Transformez ces informations en un ClaimsPrincipal et définissez-les sur la propriété context.Principal.

Si l’authentification échoue, ce vendeur renvoie une réponse 403 (Forbidden) plutôt qu’un 401 (Unauthorized), comme vous pouvez vous y attendre. Le raisonnement est que l’authentification doit se produire pendant la connexion TLS initiale. Au moment où il atteint le gestionnaire, il est trop tard. Il n’existe aucun moyen de mettre à niveau la connexion d’une connexion anonyme vers une connexion avec un certificat.

Ajoutez également app.UseAuthentication(); dans la méthode Startup.Configure. Sinon, le HttpContext.User ne sera pas défini sur ClaimsPrincipalcréé à partir du certificat. Par exemple :

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(
        CertificateAuthenticationDefaults.AuthenticationScheme)
        .AddCertificate()
        // Adding an ICertificateValidationCache results in certificate auth caching the results.
        // The default implementation uses a memory cache.
        .AddCertificateCache();

    // All other service configuration
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseAuthentication();

    // All other app configuration
}

L’exemple précédent illustre la façon par défaut d’ajouter l’authentification par certificat. Le gestionnaire construit un principal d’utilisateur à l’aide des propriétés de certificat communes.

Configurer la validation de certificat

Le gestionnaire CertificateAuthenticationOptions a des validations intégrées qui sont les validations minimales que vous devez effectuer sur un certificat. Chacun de ces paramètres est activé par défaut.

AllowedCertificateTypes = Chaîné, SelfSigned ou All (Chaîné | SelfSigned)

Valeur par défaut : CertificateTypes.Chained

Cette validation valide que seul le type de certificat approprié est autorisé. Si l’application utilise des certificats auto-signés, cette option doit être définie sur CertificateTypes.All ou CertificateTypes.SelfSigned.

ValidateCertificateUse

Valeur par défaut : true

Cette case activée vérifie que le certificat présenté par le client a l’utilisation de clé étendue (EKU) de l’authentification client ou qu’il n’y a pas d’EKU du tout. Comme le disent les spécifications, si aucune référence EKU n’est spécifiée, toutes les EKU sont considérées comme valides.

ValidateValidityPeriod

Valeur par défaut : true

Cette validation vérifie que le certificat est dans sa période de validité. À chaque demande, le gestionnaire s’assure qu’un certificat valide lors de sa présentation n’a pas expiré pendant sa session active.

RevocationFlag

Valeur par défaut : X509RevocationFlag.ExcludeRoot

Une des valeurs d'énumération qui spécifie les certificats de la chaîne qui doivent être vérifiés pour révocation.

Les vérifications de révocation sont effectuées uniquement lorsque le certificat est chaîné à un certificat racine.

RevocationMode

Valeur par défaut : X509RevocationMode.Online

Indicateur qui spécifie la façon dont les vérifications de révocation sont effectuées.

La spécification d’une validation en ligne peut entraîner un long délai pendant que l’autorité de certification est contactée.

Les vérifications de révocation sont effectuées uniquement lorsque le certificat est chaîné à un certificat racine.

Puis-je configurer mon application pour exiger un certificat uniquement sur certains chemins d’accès ?

Ce n’est pas possible. N’oubliez pas que l’échange de certificats est effectué au début de la conversation HTTPS. Il est effectué par le serveur avant que la première demande ne soit reçue sur cette connexion, de sorte qu’il n’est pas possible d’étendre en fonction des champs de demande.

Événements de gestionnaire

Le gestionnaire a deux événements :

  • OnAuthenticationFailed: appelé si une exception se produit pendant l’authentification et vous permet de réagir.
  • OnCertificateValidated: appelé après la validation du certificat, la validation réussie et la création d’un principal par défaut. Cet événement vous permet d’effectuer votre propre validation et d’augmenter ou de remplacer le principal. Voici quelques exemples :
    • Déterminer si le certificat est connu de vos services.

    • Construction de votre propre principal. Prenons l’exemple suivant dans Startup.ConfigureServices :

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

Si vous constatez que le certificat entrant ne répond pas à votre validation supplémentaire, appelez context.Fail("failure reason") avec une raison d’échec.

Pour des fonctionnalités réelles, vous souhaiterez probablement appeler un service inscrit dans l’injection de dépendances qui se connecte à une base de données ou à un autre type de magasin d’utilisateurs. Accédez au service à l’aide du contexte passé au délégué. Prenons l’exemple suivant dans 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;
            }
        };
    });

D’un point de vue conceptuel, la validation du certificat est un problème d’autorisation. L’ajout d’un case activée sur, par exemple, un émetteur ou une empreinte numérique dans une stratégie d’autorisation, plutôt qu’à l’intérieur de OnCertificateValidated, est parfaitement acceptable.

Configurer votre serveur pour exiger des certificats

Kestrel

Dans Program.cs, configurez Kestrel comme suit :

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

Notes

Pour les points de terminaison créés en appelant Listen avant l’appel de ConfigureHttpsDefaults, les valeurs par défaut ne sont pas appliquées.

IIS

Effectuez les étapes suivantes dans le Manager IIS :

  1. Sélectionnez votre site sous l’onglet Connexions.
  2. Double-cliquez sur l’option Paramètres SSL dans la fenêtre Affichage des fonctionnalités.
  3. Cochez la case Exiger SSL, puis sélectionnez la case d’option Exiger dans la section Certificats clients.

Paramètres de certificat client dans IIS

Azure et proxys web personnalisés

Consultez la documentation sur l’hôte et le déploiement pour savoir comment configurer l’intergiciel de transfert de certificat.

Utiliser l’authentification par certificat dans Azure Web Apps

Aucune configuration de transfert n’est requise pour Azure. La configuration du transfert est configurée par l’intergiciel de transfert de certificat.

Notes

L’intergiciel de transfert de certificat est requis pour ce scénario.

Pour plus d’informations, consultez Utiliser un certificat TLS/SSL dans votre code dans Azure App Service (documentation Azure).

Utiliser l’authentification par certificat dans les proxys web personnalisés

La méthode AddCertificateForwarding est utilisée pour spécifier :

  • Nom de l’en-tête du client.
  • Comment le certificat doit être chargé (à l’aide de la propriété HeaderConverter).

Dans les proxys web personnalisés, le certificat est passé en tant qu’en-tête de requête personnalisé, par exemple X-SSL-CERT. Pour l’utiliser, configurez le transfert de certificat dans Startup.ConfigureServices :

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

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

            return clientCertificate;
        };
    });
}

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

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

    return bytes;
}

Si l’application est en proxy inversé par NGINX avec la configuration proxy_set_header ssl-client-cert $ssl_client_escaped_cert ou déployée sur Kubernetes à l’aide de l’entrée NGINX, le certificat client est transmis à l’application sous forme encodée en URL. Pour utiliser le certificat, décodez-le comme suit :

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

La méthode Startup.Configure ajoute ensuite l’intergiciel. UseCertificateForwarding est appelé avant les appels à UseAuthentication et UseAuthorization :

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

    app.UseRouting();

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

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

Une classe distincte peut être utilisée pour implémenter la logique de validation. Étant donné que le même certificat auto-signé est utilisé dans cet exemple, vérifiez que seul votre certificat peut être utilisé. Vérifiez que les empreintes du certificat client et du certificat serveur correspondent. Sinon, tout certificat peut être utilisé et suffit pour s’authentifier. Cela serait utilisé à l’intérieur de la méthode AddCertificate. Vous pouvez également valider l’objet ou l’émetteur ici si vous utilisez des certificats intermédiaires ou enfants.

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

Implémenter un HttpClient à l’aide d’un certificat et d’HttpClientHandler

Le HttpClientHandler peut être ajouté directement dans le constructeur de la classe HttpClient. Soyez prudent lors de la création d’instances du HttpClient. Le HttpClient envoie ensuite le certificat avec chaque demande.

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

Implémenter un HttpClient à l’aide d’un certificat et d’un httpClient nommé à partir d’IHttpClientFactory

Dans l’exemple suivant, un certificat client est ajouté à un HttpClientHandler à l’aide de la propriété ClientCertificates du gestionnaire. Ce gestionnaire peut ensuite être utilisé dans un instance nommé d’un HttpClient à l’aide de la méthode ConfigurePrimaryHttpMessageHandler. Ceci est configuré dans Startup.ConfigureServices :

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

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

Le IHttpClientFactory peut ensuite être utilisé pour obtenir le instance nommé avec le gestionnaire et le certificat. La méthode CreateClient avec le nom du client défini dans la classeStartup est utilisée pour obtenir le instance. La requête HTTP peut être envoyée à l’aide du client en fonction des besoins.

private readonly IHttpClientFactory _clientFactory;

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

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

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

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

Si le certificat correct est envoyé au serveur, les données sont retournées. Si aucun certificat ou certificat incorrect n’est envoyé, un code d’état http 403 est retourné.

Création de certificats dans PowerShell

La création des certificats est la partie la plus difficile dans la configuration de ce flux. Un certificat racine peut être créé à l’aide de l’applet de commande New-SelfSignedCertificate PowerShell. Lorsque vous créez le certificat, utilisez un mot de passe fort. Il est important d’ajouter le paramètre KeyUsageProperty et le paramètre KeyUsage comme indiqué.

Créer l’autorité de certification racine

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

Notes

La valeur du paramètre -DnsName doit correspondre à la cible de déploiement de l’application. Par exemple, « localhost » pour le développement.

Installer dans la racine approuvée

Le certificat racine doit être approuvé sur votre système hôte. Un certificat racine qui n’a pas été créé par une autorité de certification n’est pas approuvé par défaut. Pour plus d’informations sur la façon d’approuver le certificat racine sur Windows, consultez cette question.

Certificat intermédiaire

Un certificat intermédiaire peut maintenant être créé à partir du certificat racine. Cela n’est pas obligatoire pour tous les cas d’usage, mais vous devrez peut-être créer de nombreux certificats ou activer ou désactiver des groupes de certificats. Le paramètre TextExtension est requis pour définir la longueur du chemin dans les contraintes de base du certificat.

Le certificat intermédiaire peut ensuite être ajouté au certificat intermédiaire approuvé dans le système hôte Windows.

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

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

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

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

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

Créer un certificat enfant à partir d’un certificat intermédiaire

Un certificat enfant peut être créé à partir du certificat intermédiaire. Il s’agit de l’entité de fin et n’a pas besoin de créer d’autres certificats enfants.

$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

Créer un certificat enfant à partir d’un certificat racine

Un certificat enfant peut également être créé directement à partir du certificat racine.

$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

Exemple de racine - certificat intermédiaire - certificat

$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

Lors de l’utilisation des certificats racine, intermédiaire ou enfant, les certificats peuvent être validés à l’aide de l’empreinte numérique ou de la clé publique, selon les besoins.

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

Mise en cache de la validation des certificats

ASP.NET Core 5.0 et versions ultérieures prennent en charge la possibilité d’activer la mise en cache des résultats de validation. La mise en cache améliore considérablement les performances de l’authentification par certificat, car la validation est une opération coûteuse.

Par défaut, l’authentification par certificat désactive la mise en cache. Pour activer la mise en cache, appelez AddCertificateCache dans Startup.ConfigureServices :

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

L’implémentation de mise en cache par défaut stocke les résultats en mémoire. Vous pouvez fournir votre propre cache en l’implémentant ICertificateValidationCache et en l’inscrivant avec l’injection de dépendances. Par exemple : services.AddSingleton<ICertificateValidationCache, YourCache>().

Certificats clients facultatifs

Cette section fournit des informations sur les applications qui doivent protéger un sous-ensemble de l’application avec un certificat. Par exemple, une page ou un contrôleur Razor dans l’application peut nécessiter des certificats clients. Cela présente des défis en tant que certificats clients :

  • Il s’agit d’une fonctionnalité TLS, et non d’une fonctionnalité HTTP.
  • Sont négociés par connexion et généralement au début de la connexion avant la disponibilité des données HTTP.

Il existe deux approches pour implémenter des certificats clients facultatifs :

  1. Utilisation de noms d’hôte distincts (SNI) et redirection. Bien qu’il y ait davantage de travail à configurer, cela est recommandé, car il fonctionne dans la plupart des environnements et des protocoles.
  2. Renégociation lors d’une requête HTTP. Cela présente plusieurs limitations et n’est pas recommandé.

Hôtes distincts (SNI)

Au début de la connexion, seule l’indication du nom du serveur (SNI) † est connue. Les certificats clients peuvent être configurés par nom d’hôte afin qu’un hôte en ait besoin et qu’un autre n’en ait pas besoin.

ASP.NET Core 5 et versions ultérieures ajoute une prise en charge plus pratique de la redirection vers l’acquisition de certificats clients facultatifs. Pour plus d’informations, consultez l’exemple Certificats facultatifs.

  • Pour les demandes adressées à l’application web qui nécessitent un certificat client et qui n’en ont pas :
    • Redirigez vers la même page à l’aide du sous-domaine protégé par certificat client.
    • Par exemple, redirigez vers myClient.contoso.com/requestedPage. Étant donné que la demande à myClient.contoso.com/requestedPage est un nom d’hôte différent de contoso.com/requestedPage, le client établit une connexion différente et le certificat client est fourni.
    • Pour plus d’informations, consultez Introduction aux autorisations dans ASP.NET Core.

† SNI (Server Name Indication) est une extension TLS permettant d’inclure un domaine virtuel dans le cadre de la négociation SSL. Cela signifie en fait que le nom de domaine virtuel, ou un nom d’hôte, peut être utilisé pour identifier le point de terminaison du réseau.

Renégociation

La renégociation TLS est un processus par lequel le client et le serveur peuvent réévaluer les exigences de chiffrement pour une connexion individuelle, y compris demander un certificat client s’il n’est pas fourni précédemment. La renégociation TLS est un risque de sécurité et n’est pas recommandée pour les raisons suivantes :

  • Dans HTTP/1.1, le serveur doit d’abord mettre en mémoire tampon ou consommer toutes les données HTTP en cours de vol, telles que les corps de requête POST, pour s’assurer que la connexion est claire pour la renégociation. Sinon, la renégociation peut cesser de répondre ou échouer.
  • HTTP/2 et HTTP/3 interdisent explicitement la renégociation.
  • Il existe des risques de sécurité associés à la renégociation. TLS 1.3 a supprimé la renégociation de l’ensemble de la connexion et l’a remplacée par une nouvelle extension pour demander uniquement le certificat client après le début de la connexion. Ce mécanisme est exposé via les mêmes API et est toujours soumis aux contraintes antérieures de la mise en mémoire tampon et des versions de protocole HTTP.

L’implémentation et la configuration de cette fonctionnalité varient selon la version du serveur et du framework.

IIS

IIS gère la négociation du certificat client en votre nom. Une sous-section de l’application peut activer l’option SslRequireCert de négociation du certificat client pour ces demandes. Pour plus d'informations, consultez Configuration dans la documentation IIS.

IIS met automatiquement en mémoire tampon toutes les données du corps de la demande jusqu’à une limite de taille configurée avant la renégociation. Les demandes qui dépassent la limite sont rejetées avec une réponse 413. Cette limite est par défaut de 48 Ko et est configurable en définissant la uploadReadAheadSize.

HttpSys

HttpSys a deux paramètres qui contrôlent la négociation du certificat client. Les deux doivent être définis. Le premier se trouve dans netsh.exe sous http add sslcert clientcertnegotiation=enable/disable. Cet indicateur indique si le certificat client doit être négocié au début d’une connexion et s’il doit être défini sur disable pour les certificats clients facultatifs. Pour plus d’informations, consultez la documentation netsh .

L’autre paramètre est ClientCertificateMethod. Lorsqu’il est défini sur AllowRenegotation, le certificat client peut être renégocié lors d’une demande.

NOTE L’application doit mettre en mémoire tampon ou consommer les données du corps de la demande avant de tenter la renégociation, sinon la demande risque de ne plus répondre.

Il existe un problème connu dans lequel l’activation AllowRenegotation peut entraîner la renégociation de façon synchrone lors de l’accès à la propriété ClientCertificate. Appelez la méthode GetClientCertificateAsync pour éviter cela. Ce problème a été résolu dans .NET 6. Pour plus d’informations, consultez ce problème GitHub. La remarque GetClientCertificateAsync peut retourner un certificat Null si le client refuse d’en fournir un.

Kestrel

Kestrel contrôle la négociation du certificat client avec l’option ClientCertificateMode.

Pour .NET 5 et versions antérieures, Kestrel ne prend pas en charge la renégociation après le début d’une connexion pour acquérir un certificat client. Cette fonctionnalité a été ajoutée dans .NET 6.

Microsoft.AspNetCore.Authentication.Certificatecontient une implémentation similaire à l’authentification par certificat pour ASP.NET Core. L’authentification par certificat se produit au niveau TLS, bien avant qu’elle n’arrive à ASP.NET Core. Plus précisément, il s’agit d’un gestionnaire d’authentification qui valide le certificat, puis vous donne un événement dans lequel vous pouvez résoudre ce certificat en un ClaimsPrincipal.

Configurez votre serveur pour l’authentification par certificat, qu’il s’agisse d’IIS, Kestrel, d’Azure Web Apps ou de tout autre élément que vous utilisez.

Scénarios avec un serveur et un équilibreur de charge

L’authentification par certificat est un scénario avec état principalement utilisé dans lequel un proxy ou un équilibreur de charge ne gère pas le trafic entre les clients et les serveurs. Si un proxy ou un équilibreur de charge est utilisé, l’authentification par certificat fonctionne uniquement si le proxy ou l’équilibreur de charge :

  • Gère l’authentification.
  • Transmet les informations d’authentification utilisateur à l’application (par exemple, dans un en-tête de demande), qui agit sur les informations d’authentification.

Une alternative à l’authentification par certificat dans les environnements où les proxys et les équilibreurs de charge sont utilisés est active Directory Federated Services (ADFS) avec OpenID Connect (OIDC).

Bien démarrer

Acquérir un certificat HTTPS, l’appliquer et configurer votre serveur pour exiger des certificats.

Dans votre application web, ajoutez une référence au package Microsoft.AspNetCore.Authentication.Certificate. Ensuite, dans la méthode Startup.ConfigureServices, appelez services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...); avec vos options, en fournissant un délégué pour queOnCertificateValidated effectue toute validation supplémentaire sur le certificat client envoyé avec les demandes. Transformez ces informations en un ClaimsPrincipal et définissez-les sur la propriété context.Principal.

Si l’authentification échoue, ce vendeur renvoie une réponse 403 (Forbidden) plutôt qu’un 401 (Unauthorized), comme vous pouvez vous y attendre. Le raisonnement est que l’authentification doit se produire pendant la connexion TLS initiale. Au moment où il atteint le gestionnaire, il est trop tard. Il n’existe aucun moyen de mettre à niveau la connexion d’une connexion anonyme vers une connexion avec un certificat.

Ajoutez également app.UseAuthentication(); dans la méthode Startup.Configure. Sinon, le HttpContext.User ne sera pas défini sur ClaimsPrincipalcréé à partir du certificat. Par exemple :

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(
        CertificateAuthenticationDefaults.AuthenticationScheme)
        .AddCertificate();

    // All other service configuration
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseAuthentication();

    // All other app configuration
}

L’exemple précédent illustre la façon par défaut d’ajouter l’authentification par certificat. Le gestionnaire construit un principal d’utilisateur à l’aide des propriétés de certificat communes.

Configurer la validation de certificat

Le gestionnaire CertificateAuthenticationOptions a des validations intégrées qui sont les validations minimales que vous devez effectuer sur un certificat. Chacun de ces paramètres est activé par défaut.

AllowedCertificateTypes = Chaîné, SelfSigned ou All (Chaîné | SelfSigned)

Valeur par défaut : CertificateTypes.Chained

Cette validation valide que seul le type de certificat approprié est autorisé. Si l’application utilise des certificats auto-signés, cette option doit être définie sur CertificateTypes.All ou CertificateTypes.SelfSigned.

ValidateCertificateUse

Valeur par défaut : true

Cette case activée vérifie que le certificat présenté par le client a l’utilisation de clé étendue (EKU) de l’authentification client ou qu’il n’y a pas d’EKU du tout. Comme le disent les spécifications, si aucune référence EKU n’est spécifiée, toutes les EKU sont considérées comme valides.

ValidateValidityPeriod

Valeur par défaut : true

Cette validation vérifie que le certificat est dans sa période de validité. À chaque demande, le gestionnaire s’assure qu’un certificat valide lors de sa présentation n’a pas expiré pendant sa session active.

RevocationFlag

Valeur par défaut : X509RevocationFlag.ExcludeRoot

Une des valeurs d'énumération qui spécifie les certificats de la chaîne qui doivent être vérifiés pour révocation.

Les vérifications de révocation sont effectuées uniquement lorsque le certificat est chaîné à un certificat racine.

RevocationMode

Valeur par défaut : X509RevocationMode.Online

Indicateur qui spécifie la façon dont les vérifications de révocation sont effectuées.

La spécification d’une validation en ligne peut entraîner un long délai pendant que l’autorité de certification est contactée.

Les vérifications de révocation sont effectuées uniquement lorsque le certificat est chaîné à un certificat racine.

Puis-je configurer mon application pour exiger un certificat uniquement sur certains chemins d’accès ?

Ce n’est pas possible. N’oubliez pas que l’échange de certificats est effectué au début de la conversation HTTPS. Il est effectué par le serveur avant que la première demande ne soit reçue sur cette connexion, de sorte qu’il n’est pas possible d’étendre en fonction des champs de demande.

Événements de gestionnaire

Le gestionnaire a deux événements :

  • OnAuthenticationFailed: appelé si une exception se produit pendant l’authentification et vous permet de réagir.
  • OnCertificateValidated: appelé après la validation du certificat, la validation réussie et la création d’un principal par défaut. Cet événement vous permet d’effectuer votre propre validation et d’augmenter ou de remplacer le principal. Voici quelques exemples :
    • Déterminer si le certificat est connu de vos services.

    • Construction de votre propre principal. Prenons l’exemple suivant dans Startup.ConfigureServices :

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

Si vous constatez que le certificat entrant ne répond pas à votre validation supplémentaire, appelez context.Fail("failure reason") avec une raison d’échec.

Pour des fonctionnalités réelles, vous souhaiterez probablement appeler un service inscrit dans l’injection de dépendances qui se connecte à une base de données ou à un autre type de magasin d’utilisateurs. Accédez au service à l’aide du contexte passé au délégué. Prenons l’exemple suivant dans 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;
            }
        };
    });

D’un point de vue conceptuel, la validation du certificat est un problème d’autorisation. L’ajout d’un case activée sur, par exemple, un émetteur ou une empreinte numérique dans une stratégie d’autorisation, plutôt qu’à l’intérieur de OnCertificateValidated, est parfaitement acceptable.

Configurer votre serveur pour exiger des certificats

Kestrel

Dans Program.cs, configurez Kestrel comme suit :

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

Notes

Pour les points de terminaison créés en appelant Listen avant l’appel de ConfigureHttpsDefaults, les valeurs par défaut ne sont pas appliquées.

IIS

Effectuez les étapes suivantes dans le Manager IIS :

  1. Sélectionnez votre site sous l’onglet Connexions.
  2. Double-cliquez sur l’option Paramètres SSL dans la fenêtre Affichage des fonctionnalités.
  3. Cochez la case Exiger SSL, puis sélectionnez la case d’option Exiger dans la section Certificats clients.

Paramètres de certificat client dans IIS

Azure et proxys web personnalisés

Consultez la documentation sur l’hôte et le déploiement pour savoir comment configurer l’intergiciel de transfert de certificat.

Utiliser l’authentification par certificat dans Azure Web Apps

Aucune configuration de transfert n’est requise pour Azure. La configuration du transfert est configurée par l’intergiciel de transfert de certificat.

Notes

L’intergiciel de transfert de certificat est requis pour ce scénario.

Pour plus d’informations, consultez Utiliser un certificat TLS/SSL dans votre code dans Azure App Service (documentation Azure).

Utiliser l’authentification par certificat dans les proxys web personnalisés

La méthode AddCertificateForwarding est utilisée pour spécifier :

  • Nom de l’en-tête du client.
  • Comment le certificat doit être chargé (à l’aide de la propriété HeaderConverter).

Dans les proxys web personnalisés, le certificat est passé en tant qu’en-tête de requête personnalisé, par exemple X-SSL-CERT. Pour l’utiliser, configurez le transfert de certificat dans Startup.ConfigureServices :

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

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

            return clientCertificate;
        };
    });
}

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

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

    return bytes;
}

Si l’application est en proxy inversé par NGINX avec la configuration proxy_set_header ssl-client-cert $ssl_client_escaped_cert ou déployée sur Kubernetes à l’aide de l’entrée NGINX, le certificat client est transmis à l’application sous forme encodée en URL. Pour utiliser le certificat, décodez-le comme suit :

Ajoutez l’espace de noms pour System.Net en haut de Startup.cs :

using System.Net;

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

Ajouter la méthode UrlEncodedPemToByteArray :

private static byte[] UrlEncodedPemToByteArray(string urlEncodedBase64Pem)
{
    var base64Pem = WebUtility.UrlDecode(urlEncodedBase64Pem);
    var base64Cert = base64Pem
        .Replace("-----BEGIN CERTIFICATE-----", string.Empty)
        .Replace("-----END CERTIFICATE-----", string.Empty)
        .Trim();

    return Convert.FromBase64String(base64Cert);
}

La méthode Startup.Configure ajoute ensuite l’intergiciel. UseCertificateForwarding est appelé avant les appels à UseAuthentication et UseAuthorization :

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

    app.UseRouting();

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

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

Une classe distincte peut être utilisée pour implémenter la logique de validation. Étant donné que le même certificat auto-signé est utilisé dans cet exemple, vérifiez que seul votre certificat peut être utilisé. Vérifiez que les empreintes du certificat client et du certificat serveur correspondent. Sinon, tout certificat peut être utilisé et suffit pour s’authentifier. Cela serait utilisé à l’intérieur de la méthode AddCertificate. Vous pouvez également valider l’objet ou l’émetteur ici si vous utilisez des certificats intermédiaires ou enfants.

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

Implémenter un HttpClient à l’aide d’un certificat et d’HttpClientHandler

Le HttpClientHandler peut être ajouté directement dans le constructeur de la classe HttpClient. Soyez prudent lors de la création d’instances du HttpClient. Le HttpClient envoie ensuite le certificat avec chaque demande.

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

Implémenter un HttpClient à l’aide d’un certificat et d’un httpClient nommé à partir d’IHttpClientFactory

Dans l’exemple suivant, un certificat client est ajouté à un HttpClientHandler à l’aide de la propriété ClientCertificates du gestionnaire. Ce gestionnaire peut ensuite être utilisé dans un instance nommé d’un HttpClient à l’aide de la méthode ConfigurePrimaryHttpMessageHandler. Ceci est configuré dans Startup.ConfigureServices :

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

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

Le IHttpClientFactory peut ensuite être utilisé pour obtenir le instance nommé avec le gestionnaire et le certificat. La méthode CreateClient avec le nom du client défini dans la classeStartup est utilisée pour obtenir le instance. La requête HTTP peut être envoyée à l’aide du client en fonction des besoins.

private readonly IHttpClientFactory _clientFactory;

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

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

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

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

Si le certificat correct est envoyé au serveur, les données sont retournées. Si aucun certificat ou certificat incorrect n’est envoyé, un code d’état http 403 est retourné.

Création de certificats dans PowerShell

La création des certificats est la partie la plus difficile dans la configuration de ce flux. Un certificat racine peut être créé à l’aide de l’applet de commande New-SelfSignedCertificate PowerShell. Lorsque vous créez le certificat, utilisez un mot de passe fort. Il est important d’ajouter le paramètre KeyUsageProperty et le paramètre KeyUsage comme indiqué.

Créer l’autorité de certification racine

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

Notes

La valeur du paramètre -DnsName doit correspondre à la cible de déploiement de l’application. Par exemple, « localhost » pour le développement.

Installer dans la racine approuvée

Le certificat racine doit être approuvé sur votre système hôte. Un certificat racine qui n’a pas été créé par une autorité de certification n’est pas approuvé par défaut. Pour plus d’informations sur la façon d’approuver le certificat racine sur Windows, consultez cette question.

Certificat intermédiaire

Un certificat intermédiaire peut maintenant être créé à partir du certificat racine. Cela n’est pas obligatoire pour tous les cas d’usage, mais vous devrez peut-être créer de nombreux certificats ou activer ou désactiver des groupes de certificats. Le paramètre TextExtension est requis pour définir la longueur du chemin dans les contraintes de base du certificat.

Le certificat intermédiaire peut ensuite être ajouté au certificat intermédiaire approuvé dans le système hôte Windows.

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

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

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

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

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

Créer un certificat enfant à partir d’un certificat intermédiaire

Un certificat enfant peut être créé à partir du certificat intermédiaire. Il s’agit de l’entité de fin et n’a pas besoin de créer d’autres certificats enfants.

$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

Créer un certificat enfant à partir d’un certificat racine

Un certificat enfant peut également être créé directement à partir du certificat racine.

$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

Exemple de racine - certificat intermédiaire - certificat

$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

Lors de l’utilisation des certificats racine, intermédiaire ou enfant, les certificats peuvent être validés à l’aide de l’empreinte numérique ou de la clé publique, selon les besoins.

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

Certificats clients facultatifs

Cette section fournit des informations sur les applications qui doivent protéger un sous-ensemble de l’application avec un certificat. Par exemple, une page ou un contrôleur Razor dans l’application peut nécessiter des certificats clients. Cela présente des défis en tant que certificats clients :

  • Il s’agit d’une fonctionnalité TLS, et non d’une fonctionnalité HTTP.
  • Sont négociés par connexion et généralement au début de la connexion avant la disponibilité des données HTTP.

Il existe deux approches pour implémenter des certificats clients facultatifs :

  1. Utilisation de noms d’hôte distincts (SNI) et redirection. Bien qu’il y ait davantage de travail à configurer, cela est recommandé, car il fonctionne dans la plupart des environnements et des protocoles.
  2. Renégociation lors d’une requête HTTP. Cela présente plusieurs limitations et n’est pas recommandé.

Hôtes distincts (SNI)

Au début de la connexion, seule l’indication du nom du serveur (SNI) † est connue. Les certificats clients peuvent être configurés par nom d’hôte afin qu’un hôte en ait besoin et qu’un autre n’en ait pas besoin.

ASP.NET Core 5 et versions ultérieures ajoute une prise en charge plus pratique de la redirection vers l’acquisition de certificats clients facultatifs. Pour plus d’informations, consultez l’exemple Certificats facultatifs.

  • Pour les demandes adressées à l’application web qui nécessitent un certificat client et qui n’en ont pas :
    • Redirigez vers la même page à l’aide du sous-domaine protégé par certificat client.
    • Par exemple, redirigez vers myClient.contoso.com/requestedPage. Étant donné que la demande à myClient.contoso.com/requestedPage est un nom d’hôte différent de contoso.com/requestedPage, le client établit une connexion différente et le certificat client est fourni.
    • Pour plus d’informations, consultez Introduction aux autorisations dans ASP.NET Core.

† SNI (Server Name Indication) est une extension TLS permettant d’inclure un domaine virtuel dans le cadre de la négociation SSL. Cela signifie en fait que le nom de domaine virtuel, ou un nom d’hôte, peut être utilisé pour identifier le point de terminaison du réseau.

Renégociation

La renégociation TLS est un processus par lequel le client et le serveur peuvent réévaluer les exigences de chiffrement pour une connexion individuelle, y compris demander un certificat client s’il n’est pas fourni précédemment. La renégociation TLS est un risque de sécurité et n’est pas recommandée pour les raisons suivantes :

  • Dans HTTP/1.1, le serveur doit d’abord mettre en mémoire tampon ou consommer toutes les données HTTP en cours de vol, telles que les corps de requête POST, pour s’assurer que la connexion est claire pour la renégociation. Sinon, la renégociation peut cesser de répondre ou échouer.
  • HTTP/2 et HTTP/3 interdisent explicitement la renégociation.
  • Il existe des risques de sécurité associés à la renégociation. TLS 1.3 a supprimé la renégociation de l’ensemble de la connexion et l’a remplacée par une nouvelle extension pour demander uniquement le certificat client après le début de la connexion. Ce mécanisme est exposé via les mêmes API et est toujours soumis aux contraintes antérieures de la mise en mémoire tampon et des versions de protocole HTTP.

L’implémentation et la configuration de cette fonctionnalité varient selon la version du serveur et du framework.

IIS

IIS gère la négociation du certificat client en votre nom. Une sous-section de l’application peut activer l’option SslRequireCert de négociation du certificat client pour ces demandes. Pour plus d'informations, consultez Configuration dans la documentation IIS.

IIS met automatiquement en mémoire tampon toutes les données du corps de la demande jusqu’à une limite de taille configurée avant la renégociation. Les demandes qui dépassent la limite sont rejetées avec une réponse 413. Cette limite est par défaut de 48 Ko et est configurable en définissant la uploadReadAheadSize.

HttpSys

HttpSys a deux paramètres qui contrôlent la négociation du certificat client. Les deux doivent être définis. Le premier se trouve dans netsh.exe sous http add sslcert clientcertnegotiation=enable/disable. Cet indicateur indique si le certificat client doit être négocié au début d’une connexion et s’il doit être défini sur disable pour les certificats clients facultatifs. Pour plus d’informations, consultez la documentation netsh .

L’autre paramètre est ClientCertificateMethod. Lorsqu’il est défini sur AllowRenegotation, le certificat client peut être renégocié lors d’une demande.

NOTE L’application doit mettre en mémoire tampon ou consommer les données du corps de la demande avant de tenter la renégociation, sinon la demande risque de ne plus répondre.

Il existe un problème connu dans lequel l’activation AllowRenegotation peut entraîner la renégociation de façon synchrone lors de l’accès à la propriété ClientCertificate. Appelez la méthode GetClientCertificateAsync pour éviter cela. Ce problème a été résolu dans .NET 6. Pour plus d’informations, consultez ce problème GitHub. La remarque GetClientCertificateAsync peut retourner un certificat Null si le client refuse d’en fournir un.

Kestrel

Kestrel contrôle la négociation du certificat client avec l’option ClientCertificateMode.

Pour .NET 5 et versions antérieures, Kestrel ne prend pas en charge la renégociation après le début d’une connexion pour acquérir un certificat client. Cette fonctionnalité a été ajoutée dans .NET 6.

Laissez des questions, des commentaires et d’autres commentaires sur les certificats clients facultatifs dans ce problème de discussion GitHub .