web-dev-qa-db-fra.com

Comment utiliser un certificat client pour authentifier et autoriser dans une API Web

J'essaie d'utiliser un certificat client pour authentifier et autoriser des périphériques à l'aide d'une API Web et j'ai développé une preuve de concept simple pour résoudre les problèmes liés à la solution potentielle. Je rencontre un problème où le certificat client n'est pas reçu par l'application Web. Un certain nombre de personnes ont signalé ce problème, y compris dans ce Q & A , mais aucun d'entre eux n'a de réponse. Mon espoir est de fournir plus de détails pour relancer ce problème et, espérons-le, obtenir une réponse à mon problème. Je suis ouvert à d'autres solutions. La principale exigence est qu'un processus autonome écrit en C # puisse appeler une API Web et être authentifié à l'aide d'un certificat client. 

L'API Web de ce POC est très simple et ne renvoie qu'une seule valeur. Il utilise un attribut pour valider que HTTPS est utilisé et qu'un certificat client est présent. 

public class SecureController : ApiController
{
    [RequireHttps]
    public string Get(int id)
    {
        return "value";
    }

}

Voici le code pour le RequireHttpsAttribute:

public class RequireHttpsAttribute : AuthorizationFilterAttribute 
{ 
    public override void OnAuthorization(HttpActionContext actionContext) 
    { 
        if (actionContext.Request.RequestUri.Scheme != Uri.UriSchemeHttps) 
        { 
            actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden) 
            { 
                ReasonPhrase = "HTTPS Required" 
            }; 
        } 
        else 
        {
            var cert = actionContext.Request.GetClientCertificate();
            if (cert == null)
            {
                actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
                {
                    ReasonPhrase = "Client Certificate Required"
                }; 

            }
            base.OnAuthorization(actionContext); 
        } 
    } 
}

Dans ce POC, je vérifie simplement la disponibilité du certificat client. Une fois que cela fonctionne, je peux ajouter des vérifications d'informations dans le certificat pour les valider par rapport à une liste de certificats.

Voici le réglage dans IIS pour SSL pour cette application Web.

 enter image description here

Voici le code du client qui envoie la demande avec un certificat client. Ceci est une application console.

    private static async Task SendRequestUsingHttpClient()
    {
        WebRequestHandler handler = new WebRequestHandler();
        X509Certificate certificate = GetCert("ClientCertificate.cer");
        handler.ClientCertificates.Add(certificate);
        handler.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(ValidateServerCertificate);
        handler.ClientCertificateOptions = ClientCertificateOption.Manual;
        using (var client = new HttpClient(handler))
        {
            client.BaseAddress = new Uri("https://localhost:44398/");
            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            HttpResponseMessage response = await client.GetAsync("api/Secure/1");
            if (response.IsSuccessStatusCode)
            {
                string content = await response.Content.ReadAsStringAsync();
                Console.WriteLine("Received response: {0}",content);
            }
            else
            {
                Console.WriteLine("Error, received status code {0}: {1}", response.StatusCode, response.ReasonPhrase);
            }
        }
    }

    public static bool ValidateServerCertificate(
      object sender,
      X509Certificate certificate,
      X509Chain chain,
      SslPolicyErrors sslPolicyErrors)
    {
        Console.WriteLine("Validating certificate {0}", certificate.Issuer);
        if (sslPolicyErrors == SslPolicyErrors.None)
            return true;

        Console.WriteLine("Certificate error: {0}", sslPolicyErrors);

        // Do not allow this client to communicate with unauthenticated servers.
        return false;
    }

Lorsque j'exécute cette application de test, je récupère un code d'état de 403 interdit avec une phrase de cause «Certificat du client requis» indiquant qu'il entre dans mon RequireHttpsAttribute et qu'il ne trouve aucun certificat client. En exécutant ceci via un débogueur, j'ai vérifié que le certificat était chargé et ajouté au WebRequestHandler. Le certificat est exporté dans un fichier CER en cours de chargement. Le certificat complet avec la clé privée se trouve dans les magasins Racine personnelle et de confiance de la machine locale pour le serveur d’applications Web. Pour ce test, le client et l'application Web sont exécutés sur le même ordinateur.

Je peux appeler cette méthode de l'API Web à l'aide de Fiddler, en joignant le même certificat client, et cela fonctionne correctement. Lors de l'utilisation de Fiddler, il passe les tests dans RequireHttpsAttribute, renvoie un code de statut correct de 200 et renvoie la valeur attendue.

Quelqu'un at-il rencontré le même problème où HttpClient n'envoie pas de certificat client dans la demande et a trouvé une solution?

