Integration in Windows Security mit .NET 2.0 – Teil 3

Veröffentlicht: 10. Mrz 2006

Von Dominick Baier

Windows beinhaltet zwei Authentifizierungs-Protokolle: Kerberos und NTLM. Während NTLM für Nicht-Domänen-Umgebungen und Peer-To-Peer Authentifizierung gedacht ist, ist Kerberos in die Active Directory Infrastruktur integriert und erlaubt interessante Features wie z.B. die Delegation von Credentials im Netzwerk. Leider war vor .NET 2.0 der Zugriff auf diese Einrichtungen nur C++ Entwicklern vorbehalten, bzw. gekettet an High Level APIs wie ASP.NET oder Enterprise Services.

Auf dieser Seite

 CIA - Sichere Kommunikation
 Schöne neue Welt
 NegotiateStream
 Zurück zu Tokens
 Remoting 2.0
 Web Services
 Wo stehen wir?
 Der Autor

CIA - Sichere Kommunikation

Eine sichere Kommunikation wird oft mit der Abkürzung CIA beschrieben. Das hat nichts mit dem amerikanischen Geheimdienst zu tun, sondern steht für folgende drei Begriffe

  • Confidentiality (Vertraulichkeit):
    Die Daten können nur von autorisierten Dritten gelesen werden

  • Integrity (Integrität):
    Die Daten können nicht unbemerkt von Dritten verändert werden

  • Authenticity (Authentizität):
    Der Absender der Daten ist eindeutig identifizierbar

Um diese Ziele zu erreichen, müssen kryptografische Techniken wie Verschlüsselung und Hashing angewendet werden. Dazu wird ein Schlüssel benötigt - und genau hier kommen Authentifizierungsprotokolle ins Spiel. Das ultimative Ziel einer Authentifizierung ist es, ein gemeinsames Geheimnis – den Schlüssel – auszuhandeln, nachdem Vertrauen in der Identität des Gegenübers aufgebaut wurde, z.B. über ein Passwort. NTLM und Kerberos haben dabei völlig unterschiedliche Ansätze für den Schlüsselaustausch – aber entscheidend ist, dass nach einer erfolgreichen Authentifizierung ein Schlüssel auf beiden Seiten zur Verfügung steht der benutzt werden kann, um die Kommunikation zu verschlüsseln bzw. vor unbemerkten Änderungen zu schützen – CIA eben.

Verschiedene Anwendungs-Protokolle implementieren CIA mehr oder weniger. IIS und SQL Server beispielsweise führen eine Authentifizierung durch, benutzen aber den resultierenden Schlüssel nicht (dazu muss zusätzlich SSL eingesetzt werden). Windows File Sharing unterstützt optional Integritätsschutz (SMB Signing). Das einzige Protokoll, das alle Möglichkeiten ausschöpft, ist im Moment RPC/DCOM.

 

Schöne neue Welt

Schreibt man Anwendungen, die nicht Enterprise Services oder IIS/SSL benutzen (z.B. WebServices), muss man sich um CIA selbst kümmern. Sie wollen schließlich nicht, dass Ihr Server unauthentifizierte Daten entgegennimmt, bzw. diese von Dritten gelesen oder geändert werden können. In .NET 2.0 ist der zugrundeliegende Windows SSPI (Security Support Provider Interface) API elegant in zwei Klassen gewrapped. Aufbauend auf diesen Low-Level-Klassen wurde auch Remoting in .NET 2.0 um die Möglichkeit erweitert, sichere Kommunikation und Authentifizierung zu unterstützen. Dazu muss der Remoting Server nicht mehr in IIS gehostet werden.

SSPI unterstützt drei Authentifizierungsprotokolle: NTLM und Kerberos werden in der Klasse NegotiateStream abgebildet; SSL in SslStream. In diesem Artikel werden wir uns auf die windows-integrierten Protokolle konzentrieren und heben uns SSL für ein andermal auf.

 

