Insbesondere in Zeiten, in denen Überwachung und Spionage allgegenwärtig scheint, ist es wichtig zu verstehen wie Kryptographie eigentlich funktioniert. Die Software OpenSSL ist dabei ein wichtiger Baustein, der auch in vielen anderen, großen Projekten fest integriert ist. Das Paket beinhaltet neben der Library auch einige Kommandozeilenprogramme und kann nicht nur verschlüsseln, sondern auch signieren, hashen und Schlüssel generieren. OpenSSL steht unter einen Apache-ähnlichen Lizenz – ist also Open Source, aber dennoch für kommerziellen Vertrieb offen.

Ich setze voraus, dass der Leser die Grundlagen der Kryptographie verstanden hat und Begriffe wie Verschlüsselung, Digitale Signatur und die Unterschiede zwischen symmetrischen (ein getauschter Schlüssel) und asymmetrischen Kryptosystemen (Public/Private Key) bereits geläufig sind. Was sich in etwa hinter RSA, AES und CBC verbirgt, sollte ebenfalls nicht fremd sein.

Grundlagen

Häufig werden heute hybride Verschlüsselungsmethoden verwendet. Dabei handelt es sich um eine Kombination asymmetrischer und symmetrischer Verschlüsselung. Im Vorteil ist das Verfahren, da man den Geschwindigkeitsvorteil der symmetrischen Verschlüsselung hat und trotzdem keinen Geheimschlüssel austauschen muss. Grundsätzlich funktioniert ein hybrides Verfahren in etwa so:

  1. Einen zufälligen Schlüssel erzeugen, mit dem später die Nachricht symmetrisch verschlüsselt wird – dieser wird auch „Session Key“ genannt.
  2. Den öffentlichen Schlüssel des Kommunikationspartners besorgen.
  3. Mit dem öffentlichen Schlüssel des Partners wird nur der Session Key in einem asymmetrischen Verfahren (hier RSA) verschlüsselt.
  4. Mit dem Session Key wird nun die Nachricht selbst symmetrisch verschlüsselt (hier AES).
  5. Häufig wird auch noch ein Verfahren zur Integritätssicherung verwendet, z.B. CBC.

Die verschlüsselte Datei müsste dann folgende Informationen enthalten:

  1. Länge des Session Keys
  2. Session Key selbst, mit RSA verschlüsselt
  3. Initialisierungsvektor (initialization vector)
  4. Nachricht, mit AES verschlüsselt

Schließlich entschlüsselt der Empfänger zunächst den Key mit seinem Private Key und danach die Nachricht mit dem enthaltenen Session Key.

Wie sicher das Verfahren ist, hängt dabei von verschiedenen Komponenten ab: Besonders wichtig ist, dass der RSA-Key lang genug ist. Kurze private Keys können mit ausreichend Rechenkapazität berechnet werden. Das ist in diesem Fall natürlich ungünstig, da dann der Session Key und somit die Nachricht entschlüsselt werden kann. Unter 1024 Bit sollten dabei niemals gewählt werden – besser sind 2048 oder sogar 4096 Bit. Darüber hinaus sollte man ein als sicher geltendes Verfahren wählen. Eine Übersicht der in OpenSSL verfügbaren Methoden lässt sich mit dem Befehl openssl ciphers -v betrachten. Dem Befehl können zusätzlich Filterbedingungen übergeben werden:

openssl ciphers -v 'RSA+AES'

Zeigt beispielsweise nur Verfahren an, die RSA zum Schlüsseltausch und AES zur Nachrichtenverschlüsselung verwenden. Das Plus ist dabei ein logisches UND, ein Doppelpunkt kann als ODER benutzt werden und das Ausrufezeichen als NICHT. Innerhalb der angezeigten Tabelle steht Kx für Key Exchange, Au für das Verfahren zur Prüfung der Authentizität des Senders und Mac für das Verfahren zur Integritätsprüfung (Message Authentication Code, Prüfsumme).

In diesem Tutorial wird RSA-AES256-CBC verwendet: Schlüsseltausch über RSA, Verschlüsselung mit AES256 und Integritätsprüfung durch CBC. Nebenbei erwähnt, wer einfach nur verschlüsseln will und sich nicht für Programmieren interessiert, sollte hier aufhören zu lesen und sich über die bereits vorhandenen OpenSSL Kommandozeilentools informieren, denn die können alles schon, was hier beispielhaft erklärt wird. Dieser Artikel soll interessierten Programmierern einen Überblick darüber geben, wie mit OpenSSL Dateien/Streams/Buffer verschlüsselt werden können.