Mise à jour 1:

J'ai également essayé d'obtenir le certificat à partir du magasin de certificats contenant la clé privée. Voici comment je l'ai récupéré:

    private static X509Certificate2 GetCert2(string hostname)
    {
        X509Store myX509Store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
        myX509Store.Open(OpenFlags.ReadWrite);
        X509Certificate2 myCertificate = myX509Store.Certificates.OfType<X509Certificate2>().FirstOrDefault(cert => cert.GetNameInfo(X509NameType.SimpleName, false) == hostname);
        return myCertificate;
    }

J'ai vérifié que ce certificat était correctement récupéré et qu'il était ajouté à la collection de certificats du client. Mais j'ai eu les mêmes résultats où le code serveur ne récupère aucun certificat client.

Pour être complet, voici le code utilisé pour extraire le certificat d'un fichier:

    private static X509Certificate GetCert(string filename)
    {
        X509Certificate Cert = X509Certificate.CreateFromCertFile(filename);
        return Cert;

    }

Vous remarquerez que lorsque vous obtenez le certificat d'un fichier, il renvoie un objet de type X509Certificate et lorsque vous le récupérez du magasin de certificats, il est de type X509Certificate2. La méthode X509CertificateCollection.Add attend un type de X509Certificate.

Mise à jour 2: J'essaie toujours de comprendre cela et j'ai essayé de nombreuses options différentes mais en vain. 

  • J'ai modifié l'application Web pour qu'elle s'exécute sur un nom d'hôte plutôt que sur l'hôte local.
  • Je configure l'application Web pour exiger SSL
  • J'ai vérifié que le certificat était défini pour l'authentification du client et qu'il se trouvait dans la racine de confiance.
  • En plus de tester le certificat client dans Fiddler, je l'ai également validé dans Chrome.

À un moment donné, lors de l’essai de ces options, cela a commencé à fonctionner. Ensuite, j'ai commencé à annuler les modifications pour voir ce qui le faisait fonctionner. Il a continué à travailler. Ensuite, j'ai essayé de supprimer le certificat de la racine de confiance pour confirmer que cela était nécessaire et qu'il ne fonctionnait plus. Je ne parviens plus à le faire fonctionner même si j'ai remis le certificat dans la racine de confiance. Maintenant, Chrome ne me demandera même plus un certificat similaire à celui utilisé auparavant et il échoue dans Chrome, mais fonctionne toujours dans Fiddler. Il doit y avoir une configuration magique qui me manque.

J'ai également essayé d'activer "Négocier le certificat client" dans la liaison, mais Chrome ne m'invite toujours pas à demander un certificat client. Voici les paramètres utilisant "netsh http show sslcert"

 IP:port                 : 0.0.0.0:44398
 Certificate Hash        : 429e090db21e14344aa5d75d25074712f120f65f
 Application ID          : {4dc3e181-e14b-4a21-b022-59fc669b0914}
 Certificate Store Name  : MY
 Verify Client Certificate Revocation    : Disabled
 Verify Revocation Using Cached Client Certificate Only    : Disabled
 Usage Check    : Enabled
 Revocation Freshness Time : 0
 URL Retrieval Timeout   : 0
 Ctl Identifier          : (null)
 Ctl Store Name          : (null)
 DS Mapper Usage    : Disabled
 Negotiate Client Certificate    : Enabled

Voici le certificat client que j'utilise:

 enter image description here

 enter image description here

 enter image description here

Je suis perplexe quant à la nature du problème. J'ajoute une prime à quiconque peut m'aider à comprendre cela.

30
Kevin Junghans

Le traçage m'a aidé à trouver le problème (merci Fabian pour cette suggestion). Lors de tests supplémentaires, j'ai constaté que le certificat client pouvait fonctionner sur un autre serveur (Windows Server 2012). Je testais cela sur ma machine de développement (Windows 7) afin de pouvoir déboguer ce processus. Donc, en comparant la trace à un serveur IIS qui fonctionnait et à un autre qui ne fonctionnait pas, j'ai pu localiser les lignes pertinentes dans le journal de trace. Voici une partie d'un journal où le certificat client a fonctionné. C'est la configuration juste avant l'envoi

System.Net Information: 0 : [17444] InitializeSecurityContext(In-Buffers count=2, Out-Buffer length=0, returned code=CredentialsNeeded).
System.Net Information: 0 : [17444] SecureChannel#54718731 - We have user-provided certificates. The server has not specified any issuers, so try all the certificates.
System.Net Information: 0 : [17444] SecureChannel#54718731 - Selected certificate:

Voici à quoi ressemble le journal de suivi sur la machine sur laquelle le certificat client a échoué.

System.Net Information: 0 : [19616] InitializeSecurityContext(In-Buffers count=2, Out-Buffer length=0, returned code=CredentialsNeeded).
System.Net Information: 0 : [19616] SecureChannel#54718731 - We have user-provided certificates. The server has specified 137 issuer(s). Looking for certificates that match any of the issuers.
System.Net Information: 0 : [19616] SecureChannel#54718731 - Left with 0 client certificates to choose from.
System.Net Information: 0 : [19616] Using the cached credential handle.

En se concentrant sur la ligne qui indiquait que le serveur spécifiait 137 émetteurs, j’ai trouvé ceci Questions et réponses qui semblaient similaires à mon problème . La solution pour moi n’était pas celle indiquée comme réponse car mon certificat se trouvait dans la racine de confiance. La réponse est celle en dessous où vous mettez à jour le registre. Je viens d'ajouter la valeur à la clé de registre.

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL

Nom de la valeur: SendTrustedIssuerList Type de valeur: REG_DWORD Données de la valeur: 0 (False)

Après avoir ajouté cette valeur au registre, il a commencé à fonctionner sur mon ordinateur Windows 7. Cela semble être un problème de Windows 7.

7
Kevin Junghans

Mettre à jour:

Exemple de Microsoft:

https://docs.Microsoft.com/en-us/Azure/app-service/app-service-web-configure-tls-mutual-auth#special-considerations-for-certificate-validation

Original

C’est ainsi que la certification du client a fonctionné et que j’ai vérifié qu’une autorité de certification racine spécifique l’avait émise et qu’il s’agissait d’un certificat spécifique.

J'ai d'abord édité <src>\.vs\config\applicationhost.config et apporté cette modification: <section name="access" overrideModeDefault="Allow" />

Cela me permet d’éditer <system.webServer> dans web.config et d’ajouter les lignes suivantes qui nécessiteront une certification client dans IIS Express. Note: J'ai édité ceci à des fins de développement, n'autorise pas les remplacements en production.

Pour la production, suivez un guide comme celui-ci pour configurer IIS:

https://medium.com/@hafizmohammedg/configuring-client-certificates-on-iis-95aef4174ddb

web.config:

<security>
  <access sslFlags="Ssl,SslNegotiateCert,SslRequireCert" />
</security>

Contrôleur d'API:

[RequireSpecificCert]
public class ValuesController : ApiController
{
    // GET api/values
    public IHttpActionResult Get()
    {
        return Ok("It works!");
    }
}

Attribut:

public class RequireSpecificCertAttribute : AuthorizationFilterAttribute
{
    public override void OnAuthorization(HttpActionContext actionContext)
    {
        if (actionContext.Request.RequestUri.Scheme != Uri.UriSchemeHttps)
        {
            actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
            {
                ReasonPhrase = "HTTPS Required"
            };
        }
        else
        {
            X509Certificate2 cert = actionContext.Request.GetClientCertificate();
            if (cert == null)
            {
                actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
                {
                    ReasonPhrase = "Client Certificate Required"
                };

            }
            else
            {
                X509Chain chain = new X509Chain();

                //Needed because the error "The revocation function was unable to check revocation for the certificate" happened to me otherwise
                chain.ChainPolicy = new X509ChainPolicy()
                {
                    RevocationMode = X509RevocationMode.NoCheck,
                };
                try
                {
                    var chainBuilt = chain.Build(cert);
                    Debug.WriteLine(string.Format("Chain building status: {0}", chainBuilt));

                    var validCert = CheckCertificate(chain, cert);

                    if (chainBuilt == false || validCert == false)
                    {
                        actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
                        {
                            ReasonPhrase = "Client Certificate not valid"
                        };
                        foreach (X509ChainStatus chainStatus in chain.ChainStatus)
                        {
                            Debug.WriteLine(string.Format("Chain error: {0} {1}", chainStatus.Status, chainStatus.StatusInformation));
                        }
                    }
                }
                catch (Exception ex)
                {
                    Debug.WriteLine(ex.ToString());
                }
            }

            base.OnAuthorization(actionContext);
        }
    }

