wake-up-neo.net

Verwendung eines Clientzertifikats zur Authentifizierung und Autorisierung in einer Web-API

Ich versuche, ein Clientzertifikat zur Authentifizierung und Autorisierung von Geräten mithilfe einer Web-API zu verwenden und ein einfaches Proof-of-Concept entwickelt, um Probleme mit der potenziellen Lösung durchzuarbeiten. Ich habe ein Problem, bei dem das Client-Zertifikat von der Webanwendung nicht empfangen wird. Eine Reihe von Personen berichteten über dieses Problem, auch in diesem Q & A , aber keiner von ihnen hat eine Antwort. Ich hoffe, dass ich mehr Details zur Verfügung stellen werde, um dieses Problem wiederzubeleben und hoffentlich eine Antwort auf mein Problem zu bekommen. Ich bin offen für andere Lösungen. Die Hauptanforderung ist, dass ein in C # geschriebener Standalone-Prozess eine Web-API aufrufen und mit einem Clientzertifikat authentifiziert werden kann. 

Die Web-API in diesem POC ist sehr einfach und gibt nur einen einzelnen Wert zurück. Es verwendet ein Attribut, um zu überprüfen, ob HTTPS verwendet wird und ein Clientzertifikat vorhanden ist. 

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

}

Hier ist der Code für das 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); 
        } 
    } 
}

In diesem POC prüfe ich nur die Verfügbarkeit des Client-Zertifikats. Sobald dies funktioniert, kann ich Überprüfungen auf Informationen im Zertifikat hinzufügen, um eine Liste von Zertifikaten zu überprüfen.

Hier sind die Einstellungen in IIS für SSL für diese Webanwendung.

 enter image description here

Hier ist der Code für den Client, der die Anforderung mit einem Clientzertifikat sendet. Dies ist eine Konsolenanwendung.

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

Wenn ich diese Test-App starte, erhalte ich einen Statuscode von 403 Forbidden mit der Angabe "Client certificate Required" (Grundzertifikat erforderlich). Dies zeigt an, dass es in mein RequireHttpsAttribute gerät und keine Client-Zertifikate gefunden wird. Wenn Sie dies über einen Debugger ausführen, habe ich überprüft, dass das Zertifikat geladen und zum WebRequestHandler hinzugefügt wird. Das Zertifikat wird in eine CER-Datei exportiert, die geladen wird. Das vollständige Zertifikat mit dem privaten Schlüssel befindet sich in den persönlichen und vertrauenswürdigen Stammspeichern des lokalen Computers für den Webanwendungsserver. Für diesen Test werden Client und Webanwendung auf demselben Computer ausgeführt.

Ich kann diese Web-API-Methode mit Fiddler aufrufen und dasselbe Clientzertifikat anhängen. Das funktioniert einwandfrei. Wenn Fiddler verwendet wird, bestehen die Tests in RequireHttpsAttribute, der Statuscode 200 wird zurückgegeben und der erwartete Wert zurückgegeben.

Hat jemand das gleiche Problem gefunden, dass HttpClient in der Anforderung kein Clientzertifikat sendet und eine Lösung gefunden hat?

Update 1:

Ich habe auch versucht, das Zertifikat aus dem Zertifikatspeicher abzurufen, der den privaten Schlüssel enthält. So habe ich es abgerufen:

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

Ich habe bestätigt, dass dieses Zertifikat korrekt abgerufen wurde und der Clientzertifikatsammlung hinzugefügt wurde. Ich habe jedoch dieselben Ergebnisse erhalten, bei denen der Servercode keine Clientzertifikate abruft.

Der Vollständigkeit halber wird hier der Code zum Abrufen des Zertifikats aus einer Datei verwendet:

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

    }

Wenn Sie das Zertifikat aus einer Datei abrufen, wird ein Objekt vom Typ X509Certificate zurückgegeben. Wenn Sie es aus dem Zertifikatspeicher abrufen, hat es den Typ X509Certificate2. Die X509CertificateCollection.Add-Methode erwartet einen Typ von X509Certificate.

