Wincrypt und libReSSL: Von SSL bis TLS

Eigentlich ist es recht einfach einen HTTP/1 Client zu schreiben. Wenn man auf Features wie Proxies oder Keep-Alive verzichten kann, öffnet man einfach einen Port, schickt seine GET-Zeile und eine Leerzeile, wartet auf die Antwort, liest den Header bis zur Leerzeile und dahinter liegen die Download-Daten, super, fertig.

Blöd nur, dass heute fast alles über HTTPS, also über einen SSL/TLS Tunnel, verschlüsselt übertragen wird.

… und das implementiert man selbst nicht ganz so einfach. Dafür braucht man Hilfe.


Jetzt gibt es heute zum Glück einige Bibliotheken die einem das Thema Verschlüsselung abnehmen. Doch in kommerziellen Projekte stößt man gerne auf die Auflage keinen Copyleft OpenSource Code einzusetzen.

Unter Windows darf man sich dann mit der Windows Crypto-API herumschlagen.
Und wenn ein POSIX OS erlaubt ist, so greife ich stets zu libReSSL, dem aufgeräumten Fork des älteren OpenSSL Projektes.

Nur zur Erinnerung:
Es begann alles mit dem Titel “Secure Socket Layer”, kurz SSL, wovon es eine Version 1 und 2 gab, die schnell wieder fallen gelassen wurden, weil Unsicherheiten darin auftraten. SSL 3 hielt sich recht lange und kurz danach kam gleich SSL 3.1, was in TLS (Transport Layer Security) 1.0 umbenannt wurde.
Unter dem Namen TLS wurde die Technologie dann ausgebaut und erweitert und im Jahr 2018 wurde mit TLS 1.3 der bisher neueste Stand erreicht.
Sicherheitsprobleme bei SSL 3, die ebenso in den letzten Jahren erkannt wurden, führten dazu, dass SSL 3 heute auf allen Servern deaktiviert wurde und heute praktisch nichts mehr unterhalb von TLS 1.2 genutzt wird.

… soviel zur Geschichte.

WinCrypt

Das ärgerliche an der Crypto-API ist ihr generischer Ansatz, der eigentlich super wäre, wenn sie nur besser dokumentiert wäre. Daher dauerte es bei mir eine Ewigkeit, bis ich endlich herausgefunden hatte, wie man SSL/TLS Verbindungen darüber implementieren kann.

Hier mal mein Weg für einfache SSL Clients in Kurzfassung:

  • SCHANNEL_CRED mit SCHANNEL_CRED_VERSION und den Flags SCH_CRED_NO_DEFAULT_CREDS | SCH_CRED_NO_SYSTEM_MAPPER | SCH_CRED_REVOCATION_CHECK_CHAIN erzeugen.
  • AcquireCredentialsHandle(0, SCHANNEL_NAME, SECPKG_CRED_OUTBOUND, 0, &SCHANNEL_CRED, NULL, NULL, &cred_handle, NULL) erzeugt uns ein Handle mit dem wir weiterarbeiten können
  • Jetzt müssen so lange Daten ausgetauscht werden bis ein sicherer Verbindungskanal ausgehandelt ist
    • Zuerst halten wir uns ein paar Flags bereit, nämlich: ISC_REQ_SEQUENCE_DETECT | ISC_REQ_REPLAY_DETECT | ISC_REQ_CONFIDENTIALITY ` | ISC_RET_EXTENDED_ERROR | ISC_REQ_ALLOCATE_MEMORY | ISC_REQ_STREAM ` ` | ISC_REQ_MANUAL_CRED_VALIDATION`
    • Wir brauchen einen SecBuffer mit Typ SECBUFFER_TOKEN und einen SecBufferDesc mit der Version SECBUFFER_VERSION
    • Anfangs wird InitializeSecurityContext(NULL, NULL, flags, 0, 0, NULL, 0, &ctx, &sec_buffer_desc, &out_flags, 0) aufgerufen, so lange bis SEC_E_INCOMPLETE_MESSAGE zurückkommt.
    • Nach jedem Aufruf muss der Inhalt des SecBuffer zur Gegenseite geschickt werden.
    • Bei ERROR_SUCCESS anstatt von SEC_E_INCOMPLETE_MESSAGE kommt,
      gilt die Verbindung als hergestellt und nutzbar.
  • Zum Senden von Daten nutzt man:
  • Empfangene Daten werden durch die API dekodiert, indem:
    • QueryContextAttributes(.., SECPKG_ATTR_STREAM_SIZES, ..) die Puffergrößen feststellt.
    • 4 SecBuffer als SECBUFFER_DATA und 3 mal SECBUFFER_EMPTY angelegt
    • DecryptMessage den SecBufferDesc mit den 4 Puffern dekodiert.
    • Nach dem Aufruf werden der Puffer mit dem Typ SECBUFFER_DATA mit den Nutzdaten zurückgegeben, während Puffer mit dem Typ SECBUFFER_EXTRA bei der nächsten Sendeaktion vorangestellt werden müssen.

OpenSSL und libReSSL

Die OpenSSL API, die auch in der libReSSL verfügbar ist, erlaubt die direkte Nutzung von Sockets, doch diese Variante gefällt mir nicht, da man die Kommunikationsschicht nicht unter Kontrolle hat und sie nicht in bestehende Kommunikationshandler integrieren kann.

Daher gehe ich den Weg über Memorystream-Datenpuffer. Die SSL/TLS Schicht liest und schreibt ihre Daten in diese Puffer, und wir können sie dann an die gewünschte Kommunikationsschicht (z.B.: TCP/IP) weiterreichen.

Diese Methode hilft vor allem bei der Implementierung von asynchroner Kommunikation.
Und sie funktioniert so:

  • context = SSL_CTX_new(...) mit SSLv23_client_method() oder TLS_client_method() aufrufen um ein Kontext Objekt zu erstellen.
    • SSLv23 hat einen heute etwas irreführenden Namen, es steht für das Aushandeln der bestmöglichen Verbindung. Mit aktuellen Versionen der Bibliothek stehen SSL2 und SSL3 gar nicht mehr zur Verfügung, sondern wir starten bei TLS 1.
  • session = SSL_new(context) erzeugt mit dem Kontext eine SSL Session.
  • SSL_set_connect_state(session) versetzt die Session in den Client-Modus.
  • Nun erzeugen wir zwei Puffer, einen für zu sendende Daten und einen wo empfangene Daten angefügt werden, und zwar mit: send_buffer = BIO_new(BIO_s_mem()); und receive_buffer = BIO_new(BIO_s_mem());
  • SSL_set_bio(session, receive_buffer, send_buffer) registriert die beiden Puffer im Session Objekt.
  • Bis SSL_state(session) den Wert SSL_ST_OK erreicht, muss der Handshake-Prozess durchgeführt werden.
  • Ist die Session aufgebaut, können Daten mit SSL_write(session, data_to_encrypt, data_length) verschlüsselt werden und das Resultat mit BIO_read(send_buffer, ...) zum Versenden extrahiert werden.
  • Parallel werden empfangene Daten mit BIO_write(receive_buffer, ...) zur Dekodierung weitergereicht und mit SSL_read(session, decrypted_data, data_length) final ausgelesen.

Fazit

Puh, jetzt wurde fast ein kleines Tutorial aus meinem jüngsten SSL API Ausflug. Natürlich fehlt noch einiges zum Thema “Zertifikate gezielt einsetzen”. Aber wer sich nur für die Verschlüsselung und nicht für die Authentifizierung der Verbindung interessiert, der kann damit schon einen kleinen Client schreiben.