NegotiateStream

NegotiateStream ist das Herz der gesamten NTLM/Kerberos Integration in .NET 2.0. Wie der Name verrät, ist dies eine streams-basierte Klasse und kann jeden beliebigen Stream (z.B. Sockets) wrappen. So ist es möglich, durch nur wenige Zeilen Code aus einer „normalen“ TCP/IP Kommunikation eine sichere zu machen. Gehen wir von einem einfachen TCP Client/Server aus:

Client:

static void Main()
{
    string machineName = "localhost";

    TcpClient client = new TcpClient();
    client.Connect(machineName, 4242);

    TextWriter writer = new StreamWriter(client.GetStream());

    writer.WriteLine("Hallo Server");
    writer.Flush();

    client.Close();
}

Server:

static void Main(string[] args)
{
TcpListener listener = new TcpListener(4242);
listener.Start();

TcpClient client = listener.AcceptTcpClient();

TextReader reader = new StreamReader(client.GetStream());
    string message = reader.ReadLine();

    Console.WriteLine(message);
}

Möchte man nun diese Kommunikation über einen authentifizierten und sicheren Kanal durchführen, muss man lediglich den originalen Socket-Stream an NegotiateStream übergeben und alle Kommunikation darüber abwickeln.

Um die Authentifizierung anzustossen, muss auf Client-Seite AuthenticateAsClient aufgerufen werden.

public void AuthenticateAsClient (
NetworkCredential credential,
string targetName,
ProtectionLevel requiredProtectionLevel,
TokenImpersonationLevel allowedImpersonationLevel
)

Die an diese Methode übergebenen Parameter beeinflussen entscheidend den Authentifizierungs-Prozess. Der credential Parameter entscheidet darüber, ob die Identität des aktuell angemeldeten Benutzers für die Authentifizierung benutzt werden soll. Eine andere Möglichkeit wäre es, ein Benutzername/Passwort-Paar anzugeben. Kerberos verlangt, dass der Account, unter dem der Server-Prozess ausgeführt wird, auf dem Client bekannt ist – dies wird im targetName Parameter angegeben. Ist das Konto nicht bekannt, wird automatisch ein Fallback auf NTLM durchgeführt.

requiredProtectionLevel gibt an, ob der Datenverkehr signiert oder verschlüsselt (oder beides) sein soll. Und zu guter Letzt bestimmt allowedImpersonationLevel, ob der Server die Erlaubnis bekommen soll, die Client Credentials zu impersonieren bzw. zu delegieren. Der sichere Client würde nun folgendermaßen aussehen:

static void Main()
{
  string machineName = "localhost";

  // account des servers
  string spn = "leastprivilege\server";

  TcpClient client = new TcpClient();
  client.Connect(machineName, 4242);

  NegotiateStream kerb = new NegotiateStream(client.GetStream());

  kerb.AuthenticateAsClient(
    CredentialCache.DefaultNetworkCredentials,
    spn,
    ProtectionLevel.EncryptAndSign,
    TokenImpersonationLevel.Impersonation);

    // falls authentifizierung erfolgreich war,
    // einige Status-Daten ausgeben
    if (kerb.IsAuthenticated)
    {
      Console.WriteLine("Authentifiziert?: {0}", kerb.IsAuthenticated);
      Console.WriteLine("Gegenseitig Authentifiziert?: {0}", 
        kerb.IsMutuallyAuthenticated);
      Console.WriteLine("Verschlüsselt?: {0}", kerb.IsEncrypted);
      Console.WriteLine("Signiert?: {0}", kerb.IsSigned);
      Console.WriteLine("Server Identität: {0}", 
        kerb.RemoteIdentity.Name);
    }

    TextWriter writer = new StreamWriter(kerb);

    writer.WriteLine("Hallo SSPI Server");
    writer.Flush();

    client.Close();
}

