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

Seite 2: Feintuning

Inhaltsverzeichnis

Bislang ist definiert, wie das BIO eine Verbindung mit SSL/TLS in der Protokollversion mit Prüfung über die festgelegten CA-Zertifikate aufbauen soll. Was vor dem eigentlichen Connect noch fehlt, ist das Festlegen des Wohin, also Server-Name, IP-Adresse und entsprechender Port.

Wie die Manpage BIO_s_connect(3) zeigt, gibt es mehrere Wege dafür, entweder Server-Name, IP-Adresse oder Port, gemeinsam oder getrennt, als String oder in binärer Form zu setzen. Der einfachste Weg für Strings, wie sie in Konfigurationsdateien und auf der Kommandozeile des Terminal-Programms vorkommen, ist die Funktion BIO_set_conn_hostname(). Sie erwartet neben dem BIO für das Setzen des "Wohins" einen String mit dem Hostnamen.

Der "Hostname" ist ebenso flexibel und leistungsfähig wie der der Server-Funktion BIO_new_accept() für das Accept-BIO. Lediglich Wildcard-Netzwerkadressen, wie 0.0.0.0, :: und *, sind beim Client freilich nicht zu realisieren. Der Client baut schließlich eine Verbindung zu einem konkreten Server auf. Durch Doppelpunkt abgesetzte Ports sind aber bei BIO_set_conn_hostname() ebenso möglich.

Nun folgt mit dem Aufruf von BIO_do_connect() der entscheidende Moment. Das Terminal versucht, sich mit dem Server zu verbinden. War das erfolgreich, kann man Daten verschlüsselt über das SSL-Objekt zum Server senden oder von ihm empfangen. Allerdings weiß das Programm bislang nicht, ob der Server tatsächlich der ist, für den er sich ausgibt. Grundsätzlich wurde das jedoch im Zuge der zweiten Phase des SSL-Handshakes geprüft. Nur das aufrufende Programm kennt das Ergebnis noch nicht.

Über die Funktion SSL_get_verify_result() lässt sich mit dem SSL-Objekt das Prüfungsergebnis abrufen. Bei "X509_V_OK" ist das Zertifikat gültig, bei einem anderen Rückgabewert ist es ungültig. Das WOPR-Terminal beschränkt sich lediglich darauf, einen Hinweis zu geben, ob das Zertifikat gültig war oder nicht. Weitere Maßnahmen ergreift es nicht.

Des Weiteren wäre es an der Stelle möglich (und im Echtbetrieb sinnvoll), über die Funktion SSL_get_peer_certificate() das Zertifikat des Servers auszulesen und näher zu untersuchen. Es ließe sich über X509_get_subject_name() etwa die Subject-DN des Zertifikats mit dem Hostnamen beziehungsweise den aufgelösten Namen der IP-Adresse vergleichen. Auf die Prüfungen, die unter Umständen Namensauflösungen nach sich ziehen, verzichtet das Beispiel im Zuge eines überschaubareren Programmcodes. Damit ist das Terminal für das Übertragen der Daten via Netzwerk vorbereitet.

Erwähnt seien noch ein paar Worte zum Initialisieren des PRNG. In der ersten Phase des SSL-Handshake sendet der Client eine 32-Bit-Zufallszahl an den Server, die später verwendet wird, um das "pre master secret" zu erstellen. Für das Erzeugen der Zufallszahl verwendet OpenSSL den PRNG. Er ist, bevor er seine Arbeit aufnehmen kann, mit einer Saat (= Seed) aus zufälligen Bytes zu befüllen. Das erfolgt bei OpenSSL implizit, sofern das Betriebssystem die notwendigen Random-Devices (etwa /dev/random oder /dev/urandom) bereitstellt. Auf manchen Systemplattformen sind sie jedoch optional oder gar nicht vorhanden. Beispielsweise war bei Solaris noch in Version 8 ein separater Patch einzuspielen, um ein solches Gerät bereitzustellen. Auf z/OS ist es heute noch notwendig, die "Integrated Cryptographic Service Facility" (ICSF) explizit zu aktivieren.

Ließ sich der PRNG-Acker mangels Unterstützung seitens des Systems nicht automatisch "säen", erntet man beim Connect die Fehlermeldung "PRNG not seeded". Es hilft, den PRNG händisch mit der notwendigen Saat auszustatten. Über die Funktion RAND_load_file() lassen sich Bytes aus einer Datei als Seed laden. Der erste Parameter ist die zu lesende Datei, der zweite die Anzahl Bytes.

Auf der Kommandozeile des Terminals kann man eine solche Seed-Datei angeben. Ist sie übergeben, liest das Terminal-Programm mit RAND_load_file() ein MByte an Seed-Daten für den PRNG ein. Eine solche Seed-Datei ist mit Vorsicht zu wählen. Sie muss vor unbefugtem Zugriff gesichert sein. Einem PRNG-Device wie /dev/random sollte man aus Sicherheitserwägungen den Vorzug einräumen.

