OpenSSL: Implementierung innerhalb eines Client- und Server-Programms, Teil 1

In C/C++-Programmen lässt sich mit OpenSSL verschlüsselte, sichere Netzkommunikation einfach realisieren. heise Developer stellt in einem Zweiteiler die Implementierung eines Client- und Server-Programms vor. Den Anfang macht die Serverprogrammierung.

In Pocket speichern vorlesen Druckansicht
Lesezeit: 17 Min.
Von
  • Oliver Müller
Inhaltsverzeichnis

OpenSSL ist seit über zehn Jahren eine verlässliche und plattformunabhängige SSL/TLS-Implementierung. In eigenen C/C++-Programmen lässt sich mit der Bibliothek verschlüsselte, sichere Netzkommunikation einfach realisieren. Abseits von grauer Theorie stellt heise Developer die Implementierung eines Client- und Server-Programms vor – genannt "WOPR". Den Anfang macht die Serverprogrammierung.

WOPR ist eine Hommage an den gleichnamigen Server aus dem Film "War Games". Das Beispiel stellt den mehr oder weniger logischen Dialog zwischen Terminal und Server aus dem Film nach. Die Funktionen sind an der Stelle nicht wichtig. Vielmehr lässt sich durch das Gespann aus lokalem Client und entferntem Server ein Muster zum Entwickeln von SSL-Server- und -Client-Systemen ableiten. Die Terminal-Kommunikation mit ihren entgegengesetzten Datenströmen zeigt anschaulich die Realisierung einer bidirektionalen Kommunikation und die hieraus entstehenden Herausforderungen für die SSL-Programmierung.

Dieser erste Teil geht primär auf die Programmierung des Servers ein. Ebenfalls finden die für Server und Client gleichermaßen gültigen Grundlagen Betrachtung. Im zweiten Teil folgen die Programmierung des Clients sowie ein Ausblick auf die Portabilität von OpenSSL-Programmen und die IPv6-Unterstützung.

WOPR ist für Unix-kompatible und -ähnliche Betriebssysteme ausgelegt. Das WOPR-Beispiel ist unverändert lauffähig auf Unix-Derivaten wie BSD, AIX und Solaris, auf unixoiden Systemen wie Linux und QNX sowie Unix-Layern wie die z/OS Unix System Services (USS) auf dem IBM-Mainframe und Cygwin unter Microsofts Windows. Es setzt insbesondere das Vorhandensein der Unix-Funktionen select() und fork() voraus. Die sind aber keine Bedingungen für OpenSSL, sondern lediglich der systemtechnische Rahmen, um das Beispiel überschaubar, aber zugleich auf einer breiten Palette von Plattformen ablauffähig und nachvollziehbar zu gestalten. Durch den Unix-Ansatz reicht die unterstützte Palette immerhin vom Windows-PC über die Systeme der Unix-/Linux-Welt bis hin zum IBM-Großrechner. Am Ende des zweiten Beitrags geben darüber hinaus weitere Hinweise Hilfestellung zur Portierung auf andere Plattformen.

Mehr Infos

Beispielcode

Den für für die Umsetzung in der Praxis benötigten Beispielcode findet man hier (mueller_openssl_wopr.tar).

Zum Kompilieren und Linken setzt WOPR einen modernen C++-Compiler, wie GNUs g++ ab Version 3, sowie die Installation der OpenSSL-Entwicklungspakete voraus. Das Makefile von WOPR ist ausschließlich für GNU make ausgelegt. Es kann den Build-Prozess anderer Make-Tools wie BSD make oder Microsofts nmake nicht steuern.

Beide Programme sind kommandozeilenorientiert. Sie erhalten ihre Parameter für Verbindung, Zertifikate und Schlüssel über Argumente der Kommandozeile. Gibt man gar keine, nicht genügend oder zu viele Argumente an, liefern beide Programme eine kurze "Usage" als Hilfestellung.

Der WOPR-Server besteht aus den Dateien main.cpp, wopr.cpp und woprexcept.cpp sowie den zugehörigen Header-Files wopr.h und woprexcept.h. main.cpp enthält das Hauptprogramm, das die übergebenen Kommandozeilenargumente prüft und den WOPR-Server instanziiert. woprexcept.h deklariert einfache Exception-Klassen für den WOPR-Server, die in woprexcept.cpp implementiert sind.

Der eigentliche Server ist in der Klasse WOPR_server gekapselt (siehe Dateien wopr.cpp und wopr.h). Sie implementiert einen Server, der – wie allgemein üblich – mehrere konkurrierende Verbindungen parallel bedienen kann. Hierzu setzt das System auf den alten Fork-Ansatz. Das heißt: Wann immer eine neue Client-Verbindung beim Server eingeht, erzeugt er mit der Unix-Funktion fork() eine Prozesskopie (Kindprozess) von sich, die dediziert diesen einen Client bedient. Endet die Verbindung mit dem Client, beendet sich der korrespondierende Server-Kindprozess.

Grundsätzlich wäre es ohne Weiteres realisierbar, den WOPR-Server "multithreaded" zu implementieren, denn OpenSSL unterstützt Multithread-Programme. Das würde allerdings – durch die Nebenläufigkeiten innerhalb des dann einzigen Prozesses – ein wenig mehr Programmieraufwand bedeuten. Da die bezweckte Einführung in OpenSSL dadurch Gefahr liefe, aus dem Fokus zu geraten, seien an der Stelle keine modernen Threads, sondern altmodische (Unix-)Prozesse verwendet.

Der erste Schritt, OpenSSL verwenden zu können, ist die Initialisierung der Bibliothek mit SSL_library_init(). Die Funktion registriert hauptsächlich die verfügbaren Kryptoalgorithmen und Digests. Erst danach ist das Verwenden von SSL-Funktionen überhaupt möglich. Der Aufruf befindet sich innerhalb des WOPR-Servers im Konstruktor der Klasse WOPR_server. Das boolesche Klassenattribut ssl_initialized steuert hierbei instanzübergreifend, ob die Initialisierung stattgefunden hat.

Der Aufruf von SSL_library_init() muss für ein Programm nur ein einziges Mal erfolgen. Es wäre schon aus Gründen der Performanz nicht ratsam, die Funktion mehrmals im Programm aufzurufen. Daher nutzt die Klasse WOPR_server das steuernde klasseneigene Flag.

Zugegeben, dessen Bereitstellung ist derzeit nicht notwendig, da die Implementierung von WOPR nur einen einzigen Server verwendet. Startet man jedoch mehrere Serverinstanzen in einem Programm – beispielsweise um unterschiedliche Netzwerke unterschiedlich zu bedienen oder auf mehreren Ports mehrere SSL-gesicherte Dienste anzubieten –, ist das Vorgehen über das Flag essenziell. Es empfiehlt sich, in diesem Fall das Klassenattribut in einer (abstrakten) Basisklasse zu definieren und alle weiteren Serverklassen von ihr abzuleiten. Der Aufruf von SSL_library_init() kann dann in Abhängigkeit des Flag-Werts zentral im Konstruktor der Basisklasse erfolgen.

Nach dem Aufruf von SSL_library_init() folgt obligatorisch das Laden der Fehlermeldungen. Auf diese Weise ist es im Programmverlauf möglich, statt kryptischer Fehlernummern von OpenSSL aussagekräftige Meldungen zu erhalten.

Der nächste Schritt ist das Erzeugen eines SSL-Kontext mit SSL_CTX_new(). Das Objekt dient dazu, interne SSL-Strukturen zu erzeugen und die schlüsselrelevanten Informationen zentral zu halten. Außerdem lassen sich damit Daten zwischen mehreren SSL-Verbindungen teilen. Neue Verbindungen generiert man später mit dem Context-Objekt, ohne zeitraubend Schlüssel und Zertifikate erneut zu laden und zentrale verbindungsunabhängige interne Strukturen nochmals zu initialisieren.

SSL_CTX_new() erwartet einen Zeiger vom Typ SSL_METHOD. Über den Parameter lässt sich einerseits festlegen, welchen SSL/TLS-Standard man unterstützt, andererseits, ob das Programm als Server oder Client fungieren soll. Derzeit liefern je vier Funktionen für Client und Server die SSL_METHOD-Daten. Die Namen der Funktionen haben den Aufbau "<Protokoll>_<Typ>_method()". "<Protokoll>" ist entweder SSLv2, SSLv3, TLSv1 oder SSLv23. Die ersten drei legen das jeweilige namensgleiche Protokoll verbindlich fest. SSLv23 erlaubt hingegen Verbindungen vom Typ SSLv2, SSLv3 und TLSv1; davon abhängig, was die Gegenseite akzeptiert. "<Typ>" steht entweder für Client oder für Server – je nachdem ob das Programm Verbindungen über diesen Kontext aufbauen (Client) oder auf eingehende Verbindungen warten will (Server).

Alle Schritte sind sowohl bei einem Server, als auch bei einem Client identisch auszuführen. Nach dem Aufruf von SSL_CTX_new() trennen sich nun aber die Wege von Server und Client.