Sichere Kommunikation im Internet

Veröffentlicht: 26. Apr 2006 | Aktualisiert: 08. Jun 2006

Von Dominick Baier

In der letzten Ausgabe haben wir uns mit Authentifizierung und sicherer Kommunikation in Windows-Netzwerken beschäftigt. Da für diese Techniken eine gemeinsame Vertrauens-Basis in Form eine Domäne (oder zumindest Windows Accounts) notwendig ist, sind diese meist nur in Intranets hilfreich.

Möchten Sie über das Internet sicher kommunizieren, wird meist SSL als Authentifizierungs- und Datenschutz-Protokoll eingesetzt. Ab Version 2.0 von .NET ist SSL Support für Clients und Server integriert. Wie dieser API funktioniert und worauf man achten muss, beleuchtet dieser Artikel.

Auf dieser Seite

 Authentifizierung mit SSL
 Beispiel Client/Server Kommunikation
 Der Server
 Laden des Zertifikats
 Der Client
 Client-Authentifizierung
 Troubleshooting
 Wo stehen wir?
 Download: Code-Beispiele
 Der Autor

Authentifizierung mit SSL

Wie im letzten Artikel, Authentifizierung und sicherer Kommunikation in Windows-Netzwerken, erläutert, ist das Ziel eines Authentifizierungs-Protokolls, Vertrauen in der Identität des Gegenübers herzustellen. Erst nachdem man sich sicher ist, mit wem man spricht, macht es auch Sinn die Kommunikation zu schützen.

Die Besonderheit bei SSL ist jedoch, dass hierfür keine Passwörter, sondern Zertifikate (mit den dazugehörigen privaten Schlüsseln) genutzt werden; außerdem liegt der primäre Augenmerk bei SSL auf der Authentifizierung des Servers. Die Client-Authentifizierung ist hierbei optional (vergleichen Sie hierzu Kerberos – dort ist es genau umgekehrt).

Der Server benötigt ein Zertifikat mit dem sog. „Server Authentication“-Verwendungszweck. Dieses Zertifikat beinhaltet u.a. den Namen des Servers sowie den Aussteller und das Ablaufdatum des Zertifikats. Diese Informationen werden zum Client geschickt und von diesem überprüft (Internet Explorer würde einen Warnungs-Dialog zeigen, wenn einer dieser Werte nicht akzeptabel ist, z.B. wenn der Name im Zertifikat nicht dem DNS-Namen im angefragten URL übereinstimmt).

Danach wird vom Client ein Session Key erzeugt, der mit dem Public Key des Servers verschlüsselt wird. Nur wenn der Server in der Lage ist, diesen Schlüssel mit seinem eigenen Private Key zu entschlüsseln, ist der Vertrauens-Beweis erbracht und die gesamte Kommunikation wird fortan mit diesem Session Key verschlüsselt und signiert.

Möchte sich der Client ebenfalls authentifizieren, benötigt dieser auch ein Zertifikat, dessen Private Key benutzt wird, um den erzeugten Session Key zusätzlich zu signieren.

SSL ist ein erprobtes und weit verbreitetes Protokoll, das bei weitem nicht „nur“ für den Schutz von HTTP-Verkehr verwendet wird, sondern auch zur Absicherung anderer Protokolle wie z.B. SMTP, POP3 oder LDAP eingesetzt wird.

Sehr ähnlich zu der NegotiateStream Klasse aus dem letzten Artikel, kann man in .NET 2.0 die SslStream Klasse zum Schutz beliebiger Netzwerk-Kommunikationen verwenden (um genau zu sein, leiten sich beide Klassen aus einer gemeinsamen Basisklasse mit Namen AuthenticatedStream ab).

 

Beispiel Client/Server Kommunikation

Um die Vorgehensweise zu veranschaulichen, werde ich das gleiche Beispiel wie aus dem letzten Artikel heranziehen - eine einfache Socket-basierte Client/Server-Kommunikation - und Schritt für Schritt den Code anpassen, um SSL zu unterstützen.

Client:

class Client
{
    static void Main(string[] args)
    {
        string machineName = "localhost";

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

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

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

        client.Close();
    }
}

Server:

class Server
{
    static void Main(string[] args)
    {
        TcpListener listener = 
          new TcpListener(IPAddress.Any, 4242);
        listener.Start();

        TcpClient client = listener.AcceptTcpClient();

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

        Console.WriteLine(message);
    }
}

 

Der Server