Vorbereitung

Voraussetzung ist, dass die entsprechenden Libraries installiert sind und beim Kompilieren eingebunden werden. Installation erfolgt unter Ubuntu/Debian über:

sudo apt-get install openssl libssl-dev

Dem Compiler müssen dann zusätzlich die Libraries -lssl -lcrypto übergeben werden:

gcc -g -O3 -Wall -o mycrypt main.c -lssl -lcrypto

Ein angepasstes Makefile ist im Beispiel-Tarball enthalten. Das kann hier heruntergeladen werden und enthält den gesamten Quelltext inklusive eines Beispielschlüssels.

Code

Für das Beispiel werden die folgenden Header benötigt:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include <openssl/err.h>

#include <arpa/inet.h> /* für htonl() */

Es folgt zunächst die Funktion zum Verschlüsseln. Sie erhält einen Dateideskriptor auf die geöffnete Public-Key-Datei, einen auf den Eingabestrom und einen auf den Ausgabestrom. Alle sind im binären Modus geöffnet.

int verschluesseln(FILE *rsa_pubkey_file, FILE *in, FILE *out)
{
    int retval = EXIT_FAILURE;
    /* EVP-Kontext und Schlüssel */
    EVP_CIPHER_CTX ctx;
    RSA *rsa_pubkey = NULL;
    EVP_PKEY *pubkey = EVP_PKEY_new();
    /* Ein- und Ausgabebuffer */
    unsigned char buffer[1024];
    unsigned char buffer_out[1024 + EVP_MAX_IV_LENGTH];
    /* Größe der aus der Eingabedatei gelesenen Daten */
    size_t len;
    /* Größe der verschlüsselten Daten */
    int len_out;
    /* Session Key: Länge und Pointer */
    int sesskeylen;
    uint32_t sesskeylen_nl;
    unsigned char *sesskey = NULL;
    /* Session Key */
    unsigned char iv[EVP_MAX_IV_LENGTH];

    /* Lese Public Key des Empfängers aus Datei */
    if(!PEM_read_RSA_PUBKEY(rsa_pubkey_file, &rsa_pubkey, NULL, NULL))
    {
        fprintf(stderr, "Fehler beim Laden der Datei für den Public Key.\n");
        ERR_print_errors_fp(stderr);
        return EXIT_FAILURE;
    }

    /* Den öffentliche Key an EVP übergeben */
    if(!EVP_PKEY_assign_RSA(pubkey, rsa_pubkey))
    {
        fprintf(stderr, "EVP_PKEY_assign_RSA schlug fehl.\n");
        return EXIT_FAILURE;
    }

    /* EVP Chiffre-Kontext initialisieren */
    EVP_CIPHER_CTX_init(&ctx);
    sesskey = malloc(EVP_PKEY_size(pubkey));

    /* Initialisiere EVP seal, Schlüssel für AES generieren */
    if(!EVP_SealInit(&ctx, EVP_aes_256_cbc(), &sesskey, &sesskeylen, iv, &pubkey, 1))
    {
        fprintf(stderr, "EVP_SealInit schlug fehl.\n");
        goto out_free;
    }

    /* Ändere Byte-Order des Schlüssellängenwertes in Netzwerk-Format */
    sesskeylen_nl = htonl(sesskeylen);
    /* Schreibe Schlüssellänge, Key und Initialisierungsvektor in Datei */
    if( (fwrite(&sesskeylen_nl, sizeof(sesskeylen_nl), 1, out)        != 1) ||
        (fwrite(sesskey, sesskeylen, 1, out)                          != 1) ||
        (fwrite(iv, EVP_CIPHER_iv_length(EVP_aes_256_cbc()), 1, out)  != 1) )
    {
        fprintf(stderr, "Fehler beim Schreiben in Ausgabestream/-datei.\n");
        goto out_free;
    }

    /* Eingabestream durchschleifen */
    while( (len = fread(buffer, 1, sizeof(buffer), in)) > 0 )
    {
        /* Verschlüsseln des Bufferinhalts */
        if(!EVP_SealUpdate(&ctx, buffer_out, &len_out, buffer, len))
        {
            fprintf(stderr, "EVP_SealUpdate schlug fehl \n");
            goto out_free;
        }

        /* Schreiben der verschlüsselten Daten in Ausgabestream */
        if(fwrite(buffer_out, len_out, 1, out) != 1)
        {
            fprintf(stderr, "Fehler beim Schreiben in Ausgabestream/-datei.\n");
            goto out_free;
        }
    }

    /* Prüfen, ob Read-Schleife wegen Fehler abbrach */
    if(ferror(in))
    {
        fprintf(stderr, "Fehler beim lesen des Eingabestreams/-datei.\n");
        goto out_free;
    }

    /* Übergebliebene Daten aus Block verschlüsseln... */
    if(!EVP_SealFinal(&ctx, buffer_out, &len_out))
    {
        fprintf(stderr, "EVP_SealFinal schlug fehl.\n");
        goto out_free;
    }

    /* ... und speichern */
    if(fwrite(buffer_out, len_out, 1, out) != 1)
    {
        fprintf(stderr, "Fehler beim Schreiben in Ausgabestream/-datei.\n");
        goto out_free;
    }

    retval = EXIT_SUCCESS;

out_free:
    EVP_PKEY_free(pubkey);
    free(sesskey);

    return retval;
}

