Verwendung eines Client-Zertifikats zur Authentifizierung und Autorisierung in einer Web-API

Ich versuche, mithilfe eines Client-Zertifikats Geräte mithilfe einer Web-API zu authentifizieren und zu autorisieren, und habe einen einfachen Proof of Concept entwickelt, um Probleme mit der potenziellen Lösung zu lösen. Ich habe ein Problem, bei dem das Client-Zertifikat nicht von der Webanwendung empfangen wird. Eine Reihe von Personen hat dieses Problem gemeldet,inclusive in diesem Q & A, aber keiner von ihnen hat eine Antwort. Ich hoffe, mehr Details zur Wiederbelebung dieses Problems bereitzustellen und hoffentlich eine Antwort auf mein Problem zu erhalten. Ich bin offen für andere Lösungen. Die Hauptanforderung besteht darin, dass ein in C # geschriebener eigenständiger Prozess eine Web-API aufrufen und mithilfe eines Client-Zertifikats authentifiziert werden kann.

Die Web-API in diesem POC ist sehr einfach und gibt nur einen einzelnen Wert zurück. Es wird ein Attribut verwendet, um zu überprüfen, ob HTTPS verwendet wird und ein Client-Zertifikat 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 überprüfe ich nur die Verfügbarkeit des Client-Zertifikats. Sobald dies funktioniert, kann ich im Zertifikat Überprüfungen auf Informationen hinzufügen, um anhand einer Liste von Zertifikaten zu validieren.

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

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 ausführe, erhalte ich den Statuscode 403 Verboten mit dem Begründungssatz "Client-Zertifikat erforderlich" zurück, der angibt, dass sie in mein RequireHttpsAttribute gelangt und keine Client-Zertifikate findet. Durch Ausführen dieses Befehls über einen Debugger habe ich überprüft, dass das Zertifikat geladen und dem 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 im persönlichen und vertrauenswürdigen Stammspeicher des lokalen Computers für den Webanwendungsserver. Für diesen Test werden der Client und die Webanwendung auf demselben Computer ausgeführt.

Ich kann diese Web-API-Methode mit Fiddler aufrufen, indem ich dasselbe Client-Zertifikat anhänge, und sie funktioniert einwandfrei. Bei Verwendung von Fiddler werden die Tests in RequireHttpsAttribute bestanden, ein erfolgreicher Statuscode von 200 und der erwartete Wert zurückgegeben.

Hat jemand das gleiche Problem festgestellt, bei dem 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 überprüft, ob dieses Zertifikat korrekt abgerufen und der Clientzertifikatsammlung hinzugefügt wurde. Aber ich habe die gleichen Ergebnisse erzielt, bei denen der Servercode keine Client-Zertifikate abruft.

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

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

    }

Sie werden feststellen, dass beim Abrufen des Zertifikats aus einer Datei ein Objekt vom Typ X509Certificate zurückgegeben wird und dass es beim Abrufen aus dem Zertifikatspeicher vom Typ X509Certificate2 ist. Die X509CertificateCollection.Add-Methode erwartet einen Typ von X509Certificate.

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

Ich habe die Webanwendung so geändert, dass sie auf einem Hostnamen statt auf einem lokalen Host ausgeführt wird.Ich stelle die Webanwendung so ein, dass SSL erforderlich istIch habe überprüft, ob das Zertifikat für die Clientauthentifizierung festgelegt wurde und sich im vertrauenswürdigen Stammverzeichnis befindet.Neben dem Testen des Client-Zertifikats in Fiddler habe ich es auch in Chrome validiert.

Zu einem Zeitpunkt, als diese Optionen ausprobiert wurden, funktionierte sie. Dann habe ich angefangen, Änderungen rückgängig zu machen, um zu sehen, was dazu geführt hat, dass es funktioniert hat. Es ging weiter. Dann habe ich versucht, das Zertifikat aus dem vertrauenswürdigen Stammverzeichnis zu entfernen, um zu überprüfen, ob dies erforderlich ist. Das Zertifikat funktioniert nicht mehr. Jetzt kann ich es nicht wieder verwenden, obwohl ich das Zertifikat wieder im vertrauenswürdigen Stammverzeichnis abgelegt habe. Jetzt fordert Chrome mich nicht einmal zur Eingabe eines Zertifikats auf, wie es auch verwendet wird, und es schlägt in Chrome fehl, funktioniert aber weiterhin in Fiddler. Es muss eine magische Konfiguration geben, die mir fehlt.

Ich habe auch versucht, "Negotiate Client Certificate" in der Bindung zu aktivieren, aber Chrome fordert mich immer noch nicht zur Eingabe eines Client-Zertifikats auf. 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:

Ich bin verblüfft, was das Problem ist. Ich füge ein Kopfgeld für jeden hinzu, der mir dabei helfen kann, das herauszufinden.

Antworten auf die Frage(10)

Ihre Antwort auf die Frage