Dieser Code versucht durch Angabe der Server-Identität Kerberos zu nutzen. Wenn Sie die Identität des Servers nicht kennen oder nicht hard-coden möchten, kann mit Hilfe des setspn.exe Tools (auf der Windows Server CD) ein symbolischer Name, wie z.B. „SocketServer/MyDomain“, in Active Directory registriert werden, der zur Laufzeit zu dem eigentlichen Windows Account aufgelöst wird. Wenn Sie NTLM benutzen möchten, kann anstatt des Server-Accounts auch einfach String.Empty übergeben werden.

Benutzen von CredentialCache.DefaultNetworkCredentials als Client-Identität bedeutet, dass die Credentials des Prozesses bzw. angemeldeten Benutzers für die Authentifizierung benutzt werden. Man kann auch ein explizites Credential durch Angabe eines Benutzers mitsamt Passwort übergeben:

NetworkCredential cred = new NetworkCredential(
  "user", "password", "machine");

Der neue Server mit Authentifizierungs-Unterstützung würde folgendermaßen aussehen:

static void Main(string[] args)
{
    TcpListener listener = new TcpListener(4242);
    listener.Start();

    TcpClient client = listener.AcceptTcpClient();

    NegotiateStream kerb = new NegotiateStream(client.GetStream());

    kerb.AuthenticateAsServer(
        CredentialCache.DefaultNetworkCredentials,
        ProtectionLevel.EncryptAndSign,
        TokenImpersonationLevel.Impersonation);

    TextReader reader = new StreamReader(kerb);
    string message = reader.ReadLine();

    Console.WriteLine(message);
}

AuthenticateAsServer authentifiziert den Server. Obwohl die meisten Parameter mit der Client-Variante übereinstimmen, drücken diese eine andere Intention aus.

allowedImpersonationLevel und requiredProtectionLevel beschreiben die Mindest-Anforderungen an den Client.

Ein Beispiel: Wenn der Client sich verbindet, gibt er an, ob der Server den Client Token impersonieren darf. Der Server bringt wiederum mit dem gleichen Parameter zum Ausdruck, dass er den Client impersonieren muss, um seine Arbeit ordnungsgemäß durchzuführen. Nur wenn Client und Server sich auf diese Einstellungen einigen können, kommt eine erfolgreiche Authentifizierung zustande.

 

Zurück zu Tokens

Nach der erfolgreichen Authentifizierung erzeugt Windows einen Token für den Client, der wiederum in einer WindowsIdentity gewrapped wird und in dem RemoteIdentity Property von NegotiateStream abgelegt wird. Was man alles mit Tokens machen kann, habe ich bereits in den zwei vorangegangenen Teilen dieser Serie beschrieben. So kann man z.B. im Server den Benutzer-Namen des Clients folgendermaßen in Erfahrung bringen:

Console.WriteLine("Client Identität: {0}", kerb.RemoteIdentity.Name);

Genauso kann man nun rollenbasierte Access-Checks durchführen oder den Client impersonieren. Passen Sie bei Impersonierung allerdings immer auf, dass diese auch wieder irgendwann rückgängig gemacht wird.

Hinweis: Das Impersonieren von Clients ist eine privilegierte Operation. Der Server Account benötigt das „SeImpersonatePrivilege“ (Impersonieren des Clients nach der Authentifizierung) Privileg – dies kann in secpol.msc eingestellt werden.

// rollen basierte prüfung durchführen
WindowsPrincipal p = new WindowsPrincipal(kerb.RemoteIdentity);

if (!p.IsInRole("Domain\\Gruppe"))
  throw new Exception("Nicht autorisiert!");

// impersonieren des Clients - Impersonierung wird am Ende
// des using Blocks automatisch rückgängig gemacht
using (WindowsImpersonationContext wic = 
  ((WindowsIdentity)kerb.RemoteIdentity).Impersonate())
{
  // datei öffnen mit den Credentials des Clients
  string s = File.ReadAllText("foo.txt");
}

 