Nach dem Abschließen der Vorarbeiten und dem Zustandekommen einer verifizierten Verbindung tritt der Client in eine Schleife ein. In ihr prüft das Programm mit select(), an welchem Handle (stdin oder Socket des BIO) Daten zum Lesen oder Schreiben anliegen. Je nachdem wo Daten vorzufinden sind, leitet der Client die Lese- oder Schreiboperationen ein.

Wie im WOPR-Server aus dem ersten Teil geht der Client den Weg über select(), um effizient den Multiplexbetrieb über das Socket zu realisieren. Nachdem für das Realisieren des Multiplexbetriebs für die Netzwerkverbindung ohnehin select() zum Einsatz kommt, bietet es sich für die Tastatureingaben ebenfalls an. Unix führt seinen universellen "alles ist eine Datei"-Ansatz auch und gerade in den Systemfunktionen fort. Ob ein Socket oder eine Datei – alles wird über ein Handle angesprochen. Damit lassen sich anstehende Tastatureingaben – genauer Daten über den Standardeingabekanal – ebenfalls über select() abfragen. Das Hinzufügen des Handles von stdin zu rfds genügt.

Damit ergeben sich drei parallel zu bedienende Ein-/Ausgabekanäle:

  1. Eingaben über das BIO,
  2. Ausgaben über das BIO und
  3. Eingaben über stdin.

Exakt in der Reihenfolge arbeitet das Programm die bereiten Kanäle ab und es räumt Serverantworten eine höhere Priorität ein, vor dem Senden neuer über Tastatur eingegebener Kommandos. Damit unterstützt es den zeitlichen Ablauf des Dialogs zwischen Client und Server.

Gibt FD_ISSET() zurück, dass das Socket-Handle zum Lesen bereit ist, liest SSL_read() die anstehenden Daten in den Lesepuffer rbuf ein. Dabei werden maximal BUFSIZE Bytes, die die Größe des Puffers repräsentieren, gelesen. Weist SSL_get_error() aus, dass kein Fehler (SSL_ERROR_NONE) aufgetreten ist, gibt das Programm die gelesenen Daten auf dem Standardausgabekanal aus.

Sollte das Programm unter z/OS UNIX System Services laufen, erfolgt zuvor das Konvertieren von ASCII zu EBCDIC mit __atoe(). Damit die WOPR-Varianten auf den unterschiedlichen Plattformen zusammenarbeiten können, ist der Schritt notwendig. Die Netzwerkkommunikation bei WOPR erfolgt per Definition in ASCII.

Sollte SSL_get_error() einen Fehler liefern, gehen Fehlermeldungen auf cerr (stderr) heraus. Eine Sonderstellung nimmt der Fehler SSL_ERROR_ZERO_RETURN ein, der lediglich angibt, dass 0 Bytes geliefert wurden. In dem Fall wurde schlicht die Verbindung getrennt.

Zugegeben, die alten goto-Anweisungen könnten an sich von schlechtem Stil deuten. Mit ihnen ist es jedoch möglich, platzsparend und effizient zu arbeiten. Daher kommen sie hier wohlüberlegt zum Einsatz. Da nur maximal BUFSIZE Bytes gelesen wurden, könnten noch Daten zum Lesen anstehen. Daher erfolgt das Lesen in einer Schleife. Ob noch Daten anstehen, liefert die Funktion SSL_pending().

Das Ausgeben von Daten über das BIO und das Eingeben über die Tastatur stehen in engem Zusammenhang. Schließlich sind die Eingaben Befehle, die über das BIO an den Server gehen sollen. Um das Lesen und Schreiben der Befehle zu entkoppeln, gehen die Eingaben in einen Schreibpuffer wbuf, der beim Ausgeben der Daten ausgelesen wird.

Ist das Socket-Handle zum Schreiben bereit und liegen Daten in wbuf, gehen sie über die Funktion SSL_write() ins Netzwerk. Der gesamte Puffer wandert in einem Aufruf ins Netz. War er erfolgreich, positioniert man den Schreibzeiger wbuf_offset.

Warum ein Schreibzeiger, mag sich der eine oder andere fragen. Es kann vorkommen, dass SSL_write() nicht alle Daten in einem Aufruf versenden konnte. Die Funktion liefert deshalb die Anzahl der gesendeten Bytes zurück. Ließen sich nicht alle Daten schreiben, geht wbuf_offset auf das erste noch nicht gesendete Byte. Beim nächsten Aufruf setzt das Senden an dem Byte wieder an. Konnte man hingegen alle Bytes senden, geht wbuf_offset auf das erste Byte von wbuf zurück, das man auf ein Nullbyte setzt.

Liegen Dateneingaben für stdin an, gehen sie über read() in einen temporären Puffer. Die gelesenen Daten hängt strcat() an die Daten in wbuf an. Auf z/OS erfolgt zuvor noch die Konvertierung von EBCDIC nach ASCII mit __atoe().

Das Beispiel ist recht einfach gehalten, um die Verständlichkeit zu fördern. Der Client ist in der Form durch das ungeprüfte Anhängen von Daten an wbuf anfällig für einen Buffer-Overflow. Für ein produktives System wäre das zu vermeiden.