TLS Socket, SSL Socket

Teil 3 von 7 der Serie Sockets

Das Thema Websockets schiebe ich schon seit Jahren vor mich her. Immerhin ist das RFC6455 in dem die aktuelle Websocket-Protokol-Version 13 definiert wird von 2011. HTML5 hat es schon vor langem assimiliert. Weiterer Widerstand scheint zwecklos.

Und jetzt ist es soweit! Nach 3 Tagen harter Arbeit ist das Proof-Of-Concept erstellt.

Doch beim Versuch das erste hellows in Produktion zu bringen haben die Schwierigkeiten angefangen. In der Produktion läuft nämlich ein https-Server, der sich weigert eine sich Verbindung zum meinem unverschlüsselten WS-Server aufzubauen. Also fange ich nochmal von vorne an und zwar am Anfang.

TLS Socket Server

Das erste was ich brauche ist ein Socket-Server mit Transport Layer Security für TCP-Sockets (TLS). Vielleicht ist die alte Bezeichnung Secure Socket Layer (SSL) geläufiger.

Eine Voraussetzung für TLS ist ein gültiges, signiertes Zertifikat. Das besteht aus einer signierten Zertifikat-Datei (z.B. cert.pem) und aus einer Datei mit dem privaten Schlüssel (z.B. key.pem). Ein selbstsigniertes Zertifikat ist schnell erzeugt:

Zuerst muss ein private Schlüssel erzeugt werden. An dieser Stelle ist ein Passwort Pflicht, doch es kann in einem späteren Schritt wieder entfernt werden

openssl genrsa -des3 -out server.key 1024

Mit diesem Schlüssel kann dann eine Zertifikatsignierungsanforderung erstellt werden, zu englisch: Certificate Signing Request oder kurz: CSR

openssl req -new -key server.key -out server.csr

Beispiel Eingaben:

Country Name (2 letter code) [GB]:DE
State or Province Name (full name) [Berkshire]:Bayern
Locality Name (eg, city) [Newbury]:Garching
Organization Name (eg, company) [My Company Ltd]:Tuncer Networks
Organizational Unit Name (eg, section) []:InfoTech
Common Name (eg, your name or your server's hostname) []:server.domain.com
Email Address []:company@email.address
Please enter the following 'extra' attributes to be sent with your certificate request A challenge password []:
 An optional company name []:

Jetzt Kann das Passwort wieder entfernt werden

cp server.key server.key.org
openssl rsa -in server.key.org -out server.key
rm server.key.org

Dann kommt das, was eigentlich ein Trust-Center macht, das Zertifikat signieren. Aber wir wollen das ja selbst tun, weshalb es dann die disqualifizierende Bezeichnung Self Signed Certificate erhält.

openssl x509 -req -days 365 -in server.csr -signkey server.key -out 
server.crt

Jetzt nur noch ein wenig aufzuräumen: Der CSR wird gelöscht, weil er nicht mehr gebraucht wird und ich vergebe gern standardisierte Namen für die beiden Dateien. Ich weiß! Ich hätte schon oben mit diesen Namen arbeiten können, doch ich wollte explizit zeigen, dass die Namen nicht fest vorgegeben sind.

rm server.csr
mv server.key key.pem 
mv server.crt cert.pem

Der eigentliche Socket-Server-Code hat gerade mal 140 Zeilen. Und weil an jeder Stelle klar ist, worum es gerade geht, gibt es keine Kommentare.

#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <openssl/ssl.h>
#include <openssl/err.h>

int createServerSocket(int port)
{
    int s = socket(PF_INET, SOCK_STREAM, 0);

    struct sockaddr_in addr;
    bzero(&addr, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(s, (struct sockaddr*)&addr, sizeof(addr)) < 0)
    {
        perror("bind");
        exit(1);
    }

    if ( listen(s, 10) != 0 )
    {
        perror("listen");
        exit(1);
    }
    return s;
}

SSL_CTX* createSSLContext(void)
{
    OpenSSL_add_all_algorithms();
    SSL_load_error_strings();
    const SSL_METHOD* method = SSLv23_server_method();

    SSL_CTX* context = SSL_CTX_new(method);

    if ( context == NULL )
    {
        ERR_print_errors_fp(stderr);
        exit(1);
    }
    return context;
}

void loadCertificates(SSL_CTX* context, const char* certFile, const char* keyFile)
{
    if ( SSL_CTX_use_certificate_file(context, certFile, SSL_FILETYPE_PEM) <= 0 )
    {
        ERR_print_errors_fp(stderr);
        exit(1);
    }
    if ( SSL_CTX_use_PrivateKey_file(context, keyFile, SSL_FILETYPE_PEM) <= 0 )
    {
        ERR_print_errors_fp(stderr);
        exit(1);
    }
    if ( !SSL_CTX_check_private_key(context) )
    {
        fprintf(stderr, "Privater Schluessel passt nicht zum oeffentlichen Certifikat\n");
        exit(1);
    }
}

void service(SSL* ssl)
{
    char buffer[8096];

    if ( SSL_accept(ssl) < 0 )
    {
        ERR_print_errors_fp(stderr);
    }
    else
    {
        int bytes = SSL_read(ssl, buffer, sizeof(buffer));  // Lese vom Client-Socket
        if ( bytes > 0 )
        {
            buffer[bytes] = 0;
            // printf("Gelesen: %s\n", buffer);
            SSL_write(ssl, buffer, strlen(buffer)+1);
        }
        else
        {
            ERR_print_errors_fp(stderr);
        }
    }

    int s = SSL_get_fd(ssl);
    SSL_free(ssl);
    ::close(s);
}

void
usage(const char* progname, const char *message = nullptr)
{
    fprintf(stderr, "usage: %s port\n", progname);
    if( message )
    {
        fprintf(stderr, "%s\n", message);
    }
    exit(1);
}

int main(int argc, char *argv[])
{
    if ( argc != 2 )
    {
        usage(argv[0]);
    }

    char *err;
    int port = strtol(argv[1], &err, 10);
    if( *err || port < 0 )
    {
        usage("Port is invalid");
    }

    SSL_CTX* context = createSSLContext();

    loadCertificates(context, "../cert.pem", "../key.pem");

    int server = createServerSocket(port);

    struct sockaddr_in addr;
    uint len = sizeof(addr);

    while (1)
    {
        memset(&addr, 0, sizeof(addr));

        int client = accept(server, (struct sockaddr*)&addr, &len);
        SSL* ssl = SSL_new(context);
        SSL_set_fd(ssl, client);
        service(ssl);
    }
    close(server);
    SSL_CTX_free(context);
}

Der Client-Code ist noch kürzer

#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <openssl/ssl.h>
#include <openssl/err.h>

int createSocket(const char *hostname, int port)
{
    struct hostent* host = gethostbyname(hostname);
    if ( !host )
    {
        fprintf(stderr, "host %s unbekannt\n", hostname);
        exit(1);
    }
    int s = socket(PF_INET, SOCK_STREAM, 0);

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = *(long*)(host->h_addr);
    if( connect(s, (struct sockaddr*)&addr, sizeof(addr)) < 0)
    {
        close(s);
        fprintf(stderr, "verbindung zu %s:%d gescheitert\n", hostname, port);
    }
    return s;
}

SSL_CTX* createSSLContext(void)
{
    OpenSSL_add_all_algorithms();
    SSL_load_error_strings();

    const SSL_METHOD* method = SSLv23_client_method();
    SSL_CTX* ctx = SSL_CTX_new(method);
    if ( !ctx )
    {
        ERR_print_errors_fp(stderr);
        exit(1);
    }
    return ctx;
}

void usage(const char* progname, const char* message = nullptr)
{
    fprintf(stderr, "usage: %s hostname port\n", progname);
    if( message )
    {
        fprintf(stderr, "%s\n", message);
    }
    exit(1);
}

int main(int argc, char* argv[])
{
    if ( argc != 3 )
    {
        usage(argv[0]);
    }
    char *err;
    int port = strtol(argv[2], &err, 10);
    if(*err)
    {
        usage(argv[0], "port ist ungueltig");
    }

    SSL_CTX* ctx = createSSLContext();
    int server = createSocket(argv[1], port);
    SSL* ssl = SSL_new(ctx);
    SSL_set_fd(ssl, server);
    if ( SSL_connect(ssl) < 0 )
    {
        ERR_print_errors_fp(stderr);
    }
    else
    {
        const char *message = "Hallo, ich bin der Client und schicke diese Nachricht";

        fprintf(stdout, "Verbunden mit %s Verschluesselung\n", SSL_get_cipher(ssl));
        SSL_write(ssl, message, strlen(message));

        char buffer[8096];
        int bytes = SSL_read(ssl, buffer, sizeof(buffer));
        buffer[bytes] = 0;
        fprintf(stdout, "Antwort vom Server:\n");
        fprintf(stdout, "%s\n", buffer);
        SSL_free(ssl);
    }
    close(server);
    SSL_CTX_free(ctx);
}

Mit einem einfachen, hart verdrahtetem Makefile können die beiden Programme erstellt werden. Achte darauf, dass die Zeilen 8, 12 und 15 mit [TAB] und nicht mit Blanks eingerückt sind.

Außerdem kann es sein, dass auf dem Rechner noch openssl installiert werden muss. Die dazu nötigen apt-install-Anweisungen bitte im Google nachschlagen.

CC=g++

CFLAGS=-O3 --std=c++14 -I. -I/usr/local/include
LDFLAGS=-L/usr/local/lib -pthread -lwsocks -lz

all: server client

server: server.cpp
    $(CC) $(CFLAGS) server.cpp -o $@ $(LDFLAGS) -lssl -lcrypto
    
client: client.cpp
    $(CC) $(CFLAGS) client.cpp -o $@ $(LDFLAGS) -lssl -lcrypto

clean:
    rm -f server client

Series Navigation<< Server SocketSelenium WebDriver ließt WebSocket >>