Dann folgt die Funktion zum entschlüsseln:

int entschluesseln(FILE *rsa_privkey_file, FILE *in, FILE *out)
{
    int retval = EXIT_FAILURE;
    RSA *rsa_privkey = NULL;
    EVP_PKEY *privkey = EVP_PKEY_new();
    EVP_CIPHER_CTX ctx;
    unsigned char buffer[1024];
    unsigned char buffer_out[1024 + EVP_MAX_IV_LENGTH];
    size_t len;
    int len_out;
    unsigned char *sesskey;
    unsigned int sesskeylen;
    uint32_t sesskeylen_nl;
    unsigned char iv[EVP_MAX_IV_LENGTH];

    /* Lade privaten RSA Key aus Datei */
    if(!PEM_read_RSAPrivateKey(rsa_privkey_file, &rsa_privkey, NULL, NULL))
    {
        fprintf(stderr, "Private Key kann nicht gelesen werden.\n");
        ERR_print_errors_fp(stderr);
        return EXIT_FAILURE;
    }

    /* Übergebe privaten RSA Key an EVP */
    if(!EVP_PKEY_assign_RSA(privkey, rsa_privkey))
    {
        fprintf(stderr, "EVP_PKEY_assign_RSA schlug fehl.\n");
        return EXIT_FAILURE;
    }

    /* EVP Chiffre-Kontext initialisieren */
    EVP_CIPHER_CTX_init(&ctx);
    sesskey = malloc(EVP_PKEY_size(privkey));

    /* Lese die Länge des Session Keys  */
    if(fread(&sesskeylen_nl, sizeof(sesskeylen_nl), 1, in) != 1)
    {
        fprintf(stderr, "Lesen des Eingabestreams/-datei fehlgeschlagen.\n");
        goto out_free;
    }

    /* Ändere Byteorder */
    sesskeylen = ntohl(sesskeylen_nl);

    /* Sicherstellen, dass der Session Key nicht länger als der RSA-Key ist  */
    if(sesskeylen > EVP_PKEY_size(privkey))
    {
        fprintf(stderr, "Fehler: Session Key ist länger als RSA Key (%u > %d)\n", 
                        sesskeylen, EVP_PKEY_size(privkey));
        goto out_free;
    }

    /* Lesen des verschlüsselten AES-Key aus der Datei */
    if(fread(sesskey, sesskeylen, 1, in) != 1)
    {
        fprintf(stderr, "Lesen des Eingabestreams/-datei fehlgeschlagen.\n");
        goto out_free;
    }

    /* Lesen des Initialisierungsvektors */
    if(fread(iv, EVP_CIPHER_iv_length(EVP_aes_256_cbc()), 1, in) != 1)
    {
        fprintf(stderr, "Lesen des Eingabestreams/-datei fehlgeschlagen.\n");
        goto out_free;
    }

    /* Initialisiere EVP */
    if(!EVP_OpenInit(&ctx, EVP_aes_256_cbc(), sesskey, sesskeylen, iv, privkey))
    {
        fprintf(stderr, "EVP_OpenInit fehlgeschlagen.\n");
        goto out_free;
    }

    /* Jeweils die Bufferlänge einlesen und entschlüsseln */
    while( (len = fread(buffer, 1, sizeof buffer, in)) > 0 )
    {
        /* Daten entschlüsseln */
        if (!EVP_OpenUpdate(&ctx, buffer_out, &len_out, buffer, len))
        {
            fprintf(stderr, "EVP_OpenUpdate fehlgeschlagen.\n");
            goto out_free;
        }

        /* Entschlüsselte Daten in Datei schreiben */
        if (fwrite(buffer_out, len_out, 1, out) != 1)
        {
            fprintf(stderr, "Fehler beim Schreiben in Ausgabestream/-datei.\n");
            goto out_free;
        }
    }

    /* Prüfen, ob Read-Schleife wegen Fehler abbrach */
    if (ferror(in))
    {
        fprintf(stderr, "Lesen des Eingabestreams/-datei fehlgeschlagen.\n");
        goto out_free;
    }

    /* Übergebliebene Daten aus Block entschlüsseln... */
    if (!EVP_OpenFinal(&ctx, buffer_out, &len_out))
    {
        fprintf(stderr, "EVP_SealFinal fehlgeschlagen.\n");
        goto out_free;
    }

    /* ... und speichern */
    if (fwrite(buffer_out, len_out, 1, out) != 1)
    {
        fprintf(stderr, "Fehler beim Schreiben in Ausgabestream/-datei.\n");
        goto out_free;
    }

    retval = EXIT_SUCCESS;

out_free:
    EVP_PKEY_free(privkey);
    free(sesskey);

    return retval;
}

