Eigentlich wollte ich ja seit inzwischen über zehn Jahren mal eine TLS-Library hacken, aber irgendwie ist immer irgendwas anderes attraktiver. Es ist nicht so, dass ich noch gar nicht vorangekommen wäre. ASN.1-Parsingcode habe ich von tinyldap, und damit habe ich mal einen Parser für X.509-Zertifikate gebastelt. Auch ein Client Hello habe ich mal erzeugt. Baby Steps.

Meiner Erfahrung nach ist das Problem bei dieser Symptomatik bei mir, dass der nächste Schritt nicht klein genug ist. Damit liegt die Fertigstellung des nächsten Schrittes zu weit in der Zukunft und andere Dinge, die ich jetzt tun könnte, haben immer weniger Aufwand und schnellere Fertigstellung, werden also vorgezogen.

Also habe ich mir gedacht: OK, ich muss mal den nächsten Schritt verkleinern. Wie wäre es, wenn ich erstmal die Basis lege, indem ich OpenPGP-Signaturen validieren kann.

Gesagt, getan, habe mir mal das RFC 4880 geholt (bzw. den aktualisierten Draft) und mal angefangen. Und jetzt komme ich gerade von der Straße ab und frage mich, ob ich nicht mal etwas tun kann, um Binärprotokolle leichter verarbeitbar zu machen.

Der Standardansatz ist, dass man irgendeine Library nimmt. Problem: Dann hat man die Library am Bein und muss da hinterherpflegen. Ich schreibe daher meinen Parsing-Code lieber selbst, besonders wenn es sich um Binärkram handelt.

Binärprotokolle parsen ist aber echt nervig. Bei OpenPGP-Signaturen gibt es z.B. mehrere verschiedene Versionen, wie so eine Signatur abgelegt sein könnte. Man muss da byteweise Daten lesen und inhaltlich validieren. Und bei jedem Byte muss man einzeln gucken, ob gerade EOF oder ein Lesefehler reinkam. Code mit so viel Checks sieht elend unübersichtlich und unnötig kompliziert aus. Eigentlich hätte ich gerne eine strukturelle Verbesserung, die den Code einfacher lesbar macht.

Oder man kann sagen: Ich lese die ganze Nachricht in einen Buffer, dann spare ich mir zumindest die EOF und I/O-Fehlerbehandlung bei jedem Byte. Ja, aber dafür fange ich mir möglicherweise ein Denial of Service ein, weil die Nachricht ja beliebig groß sein kann. Ich will nicht 4 GB Speicher holen, nur weil jemand das als Länge irgendwo reingeschrieben hat.

Meine aktuelle Idee ist, dass man eine Abstraktion macht, die man hinten an einen Buffer oder einen File Descriptor koppeln kann, und vorne kann man dann byteweise lesen. Aber anstatt Fehler oder EOF zu signalisieren gibt einem das Ding halt ein 0-Byte zurück, und dann gibt es ein API, mit dem man am Ende fragt, ob bis hierher irgendwelche Fehler auftraten.

Im nächsten Schritt macht man das rekursiv stapelbar, aber mit anderen Längen. Ein typischer Fall bei Binärprotokollen (auch bei ASN.1 und OpenPGP) ist es, dass man einen Header mit einer Länge für das Paket oder die Message hat, und dann lauter Unter-Header mit eigenen Längen innerhalb der Message. Da muss man jetzt jeweils beim Parsen der Länge prüfen, dass man nicht die logisch darüber liegenden Längenlimits überspringt, und dass die neue Unter-Länge in die Länge des Pakets darüber passt, und wenn man die Unterpakete gelesen hat, dann muss man prüfen, dass man jetzt genau am Ende der logisch darüberliegenden Schachtelung liegt.

Je nach Integer-Typ muss man noch numerischen Überlauf abfangen. Das wird schnell fummelig und Fehler passieren.

Zumindest die erste Hälfte davon könnte die Abstraktion einem direkt abnehmen, finde ich.

Die Frage ist, ob das wirklich eine gute Idee ist. Denn der Code ist dann zwar kürzer und lesbarer aber man braucht eben auch mehr Kontext zum Verständnis. Man muss verstanden haben, dass es kein Fehler ist, wenn man mehr liest als da war, solange der Code am Ende guckt.

Der Grund, wieso ich mir OpenPGP angeguckt habe, ist weil sich das als Testbett für eine Kompartmentalisierung gut eignen würde. Man würde am Anfang mehrere Prozesse starten. Einen "gibt mir den Public Key XY", der kriegt den public keyring auf stdin und schreibt den Key nach stdout und darf kein open() machen. Einen "berechne den Hash über diese Daten", der kriegt die Filedaten auf stdin und schreibt den Hash nach stdout und darf kein open() machen. Einen, der auf stdin die Signatur kriegt und parsed und auspackt und in kanonischer Form auf stdout ausgibt und ansonsten auch kein open() machen darf. Und ein Prozess darüber, der alle Arbeit, die potentiell schiefgehen kann, an Unterprozesse delegiert. Wenn dann ein Angreifer einen Bug zum exploiten findet, dann ist er in einer Sandbox, die nichts tun kann außer von stdin lesen und nach stdout schreiben. Mit einem Exploit kriegt man auch nur Zugriff auf die Daten, in denen der Exploit eingebettet war, und z.B. nicht auf irgendwelche Kryptoschlüssel o.ä.

Ich glaube das wäre mal eine hilfreiche Sache, wenn man das mal so durchimplementiert. Sei es nur um zu zeigen, dass das geht.

Langfristig will man so eine Privilege Separation auch für TLS haben, aber dort ist es schwieriger als bei OpenPGP. Daher erstmal OpenPGP als Proof of Concept.

29.10.2020