Der Server benötigt ein Server-Zertifikat. Dies ist auf verschiedene Art und Weise zu bewerkstelligen:

  • Kauf eines kommerziellen Zertifikates bei Firmen wie VeriSign oder TrustCenter. Dies ist empfohlen, wenn Ihr Server öffentlich zugänglich ist, da diesen Zertifikaten bereits von allen Standard Windows-Installationen vertraut wird.

  • Sie haben eine firmen-interne CA (Certification Authority), bei der Sie ein Zertifikat beantragen können. Dies eignet sich für Intranet/Extranet Szenarien.

  • Das .NET Framework beinhaltet ein Hilfsprogramm mit Namen makecert.exe, das es ermöglicht, Test-Zertifikate zu erzeugen.

Das Zertifikat mitsamt privatem Schlüssel (meist in Form einer .pfx Datei) wird danach im Zertifikats-Speicher des Servers abgelegt. Dazu kann das certmgr.msc MMC Snap-In verwendet werden. Wichtig hierbei ist die Unterscheidung zwischen Maschinen- und Benutzer-Zertifikats-Speicher. Während der Maschinen-Speicher von jedem Prozess auf dem Server aus zugänglich ist, ist der Benutzer-Speicher nur für den entsprechenden Benutzer sichtbar. Im Normalfall sollten Sie den Benutzer-Speicher für das Server-Daemon-Konto wählen. Dazu muss allerdings das Benutzer-Profil geladen sein – dies ist der Fall, wenn Ihr Server ein Konsolen/WinForms Programm ist oder als COM+ Server gehostet wird – ASP.NET allerdings lädt das Profil nicht und hier muss der Maschinen-Speicher benutzt werden.

 

Laden des Zertifikats

Die X509Store Klasse aus dem System.Security.Cryptography.X509Certificates Namensraum erlaubt den Zugriff auf den Zertifikats-Speicher. Geben Sie hierzu den Speicher-Typ an, sowie auf welchen Ordner Sie zugreifen möchten (persönliche Zertifikate werden im My Ordner abgelegt, im certmgr.msc GUI wird dieser Ordner Persönliche Zertifikate genannt). Nachdem der Speicher geöffnet ist, kann man auf verschiedene Wege auf die Zertifikate zugreifen. Üblicherweise benutzt man die Find Methode, um das richtige Zertifikat zu finden. In diesem Beispiel wird der Subject-Name verwendet, in richtigen Anwendungen sollten Sie allerdings einen eindeutigeren Wert wählen wie z.B. den Subject Key Identifier.

private static X509Certificate2 getServerCert()
{
  X509Store store = new X509Store(
    StoreName.My, StoreLocation.CurrentUser);

  store.Open(OpenFlags.ReadOnly);

  X509Certificate2Collection cert = store.Certificates.Find(
    X509FindType.FindBySubjectName, 
    "ServerCertificate", 
    true);

  store.Close();

  return cert[0];
}

Nachdem man das richtige Zertifikat ausgewählt hat, muss der original NetworkStream lediglich mit einem SslStream „gewrapped“ werden und die Methode AuthenticateAsServer aufgerufen werden. Dies startet den SSL Handshake auf der Server-Seite.

public virtual void AuthenticateAsServer (
X509Certificate serverCertificate,
bool clientCertificateRequired,
SslProtocols enabledSslProtocols,
bool checkCertificateRevocation)

serverCertificate ist das Server Zertifikat und clientCertificateRequired bestimmt, ob der Client sich ebenfalls über ein Zertifikat authentifizieren muss (dazu später mehr) - checkCertificateRevocation gibt an, ob das Client-Zertifikat gegen eine Rückruf-Liste geprüft werden soll. enabledSslProtocol spezifiziert die erlaubte SSL-Version, hier sollte die neueste (und sicherste) gewählt werden – dies entspricht SslProtocols.Tls.

Der SSL-Server sieht nun folgendermaßen aus:

class Server
{
    static void Main(string[] args)
    {
        X509Certificate2 cert = getServerCert();

        TcpListener listener = 
          new TcpListener(IPAddress.Any, 4242);
        listener.Start();

        TcpClient client = listener.AcceptTcpClient();

        // wrappen des normalen streams mit einem SSL stream
        SslStream ssl = new SslStream(
            client.GetStream(),
            false);

        // SSL handshake starten
        ssl.AuthenticateAsServer(
          cert, false, SslProtocols.Tls, false);

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

        Console.WriteLine(message);
    }

    private static X509Certificate getServerCert()
    {
        X509Store store = new X509Store(
          StoreName.My, 
          StoreLocation.CurrentUser);

        store.Open(OpenFlags.ReadOnly);
        X509CertificateCollection cert = 
          store.Certificates.Find(
            X509FindType.FindBySubjectName, 
            "SslStreamCert", true);

        store.Close();

        return cert[0];
    }
}

 

Der Client

