Training - Beratung - Projektarbeiten

www.David-Tielke.de

WCF und Username & Password Security: HowTo

Die Entwicklung von Serviceanwendungen mit der WCF ist in simplen HelloWorld-Beispielen sehr komfortabel. Möchte man allerdings erweiterte Funktionalitäten wie Sicherheit nutzen, verzweifelt man sehr schnell an den umfangreichen Einstellungsmöglichkeiten und der daraus resultierenden Konfigurationsdateien. Deshalb möchte an dieser Stelle zeigen, wie man es zum Laufen bekommt und wie das alles funktioniert, nachdem ich in den letzten Stunden selbst erfahren musste, das es doch nicht so einfach ist, wie man zu Anfang glaubt.

Ausgangslage

Wenn man auf der grünen Wiese beginnt, sollte man zunächst immer Client und Service ohne Sicherheitsmerkmale entwickeln und sicherstellen, dass dieser Teil bereits funktioniert, bevor man sich an Zertifikate & Co. heranwagt. Da ich nicht der Freund von generierten Clientproxies bin, erstelle ich den Dienst und den Client aus dem ServiceContract, der in einem shared Project liegt. Aus diesem Grund erstellen wir drei Projekte:

image

Abbildung 1: Die drei Projekte unserer Beispielanwendung

Da das Absichern eines Dienstes durch die WCF (fast) vollständig unabhängig von deren angebotenen Operationen ist, definieren wir einen möglichst einfachen ServiceContract:

namespace SharedContracts
{
    [ServiceContract]
    public interface IHelloService
    {
        [OperationContract]
        string Echo(string message);
    }
}

Listing 1: Der ServiceContract aus der Projektmappe SharedContracts

Der Contract wird von dem Dienst in der Projektmappe Service implementiert:

namespace Service
{
    class HelloService : IHelloService
    {
        public string Echo(string message)
        {
            return message;
        }
    }
}

Listing 2: Der Dienst in der Projektmappe Services

Durch die bereits erwähnte Abneigung zu generierten Proxies, fällt die Implementierung des Channels etwas komplizierter als normal aus:

namespace Client
{
    class Program
    {
        static void Main(string[] args)
        {
            var proxy = new ChannelFactory("HelloService");
            proxy.Open();
            IHelloService client = proxy.CreateChannel();
            string response = client.Echo("Huhu");
            Console.WriteLine(response);
            Console.ReadKey();
        }
    }
}

Listing 3: Der Client aus der Projektmappe Client

Authentifizierungsmodi

Die Windows Communication Foundation unterstützt diverse Möglichkeiten einen Client bei einem Service zu authentifizieren. Neben der Möglichkeit bestehende Windows-Authentifizierung (Kerberos / NTLM) oder Zertifikate zu nutzen, ist für einfachere Szenarien vor allem die Authentifizierung durch einen Benutzernamen und ein Passwort interessant. Was recht einfach klingt, entpuppt sich bei der Umsetzung durch WCF als durchaus kompliziert.

Zertifikate

Eigentlich könnte man denken, durch Angabe von Benutzername und Passwort im Client und einer entsprechenden Einstellung im Service, sollte die Anforderung erfüllt sein.Dem ist allerdings nicht so: Würde der Client seine Benutzerdaten im Klartext übertragen, wäre das sicherheitstechnisch im höchsten Maße kritisch, da Dritte diese ohne weiteres mitlesen könnten. Deshalb erfordert WCF unbedingt die Übertragung dieser Daten mit einer, durch ein Zertifikat verschlüsselten Verbindung.

Um ein solches Zertifikat zu bekommen, gibt es diverse Möglichkeiten. Eine wäre es ein solches signiertes Zertifikat bei einer vertrauenswürdigen Stelle ausstellen zu lassen, was zwar dem ganzen Szenario mehr Sicherheit verleihen würde, allerdings auch mit einem erhöhten Preis einher geht. Deshalb entscheiden wir uns in diesem Beispiel für die Möglichkeit ein solches Zertifikat selbst zu erstellen. Im SDK von Visual Studio existiert dazu das konsolenbasierte Tool makecert.exe. Da dieses Tool am Anfang durchaus recht kompliziert sein kann, empfehle ich an dieser Stelle das Tool Self-Cert, welches das Erstellen mit einer normalen Windowsoberfläche ermöglicht:

image

Abbildung 2: Das Tool Self-Cert zum Erstellen von Zertifikaten

