Blog ·
Wie ein signiertes Agenten-Update sich verweigert
Eine Prüfsumme belegt, dass ein Download heil ankam, nicht dass er von Ihnen stammt. Was Signieren kostet, und die falsch signierte Version.
Von Provibr
Jede Software, die sich selbst aktualisiert, ist entfernte Codeausführung, die Sie eingeladen haben. Das ist kein Argument gegen automatische Updates, die die einzige realistische Art sind, eine Flotte von Agenten auf Kundenhardware vor dem Verrotten zu bewahren. Es ist ein Argument dafür, genau zu benennen, was der Update-Mechanismus verspricht, denn die meisten versprechen weniger, als ihre Dokumentation nahelegt.
Wir betreiben einen Agenten auf den Servern anderer Leute. Er spricht mit unserer Plattform, er wendet Befehle auf deren Hypervisor an, und er ersetzt seine eigene Binärdatei, sobald wir eine neue Version ausliefern. Das hier mussten wir bauen, bevor wir bereit waren, ihm das zu erlauben.
Eine Prüfsumme beantwortet die falsche Frage
Fast jedes Werkzeug, das sich selbst aktualisiert, veröffentlicht neben seinem Download eine SHA-256, und viele hören genau dort auf. Eine Prüfsumme ist nützlich, und sie leistet nicht das, wofür die Leute sie halten.
| Kontrolle | Was sie tatsächlich belegt | Was sie nicht sehen kann |
|---|---|---|
| TLS auf dem Download | Die Bytes kamen von diesem Host, über diese Verbindung | Nichts darüber, wer sie erzeugt hat |
| Neben der Datei veröffentlichte SHA-256 | Der Download ist nicht abgeschnitten oder beschädigt | Wer die Datei ändern kann, kann auch die Zahl ändern |
| Signatur über die Bytes | Diese Bytes stammen vom Inhaber eines bestimmten privaten Schlüssels | Ob dieser Schlüssel noch Ihnen gehört |
Die mittlere Zeile ist das ganze Problem. Prüfsumme und Artefakt liegen üblicherweise am selben Ort und sind üblicherweise vom selben Prozess beschreibbar. Wer das eine ersetzen kann, kann auch das andere ersetzen, und die Überprüfung geht mit Begeisterung durch.
Die Prüfsumme ist also ein billiger Vorabtest und ausdrücklich nicht die Vertrauensgrenze. Sie fängt einen abgeschnittenen Download in einem Durchlauf über Bytes ab, die wir ohnehin gerade durchreichen. Der Anker ist die Signatur.
Wo der öffentliche Schlüssel liegen muss
Hier fällt die erste Entscheidung darüber, ob irgendetwas davon echt ist: Der prüfende Schlüssel darf keine Datei sein, die neben der Binärdatei ausgeliefert wird, denn dann kompromittiert ein kompromittierter Auslieferungsweg auch den Schlüssel.
Unser Agent bekommt den öffentlichen Ed25519-Schlüssel der Plattform einkompiliert, aus einer Umgebungsvariable zur Bauzeit. In einem Release-Build gibt es keine andere Quelle. In einem Debug-Build kann eine Entwicklerin ihn über eine Umgebungsvariable auf einen lokalen Schlüssel zeigen lassen, und dieser Pfad wird über ein cfg-Attribut vollständig herauskompiliert, denn ein Schlüssel, der sich zur Laufzeit überschreiben lässt, ist kein Anker, sondern ein Vorschlag.
Zwei Einzelheiten drumherum wiegen schwerer, als sie aussehen.
Das Build-Skript erklärt, dass es erneut laufen muss, wenn sich diese Umgebungsvariable ändert. Ohne diese Zeile verwendet ein Release-Build, der sich einen Ziel-Cache mit einem früheren Build teilt, munter den Schlüssel des vorigen Durchlaufs weiter, und niemandem fällt es auf, bis es bei einem Kunden ankommt.
Und der Schlüsselparser ist streng in Bezug auf die Kodierung statt auf die Länge. Ein öffentlicher Ed25519-Schlüssel in SPKI-Form besteht aus einem festen Präfix von 12 Bytes, gefolgt von 32 Bytes; ein X25519-Schlüssel ist ebenfalls 32 Bytes lang, mit einer anderen Algorithmuskennung. Alles mit 32 Bytes Länge zu akzeptieren hieße, einen Schlüssel für ein anderes Verfahren anzunehmen und später zu scheitern, an einer Stelle, an der es keinen Sinn ergibt.
Verweigern, bevor Sie herunterladen
Wenn die Plattform dem Agenten sagt, er solle aktualisieren, arbeitet der Agent die vollständige Liste der Vorbedingungen ab, bevor er ein einziges Byte holt:
- Steht in der Anweisung eine brauchbare SHA-256?
- Gibt es eine Adresse zum Herunterladen?
- Gibt es eine Signatur, und ist sie genau 64 Bytes lang?
- Läuft diese Version bereits?
- Existiert das privilegierte Anwendungsskript auf diesem Host?
- Existiert die lokale Vertrauensankerdatei?
- Hat diese Binärdatei überhaupt einen Plattformschlüssel einkompiliert?
Schritt drei verdient die Aufmerksamkeit. Eine ältere Plattform, die das Signaturfeld nicht füllt, muss hier scheitern, laut, statt still auf die schwächere Prüfung zurückzufallen, die wir früher gemacht haben. Optionale Überprüfung ist keine Überprüfung. Das fehlende Feld zu einem harten Fehler zu machen ist der Unterschied zwischen einem Mechanismus und einer Gewohnheit.
Auch die Reihenfolge der letzten drei ist Absicht. Die Meldung „auf Ihrem Host fehlt der privilegierte Schritt, bitte den Installer erneut ausführen“ ist brauchbarer als „diese Binärdatei hat keinen Plattformschlüssel“, also kommt sie zuerst.
Die Bytes signieren, nicht ihre Beschreibung
Die Signatur deckt das Artefakt genau so ab, wie es auf der Platte landet. Reines Ed25519, kein Vorab-Hash, kein kanonischer Umschlag, keine eingeschlossenen Metadaten. Der Grund ist keine Reinheitsliebe. Er ist, dass zwei völlig verschiedene Implementierungen zum selben Urteil kommen müssen.
Auf dem Agenten ist es ed25519-dalek mit verify_strict, das Signaturen mit Torsionsanteilen ablehnt. Bei einem Schlüssel und einer Datei sollte es genau eine gültige Signatur geben, und die nicht strenge Variante garantiert das nicht.
Auf dem Host prüft das privilegierte Skript, das die Aktualisierung tatsächlich installiert, dieselben Bytes mit openssl pkeyutl -verify -pubin -rawin. Derselbe Schlüssel, dieselben Bytes, dieselbe Antwort. Hätten wir vorab gehasht oder ein Manifest statt des Artefakts signiert, müsste die Prüfung auf Shell-Ebene unsere Rahmung nachbauen, und die beiden Prüfungen würden auseinanderlaufen.
Das ist auch die Prüfung, die wir nach jedem Release von Hand auf dem Server gegen den öffentlichen Produktionsschlüssel ausführen, bevor wir glauben, dass ein Release stattgefunden hat.
Das Manifest neben dem Artefakt trägt Version, Baukennung, Prüfsumme, Größe, Freigabezeitpunkt, Artefaktart und die Signatur. Es sind Metadaten für das Panel, und nichts davon ist vertrauenswürdig: Die Signatur darin gilt der Binärdatei, nicht dem Manifest.
Der Agent kann sein eigenes Update nicht installieren
Der Agent läuft als unprivilegierter Benutzer. Er kann nicht in das Verzeichnis schreiben, in dem seine eigene Binärdatei liegt, und er soll es auch nicht können.
Also legt er ab. Er schreibt zuerst die Signatur und die Prüfsumme in ein Ablageverzeichnis und benennt erst danach die geprüfte Binärdatei daneben an ihren Platz, denn das privilegierte Skript nimmt das Vorhandensein dieser Binärdatei als Startsignal, und die beiden anderen Dateien müssen dann bereits da sein. Dann beendet er sich sauber, und der Dienstverwalter startet ihn neu, wobei zuerst ein privilegierter Schritt vor dem Start läuft.
Dieses Skript ist eine Beschreibung wert, denn seine Annahmen sind das Gegenteil dessen, was man raten würde.
Es endet immer mit dem Rückgabewert null. Ein fehlschlagender Schritt vor dem Start würde verhindern, dass der Dienst überhaupt startet, und ein kaputtes Update, das einen Kunden offline nimmt, ist weit schlimmer als ein kaputtes Update, das still nicht angewandt wird.
Dem Ablageverzeichnis wird nicht vertraut. Alles darin wurde vom unprivilegierten Agentenbenutzer geschrieben: die Binärdatei, die Prüfsumme, die Signatur. Wer diesen Benutzer übernimmt, kann alle drei ersetzen. Deshalb entscheidet einzig die Signatur, geprüft mit dem Schlüssel auf der privilegierten Seite.
Es kopiert, bevor es prüft. Die abgelegten Bytes werden genau einmal in eine frische, root gehörende temporäre Datei neben dem Ziel kopiert, und jede Prüfung danach liest die Kopie. Die Datei, die geprüft wird, ist genau der Inode, der an seinen Platz umbenannt wird, es gibt also kein Fenster, in dem zwischen Prüfung und Installation eine andere Datei untergeschoben wird.
Der Rest ist bewusst langweilig: Symlinks ablehnen, für die Prüfsumme genau 64 Hexzeichen verlangen, verlangen, dass die Datei mit der ELF-Kennung beginnt, die Rechte erst nach bestandener Signaturprüfung auf ausführbar erweitern, sie einmal als Dienstbenutzer statt als root starten und erst dann über die laufende Binärdatei umbenennen. Eine Signatur sagt, woher Bytes kommen. Sie sagt nicht, dass sie root verdienen.
Das Release, das wir mit dem falschen Schlüssel signiert haben
Der Mechanismus oben funktioniert. Wir haben trotzdem ein kaputtes Release ausgeliefert, und die Art, wie es scheiterte, ist der nützliche Teil.
Unser Build-Skript nahm eine Option für „dies ist ein Produktions-Release“ entgegen. Damals steuerte diese Option nur die Veröffentlichung, nicht die Wahl des Signaturschlüssels. Ein Release, das gebaut wurde, ohne zugleich den Produktionsschlüssel auf dem Server zu benennen, wurde mit dem Entwicklungsschlüssel signiert, hatte den Entwicklungs-Lizenzschlüssel einkompiliert und wurde anschließend lokal gegen genau diesen Entwicklungsschlüssel geprüft. Die Prüfung sagte, das Artefakt sei in Ordnung, denn es war in sich stimmig. Es war nur nicht unseres.
Jeder echte Agent verweigerte das Update, zu Recht, denn die Signatur passte nicht zu dem einkompilierten Schlüssel. Der Mechanismus tat genau das, wofür er gebaut wurde. Der Fehler lag davor.
Daraus wurden zwei Schutzvorrichtungen, beide im Code statt in einem Dokument. Für die Produktion zu bauen, ohne den Produktionsschlüssel zu benennen, ist jetzt ein harter Fehler, der das Skript anhält. Und der signierende und der prüfende Schlüssel müssen auf derselben Maschine liegen: Eine Signatur mit einem anderswoher geholten Schlüssel zu prüfen beweist überhaupt nichts.
Die allgemeine Lehre gehört klar ausgesprochen. Ein Prüfschritt, dem man beide Hälften eines zusammenpassenden Paars aus der falschen Umgebung reicht, wird durchgehen, und zwar mit Überzeugung. Signaturverfahren schützen gegen Angreifer. Gegen Sie selbst schützen sie nicht.
Eine Lizenz ist keine Binärdatei, und beide sind signiert
Wir signieren zwei verschiedene Dinge mit demselben Schlüssel: Agenten-Binärdateien und Lizenzdateien. Das ist eine Gefahr, denn eine Signatur über das eine darf niemals als Signatur über das andere durchgehen.
Die Trennung ist strukturell und keine Namenskonvention. Eine Lizenzdatei signiert einen Textumschlag aus Formatkennung, Salz, Nonce und Geheimtext, mit Zeilenumbrüchen verbunden. Eine Binärdatei signiert rohe ELF-Bytes. Das Installationsskript prüft die ELF-Kennung, bevor es irgendetwas verifiziert, eine gültige Lizenzsignatur kann also nie als gültige Binärsignatur vorgelegt werden.
Der Lizenzablauf hat noch eine Eigenschaft, die es zu übernehmen lohnt: Er prüft die Signatur, bevor er den Entschlüsselungsschlüssel ableitet. Argon2id mit 64 MiB Speicherkosten ist absichtlich teuer, und diese Arbeit für eine Datei zu leisten, die nie unsere war, ist eine geschenkte Dienstblockade. Billige Echtheitsprüfungen gehören vor teure Kryptografie.
Was das nicht einbringt
Signieren bindet ein Artefakt an einen Schlüssel. Es sagt nichts darüber, ob dieser Schlüssel noch Ihnen gehört, und nichts darüber, ob die Version, die Sie signiert haben, etwas taugt. Sickert unser Signaturschlüssel ab, akzeptiert jeder Agent, was der Angreifer baut, und ihn zu wechseln bedeutet, jede Binärdatei neu zu bauen und den Vertrauensanker auf jedem Host zu ersetzen, denn der Schlüssel ist einkompiliert. Das ist der Preis dafür, dass der Schlüssel ein Anker ist und kein Vorschlag, und es ist der richtige Tausch, aber es sind echte Kosten, und sie gehören in den Plan statt in eine Fußnote.
So funktioniert das Agenten-Update bei Provibr, wo der Agent auf der eigenen Hardware des Kunden läuft, sich ausschließlich nach außen verbindet und die Hypervisor-Zugangsdaten auf diesem Host hält.