Die Vorgehensweise auf dem Client ist sehr ähnlich. Der NetworkStream wird mit einem SslStream gewrapped. Dabei kann optional eine Callback Methode angegeben werden, die das Server Zertifikat während des Handshakes prüft. Danach wird AuthenticateAsClient aufgerufen, und der erwartete Name des Zertifikats übergeben (vergleiche mit Internet Explorer, der erwartet, dass der Zertifikats-Name dem DNS Namen der Web-Seite entspricht).

Die Zertifikat-Prüfroutine kann das Server-Zertifikat untersuchen und entscheiden, ob das Zertifikat vom Client akzeptiert wird. Gründe für eine fehlerhafte Validierung könnten z.B. sein, dass der Zertifikat-Name nicht dem erwarteten Namen entspricht bzw. dass das Zertifikat abgelaufen oder nicht von einer vertrauten CA stammt. Diese Methode erlaubt es auch Zertifikate zu akzeptieren, selbst wenn sie streng genommen nicht gültig sind (für Testzwecke). Der vollständige Client sieht folgendermaßen aus:

class Client
{
    static void Main(string[] args)
    {
        string machineName = "localhost";
        
        // erwartete subject name des server zertifikats
        string serverSN = "SslStreamCert";

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

        // wrappen des streams  
        // spezifizieren der zertifikats-prüfroutine
        SslStream ssl = new SslStream(
            client.GetStream(), 
            false, 
            new RemoteCertificateValidationCallback
              (CertificateValidationCallback));

        // SSL handshake starten 
        ssl.AuthenticateAsClient(serverSN);

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

        // SSL geschützte kommunikation
        writer.WriteLine("Hallo SSL-Server");
        writer.Flush();

        client.Close();
    }

    static bool CertificateValidationCallback(
      object sender, 
      X509Certificate certificate, 
      X509Chain chain, 
      SslPolicyErrors sslPolicyErrors)
    {

        // das zertifikat akzeptieren 
        // wenn keine fehler aufgetreten sind
        if (sslPolicyErrors != SslPolicyErrors.None)
        {
            Console.WriteLine(
              "SSL Certificate Validation Error!");
            Console.WriteLine(sslPolicyErrors.ToString());
            return false;
        }
        else
            return true;
    }
}

 

Client-Authentifizierung

Bislang hat sich lediglich der Server am Client authentifiziert. Wenn der Client auch seine Identität gegenüber dem Server beweisen soll, benötigt dieser auch ein Zertifikat. Dies kann auf zwei verschiedene Weisen an den Server übermittelt werden. Der übliche Weg ist wohl, einfach das Zertifikat an AuthenticateAsClient zu übergeben. Dazu muss das Zertifikat in eine X509Certificate2Collection verpackt werden, der folgende Code bewerkstelligt das.

// client zertifikat
X509Certificate2Collection clientCerts = 
  new X509Certificate2Collection(getClientCert());

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

// wrappen des streams - spezifizieren der prüfroutine
SslStream ssl = new SslStream(
    client.GetStream(),
    false,
    CertificateValidationCallback);

// SSL handshake starten 
ssl.AuthenticateAsClient(
    serverSN,                   // server subject name
    clientCerts,                // client zertifikat
    SslProtocols.Tls,           // SSL version
    true);                      // rückruf-liste prüfen

In Fällen, in denen das benötigte Client Zertifikat dynamisch abhängig von der Server Identität gewählt werden muss, kann man auch eine Callback-Methode festlegen. An diese Methode werden alle relevanten Server-Informationen, wie die Identität und die akzeptierten CAs übergeben. Aufgrund dieser Informationen kann man dann das passende Zertifikat an den Server zurückliefern. Dazu muss beim Erzeugen des SslStream Objekts die Callback-Methode angegeben werden:

// wrappen des streams - spezifizieren der zertifikats-prüfroutine
SslStream ssl = new SslStream(
    client.GetStream(),
    false,
    CertificateValidationCallback,
    CertificateSelectionCallback);

Die Callback-Methode selbst muss vom Delegate-Typ LocalCertificateSelectionCallback sein.

static X509Certificate CertificateSelectionCallback(
  object sender, 
  string targetHost, 
  X509CertificateCollection localCertificates, 
  X509Certificate remoteCertificate, string[] acceptableIssuers)
{
  Console.WriteLine("Client-Zertifikat für: {0}", targetHost);
  Console.WriteLine("Akzeptierte Aussteller: \n{0}", 
    string.Join("\n\n", acceptableIssuers));

  return getClientCert();
}

Auf der Server-Seite kann man nun angeben, ob Client-Zertifikate erzwungen werden sollen. Dazu muss der entsprechende Parameter im AuthenticateAsServer gesetzt werden:

// SSL handshake starten
ssl.AuthenticateAsServer(
  serverCert,             // server zertifikat
  true,                   // client authentifizierung erzwingen
  SslProtocols.Tls,       // gewünschte SSL version
  true);                  // rückruf-liste prüfen

 

Troubleshooting

Nach erfolgreicher Authentifizierung kann man eine ganze Reihe von wichtigen Informationen über die Verbindung über die SslStream Klasse erfragen. Die folgende Methode gibt alle wichtigen Informationen auf der Konsole aus:

private static void showSslInfo(
  string serverName, SslStream sslStream, bool verbose)
{
    Console.WriteLine("\n\nSSL Report für         : {0}\n", 
      serverName);
    Console.WriteLine("Is Authenticated:            {0}", 
      sslStream.IsAuthenticated);
    Console.WriteLine("Is Encrypted:                {0}", 
      sslStream.IsEncrypted);
    Console.WriteLine("Is Signed:                   {0}", 
      sslStream.IsSigned);
    Console.WriteLine("Is Mutually Authenticated:   {0}\n", 
      sslStream.IsMutuallyAuthenticated);

    Console.WriteLine("Hash Algorithm:              {0}", 
      sslStream.HashAlgorithm);
    Console.WriteLine("Hash Strength:               {0}", 
      sslStream.HashStrength);
    Console.WriteLine("Cipher Algorithm:            {0}", 
      sslStream.CipherAlgorithm);
    Console.WriteLine("Cipher Strength:             {0}\n", 
      sslStream.CipherStrength);

    Console.WriteLine("Key Exchange Algorithm:      {0}", 
      sslStream.KeyExchangeAlgorithm);
    Console.WriteLine("Key Exchange Strength:       {0}\n", 
      sslStream.KeyExchangeStrength);
    Console.WriteLine("SSL Protocol:                {0}", 
      sslStream.SslProtocol);

    showCertificateInfo(sslStream.RemoteCertificate, verbose);
}

private static void showCertificateInfo(
  X509Certificate remoteCertificate, bool verbose)
{
    Console.WriteLine("Certficate Information for:\n{0}\n",  
      remoteCertificate.Subject);
    Console.WriteLine("Valid From:      \n{0}", 
      remoteCertificate.GetEffectiveDateString());
    Console.WriteLine("Valid To:        \n{0}", 
      remoteCertificate.GetExpirationDateString());
    Console.WriteLine("Certificate Format:     \n{0}\n",   
      remoteCertificate.GetFormat());

    Console.WriteLine("Issuer Name:     \n{0}", 
      remoteCertificate.Issuer);

    Console.WriteLine("Serial Number:   \n{0}", 
      remoteCertificate.GetSerialNumberString());
    Console.WriteLine("Hash:            \n{0}", 
      remoteCertificate.GetCertHashString());
    Console.WriteLine("Key Algorithm:   \n{0}", 
      remoteCertificate.GetKeyAlgorithm());
    Console.WriteLine("Key Algorithm Parameters:     \n{0}", 
      remoteCertificate.GetKeyAlgorithmParametersString());
    Console.WriteLine("Public Key:     \n{0}", 
      remoteCertificate.GetPublicKeyString());
  }
}

Wenn allerdings bereits vor der Authentifizierung Probleme auftreten, kann man zusätzlich auch Tracing für den System.Net Namensraum aktivieren. Dadurch bekommt man mehr Informationen, warum der Handshake fehlerhaft war. Fügen Sie dazu folgendes XML-Fragment in die App.Config-Datei des Clients oder Server hinzu.

<system.diagnostics>
  <trace autoflush="true" />
  <sources>
    <source name="System.Net">
      <listeners>
        <add name="TraceFile" />
      </listeners>
    </source>
  </sources>

  <sharedListeners>
    <add 
      name="TraceFile" 
      type="System.Diagnostics.TextWriterTraceListener" 
      initializeData="NetTrace.log" />
  </sharedListeners>

  <switches>
    <add name="System.Net" value="Verbose" />
  </switches>
</system.diagnostics>

Danach finden Sie eine Log-Datei mit Namen NetTrace.log im Anwendungs-Verzeichnis.

 

Wo stehen wir?

Sie kennen nun alle neuen Kommunikations-Klassen mit Security-Support in .NET 2.0. Welche der Klassen die Richtige ist, hängt von Ihrem Szenario ab. Intranet Kommunikation, die bei der Authentifizierung Windows Accounts benutzt, kann entweder auf Socket-Ebene mit NegotiateStream und auf Anwendungs-Ebene mit Secure Remoting gesichert werden. Hingegen werden bei der Internet-Kommunikation üblicherweise keine Windows Accounts benutzt werden, sondern Zertifikate. SslStream erlaubt es, eine beliebige Kommunikation mit SSL zu schützen, während die HttpWebRequest Klasse dieses für den Spezialfall HTTPS-Kommunikation implementiert.

 

Download: Code-Beispiele

Code-Beispiele zu den im Artikel erwähnten Szenarien: Download SslStream.zip

 

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.