Und schließlich noch eine kleine Main-Funktion, die den Programmaufruf regelt:

int main(int argc, char *argv[])
{
    FILE *rsa_key_file, *in, *out;
    int retval;

    /* Prüfe Anzahl der Parameter */
    if (argc < 5)
    {
        fprintf(stderr, "Usage: %s <mode> <PEM RSA Public Key File> <in> <out>\n", argv[0]);
        fprintf(stderr, "Modes: encrypt, decrypt\n");
        exit(EXIT_FAILURE);
    }

    /* Öffne Dateien/Streams (alle Binär) */
    rsa_key_file = fopen(argv[2], "rb");
    if (!rsa_key_file)
    {
        fprintf(stderr, "Fehler beim Laden der PEM RSA Schlüsseldatei.\n");
        exit(EXIT_FAILURE);
    }

    in = fopen(argv[3], "rb");
    if(!in)
    {
        fprintf(stderr, "Fehler beim Öffnen der Eingabedatei: %s\n", argv[3]);
        fclose(rsa_key_file);
        exit(EXIT_FAILURE);
    }

    out = fopen(argv[4], "wb");
    if(!out)
    {
        fprintf(stderr, "Fehler beim Öffnen der Ausgabedatei: %s\n", argv[4]);
        fclose(in);
        fclose(rsa_key_file);
        exit(EXIT_FAILURE);
    }

    if( strcmp(argv[1], "encrypt") == 0 )
    {
        retval = verschluesseln(rsa_key_file, in, out);
    }
    else if( strcmp(argv[1], "decrypt") == 0 )
    {
        retval = entschluesseln(rsa_key_file, in, out);
    }
    else
    {
        fprintf(stderr, "Invalid mode: %s\n", argv[1]);
        retval = EXIT_FAILURE;
    }

    fclose(in);
    fclose(out);
    fclose(rsa_key_file);

    exit(retval);
}

Der gesamte Beispielcode als Tarball.

Key Pair erzeugen und Testlauf

Privaten Schlüssel mit einer Länge von 4096 Bit erzeugen:

openssl genrsa -out key.pem 4096

Öffentlichen Schlüssel extrahieren:

openssl rsa -in key.pem -pubout > key.pub

Der Kommunikationspartner erhält den öffentlichen Schlüssel und kann die Datei verschlüsseln:

./mycrypt encrypt key.pub text.txt geheim.txt

Der Empfänger kann sie dann mit seinem privaten Schlüssel wieder lesbar machen:

./mycrypt decrypt key.pem geheim.txt text.txt

Optimierung und Einsatz

Dieser Artikel zeigt natürlich nur einen von sehr vielen Wegen. Als nächstes lohnt es sich, auch anderen Verfahren als RSA/AES Aufmerksamkeit zu schenken. Wer sichere Software schreiben möchte, sollte sich vor allem gut auskennen. Weiterführende Themen sind z.B. Digitale Signaturen, Authentizitätsprüfung, Public-Key Infrastruktur und Verschlüsselung auf Socketebene. OpenSSL bietet auch dafür Funktionen.

Niemals vergessen werden sollte natürlich, dass Verschlüsselung nicht all zu viel bringt, wenn andere Kanäle oder Programmteile nicht sicher sind. Wenn es um Sicherheit geht sollte grundsätzlich sauber gearbeitet, dokumentiert und verifiziert werden.