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 simple validation de principe pour résoudre les problèmes avec la solution potentielle. Je tombe sur un problème où le certificat client n'est pas reçu par l'application web. Un certain nombre de personnes ont rapporté ce problème, y compris dans cette Q&R , mais aucun d'entre eux ont une réponse. Mon espoir est de fournir plus de détails pour relancer cette question et nous espérons 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 la commande 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 juste la disponibilité du certificat client. Une fois que cela fonctionne, je peux ajouter des vérifications des informations dans le certificat pour valider par rapport à une liste de certificats.
Voici les paramètres dans IIS pour SSL pour cette application web.
voici le code pour le client qui envoie la demande avec un certificat de client. C'est une application de 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;
}
quand j'exécute cette application de test, je récupère un code de statut de 403 Interdit avec une phrase de raison de "certificat Client requis" indiquant qu'il entre dans mon Requirhttpsattribute et qu'il ne trouve pas de certificats client. En exécutant ceci avec un débogueur, j'ai vérifié que le certificat est chargé et ajouté à WebRequestHandler. Le certificat est exporté dans un fichier CER qui: est en cours de chargement. Le certificat complet avec la clé privée est situé sur les stocks de racine personnels et de confiance de la Machine locale pour le serveur d'application web. Pour ce test, le client et l'application web sont exécutés sur la même machine.
je peux appeler cette méthode de L'API Web en utilisant Fiddler, en attachant le même certificat client, et ça marche très bien. Lors de L'utilisation de Fiddler il passe les tests dans RequireHttpsAttribute et retourne un code de statut réussi de 200 et renvoie le la valeur attendue.
Quelqu'un a-t-il rencontré le même problème lorsque HttpClient n'envoie pas de certificat client dans la requête et a-t-il trouvé une solution?
mise à Jour 1:
j'ai également essayé d'obtenir le certificat de la boutique de certificats qui comprend 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 récupéré correctement et il a été ajouté à la collection de certificats clients. Mais j'ai eu les mêmes résultats où le code du serveur ne récupérait aucun certificat client.
par souci d'exhaustivité, 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 à partir d'un fichier il retourne un objet de type X509Certificate et que lorsque vous le récupérez à partir de la mémoire de certificat il est de type X509Certificate2. Le X509CertificateCollection.Ajouter la méthode 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 changé l'application web pour exécuter sur un nom d'hôte au lieu d'hôte local.
- j'ai paramétré l'application web pour Exiger SSL
- j'ai vérifié que le certificat était L'Authentification du Client et qu'il est dans la racine de confiance
- en plus de tester le certificat client dans Fiddler, je l'ai également validé dans Chrome.
à un moment donné au cours de l'essai de ces options, il a commencé à fonctionner. Puis j'ai commencé à annuler les changements pour voir ce qui a fait que ça a marché. Il a continué à travailler. Puis j'ai essayé de retirer le certificat de la racine de confiance pour valider que cela était nécessaire et il a cessé de fonctionner et maintenant je ne peux pas obtenir il fonctionne à nouveau même si j'ai remis le certificat dans la racine de confiance. Maintenant Chrome ne me demandera même pas un certificat comme il l'a utilisé et il échoue dans Chrome, mais fonctionne toujours dans Fiddler. Il doit y avoir une configuration magique qui me manque.
j'ai aussi essayé d'activer" Negotiate Client Certificate " dans la reliure, mais Chrome ne me demandera toujours pas de certificat client. Voici les paramètres en 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:
je suis perplexe quant à la question. J'ajoute une prime pour quiconque qui peut m'aider à comprendre ça.
5 réponses
m'a aidé à trouver quel était le problème (merci Fabian pour cette suggestion). J'ai trouvé avec d'autres tests que je pouvais obtenir le certificat client pour travailler sur un autre serveur (Windows Server 2012). J'ai testé ceci sur ma machine de développement (Window 7) pour pouvoir déboguer ce processus. Ainsi, en comparant la trace à un serveur IIS qui fonctionnait et à un serveur qui ne fonctionnait pas, j'ai pu identifier les lignes pertinentes dans le journal de trace. Voici une partie d'un journal où le certificat du client travaillé. C'est la configuration juste avant l'envoi de
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 ressemblait le journal de trace sur la machine où 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é ce Q&A qui semblait similaire à mon émission . La solution pour moi n'était pas celui marqué comme réponse depuis mon certificat a la racine de confiance. La réponse est l'un, sous où vous mettez à jour le registre. J'ai juste ajouté la valeur à la clé du registre.
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL
nom de la Valeur: SendTrustedIssuerList Valeur de type: REG_DWORD Valeur de données: 0 (False)
après avoir ajouté cette valeur au registre, il a commencé à travailler sur ma machine Windows 7. Cela semble pour être un Windows 7 problème.
mise à jour:
exemple de Microsoft:
Original
C'est ainsi que j'ai fait fonctionner la certification client et vérifié qu'une AC racine spécifique l'avait aussi émise. comme étant un certificat spécifique.
J'ai d'abord édité <src>\.vs\config\applicationhost.config
et fait ce changement: <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, ne permettent pas les dérogations en production.
pour la production:
https://medium.com/@hafizmohammedg/configuring-client-certificates-on-iis-95aef4174ddb
web.config:
<security>
<access sslFlags="Ssl,SslNegotiateCert,SslRequireCert" />
</security>
API Controller:
[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;
}
}
peut alors appeler L'API avec la certification client comme ceci, testé à 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();
}
}
}
assurez-vous que HttpClient ait accès au certificat client complet (y compris la clé privée).
vous appelez GetCert avec un fichier" ClientCertificate.cer" qui conduit à l'hypothèse qu'il n'y a pas de clé privée contenue - devrait plutôt être un fichier pfx dans windows. Il est peut-être même préférable d'accéder au certificat à partir de la boutique Windows cert et de le rechercher à l'aide de l'empreinte digitale.
faites attention lorsque vous copiez l'empreinte: là sont des caractères non imprimables lors de la visualisation dans la gestion de cert (copiez la chaîne de caractères vers le bloc-notes++ et vérifiez la longueur de la chaîne affichée).
j'ai eu en fait un problème similaire, où nous avons dû à de nombreux certificats racine de confiance. Notre webserver fraîchement installé avait plus d'un hunded. Notre racine a commencé avec la lettre Z et a fini à la fin de la liste.
le problème était que le IIS envoyé seulement les vingt premiers racines de confiance au client et tronqué le reste , y compris le nôtre. C'était il y a quelques années, je ne me souviens plus du nom de l'outil... il faisait partie de l'administration IIS suite, mais Fiddler devrait faire aussi bien. Après avoir réalisé l'erreur, nous avons enlevé beaucoup de racines de confiance que nous n'avons pas besoin. Cela a été fait par tâtonnements, alors faites attention à ce que vous supprimez.
après le nettoyage, tout a fonctionné à merveille.
en Regardant le code source je pense aussi qu'il doit y avoir un problème avec la clé privée.
ce qu'il fait est en fait de vérifier si le certificat qui est passé est de type X509Certificate2 et s'il a la clé privée.
S'il ne trouve pas la clé privée, il essaie de trouver le certificat dans le magasin CurrentUser puis dans le magasin LocalMachine. Si 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 d' .pfx - avec clé privée) et sur quel magasin il pourrait ne pas trouver le bon et la demande.ClientCertificate ne sera pas rempli.
, Vous pouvez activer Traçage Réseau pour essayer de débogage. Il vous donnera la sortie comme ceci:
- en Essayant de trouver un correspondant de certificat dans le magasin de certificats
- ne peut trouver le certificat ni dans le magasin LocalMachine ni dans le magasin CurrentUser.