Update 2: Ich versuche immer noch, dies herauszufinden und habe viele verschiedene Optionen ausprobiert, aber ohne Erfolg. 

  • Ich habe die Webanwendung so geändert, dass sie auf einem Hostnamen anstelle des lokalen Hosts ausgeführt wird.
  • Ich habe die Webanwendung so eingestellt, dass SSL erforderlich ist
  • Ich habe überprüft, dass das Zertifikat für die Clientauthentifizierung festgelegt wurde und sich im vertrauenswürdigen Stamm befindet
  • Neben dem Testen des Client-Zertifikats in Fiddler habe ich es auch in Chrome validiert.

Zu einem Zeitpunkt, als diese Optionen ausprobiert wurden, begann es zu funktionieren. Dann fing ich an, Änderungen rückgängig zu machen, um zu sehen, warum es funktioniert. Es hat weiter funktioniert. Dann habe ich versucht, das Zertifikat aus dem vertrauenswürdigen Stamm zu entfernen, um zu bestätigen, dass dies erforderlich war, und es funktionierte nicht mehr. Jetzt kann ich es nicht wieder zum Laufen bringen, obwohl ich das Zertifikat wieder im vertrauenswürdigen Stamm abgelegt habe. Jetzt fordert Chrome mich nicht einmal zur Eingabe eines Zertifikats auf, wie es auch verwendet wurde, und es fällt in Chrome aus, funktioniert aber weiterhin in Fiddler. Es muss eine magische Konfiguration geben, die ich vermisse.

Ich habe auch versucht, "Negotiate Client Certificate" in der Bindung zu aktivieren, Chrome wird jedoch immer noch nicht zur Eingabe eines Clientzertifikats aufgefordert. Hier sind die Einstellungen mit "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

Hier ist das Client-Zertifikat, das ich verwende:

 enter image description here

 enter image description here

 enter image description here

Ich bin verblüfft über das Problem. Ich füge eine Belohnung für jeden hinzu, der mir helfen kann, das herauszufinden.

30
Kevin Junghans

Durch die Rückverfolgung konnte ich herausfinden, was das Problem war (Danke, Fabian, für diesen Vorschlag). Bei weiteren Tests habe ich festgestellt, dass ich das Client-Zertifikat auf einem anderen Server (Windows Server 2012) verwenden kann. Ich habe dies auf meinem Entwicklungscomputer (Window 7) getestet, um diesen Prozess zu debuggen. Durch den Vergleich der Ablaufverfolgung mit einem IIS -Server, der funktioniert hat und der nicht funktioniert hat, konnte ich die relevanten Zeilen im Ablaufverfolgungsprotokoll lokalisieren. Hier ist ein Teil eines Protokolls, in dem das Clientzertifikat funktioniert hat. Dies ist das Setup direkt vor dem Senden

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:

Das Traceprotokoll sah auf dem Computer aus, auf dem das Clientzertifikat fehlgeschlagen ist.

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.

Ich konzentrierte mich auf die Zeile, in der der Server angegeben hatte, dass es 137 Aussteller gab. Ich fand diese Fragen und Antworten, die meinem Problem ähnlich waren . Die Lösung für mich war nicht die als Antwort markierte Lösung, da sich mein Zertifikat im vertrauenswürdigen Stamm befand. Die Antwort ist die darunter wo Sie die Registrierung aktualisieren. Ich habe gerade den Wert zum Registrierungsschlüssel hinzugefügt.

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL

Wertname: SendTrustedIssuerList Wertetyp: REG_DWORD Wertdaten: 0 (False)

Nach dem Hinzufügen dieses Werts zur Registrierung begann es auf meinem Windows 7-Computer zu funktionieren. Dies scheint ein Windows 7-Problem zu sein.

7
Kevin Junghans

Update:

Beispiel von Microsoft:

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

Original

So habe ich die Client-Zertifizierung in Betrieb genommen und überprüft, ob eine bestimmte Stammzertifizierungsstelle sie ausgestellt hat und dass es sich um ein bestimmtes Zertifikat handelt.

Zuerst habe ich <src>\.vs\config\applicationhost.config bearbeitet und diese Änderung vorgenommen: <section name="access" overrideModeDefault="Allow" />

Dadurch kann ich <system.webServer> in web.config bearbeiten und die folgenden Zeilen hinzufügen, für die eine Client-Zertifizierung in IIS Express erforderlich ist. Hinweis: Ich habe dies zu Entwicklungszwecken bearbeitet, keine Überschreibungen in der Produktion zulassen.

Für die Produktion folgen Sie einer Anleitung wie dieser, um den IIS einzurichten:

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

