Warum ich auf meiner HTTPS-Website kein Old-School-Zertifikat mehr verwende
(rachelbythebay.com)- Die Autorin hatte wegen der Komplexität des ACME-Protokolls und der Implementierungsrisiken jahrelang eine Abneigung dagegen
- Bestehende ACME-Clients enthielten oft sicherheitsriskanten oder schwer verständlichen Code, weshalb sie sie nur ungern selbst ausgeführt hätte
- Doch durch den Qualitätsverlust und die Preiserhöhungen beim Domain-Registrar Gandi begann sie, selbst ein Tool zur Zertifikatserneuerung zu implementieren
- Nach zahllosen Fehlversuchen stellte sie erfolgreich ein Tool fertig, das Zertifikate direkt über Let's Encrypt ausstellt
- Im hinteren Teil des Artikels werden der tatsächliche Ablauf des ACME-Protokolls sowie Low-Level-Implementierungsdetails zu JSON, base64 und Signaturen ausführlich erläutert
Why I no longer have an old-school cert on my https site
Hintergrund und Auslöser
- Anfang 2023 erklärte sie noch in warum sie weiterhin ein Old-School-Zertifikat nutzte, doch 2025 teilt sie nun die Gründe, warum sie diesen Ansatz aufgegeben hat
- Die Abneigung gegen das ACME-Protokoll bestand seit 2018; komplexe Webtechnologien und schwer zugängliche Kodierungsverfahren waren eine große Hürde
- Die meisten ACME-Clients wirkten nicht vertrauenswürdig, und es erschien zu riskant, sie mit Root-Rechten laufen zu lassen
- Nachdem Gandi von einer Private-Equity-Firma übernommen worden war, sank die Qualität und die Preise stiegen, sodass es keinen Grund mehr gab, das bisherige Zertifikatssystem beizubehalten
Beginn der Eigenimplementierung
- Statt bestehende Tools zu verwenden, begann sie damit, kleine Utility-Funktionen einzeln selbst zu implementieren
- Zuerst kapselte sie die JSON-Bibliothek jansson für C so, dass sie in C++ verwendet werden konnte
- Sie prüfte mehrere Bibliotheken zur Erzeugung von JWKs (Key-Strukturen), doch die meisten halfen nicht weiter, sodass sie sich für eine eigene Implementierung entschied
- Mehrfach unterbrach sie die Arbeit und begann erneut, wobei sie nach und nach kleine Komponenten miteinander verband
Testumgebung und realer Einsatz
-
Um die produktiven Server von Let's Encrypt nicht direkt anzusprechen, nutzte sie in einer isolierten Umgebung „pebble“ als ACME-Testserver
-
Nach zahlreichen Fehlschlägen stellte sie ein frühes Tool fertig, das aus einem CSR ein Zertifikat ausstellt, und
- der Test auf dem Let's Encrypt-Staging-Server war erfolgreich
- auch in der Produktionsumgebung funktionierte es
- und es wurde bereits auf der echten Website eingesetzt
Detaillierte Erklärung des ACME-Protokolls
- Es werden ein RSA-Schlüssel erzeugt und ein CSR (Certificate Signing Request) mit CN und SAN erstellt
- Aus der ACME-Directory-URL wird JSON geparst, um Endpunkte wie newNonce, newAccount und newOrder zu extrahieren
- Aus dem privaten Schlüssel werden Modulus und öffentlicher Exponent extrahiert und in ein webtaugliches base64url-Encoding umgewandelt
- Nach der Erzeugung des JWK wird zusammen mit dem JSON-Payload eine RSA-SHA256-Signatur erstellt
- Per HTTP-HEAD-Request wird ein Nonce geholt, danach wird der signierte Request per POST zum Anlegen des Accounts gesendet
- Der
Location-Header der Antwort dient nicht als eigentliche Weiterleitung, sondern als URL des Account-Identifiers
Die Komplexität des ACME-Protokolls
- Obwohl es nur um die Ausstellung eines Zertifikats geht, kommen bereits zum Einsatz:
- SHA256-Hashing, base64web, JSON-in-JSON-Strukturen und RSA-Signaturen
- HEAD-Requests, Account-Identifikation per Location-Header und einmalig nutzbare Nonces
- Sie erwähnt, dass selbst Themen wie Zertifikatsbestellung, Nachweis des Domain-Besitzes (TXT-Records usw.) und Abschluss der Validierung noch gar nicht behandelt wurden
- Manche Clients funktionieren sogar dann, wenn die Kodierung des publicExponent fehlerhaft implementiert ist, was sie als Hinweis auf die gewisse Unschärfe des Standards anführt
Fazit
- ACME ist extrem komplex und ein System, dessen eigene Implementierung enorm viel Trial-and-Error und Aufwand erfordert
- Trotzdem berichtet sie, dass sie das Old-School-Zertifikat aufgegeben und den Wechsel zu einem vollständig automatisierten Ansatz erfolgreich geschafft hat
- Zum Schluss macht sie noch den scherzhaften Kommentar, ob diese Komplexität vielleicht nicht am Ende nur dazu dient, irgendjemandes Arbeitsplatz zu sichern
1 Kommentare
Hacker-News-Kommentare
Ich bin Technical Lead im SRE-/Infra-Team von Let’s Encrypt und beschäftige mich mit solchen Problemen sehr intensiv.
JSON Web Signature ist wirklich ein kniffliges Format, und auch die ACME-API nimmt RESTfulness sehr ernst.
Hätte ich es selbst entworfen, hätte ich es nicht so gebaut.
Ich denke, dass dazu sowohl die Absicht beigetragen hat, innerhalb der IETF möglichst viele IETF-Standards wiederzuverwenden, als auch das typische Committee-Design.
Mit ein paar Bibliotheken für JSON, JWS und HTTP wird es deutlich besser, aber gerade in C ist schon die Nutzung dieser Bibliotheken nicht besonders einfach.
Auch die Sprache der RFCs selbst ist komplex und verweist oft auf andere Dokumente, daher arbeiten wir zusätzlich an einem interaktiven Client und an Dokumentation, um dabei zu helfen.
Ich verstehe nicht ganz, warum JSON Web Signature als kniffliges Format bezeichnet wird.
Ich habe viel mit komplexen Dingen wie ASN.1, Kerberos und PKI zu tun und finde JWS als Format nicht besonders schwierig.
Selbst wenn man es direkt selbst implementiert, halte ich es für deutlich einfacher als S/MIME, CMS oder Kerberos.
Es bräuchte mehr Erklärung dazu, worin JWS genau „knifflig“ sein soll.
Falls das Problem eher JWT ist, dann ist aus meiner Sicht entscheidender, dass nicht klar standardisiert ist, wie HTTP User Agents JWTs anfordern oder empfangen sollen.
Jemand meinte, man müsse bezahlen, wenn man mehr als drei Zertifikate ausstellen lasse, aber ich nutze es seit fünf Jahren und habe nie eine Rechnung bekommen; das klingt nach einem Missverständnis oder nach Fehlinformation.
Im Zusammenhang mit der Verwendung von „e=AQAB“ statt „e=65537“ wird erklärt, dass die Ursache in den Schwächen von JSON beim Umgang mit Zahlen liegt.
Wenn man einen sehr großen Wert wie 4723476276172647362476274672164762476438 an einen JSON-Parser übergibt, schneiden die meisten JSON-Parser ihn stillschweigend auf 64-Bit-Integer oder float zurecht, oder mit etwas Glück werfen sie einen Fehler.
In einer Sprache wie Common Lisp wäre das wohl kein Problem, aber in der Praxis entwickeln nicht viele Leute in so einer Umgebung.
Deshalb erscheint es sinnvoller, große Zahlen in JSON zuverlässig als Byte-Array in base64 zu übertragen.
Auch wenn es so aussieht, als würde das problemlos funktionieren, ist genau das die Ursache vieler Sicherheitsprobleme, daher ist es vertretbar, im Protokoll alle Zahlen so zu behandeln.
Der Nachteil ist natürlich, dass dabei die menschenfreundliche Lesbarkeit von JSON verloren geht, und persönlich halte ich standardisierte S-Expressions für die deutlich bessere Wahl.
Aber die Welt hat sich nun einmal für JSON entschieden.
Wer nicht versteht, warum sich die Welt für JSON entschieden hat, ignoriert das meiner Meinung nach absichtlich.
JSON lässt sich für die meisten Daten leicht von Menschen direkt schreiben, bearbeiten und lesen.
Canonical S-Expression verlangt dagegen für jedes Element eine vorangestellte Längenangabe, was bei Handarbeit extrem umständlich ist.
Man muss beim Schreiben von S-Expressions ständig Zeichen zählen und Präfixe anpassen, was sehr lästig ist.
Anders als man vielleicht erwarten würde, ist genau diese leichte manuelle Schreib- und Änderbarkeit der Grund, warum JSON sich durchgesetzt hat.
Nebenbei: Der Ruby-JSON-Parser kann auch große Zahlen korrekt verarbeiten.
Ich hatte einmal in einer C#-App einen JSON-Serializer, der
BigIntals Zahl ausgegeben hat, und in JS wurde das dann stillschweigend falsch interpretiert.Es überrascht mich bis heute, dass Overflow statt eines Fehlers das Standardverhalten ist.
Seitdem habe ich mir angewöhnt, Zahlen größer als 32 Bit grundsätzlich als Strings zu behandeln.
Der Vergleich zwischen
{"e":"AQAB"}und{"e":65537}hat zwar etwas für sich, aber wenn man stattdessen mit{"e":"65537"}vergleicht, ist das Ergebnis in allen JSON-Parsern ebenfalls gleich.Ob Zahl oder String, die Umwandlung ist eindeutig.
Natürlich gibt es Sprach- oder Parserprobleme, wenn der Wert zu groß ist, um in ein
doublezu passen, aber das ist aus meiner Sicht ein separates Problem der Laufzeitumgebung und nicht der Darstellungsform selbst.Das Problem bei JSON ist aus meiner Sicht nicht das Format selbst, sondern dass die Parser ursprünglich für das Mapping auf JS-Typen gebaut wurden.
Manche Parser können damit zwar besser umgehen, aber dann geht die Portabilität von JSON verloren.
Auch bei base64 tritt dasselbe Problem auf, weil es nicht Teil des Standards ist.
Mit
replacerundreviverist zwar benutzerdefiniertes Parsen möglich, aber diese Funktionen sind nicht in jeder Umgebung garantiert verfügbar.Letztlich liegt die Fehlerquelle in der Annahme, dass JSON mit einem Standardparser interpretiert werden soll.
Würde man es statt JSON anders nennen, gäbe es vielleicht weniger dieser Probleme, aber sobald etwas wie JSON aussieht, werden Menschen es trotzdem direkt in einen JSON-Parser werfen.
In Go kann man Zahlen über den Typ
json.Numberverlustfrei als Strings dekodieren.Dabei wird auch einer meiner fast liebsten Decimal-Typen mit beliebiger Präzision erwähnt: https://github.com/ncruces/decimal?tab=readme-ov-file#decimal-arithmetic
Halb im Scherz gesagt verstehe ich in diesem Fall nicht so recht, warum S-Expressions besser sein sollen.
Es gibt auch LISP-Varianten ohne Unterstützung für Arithmetik mit beliebiger Präzision.
Ich fand es merkwürdig, warum der Autor ACME und mehreren Clients gegenüber so kritisch war.
Das schien mir nicht bloß ein Anwendungsproblem zu sein, sondern eher eine generelle Abneigung gegen das ACME-Konzept und das gesamte Ökosystem darum herum.
Wir haben seit 2019 einige Sites auf LE-Basis betrieben und dabei verschiedene ACME-Clients ausprobiert.
Für unsere Zwecke war zum Beispiel Crypt-LE in Ordnung, und als
le64für die Anbindung an Sectigo ACME nicht ausreichte, haben wir unter anderem certbot, lego und posh-acme getestet.Am Ende haben wir certbot mit einem behobenen GHA-Umgebungsproblem eingesetzt, und auch posh-acme war gut.
Beim erneuten Lesen wurde klar, dass sich der scharfe Ton des Autors nicht gegen ACME oder die Clients richtete, sondern gegen die Spezifikation selbst.
Die Idee hinter ACME sei gut, aber Umsetzung und praktische Anwendung seien enttäuschend.
Ich denke ähnlich wie der Autor.
Er sagt sinngemäß: „Viele bestehende Clients sind riskanter Code, und ich vertraue ihnen nicht genug, um sie mit Root-Rechten auf meinem Server auszuführen.“
Bei sicherheitskritischen Aufgaben halte ich diese Vorsicht für gerechtfertigt.
Für Leute, denen der Ton des Originaltexts schwer verständlich war, wird zur Einordnung auf ältere Beiträge verwiesen.
Es gibt viele Menschen, die es grundsätzlich nicht mögen, auf ihrem Server irgendetwas Unverständliches laufen zu lassen, und ich kann das nachvollziehen.
Gleichzeitig ist der Sicherheitsbereich nun einmal ein Katz-und-Maus-Spiel und muss sich daher ständig verändern; am Ende muss man eben mitgehen.
Zum Glück lässt ACME einem die Freiheit, einen eigenen Client zu bauen.
Man muss nicht unbedingt certbot verwenden, und es ist auch keine Struktur wie TPM, die einem die eigenen Ressourcen entzieht.
Wer einen ACME-Client von Grund auf selbst implementieren will, berichtet, dass das direkte Lesen der RFCs (und der zugehörigen JOSE-Dokumente) überraschend gut machbar sei.
Er hat selbst eine Implementierung geschrieben und außerdem eine Zusammenfassung erstellt, die den ACME-v2-Ablauf erklärt: https://www.arnavion.dev/blog/2019-06-01-how-does-acme-v2-work/
Sie ersetzt die offiziellen RFCs nicht, eignet sich aber gut als Flussdiagramm und als Index nach Vorgehensweisen.
Als Abschlussprojekt in einem MIT-Sicherheitskurs musste man auch selbst einen ACME-Client implementieren: https://css.csail.mit.edu/6.858/2023/labs/lab5.html
Spöttisch wird angemerkt, dass man offenbar nicht das Handbuch lesen sollte, sondern lieber auf Hacker News einen englischen Beitrag schreibt, der den gesamten Prozess ausformuliert erklärt, weil das mehr Internetpunkte bringt.
Es wird dem Autor dafür gedankt, dass er auf die immer weiter zunehmende Komplexität von Webinfrastruktur-Protokollen hinweist.
Solche Standards sind nicht nur für Entwickler belastend, die einfach nur Tools oder Clients verwenden wollen, sondern wirken wie eine Art „regulatorische Eintrittsbarriere“, durch die letztlich nur noch große etablierte Unternehmen die Anforderungen zum Betrieb von Internetdiensten erfüllen können.
ACME allein ist noch keine unüberwindbare Hürde, aber in der Summe wächst daraus eben eine Mauer.
OpenBSD hat einen sehr einfachen und leichtgewichtigen ACME-Client, der bereits im Basis-OS enthalten ist.
Soweit ich gehört habe, wurde er neu entwickelt, weil die bestehenden Alternativen zu schwergewichtig waren und der Unix-Philosophie widersprachen.
Schade, dass der Autor diese Richtung offenbar nicht in Betracht gezogen hat.
Mit etwas Mühe ließe sich das vermutlich auch auf andere Betriebssysteme portieren.
Dieser OpenBSD-Client ist aus meiner Sicht eher ein Beispiel dafür, dass die OpenBSD-Philosophie nicht versteht, warum Sicherheit so komplex geworden ist.
Der Client ist dafür gedacht, auf genau dieser Maschine installiert und verwendet zu werden, wobei durch die Trennung der Komponenten verhindert werden soll, dass sie sich gegenseitig beeinflussen.
Das ACME-Protokoll selbst erlaubt aber vollständige Trennung bis hin zum Air-Gap: Webserver, Zertifikatsanforderer und DNS-Server können in völlig getrennten Umgebungen laufen.
Wenn man den integrierten OpenBSD-Client nicht nutzt, wird es zwar komplexer, aber aus Sicht sauberer Sicherheitsarchitektur ist dieser Weg überlegen.
„Einfach OpenBSD installieren und fertig“ ist letztlich nur der bequemere Weg.
Erwähnt wird auch
uacme(https://github.com/ndilieto/uacme).Es ist leichtgewichtiger C-Code, den jemand nach ständigen Problemen mit dem Python-Client von LE als stabile Alternative verwendet hat.
Jemand berichtet, den ACME-Client von OpenBSD selbst zu nutzen, und dass er sehr gut funktioniert.
Die Empfehlung, einen 4096-Bit-RSA-Privatschlüssel zu erzeugen, sei eher schädlich, weil sie nur die Geschwindigkeit für Besucher verschlechtere, während der reale Sicherheitsgewinn gegenüber 2048 Bit gering sei.
Es sei deutlich besser, ein 2048-Bit-Leaf-Zertifikat zu verwenden.
Es wird gefragt, ob 4096 Bit nicht widerstandsfähiger gegen passive Aufzeichnung mit späterer Entschlüsselung seien.
Außerdem wird gefragt, ob die Sicherheit von Zwischenzertifikaten ebenfalls Auswirkungen auf asynchrone Angriffe hat.
Da ein Webhoster nur RSA-Schlüssel unterstützt, nutzt jemand absichtlich 4096-Bit-RSA, um Druck zu machen, damit endlich EC-Schlüssel unterstützt werden.
Solche Dinge selbst umzusetzen verbessert zwar die eigenen Fähigkeiten, aber der Ton des Autors wirkte trotzdem eher wie allgemeiner Frust über das Protokoll oder den Ablauf bei Let’s Encrypt.
Mit leichtgewichtigen ACME-Bibliotheken wie https://github.com/jmccl/acme-lw lässt sich das doch ausreichend automatisieren; deshalb ist unklar, warum man es sich so schwer macht.
Die ganzen Flat-/Bitfield-Probleme seien historische Altlasten von ASN.1/X.509; die mathematische Komplexität sei enorm, und alle Bibliotheken und die gesamte Softwarewelt hingen noch an den technischen Grenzen der 80er Jahre.
Bei der Einführung von Let’s Encrypt oder auch von HTTP/2 habe es jeweils die letzte Chance gegeben, dieses Chaos aufzuräumen, aber in der Realität musste eine ACME-CA schon mit Shell-Skripten, OpenSSL und Alkohol auskommen, und wegen der Kompatibilität zu bestehender Software sei der große Sprung ausgeblieben.
Es wird die Erfahrung geteilt, dass der Druck zur vollständigen Umstellung auf HTTPS immer weiter zunimmt.
So lassen sich etwa HTTP-Links in WhatsApp inzwischen nicht mehr öffnen.
Mit Proxys und Caching könne man die Traffic-Last reduzieren, was für kleine Server eine gute Methode sei.
Es wird betont, dass ACME trotz aller Komplexität immer noch viel besser ist als gar keine TLS-Unterstützung.
Es wird aufgezählt: „RSA-Schlüssel, SHA256-Digest, RSA-Signatur, base64 das in Wirklichkeit kein base64 ist, String-Verkettung, JSON in JSON, der
Location-Header als Identifikator statt 301-Redirect, HEAD-Requests für einen einzelnen Header-Wert, für jede Anfrage ein separater Request nur für den Nonce usw.“„Und dann kommen noch kompliziertere Schritte wie das Erstellen der Zertifikats-Order, das Verarbeiten von Autorisierungen und Challenges, der Key-Thumbprint, der Aufbau des TXT-Records und mehr.“
Die Komplexität sei kaum zu glauben, und es wird ausdrücklich dafür gedankt, dass jemand die Zusammenhänge einmal aufbereitet und geteilt hat.