    private bool CheckCertificate(X509Chain chain, X509Certificate2 cert)
    {
        var rootThumbprint = WebConfigurationManager.AppSettings["rootThumbprint"].ToUpper().Replace(" ", string.Empty);

        var clientThumbprint = WebConfigurationManager.AppSettings["clientThumbprint"].ToUpper().Replace(" ", string.Empty);

        //Check that the certificate have been issued by a specific Root Certificate
        var validRoot = chain.ChainElements.Cast<X509ChainElement>().Any(x => x.Certificate.Thumbprint.Equals(rootThumbprint, StringComparison.InvariantCultureIgnoreCase));

        //Check that the certificate thumbprint matches our expected thumbprint
        var validCert = cert.Thumbprint.Equals(clientThumbprint, StringComparison.InvariantCultureIgnoreCase);

        return validRoot && validCert;
    }
}

On peut ensuite appeler l'API avec une certification client comme celle-ci, testée à partir d'un autre projet Web.

[RoutePrefix("api/certificatetest")]
public class CertificateTestController : ApiController
{

    public IHttpActionResult Get()
    {
        var handler = new WebRequestHandler();
        handler.ClientCertificateOptions = ClientCertificateOption.Manual;
        handler.ClientCertificates.Add(GetClientCert());
        handler.UseProxy = false;
        var client = new HttpClient(handler);
        var result = client.GetAsync("https://localhost:44331/api/values").GetAwaiter().GetResult();
        var resultString = result.Content.ReadAsStringAsync().GetAwaiter().GetResult();
        return Ok(resultString);
    }

    private static X509Certificate GetClientCert()
    {
        X509Store store = null;
        try
        {
            store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
            store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);

            var certificateSerialNumber= "‎81 c6 62 0a 73 c7 b1 aa 41 06 a3 ce 62 83 ae 25".ToUpper().Replace(" ", string.Empty);

            //Does not work for some reason, could be culture related
            //var certs = store.Certificates.Find(X509FindType.FindBySerialNumber, certificateSerialNumber, true);

            //if (certs.Count == 1)
            //{
            //    var cert = certs[0];
            //    return cert;
            //}

            var cert = store.Certificates.Cast<X509Certificate>().FirstOrDefault(x => x.GetSerialNumberString().Equals(certificateSerialNumber, StringComparison.InvariantCultureIgnoreCase));

            return cert;
        }
        finally
        {
            store?.Close();
        }
    }
}
5
Ogglas

En fait, j'ai eu un problème similaire, où nous avions plusieurs certificats racine de confiance. Notre serveur Web fraîchement installé avait été surchauffé. Notre racine a commencé avec la lettre Z, donc elle s'est retrouvée à la fin de la liste. 

Le problème était que le IIS n’envoyait que les vingt premières racines de confiance au client et tronquait le reste , le nôtre compris. C'était il y a quelques années, je ne me souviens plus du nom de l'outil ... il faisait partie de la suite d'administration IIS, mais Fiddler devrait en faire de même. Après avoir réalisé l'erreur, nous avons supprimé beaucoup de racines de confiance dont nous n'avons pas besoin. Cela a été fait par essais et erreurs, alors faites attention à ce que vous supprimez.

Après le nettoyage, tout a fonctionné à merveille.

1
Brandtware

Assurez-vous que HttpClient a accès au certificat client complet (y compris la clé privée). 

Vous appelez GetCert avec un fichier "ClientCertificate.cer", ce qui laisse supposer qu’il n’ya pas de clé privée contenue - devrait plutôt être un fichier pfx dans Windows. Il peut même être préférable d’accéder au certificat à partir du Windows Cert Store et de le rechercher à l’aide de l’empreinte digitale.

Faites attention lorsque vous copiez l'empreinte digitale: Lors de l'affichage dans Cert Management, il existe des caractères non imprimables (copiez la chaîne dans le bloc-notes ++ et vérifiez la longueur de la chaîne affichée).

1
Daniel Nachtrub

En regardant le code source, je pense aussi qu'il doit y avoir un problème avec la clé privée.

En fait, il vérifie si le certificat transmis est de type X509Certificate2 et s'il possède la clé privée. 

S'il ne trouve pas la clé privée, il tente de trouver le certificat dans le magasin CurrentUser, puis dans le magasin LocalMachine. S'il trouve le certificat, il vérifie si la clé privée est présente.

(voir code source de la classe SecureChannnel, méthode EnsurePrivateKey)

Donc, selon le fichier que vous avez importé (.cer - sans clé privée ou .pfx - avec clé privée) et sur quel magasin, il est possible que le fichier correct ne soit pas trouvé et que Request.ClientCertificate ne soit pas rempli.

Vous pouvez activer Suivi du réseau pour essayer de déboguer ceci. Cela vous donnera une sortie comme ceci:

  • Essayer de trouver un certificat correspondant dans le magasin de certificats
  • Impossible de trouver le certificat dans le magasin LocalMachine ou dans le magasin CurrentUser.
0
Fabian