Kann dann die API mit einer solchen Client-Zertifizierung aufrufen, die von einem anderen Webprojekt getestet wurde.

[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

Ich hatte tatsächlich ein ähnliches Problem, bei dem wir viele vertrauenswürdige Stammzertifikate hatten. Unser frisch installierter Webserver hatte über hundert. Unsere Wurzel begann mit dem Buchstaben Z und endete am Ende der Liste. 

Das Problem war, dass der IIS nur die ersten zwanzig vertrauenswürdigen Roots an den Client schickte und den Rest einschließlich unseres. Es war vor ein paar Jahren, ich kann mich nicht an den Namen des Tools erinnern ... es war Teil der IIS Admin-Suite, aber Fiddler sollte es auch tun. Nachdem wir den Fehler erkannt hatten, haben wir viele vertrauenswürdige Roots entfernt, die wir nicht brauchen. Dies wurde probeweise ausgeführt, seien Sie also vorsichtig, was Sie löschen.

Nach der Aufräumung funktionierte alles wie ein Zauber.

1
Brandtware

Stellen Sie sicher, dass HttpClient Zugriff auf das vollständige Clientzertifikat (einschließlich des privaten Schlüssels) hat. 

Sie rufen GetCert mit einer Datei "ClientCertificate.cer" auf, die zu der Annahme führt, dass kein privater Schlüssel enthalten ist - sollte eher eine pfx-Datei innerhalb von Windows sein. Es ist möglicherweise noch besser, auf das Zertifikat aus dem Windows-Zertifikatspeicher zuzugreifen und es mithilfe des Fingerabdrucks zu durchsuchen.

Seien Sie beim Kopieren des Fingerabdrucks vorsichtig: Es gibt einige nicht druckbare Zeichen, wenn Sie in cert Management anzeigen (kopieren Sie die Zeichenfolge nach Notepad ++ und überprüfen Sie die Länge der angezeigten Zeichenfolge).

1
Daniel Nachtrub

Beim Betrachten des Quellcodes denke ich auch, dass es ein Problem mit dem privaten Schlüssel geben muss.

Zu diesem Zweck wird geprüft, ob das übergebene Zertifikat den Typ X509Certificate2 hat und ob es den privaten Schlüssel besitzt. 

Wenn der private Schlüssel nicht gefunden wird, versucht er, das Zertifikat im CurrentUser-Store und dann im LocalMachine-Store zu finden. Wenn es das Zertifikat findet, prüft es, ob der private Schlüssel vorhanden ist.

(siehe Quellcode aus der Klasse SecureChannnel, Methode EnsurePrivateKey)

Abhängig davon, welche Datei Sie importiert haben (.cer - ohne privaten Schlüssel oder .pfx - mit privatem Schlüssel) und von welchem ​​Speicher sie möglicherweise nicht den richtigen findet, und Request.ClientCertificate wird nicht aufgefüllt.

Sie können Network Tracing aktivieren, um zu debuggen. Es wird Ihnen folgende Ausgabe geben:

  • Es wurde versucht, ein übereinstimmendes Zertifikat im Zertifikatspeicher zu finden
  • Das Zertifikat kann weder im LocalMachine-Store noch im CurrentUser-Store gefunden werden.
0
Fabian

Ich bin kürzlich auf ein ähnliches Problem gestoßen, und wenn ich Fabians Rat befolgt habe, bin ich tatsächlich zur Lösung gekommen. Bei Client-Zertifikaten müssen Sie zwei Dinge sicherstellen:

  1. Der private Schlüssel wird tatsächlich als Teil des Zertifikats exportiert.

  2. Die Anwendungspoolidentität, mit der die App ausgeführt wird, hat Zugriff auf den privaten Schlüssel.

In unserem Fall musste ich:

  1. Importieren Sie die pfx-Datei in den lokalen Serverspeicher, und aktivieren Sie das Kontrollkästchen für den Export, um sicherzustellen, dass der private Schlüssel gesendet wurde.
  2. Gewähren Sie über die MMC - Konsole dem verwendeten Dienstkonto den Zugriff auf den privaten Schlüssel für das Zertifikat.

Das in anderen Antworten erläuterte vertrauenswürdige Stammproblem ist gültig, es war in unserem Fall einfach nicht das Problem.

0