Remoting 2.0

Wem das alles zu „nahe am Metall“ ist, den wird es erfreuen, dass die eben vorgestellten Möglichkeiten auch alle im neuen TCP Channel von Remoting gekapselt sind. Hier ist es lediglich notwendig, die nötigen Informationen in der Konfigurations-Datei zu spezifizieren und die Remoting Infrastruktur kümmert sich um den Rest. Da die Konfigurations-Einstellungen nicht dokumentiert sind, hier die Bedeutung der Settings im Einzelnen.

Client Konfigurations-Einstellungen:

Name

Bedeutung

Secure

true/false – Schaltet Sicherheit ein/aus

username, password, domain

Wenn nicht die Identität des aktuellen Prozesses oder Benutzers verwendet werden sollen, kann man hier explizite Credentials angeben

tokenImpersonationLevel

Identification: Der Server kann den Client Token nur für Identitätsinformationen und rollenbasierte Checks verwenden

Impersonation: Der Server kann den Token impersonieren und damit auf Server-lokale Ressourcen zugreifen

Delegation: Der Server kann die Client Credentials im Netzwerk weiterleiten.

protectionLevel

None: Klartext-Kommunikation

Encrypt: Verschlüsselt

Sign: Signiert

EncryptAndSign: Empfohlene Einstellung

servicePrincipalName

Der Account des Servers. Wird für Kerberos benötigt.

Eine Client-Konfigurations-Datei könnte folgendermaßen aussehen:

<configuration>
    <system.runtime.remoting>
        <application>
            <channels>
                <channel 
ref="tcp"
secure="true"
tokenImpersonationLevel="Impersonation" 
protectionLevel="EncryptAndSign" 
servicePrincipalName="Server/Leastprivilege" />
            </channels>
        </application>
    </system.runtime.remoting>
</configuration>

Der eigentliche Remoting Code in Client oder Server ändert sich dabei nicht.

Auf der Server-Seite kann man durch Hinzufügen des impersonate Attributs den Client á la ASP.NET auch automatisch für die Länge des Aufrufs impersonieren. Bei der automatischen Impersonierung gilt zu beachten, dass der Client NTFS Leserechte auf die gemeinsamen Assemblies des Servers benötigt, z.B. auf das Interface.

<channels>
  <channel ref="tcp" secure="true" port="8080" impersonate="true" />
</channels>

Ebenso wie in ASP.NET wird auch bei Remoting die Client-Identität in Thread.CurrentPrincipal abgelegt, und darüber kann man auch wiederum auf den Client Token zugreifen. Nachfolgender Code auf dem Server gibt die Client-Identität aus, ob gegenwärtig impersoniert wird und in welchem Sicherheits-Kontext der Aufruf durchgeführt wird.

void ISharedInterface.HelloWorld(string input)
{
  string securityContext = WindowsIdentity.GetCurrent().Name;
  string clientIdentity = Thread.CurrentPrincipal.Identity.Name;

  if (WindowsIdentity.GetCurrent(true) != null)
    Console.WriteLine("Impersonierung ist aktiviert");

  Console.WriteLine("Client Identität: " + clientIdentity);
  Console.WriteLine("Sicherheits-Kontext: " + securityContext);
}

 

Web Services

Web Services werden üblicherweise in IIS gehostet. Wie bereits erwähnt, unterstützt IIS verschiedene Authentifizierungs-Protokolle, z.B. die Passwort-basierte Basic Authentication aber auch NTLM und Kerberos.

Um in intranet-basierten Umgebungen integrierte Windows Authentifizierung zu verwenden, wurde der Web Service Proxy um das neue useDefaultCredentials Property erweitert.

Service proxy = new Service();
proxy.UseDefaultCredentials = true;

Natürlich können Sie auch explizite Credentials angeben, z.B.