Dabei gibt es, wie auch bei makecert.exe, zwei Möglichkeiten die Zertifikate generieren zu lassen. Zum einen kann ein generiertes Zertifikat als Datei exportiert werden und darüber hinaus direkt in den Zertifikatsspeicher von Windows gespeichert werden. Ich wähle als Namen david-tielke.de und speichere das Zertifikat direkt in meinem Zertifikatsspeicher. Wichtig dabei ist das die Option "Exportale private key…" aktiviert ist.

Um sicherzustellen, dass wir alles richtig gemacht haben, schauen wir uns den Zertifikatsspeicher im Windows einmal genauer an: (Start –> Ausführen (Oder WIN + R) –> mmc.exe –> Datei –> Snapin hinzufügen –> Zertifikate –> hinzufügen –> OK). Wenn das Zertifikat im Speicher gefunden wurde, war der Vorgang erfolgreich.

image

Abbildung 3: Das generierte Zertifikat wurde im Ordner Eigene Zertifikate abgelegt.

Wie für die sichere Kommunikation mit Zertifikaten erforderlich, wird der WCF-Service später das Zertifikat mit dem enthaltenen public Key an den Client senden, welcher dann diesen public Key nutzen wird, um die Kommunikation zu verschlüsseln. Damit der Service die Daten entschlüsseln kann, nutzt er den private Key. Während das Zertifikat mehr oder weniger frei zugänglich ist, muss ein private Key natürlich gesondert geschützt werden. Bei Windows werden die private Keys in speziellen Dateien gespeichert, welche im Ordner C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys zu finden sind. Allerdings ist die Ermittlung des zu einem Zertifikats assoziierten Dateinamens nicht so einfach.

image

Abbildung 4: Der Ordner mit verschiedenen private Keys.

Um unserem Service die Berechtigung zum lesen des private Keys zu geben, müssen wir ihm (bzw. dem ausführenden Benutzeraccount) die benötigten Dateirechte geben. Um nicht mühsam den Dateinamen zu einem Zertifikat zu ermitteln, können diese Rechte  im Zertifikatsmanager geändert werden (Rechtsklick auf Zertifikat –> Alle Aufgaben –> Private Schlüssel verwalten).

image

Abbildung 5: Die Berechtigungen für private Schlüssel können im Zertifikatsmanager geändert werden

image

Abbildung 6: Der ausführende Benutzer benötigt Leserechte auf den privaten Schlüssel

Hier wurde jedem Benutzer das lesen gestattet, im späteren Betrieb sollte das natürlich angepasst werden.

Damit ist unser Zertifikat erstellt, eingebunden und durch unseren Service nutzbar. Neben der Möglichkeit das Ganze durch den Zertifikatsspeicher von Windows verwalten zu lassen, bietet sich natürlich auch die Möglichkeit das Zertifikat direkt aus einer Datei zu laden, was allerdings nicht weniger kompliziert ist.

Authentifizierung des Benutzers

Damit der Service das Tupel aus Benutzername und Password auch auswerten kann, stehen diverse mögliche Validatoren zur Verfügung. Wir entscheiden uns für eine eigenen Implementierung, um beispielsweise einen eigenen Credentialstore zu nutzen. Dazu leiten wir eine Klasse von der Frameworkklasse UsernamePasswordValidator aus dem Namensbereich System.IdentityModel.Selectors ab und implementieren die abstrakte Methode Validate.

namespace Service
{
    class UserAuthenticator : UserNamePasswordValidator
    {
        public override void Validate(string userName, string password)
        {
            if (userName == "David" && password == "geheim")
                return;

            throw new SecurityException("Access denied");
        }
    }
}

Listing 4: Die Klasse UserAuthenticator prüft die empfangenen Credentials.

Konfiguration des Services

Das schöne an WCF (oder furchtbare, je nachdem wen man fragt…) ist Möglichkeit, die Sicherheit komplett in der Konfigurationsdatei zu beschreiben, ohne dazu unseren C#-Code verändern zu müssen.

Zunächst muss in dem entsprechenden Binding der CredentialMode Username aktiviert werden. Wir nutzen hier das wsHttpBinding, allerding sollte man vor Verwendung anderer Bindings in der MSDN überprüfen, ob dieses den CredentialMode Username auch unterstützt.

<!-- wsHttpBinding für Security umkonfigurieren -->    
<bindings>
  <wshttpbinding>
    <binding>
      <security>
        <message clientcredentialtype="UserName" />
      </security>
    </binding>
  </wshttpbinding>
</bindings>

Listing 5: Konfigurieren des wsHttpBindings

Danach konfigurieren wir das Serviceverhalten:

<!-- Servicezertifikat für Endpunkt definieren -->
<behaviors>
  <serviceBehaviors>
    <behavior name="wsEndpointBehavior">
      <serviceCredentials>
        
        <!-- Zertifikat definieren-->
        <serviceCertificate 
          findValue="david-tielke.de" 
          storeLocation="LocalMachine" 
          storeName="My" 
          x509FindType="FindBySubjectName"/>
            
        <!-- Custom NamePassword Validator definieren -->
        <userNameAuthentication userNamePasswordValidationMode="Custom" 
                                customUserNamePasswordValidatorType=
                                "Service.UserAuthenticator, Service"/>
       </serviceCredentials>
    </behavior>
  </serviceBehaviors>
</behaviors>

Listing 6: Konfiguration des Diensteverhaltens

Im Bereich “Zertifikat definieren” wird das zum Übertragen der Credentials eingerichtete Zertifikat angegeben und danach teilen wir der WCF mit, das die Credentials mit der selbsterstellen Klasse UserAuthenticator überprüft werden sollen.

Als letztes ist der Service selbst an der Reihe:

<!-- Service definieren -->
<services>
  <service name="Service.HelloService" 
              behaviorConfiguration="wsEndpointBehavior">
    <!-- Endpunkt-->
    <endpoint address="http://localhost:7940/HelloService"
              binding="wsHttpBinding"
              contract="SharedContracts.IHelloService"/>
  </service>      
</services>

Listing 7: Konfiguration des Services

Damit ist der Service fertig konfiguriert und kann aufgerufen werden.

Konfiguration des Clients

Während das erzeugte Zertifikat im Zertifikatsspeicher des Service-Hosts importiert werden musste, ist dies im Client nicht notwendig. Dieser bekommt nur zu Beginn der Kommunikation das Zertifikat und sendet dann seine Daten, welche mit dem public Key verschlüsselt wurden. Damit kann nur der Besitzer des private Keys des zugeordneten Zertifikats diese Daten entschlüsseln, also der Besitzer. Daher ist es enorm wichtig, dass der Absender auch wirklich vertrauenswürdig ist, denn mit der von uns gewählten Variante der Zertifikatserzeugung hätte sich auch jeder anderer ein Zertifikat mit meinem Namen erzeugen lassen können. Damit ein Zertifikat vertrauenswürdig ist, muss es von einer sogenannten Trusted Authority (TA) ausgestellt werden, welche den Zertifikatsinhaber zweifelsfrei identifizieren, beispielsweise durch einen Lichtbildausweis. Nachdem eine TA solch ein Zertifikat erstellt hat, bildet es aus den Zertifikatsdaten einen Hashwert und verschlüsselt ihn mit dem privaten Schlüssel der TA (die sogn. Signatur). Ein Beispiel für eine solche TA ist Verisign. Deren Zertifikat (mit public Key) ist auf einem Windows 7 Rechner bereits vorinstalliert, daher ist sichergestellt (insofern das Installationsmedium nicht manipuliert wurde), dass dieses Zertifikat echt ist. Bekommt nun der Client das Zertifikat vom Service welches  von Verisign signiert wurde, kann dieser mit dem public Key den signierten Hashwert entschlüsseln, selbst den Hashwert aus den Zertifikatsdaten berechnen und diese vergleichen. Sind beide Werte gleich, wurde das Zertifikat nicht verändert und ist echt. Ist als Absender der Name von Bob (sorry, ein PKI-Beispiel ohne Bob geht einfach gar nicht…) eingetragen, ist es auch definitiv Bobs Zertifikat. Stimmen die beiden Hashcodes nicht, wurde das Zertifikat nach dem signieren manipuliert und kann daher abgelehnt werden.

Standardmäßig akzeptiert ein WCF-Client nur vertrauenswürdige Zertifikate. Dies hat jedoch einige Einschränkungen und ist nicht zuletzt mit finanziellem Aufwand verbunden. Daher müssen wir im Serviceverhalten diese Überprüfung abschalten. Das damit verbundene Risiko nehmen wir bei diesem Beispiel in Kauf.

<!-- ENDPOINTVERHALTEN-->
<behaviors>
  <endpointBehaviors>
    <behavior name="endpointBehavior">
      <clientCredentials>
        <serviceCertificate>
          <!-- Auch nicht signierte Zertifikate akzeptieren -->
          <authentication certificateValidationMode="None"/>
        </serviceCertificate>
      </clientCredentials>
    </behavior>
  </endpointBehaviors>
</behaviors>

Listing 8: Deaktivierung der Zertifikatsvalidierung

Als nächstes müssen wir, wie bereits im Service, den Credentialtype Username im Binding aktivieren:

<!-- BINDINGKONFIGURATION-->
<bindings>
  <wsHttpBinding>
    <binding>
      <security>
        <message clientCredentialType="UserName"/>
      </security>
    </binding>
  </wsHttpBinding>
</bindings>

Listing 9: Aktivierung des Credentialtypes Username in der Bindingkonfiguration.

Als letztes müssen wir den Clientbereich selbst konfigurieren:

<client>
  <!-- Endpunkt zum Service-->
  <endpoint name="HelloService"
            address="http://localhost:7940/HelloService"
            binding="wsHttpBinding"
            contract="SharedContracts.IHelloService"
            behaviorConfiguration="endpointBehavior">
    <!-- Jeden Zertifikatsnamen akzeptieren -->
    <identity>
      <dns value="david-tielke"/>
    </identity>
  </endpoint>
</client>    

Listing 10: Der Endpunkt

Zunächst wird der Endpunkt definiert und mit dem Behavior konfiguriert. Danach ignorieren wir eine weitere Eigenart der Überprüfung von Zertifikaten: Normalerweise muss der Name eines Zertifikats dem Hostnamen entsprechen, in unserem Fall also Localhost. Ist dem nicht so, löst WCF eine Ausnahme aus. Damit der von uns definierte Wert david-tielke.de akzeptiert wird, definieren wir ihn hier direkt als Identitätswert.

Aufruf des Clients

Nachdem wir nun sowohl Client als auch Service für die Verwendung von Username/Passwort zur Authentifizierung eingerichtet haben, müssen wir diese Credentials natürlich auch beim Start des Dienstes angeben:

static void Main(string[] args)
{
    var proxy = new ChannelFactory<IHelloService>("HelloService");
    proxy.Credentials.UserName.UserName = "David";
    proxy.Credentials.UserName.Password = "geheim";
    proxy.Open();
    IHelloService client = proxy.CreateChannel();
    string response = client.Echo("Huhu");
    Console.WriteLine(response);
    Console.ReadKey();
}

Listing 11: Angabe von Benutzername und Kennwort beim Serviceaufruf.

Damit sollte alles erledigt sein und der Dienst ist jetzt nur noch durch Angabe von Benutzernamen und Passwort aufrufbar.

Fazit

Auch wenn die Anforderung recht einfach aussieht, ist es nicht unbedingt einfach einen Service durch Angabe von Benutzernamen und Passwort zu schützen. Ich hoffe das ich mit dieser kleinen Anleitungen einigen Entwicklern die mühseligen Stunden im Debugger oder in der MSDN ersparen konnte. Angemerkt sein sollte jedoch noch, das die hier verwendete Konfiguration im Produktivbetrieb natürlich noch durch ein von einer TA signierten Zertifikat erweitert werden sollte, denn sonst ist die Identität des Dienstes nicht sichergestellt und die leserechte für den privaten Schlüssel sollte natürlich auch nur dem Benutzer gewährt werden, der den jeweilgen Hostprozess ausführt.

Die Schritte noch einmal zusammengefasst:

  1. Client und Dienst ohne Security entwickeln und ggf. testen
  2. Zertifikat erzeugen und im Zertifikatsspeicher ablegen
  3. Rechte auf Zertifikat anpassen
  4. Ggf. eigenen CredentialValidator schreiben
  5. Service
    1. Binding konfigurieren (clientcredentialType="Username")
    2. Serviceverhalten konfigurieren (Zertifikat und CustomValidator)
    3. Service konfigurieren und Verhalten linken
  6. Client
    1. Endpunktverhalten konfigurieren (Zertifikatsvalidierung unterdrücken)
    2. Binding konfigurieren (clientcredentialType="Username")
    3. Endpunkt konfigurieren und Identität anpassen
    4. Credentials im Code setzen

Kommentare (2) -

  • Hacky

    12.12.2012 16:16:07 | Antwort

    Hallo David, ich bekomme leider eine Exception:

    System.ServiceModel.ProtocolException:
    {"Der Remoteserver hat einen Fehler zurückgegeben: (415) Cannot process the message because the content type 'application/soap+xml; charset=utf-8' was not the expected type 'text/xml; charset=utf-8'.."}

    Muss noch eine Einstellung im Client gesetzt werden damit der richtige "content typ" gesetzt ist ? Wo mach ich das ? Ansonsten tolles Tutorial !

    mfg Hacky

  • Hacky

    12.12.2012 16:24:20 | Antwort

    Hallo David, ich bekomme leider ein Exception:
    System.ServiceModel.ProtocolException:
    {"Der Remoteserver hat einen Fehler zurückgegeben: (415) Cannot process the message because the content type 'application/soap+xml; charset=utf-8' was not the expected type 'text/xml; charset=utf-8'.."}

    muss noch irgendwo der "content typ" genauer definiert werden ?

    mfg Lars

Kommentar schreiben

Loading