Wincrypt und libReSSL: Von SSL bis TLS
« | 28 Mar 2019 | »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
mitSCHANNEL_CRED_VERSION
und den FlagsSCH_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 VersionSECBUFFER_VERSION
- Anfangs wird
InitializeSecurityContext(NULL, NULL, flags, 0, 0, NULL, 0, &ctx, &sec_buffer_desc, &out_flags, 0)
aufgerufen, so lange bisSEC_E_INCOMPLETE_MESSAGE
zurückkommt. - Nach jedem Aufruf muss der Inhalt des SecBuffer zur Gegenseite geschickt werden.
- Bei
ERROR_SUCCESS
anstatt vonSEC_E_INCOMPLETE_MESSAGE
kommt,
gilt die Verbindung als hergestellt und nutzbar.
- Zuerst halten wir uns ein paar Flags bereit, nämlich:
- Zum Senden von Daten nutzt man:
QueryContextAttributes(&hctxt, SECPKG_ATTR_STREAM_SIZES, &spc_stream_sizes)
zur Feststellung von Puffergrößen- Befüllt 4 SecBuffer mit den Typen
SECBUFFER_STREAM_HEADER
,SECBUFFER_DATA
,SECBUFFER_STREAM_TRAILER
undSECBUFFER_EMPTY
und ruftEncryptMessage
mit einem SecBufferDesc zu den 4 Puffern auf. - Am Ende werden die Daten des Header, Data und Trailer Puffers hintereinander zur Gegenseite gesendet.
- Empfangene Daten werden durch die API dekodiert, indem:
QueryContextAttributes(.., SECPKG_ATTR_STREAM_SIZES, ..)
die Puffergrößen feststellt.- 4 SecBuffer als
SECBUFFER_DATA
und 3 malSECBUFFER_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 TypSECBUFFER_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(...)
mitSSLv23_client_method()
oderTLS_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());
undreceive_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 WertSSL_ST_OK
erreicht, muss der Handshake-Prozess durchgeführt werden.SSL_do_handshake(session)
wird aufgerufen während parallel die lokal generierten Daten aus dem Sende-Puffer abgeholt und verschickt werden und die vom Server empfangenen Daten in den Empfangspuffer eingepflegt werden.BIO_write(receive_buffer, received_data, received_data_length)
erledigt das EmpfangenBIO_read(send_buffer, output_data, ...)
ist für das Extrahieren der zu sendenden Daten zuständig.
- Ist die Session aufgebaut, können Daten mit
SSL_write(session, data_to_encrypt, data_length)
verschlüsselt werden und das Resultat mitBIO_read(send_buffer, ...)
zum Versenden extrahiert werden. - Parallel werden empfangene Daten mit
BIO_write(receive_buffer, ...)
zur Dekodierung weitergereicht und mitSSL_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.