NetworkCredential cred = 
  new NetworkCredential(“bob", “password", “leastprivilege");
proxy.Credentials = cred;

Aber IIS nutzt diese Protokolle nicht, um die Kommunikation zu schützen. Da HTTP ein verbindungsloses Protokoll ist, muss die Authentifizierung bei jedem Request zum Web Server durchgeführt werden. Dies bedeutet, dass Ihre Credentials bei jedem Zugriff auf dem Kabel sichtbar sind – ganz abgesehen von den eigentlichen Nutzdaten. Sie möchten natürlich nicht, dass diese jemand mit einem Netzwerk-Sniffer stehlen kann (mit anderen Worten: das ist sehr gefährlich und ganz und gar nicht empfohlen!).

Die einzige Lösung dafür ist das Aktivieren von SSL auf dem Server. SSL ist ein zertifikats-basiertes Authentifizierungs-Protokoll, das bereits vor dem IIS Authentifizierungs-Handshake ein Geheimnis aushandelt, das für den Schutz der Verbindung genutzt wird.

Um den SSL Schutz vom Client aus zu nutzen, müssen Sie lediglich den URL des Web Services von „http“ auf „https“ ändern. Eine einfache Änderung, die aber einen enormen Sicherheits-Zuwachs mit sich bringt.

// ssl url
proxy.Url = "https://server/service.asmx";

Ein anderer Haken an IIS-basierter Authentifizierung ist, dass IIS nur mit Windows-basierten Accounts arbeiten kann. Mit anderen Worten: all Ihre Benutzer benötigen ein Windows Konto, sei es in einer Domäne oder auf dem Web Server direkt. Dies mag für Intranet-basierte Services in Ordnung sein, bei öffentlichen bzw. Extranet-Diensten ist die oft nicht erwünscht.

Möchten Sie eine Custom-Authentifizierung gegen Credentials aus einer anderen Quelle, z.B. SQL Server, durchführen, sind Sie auf sich alleine gestellt.

Dieses Problem soll der WS-Security Standard lösen, zusammen mit der Fragestellung, wie ich eine Web Service Kommunikation schützen kann, wenn SSL keine Option ist (z.B. beim Hosting außerhalb von IIS). Dies ist ein sehr grosses und facettenreiches Thema, und wie bereits angedeutet, wird diese ganze Problematik zusammen mit der neuen SSL-Implementierung in .NET 2.0 im nächsten Artikel untersucht.

 

Wo stehen wir?

Sicherheit ist immer ein Problem bei Daten-Übertragung. Sie möchten weder, dass Ihr Client oder Server unauthentifizierte Daten verarbeitet noch, dass die Daten von Dritten gelesen oder verändert werden können. Bei Web-Anwendungen und Diensten kann man dies einfach durch „einschalten“ von SSL bewerkstelligen. Enterprise Services unterstützen diese Features auch out-of-the-box. Bei anderen Protokolle (z.B. einem in Windows Sicherheit integrierten Mail-Client) musste man sich bislang selbst um die Implementierung kümmern. NegotiateStream in .NET 2.0 kapselt alle Details in einer einfach zu nutzenden stream-basierten Klasse. Dadurch, dass Remoting NegotiateStream intern benutzt, bekommt man diese Features auch hier einfach frei Haus und es ist nicht mehr nötig, Remoting Server in IIS zu hosten, um Authentizierung und sichere Übertragung zu ermöglichen.

 

Der Autor

Dominick Baier entwickelt ASP.NET- und Security-Kurse bei DevelopMentor, einer Entwickler-Schulungsfirma. Darüber hinaus unterstützt er Unternehmen beim Schreiben von sicheren verteilten Anwendungen. Als Referent kann man ihn auf diversen Konferenzen antreffen (BASTA, WinDev, DevWeek u.a.); Online findet man ihn auf seinem Blog unter www.leastprivilege.com.

Artikel-Serie „ Integration in Windows Security mit .NET 2.0“ im Überblick: