Im August ging es hier darum, wie ich meinen OpenPGP-Schlüssel über Keyoxide an DNS, GitHub und Matrix gebunden habe. Seitdem hat sich beim Werkzeug selbst einiges getan, und das ist einen kurzen Nachtrag wert: es gibt jetzt eine Linux-App im Flathub, die Android-App wurde aktualisiert, und für Fragen rund um Keyoxide gibt es einen eigenen Matrix-Kanal.
Die Linux-App im Flathub
Bisher habe ich mein eigenes Profil und die meiner Kontakte immer im Browser aufgerufen. Jetzt gibt es dafür eine eigenständige Anwendung, installierbar direkt aus dem Flathub. Wichtig für die Einordnung: das ist ein Community-Paket, gepflegt vom Entwickler Berker und nicht offiziell vom Keyoxide-Projekt selbst herausgegeben, auf der Flathub-Seite steht das auch so. Unter der Haube steckt dieselbe Flutter-Codebasis wie in der Android-App, `keyoxide-flutter`.
Auf meinem Linux Mint installiert, sieht der Einstieg schlicht aus: ein Suchfeld für ein beliebiges Profil, und darunter die eigene Profilansicht mit allen bestätigten Identity-Claims.
Die Keyoxide-App aus dem Flathub, Startbildschirm mit Suchfeld.
Das eigene Profil in der App, alle vier Claims mit grünem Haken.
Inhaltlich ändert das nichts an dem, was im August-Beitrag steht, DNS, GitHub und Matrix bleiben bestätigt, aber eine native App ist für den täglichen Gebrauch angenehmer als jedes Mal die richtige URL zu tippen.
Das Android-Update
Parallel zur Linux-App wurde auch die Android-Version aktualisiert, Version 2.4.4 auf Codeberg. Gleicher Entwickler, gleiche Codebasis, zwei Plattformen aus einer Quelle.
Der QR-Code, einmal nachgemessen
Die App zeigt unter „Profile ID QR“ einen QR-Code fürs eigene Profil, darunter als Klartext den sogenannten Key Link, die WKD-Adresse des öffentlichen Schlüssels. Bevor der hier landet, wollte ich wissen, ob der Code wirklich auf die richtige Datei zeigt und nicht auf einen veralteten Stand.
Profile ID QR mit dem Key Link im Klartext darunter.
Zwei Dinge lassen sich hier unabhängig nachrechnen. Erstens der Pfad selbst: Web Key Directory bildet die Adresse aus einem SHA-1-Hash des lokalen Teils der E-Mail-Adresse, Z-Base-32-kodiert. Für kernel-error@kernel-error.com ergibt sha1("kernel-error"), Z-Base-32-kodiert, exakt 3gyjbxx9xfdggpkmx5qdd793xy431w5u, also genau der Wert im Key Link. Zweitens der Inhalt: die Datei unter diesem Pfad lässt sich herunterladen und mit gpg --show-keys auswerten, der Fingerabdruck darin ist 45FC D081 ADB5 4872 EA5B 06B9 893D E0CD DE98 6DEB, derselbe wie im Screenshot und derselbe wie der seit Juli aktuelle Schlüssel. Der QR-Code zeigt also tatsächlich auf die richtige, aktuelle Datei, nicht auf einen Cache-Stand oder eine falsche Kodierung.
Ein Matrix-Kanal für Fragen
Wer selbst mit Keyoxide anfangen will oder unterwegs hängen bleibt, findet jetzt einen eigenen Raum für allgemeine Fragen: #keyoxide:matrix.org. Ich lese dort mit.
Moin. Der letzte Beitrag endete mit einem Satz, der mir seitdem im Kopf herumspukt: „Nichts hier braucht eine Smartcard, und nichts hier braucht root.“ Das stand in der Restrisiken-Liste des Ed25519-Beitrags, gleich neben dem Eingeständnis, dass eine Smartcard das größte verbleibende Risiko zwar verkleinern, aber nicht auflösen würde. Genau diese Lücke wollte ich mir ansehen: taugt eine OpenPGP-Karte dafür, den Primärschlüssel offline vorzuhalten, statt ihn nur auf einem Cold-Storage-Stick liegen zu lassen?
Die kurze Antwort: nein, technisch geht das nicht. Eine OpenPGP-Karte kennt genau drei Schlüsselslots, Signatur, Verschlüsselung und Authentifizierung. Für den Zertifizierungsschlüssel, also den, der die Unterschlüssel überhaupt erst beglaubigt, gibt es keinen Slot. Der Primärschlüssel mit der Fähigkeit [C] passt schlicht nicht drauf.
Die längere Antwort ist der eigentliche Grund für diesen Beitrag. Die Karte macht etwas anderes, mindestens genauso viel wert. Sie nimmt die drei Unterschlüssel auf, die du im Alltag tatsächlich benutzt, und macht sie hardwaregebunden. Nicht kopierbar, nicht extrahierbar, während der Primärschlüssel unverändert offline liegen bleibt. Diese Erwartungskorrektur gehört an den Anfang und nicht als Fußnote irgendwo im Text, weil sie der Grund ist, warum dieser Beitrag neben dem Ed25519-Beitrag überhaupt eine eigene Daseinsberechtigung hat.
So kommt eine OpenPGP-Karte an, hier eine blanko Smart Card V3.4 vom FLOSS-Shop. Der Chip schimmert schon durch den Umschlag.
Das Testsetup
Bevor irgendein echter Schlüssel in die Nähe der Karte kommt, brauchte es eine Umgebung, in der ich ohne Risiko herumprobieren kann. Die Eckdaten:
Karte
OpenPGP Smart Card V3.4, ID-1 blanko, FLOSS-Shop, Serie 0000D961, Chip von ZeitControl
Screenshots dürfen ungeschwärzt raus, und ich wollte mit factory-reset und wiederholten Fehlversuchen experimentieren können, ohne das eigentliche Setup zu gefährden
Der Reader ist derselbe cyberJack pinpad(a), den ich mir vor einer Weile aus Neugier auseinandergenommen hatte. Für diesen Beitrag zählt an ihm eigentlich nur eine Eigenschaft: er hat eine eigene Zifferntastatur samt Display, PINs gehen also nie über die Tastatur deines Rechners und damit auch nie durch dessen Speicher.
Reader-Setup: zwei Fallstricke, bevor überhaupt eine Karte drinsteckt
Der erste Stolperstein kam, bevor ich überhaupt eine Karte eingesteckt hatte. scdaemon, der Smartcard-Daemon von GnuPG, versucht standardmäßig, den internen CCID-Treiber zu benutzen. Der cyberJack spricht aber ein eigenes Vendor-Protokoll über PC-SC, kein CCID, und scdaemon quittiert das mit einem schlichten „No such device“. Die Lösung steht in der Konfigurationsdatei, nicht auf der Kommandozeile:
disable-ccid
Die Zeile gehört in scdaemon.conf. Danach findet gpg den Reader über den PC-SC-Layer, und pcscd übernimmt.
Der zweite Stolperstein trat immer wieder auf, sobald ich scdaemon während der Arbeit neu gestartet oder abgeschossen habe. Der Reader blieb dann als „busy“ hängen, pcscd quittierte jeden Zugriffsversuch mit pcsc_connect: sharing violation. Ein gpgconf --kill scdaemon allein hat das nie behoben. Zuverlässig geholfen hat nur ein Neustart des Dienstes selbst:
systemctl restart pcscd
Beide Punkte sind auf jeden anderen PC-SC-Reader mit eigenem Vendor-Protokoll übertragbar, nicht nur auf den cyberJack, deswegen stehen sie hier als eigener Abschnitt.
Der cyberJack pinpad(a) mit eingesteckter Karte. Alles, was danach an PIN eingegeben wird, läuft über die Tasten hier und nicht über die Rechnertastatur.
Erster Schritt: die Standard-PINs ändern
Der Karte liegt eine kleine Karteikarte bei, und die erklärt in aller Ruhe, warum der allererste Schritt kein optionaler ist:
Standard-PIN 123456, Admin-PIN 12345678, bei jeder blanko Karte identisch. Drei falsche PIN-Versuche sperren die Karte, drei falsche Admin-PIN-Versuche löschen die Daten.
Standard-PIN und Admin-PIN sind bei jeder blanko Karte identisch, stehen also öffentlich in jedem Handbuch. Wer sie nicht ändert, hat effektiv gar keinen PIN-Schutz. In gpg --card-edit geht das über admin, gefolgt von passwd:
Pinentry zeigt nur den Hinweis, das Pinpad des Readers zu benutzen. Der Admin-PIN selbst taucht auf dem Bildschirm nie auf.
Derselbe Dialog für den normalen PIN. Der Retry-Zähler danach steht bei drei von drei, unverändert gegenüber vorher.
Wichtig an beiden Dialogen ist derselbe Punkt: der eigentliche PIN geht nie über den Rechner, weder alt noch neu. Pinentry zeigt nur einen Hinweis an, die Eingabe passiert komplett am Pinpad. Genau dafür gibt man das Geld für einen Reader mit eigener Tastatur aus, sonst könnte ein kompromittierter Rechner den PIN einfach mitschneiden.
Das erste Kartenlimit: Curve 25519 scheitert
Im jungfräulichen Zustand meldet die Karte für alle drei Slots rsa2048, keine Schlüssel gesetzt:
Der Zustand direkt nach dem Auspacken. rsa2048 überall, keine Schlüssel, die Referenz für alles, was danach kommt.
Weil der Ed25519-Beitrag genau davon handelte, war meine erste Idee naheliegend: die Kartenattribute per key-attr auf Curve 25519 umstellen, dieselbe Kurve wie beim Primärschlüssel. Curve 25519 steht im Menü sogar als Default. Die Karte lehnt sie trotzdem ab, mit Statuswort 6A80, sowohl für den Signatur- als auch für den Verschlüsselungsslot:
key-attr bietet Curve 25519 als Default an, aber die Karte quittiert die Umstellung mit Card error.
Tückisch daran: scdaemon meldet bei den ersten beiden Versuchen im Terminal nicht einmal einen klaren Fehler, sondern läuft einfach in die nächste Abfrage weiter. Wer sich auf den Bildschirmtext verlässt statt hinterher mit card-status nachzuschauen, merkt das Scheitern leicht gar nicht. Das FLOSS-Shop-Datenblatt zur Karte bestätigt im Nachhinein, warum: gelistet sind nur „NIST/ANSI“ und „Brainpool“, Curve 25519 taucht in der Aufzählung schlicht nicht auf. Funktioniert haben stattdessen NIST P-384 und Brainpool P-256:
Brainpool P-256 für Signatur- und Verschlüsselungsslot läuft sauber durch, jede Umstellung verlangt erneut den Admin-PIN am Pinpad.
Für Signatur und Verschlüsselung bin ich bei Brainpool P-256 geblieben, den Authentifizierungsslot habe ich später aus einem eigenen Grund auf NIST P-384 umgestellt. Dazu gleich mehr, erst kommt aber ein zweites Kartenlimit, das mit Kurven gar nichts zu tun hat.
Der Admin-PIN-Timeout: kein Zufall, sondern ein hartes Zeitlimit
Bei den ersten Versuchen mit key-attr schlug die Admin-PIN-Eingabe am Pinpad immer wieder mit Statuswort 6400 fehl, ohne dass der PIN-Retry-Zähler sich bewegte. Mein erster Verdacht war eine Race Condition zwischen scdaemon und dem Reader. Also habe ich mitgestoppt, wie lange die Eingabe am Pinpad tatsächlich dauert, bis das OK gedrückt ist:
Eingabedauer
Ergebnis
Fälle
3 bis 5 Sekunden
SW 9000, Erfolg
8 von 8
7 bis 16 Sekunden
SW 6400, Fehler
14 von 14
Keine Race Condition also, sondern ein hartes Zeitlimit von etwa fünf bis sechs Sekunden für die PIN-Eingabe am Pinpad selbst. Der PIN-Retry-Zähler bleibt bei einem Timeout unberührt, du verlierst also keinen Versuch, nur Zeit. Einen Konfigurationsschalter dagegen habe ich nicht gefunden, ich habe die komplette Flag-Liste der cyberjack.conf durchsucht. Die Option enable-pinpad-varlen in scdaemon.conf senkt die Fehlerquote spürbar, weil sie variable PIN-Längen am Pinpad erlaubt statt auf eine feste Länge zu warten, behebt das Problem aber nicht vollständig. Praktisch heißt das: PIN vorher im Kopf bereithalten, zügig eintippen, und bei einem Fehlschlag sofort denselben Schritt wiederholen statt lange zu überlegen.
Den Demo-Schlüsselsatz bauen
Mit den bekannten Kartenattributen im Kopf habe ich einen Demo-Schlüsselsatz gebaut, passend zugeschnitten: ein Ed25519-Primärschlüssel mit ausschließlich der Zertifizierungsfähigkeit [C], dazu drei Unterschlüssel in brainpoolP256r1 für Signatur und Verschlüsselung. Der naheliegende Befehl dafür scheitert allerdings:
Bei NIST- und Brainpool-Kurven kennt der Quick-Befehl offenbar nur ECDH als Default-Verwendungszweck, eine Signaturfähigkeit lehnt er direkt ab. Der Umweg über den ausführlichen Editiermodus funktioniert dagegen anstandslos, dort lässt sich die Kurve explizit wählen und die Fähigkeit im Nachhinein umschalten:
gpg --expert --edit-key 7C7A529A359FA9F8A13DF0868AEA158652F8C926
gpg> addkey
Please select what kind of key you want:
(11) Existing key
Your selection? 11
Please select which elliptic curve you want:
(6) Brainpool P-256
Your selection? 6
Possible actions for this ECDH key: Sign Encrypt
Current allowed actions: Encrypt
(S) Toggle the sign capability
(Q) Finished
Your selection? S
Your selection? Q
So entstanden, in Reihenfolge, der Verschlüsselungs-, der Signatur- und vorläufig auch der Authentifizierungs-Unterschlüssel, alle drei in Brainpool P-256. Der fertige Satz vor dem eigentlichen Übertragen auf die Karte:
Ed25519-Primärschlüssel mit reiner Zertifizierungsfähigkeit, darunter drei brainpoolP256r1-Unterschlüssel. Noch alle im lokalen Schlüsselbund, noch keiner auf der Karte.
keytocard für alle drei Unterschlüssel
Das eigentliche Übertragen läuft für jeden Unterschlüssel einzeln über keytocard, mit den üblichen Wiederholungen, wenn der Admin-PIN-Timeout wieder einmal zuschlägt:
gpg --edit-key 7C7A529A359FA9F8A13DF0868AEA158652F8C926
gpg> key 1
gpg> keytocard
Please select where to store the key:
(2) Encryption key
Your selection? 2
gpg> key 1
gpg> key 2
gpg> keytocard
Please select where to store the key:
(1) Signature key
Your selection? 1
Verifiziert wird das Ergebnis über gpg --card-status. Alle drei Fingerabdrücke auf der Karte stimmen mit dem lokalen Schlüsselbund überein, und dort steht jetzt ssb> statt nur ssb, also der Hinweis, dass der private Schlüsselteil nicht mehr lokal liegt, sondern nur noch als Verweis auf die Karte:
Alle drei Slots besetzt, alle drei Fingerabdrücke stimmen, und ssb> markiert im Schlüsselbund, dass der private Teil nur noch auf der Karte existiert.
Der SSH-Nachtrag: Brainpool kennt SSH nicht
Den Authentifizierungs-Unterschlüssel wollte ich zusätzlich als SSH-Schlüssel benutzen, dafür exportiert gpg-agent ihn über gpg --export-ssh-key. Für den Brainpool-Auth-Unterschlüssel scheitert das mit „Unknown elliptic curve“. Der Grund liegt nicht bei GnuPG: RFC 5656, der SSH-Standard für elliptische Kurven, kennt Brainpool schlicht nicht, nur nistp256, nistp384 und nistp521. Also habe ich den Authentifizierungsslot noch einmal neu erzeugt, diesmal mit NIST P-384, während Signatur und Verschlüsselung bei Brainpool P-256 geblieben sind, und den neuen Unterschlüssel erneut per keytocard übertragen.
Dabei sind mir zwei weitere Kleinigkeiten begegnet, beide nicht offensichtlich:
Meine eigene ~/.ssh/config mit einem globalen Host * / IdentitiesOnly yes blendet Card-Keys komplett aus, unabhängig davon, ob man den Agenten zusätzlich per -o IdentityAgent=... explizit angibt. Für einen sauberen Test half nur -F /dev/null, um die eigene Konfiguration ganz zu umgehen.
gpg-agent verlangt den Keygrip des Auth-Unterschlüssels explizit in sshcontrol, sonst kommt „agent refused operation“, und zwar noch bevor überhaupt ein Pinpad-Prompt erscheint. Kein PIN-Fehler, sondern eine reine Allowlist-Sache, die sich leicht mit einem PIN-Problem verwechseln lässt.
Drei Live-Tests, alle drei erfolgreich
Zum Schluss die eigentliche Nagelprobe, alle drei Unterschlüssel einzeln gegen die Karte getestet, mit dem Pinpad als einzigem Ort für die PIN-Eingabe.
Signatur. Eine Testnachricht clearsignen und gleich wieder verifizieren:
clearsign fragt den PIN am Pinpad ab, verify bestätigt danach eine gute Signatur mit dem ECDSA-Schlüssel der Karte.
Verschlüsselung. Ein vollständiger Roundtrip aus Verschlüsseln und Entschlüsseln, wieder mit PIN-Abfrage am Pinpad beim Entschlüsseln, der Klartext kommt danach unverändert zurück:
echo "Test von der OpenPGP-Karte" | gpg --encrypt --recipient card-demo@example.invalid | gpg --decrypt
gpg: encrypted with brainpoolP256r1 key, ID ..., created ...
"OpenPGP Card Demo (throwaway key for blog/card demo, not for real use) <card-demo@example.invalid>"
Test von der OpenPGP-Karte
SSH. Ein Login gegen den eigenen Rechner über den Auth-Unterschlüssel, mit einem nur für den Test temporär ergänzten und danach wieder entfernten Eintrag in authorized_keys:
Der Login fragt am Pinpad nach dem PIN und ist danach ein ganz normales SSH-Login, der einzige Unterschied gegenüber einem Schlüssel auf der Festplatte ist der Prompt am Reader.
Fazit: was die Karte wirklich bringt
Zurück zur Ausgangsfrage. Nein, eine OpenPGP-Karte hält deinen Primärschlüssel nicht offline vor, weil sie keinen Zertifizierungsslot hat und ihn deswegen gar nicht aufnehmen kann. Wer genau das sucht, bleibt bei einem verschlüsselten Cold-Storage-Medium, wie im Ed25519-Beitrag beschrieben.
Was die Karte stattdessen bringt, ist aus meiner Sicht mindestens genauso viel wert: die drei Unterschlüssel, mit denen du tatsächlich täglich arbeitest, Signatur, Verschlüsselung und Authentifizierung, wandern hardwaregebunden auf ein Stück Plastik. Kein keytocard lässt sich rückgängig machen, kein Angreifer mit Zugriff auf deine Festplatte bekommt das Schlüsselmaterial zu fassen, denn es liegt dort gar nicht mehr. Der Primärschlüssel bleibt davon komplett unberührt und weiterhin offline, genau da, wo er laut Restrisiken-Liste vom letzten Mal ohnehin hingehört. Die Smartcard aus dem Satz „Nichts hier braucht eine Smartcard“ wollte etwas anderes lösen, als eine Karte lösen kann. Für das, was sie tatsächlich kann, würde ich sie nach diesem Test jederzeit empfehlen, nur eben mit einem echten statt einem Wegwerf-Demo-Schlüssel und mit etwas Geduld für den Admin-PIN-Timeout.
Wenn du selbst gerade zwischen Karten oder Kurven schwankst, oder wenn dir am Kartenlimit oder am PIN-Timeout noch ein Detail auffällt, das ich übersehen habe, dann dürft ihr mich sehr gerne fragen.
Am kommenden Wochenende ist wieder FrOSCon. Samstag der 15. und Sonntag der 16. August, wie immer an der Hochschule Bonn-Rhein-Sieg in Sankt Augustin, Grantham-Allee 20. Der Eintritt ist frei, es laufen sieben Vortragsschienen und zwei Workshop-Räume parallel, samstagabend gibt es das Social Event. Das Programm steht online.
Es ist die 21. Ausgabe. Die erste war 2006, die Konferenz wird also gerade zwanzig. Ich bin an beiden Tagen dort und würde mich sehr freuen, wenn wir uns treffen und ein bisschen quatschen. Also: ist noch jemand von euch da? Sag mir gerne vorher Bescheid, dann verabreden wir uns, oder sprich mich einfach an, wenn du mich irgendwo im Foyer siehst.
Und falls du noch nie dort warst: fahr hin. Der Eintritt kostet nichts, das Programm ist gut, und diese Mischung aus Vorträgen, Ständen und Leuten, die dir ungefragt ihr Projekt erklären wollen, gibt es in der Form nicht oft.
Warum es nicht jedes Jahr klappt
Ich versuche eigentlich jedes Jahr, auf die FrOSCon zu kommen. Das scheitert regelmäßig daran, dass meine Kinder rund um dieses Wochenende Geburtstag haben, und das geht dann selbstverständlich vor. Dieses Jahr passt es, deshalb schreibe ich das hier überhaupt.
Beim Zusammensuchen der alten Beiträge habe ich dann etwas gefunden, das mich kurz sehr still gemacht hat. In meinen Entwürfen liegt ein Beitrag vom 9. August 2013, der nie veröffentlicht wurde. Darin steht:
Ich habe gerade die Bestätigung meines FrOSCon Tickets bekommen 🙂 Die FrOSCon ist dieses Jahr vom 24.08-25.08.2013 in Sankt Augustin. Ich schaffe es nicht immer zur FrOSCon denn meine Kinder haben immer um diesen Dreh Geburtstag. Das geht dann vor! In diesem Jahr klappt es genau und dieses freut mich natürlich umso mehr! Wir sehen uns also auf der FrOSCon….
Ich, in einem Entwurf vom 9. August 2013, den ich nie veröffentlicht habe
Dreizehn Jahre später sitze ich hier und schreibe im Grunde denselben Absatz noch einmal. Gleiche Konferenz, gleicher Ort, gleiche Terminkollision, gleiche Freude, dass es diesmal passt. Nur die Kinder sind inzwischen deutlich größer. Den Beitrag von 2013 lasse ich übrigens in der Schublade, der hat sich seinen Platz dort verdient.
Und die Kinder?
Möglicherweise dreht sich die Sache dieses Jahr sogar. Teckids e.V. macht an beiden Tagen die Mini-FrogLabs, das Jugendprogramm der Konferenz. Samstag geht es um Luanti, also eigene Mods und Kreaturen in 3D, Sonntag um Spieleprogrammierung mit Python. Gedacht ist das etwa ab der vierten Klasse bis sechzehn, Vorkenntnisse braucht niemand, bezahlt wird so viel man mag, und man meldet sich für jeden Tag einzeln an. Das schaue ich mir für meine Kinder auf jeden Fall an. Nach all den Jahren, in denen die Kinder der Grund waren, nicht hinzufahren, wäre das eine schöne Wendung.
Wie das bei mir angefangen hat
Wann ich zum ersten Mal dort war, kriege ich nicht mehr sicher zusammen. Mein Gefühl sagt 2006, belegen kann ich 2007. Es gibt Bilder aus dem Jahr in meiner Galerie, in einem Beitrag von 2013 hängt ein Schlüsselband von der FrOSCon 2007 an meinem Schlüsselbund, und der Ausweis hängt bis heute bei mir im Arbeitszimmer an der Wand.
Der Ausweis von 2007 hängt noch im Arbeitszimmer, rechts senkrecht steht Aussteller. Darunter das hellblaue Schild von 2013, das belegt, dass ich in dem Jahr tatsächlich dort war, auch wenn der Beitrag dazu nie erschienen ist. Rechts daneben der Besucherausweis der OpenRheinRuhr 2010.
Damals habe ich am Stand von CAcert mitgeholfen, einer von der Gemeinschaft betriebenen Zertifizierungsstelle, die kostenlos Zertifikate ausgestellt hat, lange bevor das normal war. Das Vertrauen kam dort nicht von einem Audit, sondern von Menschen: Punkte gab es, wenn man einem Assurer seinen Ausweis zeigte. Genau das haben wir den ganzen Tag gemacht, und ich fand das damals ganz große Klasse.
Geworden ist daraus nichts. Der Antrag auf Aufnahme in den Truststore von Mozilla wurde noch im selben Jahr zurückgezogen, und bis heute vertraut kein Browser dieser Wurzel. Ein curl auf die Seite bricht immer noch mit self-signed certificate in certificate chain ab, gerade nachgesehen. Gelöst hat das Problem später Let’s Encrypt, mit einem Protokoll statt mit Menschen und Ausweisen. Technisch die bessere Antwort, ein bisschen wehmütig macht es mich trotzdem.
Hängengeblieben ist trotzdem etwas. Mein Interesse an Zertifikaten, Schlüsseln und dem ganzen Vertrauenskram hat genau dort angefangen, und das hat mich seitdem nicht mehr losgelassen.
Die Keysigning-Party
Zum selben Themenkreis gehört das andere, wozu ich früher immer gegangen bin, wenn es sich einrichten ließ: die GPG-Keysigning-Party. Das Prinzip war schön unaufgeregt. Vorher schickt jeder seinen Schlüssel-Fingerprint ein, vor Ort steht man in einer Reihe, vergleicht die Zeile auf dem ausgedruckten Zettel mit dem, was die andere Person vorliest, sieht sich einen Ausweis an, hakt ab. Zu Hause signiert man dann die Schlüssel, deren Zeile gestimmt hat. Eine Stunde Anstehen, ein Blatt Papier, und danach hängt das eigene Schlüsselbund ein Stück fester im Netz.
Im offiziellen Zeitplan für dieses Jahr finde ich keine Keysigning-Party. Das muss nichts heißen, solche Runden organisieren sich oft nebenbei und stehen dann in keinem Programmheft. Falls sich jemand findet, bin ich sofort dabei. Und ganz unabhängig davon, weil es der einfachere Weg ist: wenn wir uns auf der FrOSCon über den Weg laufen, würde ich mich sehr über ein Cross-Signing meines neuen Schlüssels freuen. Der ist frisch, trägt die Kennung 0x893DE0CDDE986DEB, den Fingerprint bringe ich ausgedruckt mit und den Personalausweis sowieso. Wie ich ihn gebaut und woran ich ihn beim Härten kaputt gemacht habe, steht hier im Blog. Die Rolle, die früher der Ausweis am Klapptisch hatte, übernehmen bei mir mittlerweile nachprüfbare Identity-Claims. Funktioniert gut, ersetzt aber das Anstehen nicht.
Ein Gruß an Jens Link, und eine offene Frage
Aus den frühen Jahren ist mir ein Vortrag bis heute im Kopf geblieben, und zwar einer über IPv6 von Jens Link. Der Mann hat es geschafft, mich mit Adressarchitektur und Autokonfiguration zum Lachen zu bringen und mir im selben Vortrag klarzumachen, dass da erheblich mehr dahintersteckt als eine Pflichtaufgabe, die man irgendwann mal erledigen muss. Das können nicht viele. Ich bin danach an dem Thema hängengeblieben und habe es nie wieder losgelassen.
Fair muss ich dazusagen, dass ich nicht durch ihn angefangen habe. Mein ältester Beitrag zu IPv6 hier im Blog ist vom März 2003, vier Jahre vor diesem Wochenende, es ging um Adressarchitektur und radvd unter Linux. Dass daraus vierunddreißig Beiträge geworden sind, verteilt über die Jahre bis 2021, dafür trägt dieser Vortrag aber einen guten Teil der Schuld. Wenn man einmal gesehen hat, wie jemand über so ein Thema begeistert und komisch reden kann, schaut man danach anders hin. Einen Gruß nach Berlin also, und ein Dankeschön obendrauf.
Genau erinnern kann ich mich nach knapp zwanzig Jahren natürlich nicht mehr, weder an das Jahr noch an die Folien. Der Eindruck ist geblieben, das reicht mir.
Und eine Frage, bei der ihr mir helfen könnt. In meiner Erinnerung wollte er damals ein Buch über IPv6 schreiben. Ich habe es nie gesehen, was aber genauso gut daran liegen kann, dass es an mir vorbeigegangen ist. Auf seiner Seite finde ich keins, und sein Vortragsarchiv reicht nicht so weit zurück, dass ich damit etwas belegen könnte. Weiß es jemand? Ist das Buch erschienen, ist es liegen geblieben, oder habe ich mir das über die Jahre selbst zusammengebaut? Im Fediverse ist er als @quux unterwegs.
Worauf ich schauen will
Festlegen will ich mich nicht, das wird bei mir sowieso spontan. Auf dem Zettel stehen aber diese acht:
Wenn ich die Liste so ansehe, ist das Muster ziemlich eindeutig. Sechs von acht liegen in der Security-Schiene, die anderen zwei kommen aus dem Themenblock zum Recht auf Reparatur, und mehrfach geht es um Haftung und Regulierung statt um Technik. Das ist neu für mich. Drei davon laufen am Samstag im selben Raum, zwei sogar direkt hintereinander, das kommt mir sehr entgegen. Ob ich es aus dem CrypTool-Workshop rechtzeitig rausschaffe, steht auf einem anderen Blatt, der Zeitplan verrät keine Endzeiten.
Die Stände, an denen ich hängen bleiben werde
Auf meiner Runde stehen das CrypTool Projekt, ElectroBSD, Freie Software und Bildung, illumos, Matrix, The PHP Foundation, XMPP und der Datenburg e.V. Bei illumos werde ich vermutlich am längsten stehen, aus reiner Nostalgie, mein Solaris-Archiv hier im Blog hat immerhin achtunddreißig Beiträge. Bei The PHP Foundation habe ich sogar eine konkrete Frage im Gepäck, nachdem ich mich neulich durch einen PHP-Absturz bis in den Laufzeitlinker von FreeBSD gegraben habe.
Auf der Ausstellerliste steht übrigens ein Name, bei dem ich kurz grinsen musste: CAcert ist dieses Jahr wieder mit einem Stand da, neunzehn Jahre später, im selben Gebäude. Die Wurzel liegt weiterhin in keinem Truststore, die Leute stehen trotzdem wieder hinter dem Tisch. Da gehe ich vorbei und sage einmal Danke.
Zwanzig Jahre
Was mich beim Schreiben am meisten getroffen hat, ist nicht die CAcert-Geschichte, sondern die Rechnung dahinter. Von meinem belegten ersten Besuch bis heute sind es neunzehn Jahre. Rechne ich mein Gefühl mit, dass es 2006 war, sind es zwanzig. Die Konferenz gibt es genauso lange. Zwischen dem Klapptisch mit den Assurance-Formularen und dem Beitrag, den ich neulich über einen TPM-Chip geschrieben habe, liegt also ziemlich genau mein halbes Leben mit diesem Hobby. Ich habe mich beim Nachrechnen ehrlich alt gefühlt.
Andererseits: das Wochenende ist immer noch dasselbe Wochenende. Gleiche Hochschule, freier Eintritt, Leute, die einem an einem Stand irgendetwas erklären, wovon sie viel zu begeistert sind. Das ist eine ziemlich gute Konstante.
Siehe auch:
FrOSCon 2017, der letzte Beitrag dieser Sorte hier im Blog
Wenn du auch auf der FrOSCon bist, sag gerne Bescheid, ich freue mich über jeden Kaffee im Stehen und über jede Signatur. Und wenn du die Sache mit dem IPv6-Buch aufklären kannst, oder sonst etwas dazu wissen möchtest, dann darfst du mich sehr gerne fragen.
Moin. Im letzten Beitrag ging es darum, wie ich meinen OpenPGP-Schlüssel an Domain, GitHub und Matrix gebunden habe, also um die Frage, woher eigentlich irgendwer wissen soll, dass dieser Schlüssel mir gehört. Ganz am Ende stand dort die Ankündigung eines zweiten Teils: wie das Ding überhaupt gebaut ist und wie sicher es wirklich ist. Der Teil ist jetzt hier.
Der Beitrag hat zwei Hälften, und die kannst du unabhängig voneinander lesen. Teil A ist ein Rezept. Wenn du einen Schlüssel nach demselben Muster bauen willst, arbeitest du dich von oben nach unten durch, inklusive der kompletten GnuPG-Konfiguration, alles kopierbar und in der richtigen Reihenfolge. Teil B erklärt, was die einzelnen Bauteile bedeuten, und endet mit einer ehrlichen Einschätzung, wie sicher das alles am Ende ist.
Der eigentliche Grund, warum ich das aufschreibe, ist aber ein anderer. Zwischen „mein Schlüssel ist modern“ und „mein Schlüssel ist richtig konfiguriert“ liegt eine Lücke, und in die bin ich mit Anlauf hineingesprungen. Der Schlüssel ist Lehrbuch: Ed25519, ein Hauptschlüssel, der ausschließlich zertifiziert, drei getrennte Unterschlüssel, fünf Jahre Laufzeit, über sieben Kanäle veröffentlicht, dazu eine eID-Zertifizierung. Und trotzdem wurde er vom ersten Tag an schwächer angesprochen als der Schlüssel, den er ablöst.
Aufgefallen wäre das niemandem, und genau das ist der interessante Teil. Das einzige Symptom war eine Warnzeile auf dem Terminal von jemandem, der mir eine verschlüsselte Mail schreibt. Also auf einem Rechner, den ich nie zu Gesicht bekomme. Und die Ursache war ausgerechnet eine Härtungsmaßnahme, die ich in bester Absicht eingebaut hatte, um einen musealen Algorithmus loszuwerden. Härtung, die etwas leise verschlechtert, finde ich als Geschichte deutlich spannender als noch eine Schlüsselerzeugungsanleitung. Nachbauen kannst du das übrigens in zwei Minuten, das Labor dazu steht weiter unten.
Der Schlüssel, um den es geht
Bestandteil
Wert
Hauptschlüssel
ed25519 0x893DE0CDDE986DEB, Verwendung [C], also nur zertifizieren
Fingerabdruck
45FC D081 ADB5 4872 EA5B 06B9 893D E0CD DE98 6DEB
Signatur-Unterschlüssel
ed25519 0xD788641D8588A674
Verschlüsselungs-Unterschlüssel
cv25519 0x429D03637892821A
Authentisierungs-Unterschlüssel
ed25519 0x22F2E3234664DDBE
UIDs
Sebastian van de Meer, dazu eine Foto-UID mit 10851 Byte
Gültigkeit
erzeugt am 24.07.2026, läuft am 23.07.2031 ab
Vorgänger
ed25519 0x5F279C362EEAB216, gültig bis 31.12.2026, zeichnet den neuen gegen
Fremdzertifizierung
Governikus/eID 0x5E5CCCB4A4BF43D7, Level 3
Umgebung
GnuPG 2.4.4, libgcrypt 1.10.3
Die Aufgabenteilung steht nicht nur in der Doku, die steht maschinenlesbar im Schlüssel selbst. Jede Bindungssignatur trägt ein Feld mit Schlüssel-Flags, und das sieht bei mir so aus:
Teil A ist der Ablauf, den ich für richtig halte, und bis auf die markierten Stellen genau der, den ich gegangen bin. Dort, wo der Befehl unten besser ist als das, was ich damals getippt habe, steht ein Hinweis dazu.
GnuPG 2.4.x ist die Annahme für den ganzen Beitrag. Zwei Dinge gleich vorweg, weil sie sonst später wehtun:
--quick-generate-keyverweigert den Dienst, wenn die UID bereits auf einem anderen Schlüssel im Schlüsselbund existiert. Genau deshalb ist dieser Schlüssel über eine Parameterdatei mit --batch --gen-key entstanden. Das trifft dich zuverlässig genau dann, wenn du einen Schlüssel rotierst, also in dem Moment, in dem der alte noch da ist.
Nichts hier braucht eine Smartcard, und nichts hier braucht root.
Wenn du bei GPG ganz am Anfang stehst, sind die Grundlagen zum Signieren und Verschlüsseln hier im Blog der bessere Einstieg. Dieser Beitrag setzt voraus, dass du weißt, was ein öffentlicher Schlüssel und ein Unterschlüssel sind.
Erst aufräumen, dann anfangen
Und zwar wirklich vorher, nicht hinterher. Mein ~/.gnupg war in einem Zustand, den ich hier nur ungern zugebe.
chmod 700 ~/.gnupg
find ~/.gnupg -type f -exec chmod 600 {} +
# Erst alle GnuPG-Frontends schließen und die Daemons beenden, sonst löschst du
# unter Umständen echte, aktive Locks:
gpgconf --kill all
# Übriggebliebene Lock-Dateien abgestürzter Läufe. Bei mir waren es 93 Stück.
find ~/.gnupg -name '.#lk*' -delete
# Altlasten, die keine 2.4er Installation mehr braucht
mkdir -p ~/gnupg-attic-$(date +%F)
mv ~/.gnupg/{cert8.db,key3.db,secmod.db} ~/gnupg-attic-$(date +%F)/ 2>/dev/null
Ein ~/.gnupg mit 775 und Dateien mit 664 ist erschreckend verbreitet, und ich will das gar nicht größer machen als es ist: Auf einem Einzelplatzrechner ist das keine Katastrophe. Es wird aber genau in dem Moment eine, in dem das Home-Verzeichnis in ein Backup, in einen Container-Mount oder in einen synchronisierten Ordner wandert. Und das passiert schneller, als man denkt.
Die gpg.conf, komplett
Das ist die tatsächlich laufende Konfiguration, so wie sie bei mir liegt, mit ihren Originalkommentaren. Der Block mit dem Warnhinweis ist die wichtigste Stelle im gesamten Beitrag, warum, steht in Teil B.
# ---- UX / output ----
keyid-format 0xlong
with-fingerprint
with-subkey-fingerprint
utf8-strings
# ---- Key discovery (local first, then WKD/DANE; email-validated keyserver last) ----
auto-key-retrieve
auto-key-locate local,wkd,dane,keyserver
keyserver hkps://keys.openpgp.org
# ---- Strong defaults ----
# cipher-algo / digest-algo are intentionally NOT forced: forcing them would override
# the recipient's stated capabilities. personal-*-preferences below select strong
# algorithms interoperably instead.
cert-digest-algo SHA512
# ---- KDF hardening for passphrase-derived symmetric keys (gpg -c / key export) ----
s2k-mode 3
s2k-digest-algo SHA512
s2k-cipher-algo AES256
s2k-count 65011712
# ---- WARNING. Legacy ciphers: do NOT disable 3DES here ----
# An earlier version of this file carried:
# disable-cipher-algo 3DES
# disable-cipher-algo IDEA
# disable-cipher-algo CAST5
# disable-cipher-algo BLOWFISH
# disable-cipher-algo TWOFISH
# The 3DES line alone silently strips ALL algorithm preferences from every key you
# generate while it is active, and it also blocks DECRYPTION of old archives.
# New encryption never selects a legacy cipher anyway, because
# personal-cipher-preferences lists AES only.
# ---- Privacy / minimal metadata ----
no-comments
no-emit-version
export-options export-minimal
# ---- Listing/verification quality-of-life ----
verify-options show-uid-validity
list-options show-uid-validity
# ---- Trust model ----
trust-model tofu+pgp
# ---- Your key ----
default-key 0x45FCD081ADB54872EA5B06B9893DE0CDDE986DEB
# hidden-encrypt-to 0x45FCD081ADB54872EA5B06B9893DE0CDDE986DEB # optional, off
# ---- Local policy: what *you* prefer when sending ----
personal-cipher-preferences AES256 AES192 AES
personal-digest-preferences SHA512 SHA384 SHA256
weak-digest SHA1
force-ocb
# ---- Preferences baked into keys you create from here on ----
default-preference-list SHA512 SHA384 SHA256 AES256 AES192 AES ZLIB BZIP2 ZIP Uncompressed
Zwei Zeilen darin sind Entscheidungen und keine Selbstverständlichkeiten, deshalb je ein Satz dazu.
force-ocb ist keine Präferenz, sondern eine Erzwingung. Es sorgt dafür, dass ausgehende Nachrichten die OCB-Variante aus der LibrePGP-Linie benutzen, und setzt sich dabei über das hinweg, was der Empfängerschlüssel ankündigt. Sehr alte oder anders implementierte Empfänger können solche Nachrichten nicht lesen. Das steht in einer gewissen Spannung zu dem Prinzip ein paar Zeilen weiter oben, wo ich cipher-algo bewusst nicht erzwinge. Für mich ist das in Ordnung, in einer Konfiguration zum Abschreiben würde ich die Zeile weglassen.
auto-key-retrieve holt unbekannte Schlüssel beim Prüfen automatisch nach. Sehr bequem, und es verrät dem Keyserver, welche signierten Nachrichten du wann liest. Für mich ein akzeptabler Tausch, bei einem anderen Bedrohungsmodell schaltest du das besser ab.
Dazu die beiden kleinen Geschwisterdateien. gpg-agent.conf:
Ein Hauptschlüssel, der nur zertifiziert, über eine Parameterdatei. Die Zeile Preferences: war in meinem echten Lauf nicht drin, nimm sie mit. Sie kostet nichts und fängt genau den Fehler ab, um den es in Teil B geht. Das ist keine Vermutung, ich habe es nachgemessen: Mit dieser Zeile bleiben die Präferenzen selbst dann im Schlüssel, wenn die defekte Konfiguration aktiv ist. Die eigentliche Kontrolle bleibt trotzdem der Paket-Dump gleich darunter, denn eine Parameterdatei ersetzt nie die Prüfung des fertigen Artefakts.
cat > keyparams.txt <<'EOF'
Key-Type: eddsa
Key-Curve: Ed25519
Key-Usage: cert
Name-Real: Sebastian van de Meer
Name-Email: kernel-error@kernel-error.com
Expire-Date: 5y
Preferences: AES256 AES192 AES SHA512 SHA384 SHA256 ZLIB BZIP2 ZIP Uncompressed
%ask-passphrase
%commit
EOF
gpg --batch --gen-key keyparams.txt
shred -u keyparams.txt
Und jetzt sofort nachsehen, ob die Präferenzen auch wirklich im Schlüssel gelandet sind. Genau diese Prüfung fehlte bei mir im Juli, und deshalb steht sie hier direkt hinter der Erzeugung und nicht irgendwo weiter unten.
Du willst dort pref-sym-algos, pref-hash-algos, pref-zip-algos und ein features mit gesetztem Bit 0x01 sehen. Wenn dort nur eine features-Zeile steht und sonst nichts, dann hör hier auf und lies erst den Abschnitt über den Defekt in Teil B. Dann stimmt etwas mit deiner Konfiguration nicht, und der Schlüssel trägt den Fehler ab sofort dauerhaft mit sich herum.
Zum shred oben noch ein Wort, weil es sonst falsche Sicherheit erzeugt: Es entfernt die sichtbare Arbeitsdatei, mehr nicht. Auf SSDs, auf ZFS, bei Snapshots und auf journalenden Dateisystemen ist damit überhaupt nicht garantiert, dass keine alten Blöcke mehr herumliegen. Solche Zwischendateien sollten deshalb von vornherein nur auf einem verschlüsselten Dateisystem entstehen.
Die drei Unterschlüssel
Interaktives --quick-add-key ist mir in einer nicht-interaktiven Shell mit einem /dev/tty-Fehler um die Ohren geflogen. Mit --batch läuft es durch.
240×288 Pixel, Graustufen-JPEG, 10851 Byte. Halt das Bild klein, es reist in jedem vollständigen Export mit und ist der mit Abstand größte Posten in der Schlüsselgröße.
gpg --edit-key $FPR
> addphoto
> /pfad/zu/sebastian-photo-uid.jpg
> save
Eine Sache dazu, die man vorher wissen sollte: Keiner der drei Keyserver, die ich benutze, hat die Foto-UID nach dem Upload je wieder herausgerückt. Für jeden Keyserver da draußen lege ich die Hand nicht ins Feuer, für die drei, auf die es ankommt, schon. Das Bild überlebt damit nur auf den Kanälen, die ich selbst hoste. Damit ist die Foto-UID Dekoration und keine Funktion. Das ist ein völlig legitimer Grund, sie trotzdem mitzunehmen, man sollte sich nur nichts vormachen.
Backup und Widerrufszertifikat, bevor irgendetwas veröffentlicht wird
Zwei Dinge müssen existieren, bevor der Schlüssel irgendwo landet: ein Backup des geheimen Schlüssels und ein Widerrufszertifikat. Beides später nachzuholen ist der Klassiker, den man genau einmal bereut.
Ein Widerrufszertifikat legt GnuPG bei der Erzeugung ohnehin schon selbst unter ~/.gnupg/openpgp-revocs.d/<FPR>.rev ab. Das Backup dagegen ist erst dann eines, wenn du es einmal zurückgespielt hast. Also rein damit in ein Wegwerf-GNUPGHOME und nachsehen, ob der geheime Hauptschlüssel dort auch wirklich auftaucht und nicht bloß die Datei lesbar war. Danach wandern beide auf Offline-Medien.
Wichtig, weil es ein sehr verbreitetes Missverständnis ist: Der s2k-*-Block aus der gpg.conf betrifft den passphrasenbasierten Schutz, also etwa gpg -c und den Export. Er sagt nichts darüber aus, wie der geheime Schlüssel in ~/.gnupg auf der Platte geschützt ist. Darum kümmert sich der gpg-agent mit eigenen Parametern, und bereits vorhandenes Schlüsselmaterial wird durch eine geänderte gpg.conf nicht rückwirkend neu verpackt. Wer die beiden verwechselt, glaubt an eine Härtung, die an dieser Stelle gar nicht wirkt. Was tatsächlich in deinem Backup steht, siehst du wie immer am Artefakt selbst, per gpg --list-packets secret-key-backup.asc.
Den Hauptschlüssel aus dem Alltag nehmen
Jetzt kommt der Schritt, der die ganze Aufteilung von oben überhaupt erst einlöst. Der Hauptschlüssel hat im Alltag nichts verloren. Gebraucht wird er nur, wenn ein Unterschlüssel verlängert, ersetzt oder widerrufen wird, wenn eine UID dazukommt oder wenn du einen fremden Schlüssel zertifizierst. Also fliegt sein geheimer Teil vom Arbeitsrechner herunter, und zwar genau jetzt, nachdem Backup und Widerrufszertifikat existieren und nicht vorher.
# nur die Unterschlüssel exportieren, ohne den geheimen Hauptschlüssel
gpg --export-secret-subkeys --armor $FPR > subkeys.asc
# das gesamte geheime Material aus dem Alltags-Keyring werfen
gpg --delete-secret-keys $FPR
# und danach ausschließlich die Unterschlüssel zurückholen
gpg --import subkeys.asc
shred -u subkeys.asc
Das Doppelkreuz hinter sec ist der ganze Punkt. Es bedeutet: GnuPG kennt den Hauptschlüssel, hat sein geheimes Gegenstück aber nicht mehr. Signieren, Entschlüsseln und Authentisieren laufen unverändert weiter, dafür sind die Unterschlüssel zuständig. Was nicht mehr geht, ist alles, was die Identität selbst betrifft, also neue Unterschlüssel anlegen, Laufzeiten verlängern, UIDs ergänzen und fremde Schlüssel zertifizieren.
Der geheime Hauptschlüssel liegt ab hier zusammen mit dem Widerrufszertifikat auf verschlüsseltem Wechselmedium. Wenn ich ihn brauche, kommt er ausdrücklich nicht zurück nach ~/.gnupg, sondern in ein temporäres GNUPGHOME, das hinterher wieder verschwindet.
Ein Detail dabei ist wichtig genug für einen eigenen Absatz, weil es sonst still danebengeht: Das Offline-Backup ist ein Schnappschuss. Es entstand oben, bevor der alte Schlüssel gegengezeichnet hat und bevor die Governikus-Zertifizierung da war. Diese Fremdsignaturen hängen am öffentlichen Teil, der im Alltags-Keyring liegt, nicht im Backup. Wer nur das Backup einspielt und dort arbeitet, exportiert hinterher einen Schlüssel, dem genau diese Signaturen fehlen. Also immer beides einspielen:
# aktuellen öffentlichen Stand samt Fremdsignaturen aus dem Alltags-Keyring mitnehmen
gpg --armor --export-options no-export-minimal --export $FPR > /tmp/pub-aktuell.asc
export GNUPGHOME=$(mktemp -d); chmod 700 "$GNUPGHOME"
trap 'rm -rf "$GNUPGHOME"' EXIT HUP INT TERM # auch bei Abbruch aufräumen
gpg --import /media/offline/secret-key-backup.asc
gpg --import /tmp/pub-aktuell.asc
# hier die Arbeit am Schlüssel, etwa eine Laufzeit verlängern
gpg --armor --export-options no-export-minimal --export $FPR > /tmp/pub-neu.asc
rm -rf "$GNUPGHOME"; unset GNUPGHOME
Danach importierst du die aktualisierte öffentliche Hälfte in den Alltags-Keyring und rollst sie über alle Veröffentlichungskanäle aus. Das ist unbequem. Es soll unbequem sein, denn genau diese Unbequemlichkeit ist der Grund, warum der Hauptschlüssel selten angefasst wird und deshalb schwer zu verlieren ist.
Den alten Schlüssel den neuen gegenzeichnen lassen
Ein weicher Übergang statt einer harten Kante: Der alte Schlüssel bleibt bis zu seinem Ablauf gültig und zertifiziert den neuen. Wer dem alten Schlüssel schon vertraut, bekommt damit einen kryptografischen Pfad zum neuen, ohne mich irgendwo anrufen zu müssen.
Das ist ein eigenes Thema, deshalb hier nur zusammengefasst: Governikus ist nach meinem Stand vom August 2026 der einzige mir bekannte noch aktive Dienst in Deutschland, der OpenPGP-Schlüssel über die Online-Ausweisfunktion zertifiziert. Du schickst deinen öffentlichen Schlüssel hin, weist dich mit dem Personalausweis aus und bekommst eine Zertifizierung mit Level 3 zurück. Die Volksverschlüsselung fällt als Alternative aus, die macht ausschließlich S/MIME nach X.509, erzeugt die Schlüssel selbst und wird ohnehin eingestellt.
Strukturell wichtig für den Rest des Beitrags ist nur ein Punkt: Diese Zertifizierung ist eine Fremdsignatur. Und die Hälfte meiner Veröffentlichungskanäle wirft Fremdsignaturen weg. Warum, steht gleich.
Vier Exporte für vier Aufgaben
Das ist der Teil, den fast alle Anleitungen überspringen, und ausgerechnet hier fallen die echten Entscheidungen. Vier verschiedene Exporte desselben Schlüssels für vier verschiedene Jobs, mit den gemessenen Größen der Artefakte, die tatsächlich ausgeliefert werden:
Alle vier Befehle habe ich gegen die live ausgelieferten Dateien geprüft, sie reproduzieren die Artefakte byteidentisch, sha256 stimmt jeweils überein. Du kannst sie also so übernehmen.
Und jetzt die fiese Falle. In meiner gpg.conf steht global export-options export-minimal. Export-Optionen summieren sich, sie ersetzen einander nicht. --export-options export-clean hebt export-minimal also nicht auf. Du bekommst weiterhin einen minimalen Export, still und ohne Warnung, mit weggeworfenen Fremdsignaturen. Nur die ausdrückliche Verneinung funktioniert:
Das ist genau die Sorte Fehler, bei der du fest davon überzeugt bist, einen Schlüssel mit allen Signaturen veröffentlicht zu haben, während in Wahrheit die eID-Zertifizierung nie das Haus verlassen hat.
Die zweite Stolperfalle beim Export: export-minimal wirft zwar Fremdsignaturen weg, behält aber die Foto-UID. Für den DANE-Record muss auch das Bild raus, sonst wächst der Record von rund einem Kilobyte auf gute zwölf. Dieser Befehl hat den ausgelieferten Record mit 1298 Byte erzeugt:
Zwei Anmerkungen noch zu den Formaten, weil sich in der 2.4er Reihe etwas geändert hat:
--print-dane-recordsgibt es nicht mehr. Der Nachfolger heißt --export-options export-dane.
Verwechsle die beiden Ausgaben aber nicht. export-dane liefert einen fertigen DNS-Präsentationsblock, also $ORIGIN, Kommentarzeilen und generische TYPE61-Rdata, bei mir 25642 Byte. Das ist kein binäres Schlüsselmaterial. Zum Lesen ist es praktisch, der ausgelieferte Record entstand aber aus dem binären Export oben plus base64 -w0 openpgpkey.bin. Beide Wege sind gültige Zonefile-Syntax, RFC 7929 definiert die base64-Präsentationsform und RFC 3597 die generische TYPE61-Form, BIND frisst beides. Der Einzeiler passte einfach besser in meine bestehende Zone.
Wo der Schlüssel liegt, und warum genau dort
Sieben Kanäle, und jeder existiert aus einem anderen Grund. Das „warum“ ist dabei die interessantere Hälfte:
Kanal
Format
Warum dieser Kanal
keys.openpgp.org
voll hochgeladen, Server wirft Signaturen und Foto weg
der einzige Keyserver, der die Mailadresse validiert, und der, den moderne Clients abfragen
Vertrauen hängt an DNSSEC statt an einem Signaturgraphen, muss klein bleiben, es ist ein DNS-Record
WKD advanced
kein eigener Export, CNAME auf wkd.keys.openpgp.org
spiegelt keys.openpgp.org automatisch, lässt sich hier nicht selbst hosten
WKD direct (Apex)
voll binär, 13108 B
selbst gehostet, deshalb die einzige Stelle mit Foto und Fremdsignaturen
security.txt und Direktdownload
voll ASCII, 17833 B
auffindbar für Menschen und für Scanner
Zwei abgeleitete Namen tauchen dabei auf, und die werden regelmäßig falsch gebildet. Beide habe ich unabhängig nachgerechnet, sie stimmen mit dem überein, was ausgeliefert wird:
# WKD: z-base32 des SHA-1 vom lokalen Teil, ASCII-Großbuchstaben vorher klein gemacht
# sha1("kernel-error") -> z-base32 -> 3gyjbxx9xfdggpkmx5qdd793xy431w5u
gpg --with-wkd-hash -k kernel-error@kernel-error.com
# DANE (RFC 7929): SHA-256 des kanonisierten UTF-8-Lokalteils, auf 28 Oktette gekürzt,
# hex-kodiert, dann ._openpgpkey.<domain>
# 70e1c7d87e825b3aba45e2a478025ea0d91d298038436abde5a4c2d0._openpgpkey.kernel-error.com
Zwei Präzisierungen, damit hier keine Regel steht, die breiter ist als die Spezifikationen: WKD bildet ASCII-Großbuchstaben auf Kleinbuchstaben ab, das ist kein allgemeines Unicode-Lowercasing. Und DANE übernimmt diese Abbildung nicht einfach, RFC 7929 hat seine eigene Kanonisierung des lokalen Teils. Bei mir fallen beide zusammen, weil kernel-error schon reines Kleinbuchstaben-ASCII ist. Genau deshalb ist mir der Unterschied nie begegnet. Wer einen Lokalteil mit Großbuchstaben oder Nicht-ASCII hat, darf die beiden auf keinen Fall gleichsetzen.
Und weil ich schon dabei bin, mich zu blamieren: Bei einem früheren Review habe ich DANE als „fehlt“ markiert, weil ich den z-base32-Namen aus WKD gegen das DNS geprüft habe. Also den falschen Namen. DANE lief die ganze Zeit. Wenn du deinen eigenen Aufbau prüfst, prüfe bitte den Namen, den die jeweilige Spezifikation vorschreibt, und nicht den, den du gerade im Kopf hast. Wie man einen OPENPGPKEY-Record überhaupt in die Zone bekommt und wie man die Zone dafür mit BIND signiert, steht hier im Blog.
Eine Konsequenz aus der Tabelle überrascht die Leute regelmäßig, deshalb schreibe ich sie deutlich hin:
Die Governikus-Zertifizierung ist nur auf den klassischen Keyservern und auf den selbst gehosteten Kopien sichtbar. keys.openpgp.org wirft sie weg, und gpg bevorzugt die WKD-advanced-Methode, die wiederum auf keys.openpgp.org zeigt. Der Kanal, den die meisten Clients benutzen, ist also ausgerechnet der mit den wenigsten Signaturen.
Prüfen wie ein Fremder
Der letzte Schritt in Teil A, und der wichtigste: Hol dir deinen eigenen Schlüssel so, wie es jemand tut, der dich nicht kennt. Einmal pro Suchpfad, jedes Mal in einem frischen Schlüsselbund.
# den Schlüssel holen wie ein Fremder, einmal pro Lookup-Pfad
for m in dane wkd keyserver; do
GNUPGHOME=$(mktemp -d) gpg --auto-key-locate clear,$m --locate-external-keys kernel-error@kernel-error.com
done
# welche Präferenzen bewirbt der Schlüssel tatsächlich?
gpg --export $FPR | gpg --list-packets | grep -E 'pref-|features'
# welchen Cipher wählt ein Absender wirklich?
gpg --status-fd 1 -d message.gpg | grep DECRYPTION_INFO
Damit endet das Rezept. Ab hier geht es darum, was die Teile bedeuten, und um die Frage, wie sicher der Aufbau am Ende wirklich ist.
Teil B: was das alles bedeutet
Wenn du bis hierher gekommen bist, hast du ein funktionierendes Rezept. Was du noch nicht hast, ist ein Gefühl dafür, warum die Teile so und nicht anders geschnitten sind, und wo der Aufbau trotz allem nachgibt. Genau darum geht es jetzt, und zwar in dieser Reihenfolge: erst die Struktur, dann die Zahlen, dann der Fehler und am Ende die Rechnung ohne Schönfärberei.
Warum ein Hauptschlüssel, der nur zertifiziert
Der Hauptschlüssel hat genau eine Aufgabe: zu sagen, wer zu diesem Schlüssel gehört. Er signiert die UIDs und er signiert die Unterschlüssel. Er signiert nie eine Mail und entschlüsselt nie irgendetwas. Alles Operative ist delegiert:
[C] certify Identitätsanker. Wird selten benutzt. Eine Kompromittierung ist
nicht reparierbar, ein Verlust nur über das Offline-Backup.
[S] sign Signieren im Alltag
[E] encrypt Entschlüsseln im Alltag
[A] auth Authentisierung, etwa als SSH-Schlüssel
Der Gewinn: Ein kompromittierter oder ausgedienter Unterschlüssel lässt sich widerrufen und ersetzen, ohne die Identität anzufassen, ohne die gesammelten Fremdsignaturen zu verlieren und ohne den Fingerabdruck zu ändern, der an sieben Stellen veröffentlicht ist. Dass [S] und [E] getrennt sind, hat noch einen zweiten Grund: Sie haben unterschiedliche Lebenszyklen. Einen alten Signaturschlüssel zu vernichten ist harmlos. Einen alten Verschlüsselungsschlüssel zu vernichten heißt, dass du an dein Archiv nicht mehr herankommst.
Das Ganze zahlt sich allerdings nur aus, wenn du auch die Konsequenz ziehst: Der volle Nutzen dieser Aufteilung stellt sich erst ein, wenn der Hauptschlüssel offline liegt. Genau deshalb liegt er hier offline, auf verschlüsseltem Wechselmedium und zusammen mit dem Widerrufszertifikat. Er kommt nur heraus, wenn ein Unterschlüssel verlängert, ersetzt oder widerrufen werden muss, und dann in ein temporäres GNUPGHOME und nicht zurück in den Alltags-Keyring. Auf der Arbeitsmaschine liegen ausschließlich die drei Unterschlüssel. Ein Hauptschlüssel, der nur zertifiziert, aber trotzdem neben dem Mailclient herumliegt, ist am Ende nur Kosmetik.
Der [A]-Unterschlüssel existiert übrigens, ist aber bewusst nicht im gpg-agent für SSH verdrahtet, sshcontrol ist leer. Ich habe ihn erzeugt, um mir die Option offenzuhalten. Mehr ist es im Moment nicht.
Ed25519 und X25519, was die Zahlen bedeuten
Ed25519 zum Signieren und X25519, in GnuPG als cv25519 bezeichnet, zum Verschlüsseln liegen beide bei rund 128 Bit klassischem Sicherheitsniveau. Das ist vergleichbar mit RSA-3072 und deutlich oberhalb von RSA-2048.
Sie sind dramatisch kleiner und schneller. Deshalb passt der komplette Schlüssel mit drei Unterschlüsseln, Foto und drei Fremdsignaturen immer noch in 13 KB.
Curve25519 ist fehlbedienungsresistent entworfen: keine Parameter, die man falsch wählen kann, keine Invalid-Curve-Fallen, deterministische Nonces bei Ed25519. Ein großer Teil der historischen Signatur-Unfälle in der Praxis kam aus genau den Ecken, die diese Kurve wegräumt.
Ein Punkt dazu, den du im Kopf behalten solltest, weil er gleich noch wichtig wird: 128 Bit klassische Sicherheit sind die Obergrenze dieses Schlüssels. Merk dir die Zahl. Sie ist der Grund, warum die AES-Geschichte weiter unten weniger dramatisch ist, als sie zunächst klingt.
Algorithmen-Präferenzen: was sie sind und wer sie liest
Dieses Konzept muss sitzen, sonst funktioniert die Geschichte danach nicht. Vorweg ein Satz, den viele nie gehört haben: Eine OpenPGP-Nachricht ist immer hybrid. Der asymmetrische Teil, also dein Ed25519- und X25519-Material, verpackt ausschließlich einen zufällig erzeugten symmetrischen Sitzungsschlüssel. Die eigentlichen Nutzdaten verschlüsselt ein symmetrisches Verfahren, in der Regel AES. Der Fehler, um den es gleich geht, saß in dieser symmetrischen Hälfte.
Ein OpenPGP-Schlüssel trägt nämlich nicht nur öffentliche Schlüssel spazieren. Die Selbstsignatur jeder UID enthält Subpakete, die ankündigen, was der Besitzer verarbeiten kann:
Das features-Oktett ist die Stelle, an der ich mich in meinen eigenen Notizen vertan hatte, deshalb hier die Dekodierung. In der Linie, die GnuPG 2.4 implementiert, bedeuten die Bits des ersten Oktetts:
0x01 SEIPD-v1 mit Modification Detection Code (MDC)
0x02 AEAD
0x04 Unterstützung für das v5-Schlüssel- und Fingerabdruckformat
features 05 ist also 0x01 + 0x04, das heißt MDC und kein AEAD. features 07 wäre 0x01 + 0x02 + 0x04, und genau das bekommt ein Schlüssel, den GnuPG 2.4 mit Standardeinstellungen erzeugt. Der kaputte Zustand war features 04, also 0x04 ganz allein: weder MDC noch AEAD angekündigt.
Eine Versionsfußnote gehört dazu: Diese Bitbelegung und das Subpaket 34 stammen aus der LibrePGP-Linie, die GnuPG 2.4 umsetzt. RFC 9580 hat beides geändert, dort sind die AEAD-Präferenzen in Subpaket 39 umgezogen und das Feature-Modell wurde umgebaut. Diese Spaltung kommt weiter unten noch einmal zurück.
Wenn dir jemand etwas verschlüsselt, liest dessen GnuPG deine Präferenzliste und wählt daraus das stärkste Verfahren, das beide können. Das ist der ganze Mechanismus. Daraus folgen zwei Dinge, die den meisten Leuten gegen den Strich gehen:
Deine eigenen personal-cipher-preferences schützen dich nicht. Die bestimmen, was du verschickst. Was Leute dir schicken, bestimmt allein das, was dein veröffentlichter Schlüssel ankündigt.
Die Präferenzen stehen in einer Signatur. Sie zu ändern heißt, die Selbstsignatur neu auszustellen. Und damit ist jede veröffentlichte Kopie deines Schlüssels veraltet, bis du sie erneuerst. Bei mir sind das sieben Kanäle und eine Änderung an der DNS-Zone.
Und wenn ein Schlüssel gar nichts ankündigt? Dann greift der Rückfall. RFC 4880 schreibt dafür 3DES vor, den verpflichtend zu implementierenden Algorithmus. Moderne GnuPG-Versionen landen dort allerdings nicht mehr: Seit der 2.3er Reihe verschlüsselt gpg grundsätzlich nicht mehr mit 64-Bit-Blockchiffren, dafür müsstest du ausdrücklich --allow-old-cipher-algos setzen. Der Rückfall endet deshalb bei AES-128. Nicht kaputt, aber eben auch nicht das, was der Schlüssel bekommen sollte.
Der Defekt, gemessen
Ein x-beliebiger Absender, der meinem brandneuen Schlüssel etwas verschlüsselt, sah das hier:
gpg: WARNING: cipher algorithm AES not found in recipient preferences
gpg: AES.CFB encrypted data
[GNUPG:] DECRYPTION_INFO 2 7 0 # 7 = AES128
Derselbe Absender, wenn er dem Schlüssel von 2023 schreibt, den der neue gerade ablöst:
[GNUPG:] DECRYPTION_INFO 2 9 0 # 9 = AES256
Der neue Schlüssel hatte überhaupt keinepref-sym-algos, keine pref-hash-algos und keine pref-zip-algos, dazu features 04 statt 05. Er wurde also schwächer angesprochen als sein Vorgänger, lautlos, und der einzige Hinweis darauf erschien auf einem Terminal, das jemand anderem gehört.
Die Ursache, und eine Korrektur an mir selbst
In meinen Arbeitsnotizen stand als Ursache: „mit einer --batch --gen-key-Parameterdatei ohne Preferences:-Zeile gebaut“. Das ist falsch. Eine Parameterdatei ohne diese Zeile erzeugt völlig ordentliche Präferenzen aus den eingebauten Defaults von GnuPG. Nachgeprüft, es kommt das hier heraus:
Die echte Ursache saß in der gpg.conf. Ich habe die Juli-Konfiguration in einem Wegwerf-Schlüsselbund nachgestellt, der Defekt war sofort wieder da. Danach habe ich die Datei halbiert, bis eine einzige Zeile übrig blieb:
Konfiguration im Test
Ergebnis
Juli-gpg.conf, unverändert
features: 04, gar keine pref-*
ohne disable-cipher-algo 3DES
pref-sym-algos: 9 8 7 2, features: 07
ohne alledisable-cipher-algo-Zeilen
pref-sym-algos: 9 8 7 2, features: 07
ohne cipher-algo AES256
weiterhin kaputt, features: 04
nurdisable-cipher-algo 3DES vorhanden
kaputt, features: 04
disable-cipher-algo 3DES plus explizite Preferences:-Zeile
sauber, pref-sym-algos: 9 8 7, features: 05
disable-cipher-algo 3DES ist also notwendig und hinreichend, um den Defekt auszulösen. Eine Zeile, mehr nicht.
Der Mechanismus dahinter ist die eigentliche Pointe des ganzen Beitrags. 3DES ist in OpenPGP der verpflichtend zu implementierende Algorithmus, und in der Standardliste steht er auch sichtbar drin: pref-sym-algos: 9 8 7 2 endet auf der 2, und die 2 ist 3DES. Nimmst du GnuPG diesen einen Algorithmus lokal weg, verschwindet nicht nur er aus der Liste, sondern die Liste als Ganzes.
Beim Warum bleibe ich vorsichtig. Dass GnuPG intern keine gültige Liste mehr konstruieren kann und deshalb gar keine schreibt, ist die naheliegende Erklärung, beweisen lässt sie sich von der Kommandozeile aus nicht. Belegt ist der Auslöser, nicht der Code-Pfad dahinter. Der Effekt selbst ist dagegen eindeutig: Die Härtungszeile hat nicht einen schwachen Algorithmus aus der Liste entfernt, sie hat die komplette Liste entfernt. Und damit dafür gesorgt, dass Absender zurückfallen, und zwar auf etwas Schwächeres als das, was ich gerade weghärten wollte.
Das Labor zum Selbernachbauen
Zwei Minuten, kein echter Schlüssel wird angefasst, alles passiert in Wegwerf-Verzeichnissen unter /tmp. Wenn du mir nicht glaubst, ist das hier der schnellste Weg, es selbst zu sehen:
Bei mir kommt das hier heraus, und die eine Zeile Unterschied ist der ganze Defekt:
nopref: gpg: WARNING: cipher algorithm AES not found in recipient preferences
gpg: AES.CFB encrypted data
withpref: gpg: AES256.OCB encrypted data
Die Reparatur
Der Fix selbst ist unspektakulär, ein einziger Befehl im Editiermodus. Weil setpref die Selbstsignatur neu ausstellt, braucht er den Hauptschlüssel. Das Ganze läuft also einmal im temporären GNUPGHOME von weiter oben, mit dem Offline-Schlüssel und dem aktuellen öffentlichen Stand:
gpg --edit-key $FPR
> setpref AES256 AES192 AES SHA512 SHA384 SHA256 ZLIB BZIP2 ZIP Uncompressed
> y
> save
Danach steht da pref-sym 9 8 7, pref-hash 10 9 8, pref-zip 2 3 1 0 und features 05. Der Fingerabdruck bleibt unverändert, und die Governikus-Zertifizierung, die Gegensignatur des alten Schlüssels, die Foto-UID und sämtliche Ablaufdaten überleben. Das schreibe ich so ausdrücklich hin, weil viele Leute Angst vor setpref haben und glauben, es beschädige den Schlüssel. Tut es nicht. Was es tut: Es stellt die Selbstsignatur neu aus. Und damit müssen anschließend alle sieben Veröffentlichungskanäle aufgefrischt werden.
Zur Sicherheit gleich hinterher: default-preference-list in der gpg.conf festgenagelt, und die disable-cipher-algo-Zeilen sind endgültig raus.
Wie schlimm war es wirklich?
Hier will ich ehrlich sein statt dramatisch, sonst wird das hier auch nur wieder eine von diesen „ich habe einen Bug gefunden“-Geschichten.
AES-128 statt AES-256 ist real, aber überschaubar. AES-128 ist nicht gebrochen, es gibt keinen praktischen Angriff darauf. Und erinnere dich an die Zahl von weiter oben: Der asymmetrische Teil dieses Schlüssels liefert ohnehin rund 128 Bit klassische Sicherheit. Gemessen an der reinen Schlüsselsuche war AES-128 also nicht das schwächste Glied in der Kette, es hat lediglich mit dem Rest gleichgezogen. Über Implementierungsfehler, Seitenkanäle oder Protokollschwächen sagt dieser Vergleich nichts. Die faire Einordnung lautet: Das war ein Hygiene- und Signalisierungsfehler, keine ausnutzbare Schwachstelle. Bemerkenswert ist er, weil er lautlos war, automatisch passierte und von einer Härtungsmaßnahme verursacht wurde.
Das fehlende MDC-Bit sah schlimmer aus, als es war.features 04 heißt, dass das Bit 0x01 fehlte, mit dem ein Schlüssel Modification Detection ankündigt. Auf dem Papier lädt das einen Absender dazu ein, auf ein Paket ohne Integritätsschutz zurückzufallen, und das ist die Ecke, aus der EFAIL kam. Gemessen kam aber das hier heraus:
[GNUPG:] DECRYPTION_INFO 2 7 0
[GNUPG:] GOODMDC
Ein Absender mit GnuPG 2.4.x in Standardkonfiguration hat trotz des fehlenden Bits ein integritätsgeschütztes SEIPD-v1-Paket erzeugt und GOODMDC gemeldet. MDC ist dort schlicht immer an. Herauskommen aus dem Integritätsschutz muss man in dieser Version aktiv wollen, etwa über --rfc2440, das ausdrücklich den alten Modus ohne MDC erzeugt. Die tatsächliche Integritätslücke gegenüber einem 2.4.x-Absender mit Standardeinstellungen war damit null.
Diese Aussage gilt exakt so weit wie die Messung und keinen Meter weiter. Über andere Implementierungen oder ältere GnuPG-Versionen sagt sie nichts, und genau dort könnte ein fehlendes MDC-Signal im Prinzip sehr wohl noch eine Rolle spielen. „Kein aktueller Absender ist betroffen“ wäre schlicht gelogen, und irgendwer würde es nachprüfen.
Was wirklich Alarm verdient, ist keines von beiden für sich, sondern die Art des Versagens. Ein Schlüssel kann strukturell perfekt sein und trotzdem still auf den nackten Rückfallwert heruntergehandelt werden, ohne dass irgendwer etwas davon mitbekommt. Die einzige Diagnose erscheint auf einer Maschine, die dir nicht gehört. Weder gpg --list-keys noch --check-sigs noch irgendeine Keyserver-Seite zeigt dir das. Du musst dir Signatur-Subpakete ansehen, und das macht praktisch niemand.
Ein Punkt bleibt offen: AEAD
Der reparierte Schlüssel steht bei features 05 und hat keine pref-aead-algos. Gemessen bedeutet das:
AES256.CFB encrypted data # reparierter Schlüssel, features 05
AES256.OCB encrypted data # frisch erzeugter Schlüssel, features 07 plus pref-aead-algos: 2
setpref hat das MDC-Bit zurückgeholt, aber nie eine AEAD-Ankündigung ergänzt, weil der Schlüssel aus dem kaputten Zustand heraus repariert und nicht neu erzeugt wurde. Nachrüsten ginge, setpref … OCB liefert pref-aead-algos: 2 und features 07, auch das habe ich geprüft.
Ich lasse es trotzdem so. AES256-CFB mit MDC ist solide. Vor allem aber ist AEAD genau die Stelle, an der OpenPGP derzeit auseinanderläuft: Das AEAD von GnuPG 2.4 folgt der LibrePGP-Linie, RFC 9580 spezifiziert eine andere Konstruktion namens SEIPD v2. AEAD auf einem breit veröffentlichten Schlüssel anzukündigen bringt heute eine marginale Verbesserung und ein echtes Interoperabilitätsrisiko. Wer sich für die andere Baustelle im selben Themenfeld interessiert: Was in einem modernen Handshake steckt, habe ich am Beispiel X25519MLKEM768 auseinandergenommen. Dasselbe Argument gilt übrigens für force-ocb in meiner lokalen Konfiguration, das betrifft nur, was ich selbst verschicke, und ist eine bewusst etwas vorwärtsgewandte Entscheidung.
Ehrliche Gesamteinschätzung
Die Kryptografie ist in Ordnung. Ed25519, X25519, SHA-512 und AES-256 sind eine moderne, unaufgeregte Auswahl ohne bekannte praktische Schwächen, bei rund 128 Bit klassischer Sicherheit.
Die Struktur ist besser als der Durchschnitt. Hauptschlüssel nur zum Zertifizieren, getrennte Unterschlüssel, begrenzte Laufzeit von fünf Jahren, vorab erzeugtes Widerrufszertifikat und ein Backup, das nachweislich wiederherstellbar ist.
Das schwächste Glied ist kein Algorithmus. Es ist die Tatsache, dass die drei Alltags-Unterschlüssel auf einer dauerhaft laufenden Arbeitsmaschine liegen. Wer diese Maschine und die Passphrase bekommt, erzeugt bis zum Widerruf vollgültige Signaturen in meinem Namen und liest mein Archiv mit. Nach außen ist er also erst einmal ich. Was er nicht bekommt, ist der Identitätsanker: keine neue UID zertifizieren, keinen neuen Unterschlüssel binden, und der Fingerabdruck bleibt meiner. Ein kompromittierter Unterschlüssel fliegt raus und wird ersetzt, die Identität überlebt. Eine Smartcard würde den Rest weiter verkleinern, weil sich Schlüsselmaterial von der Karte nicht kopieren lässt. Sie löst es nicht: Steckt die Karte und ist sie entsperrt, wird eben auf der Karte signiert. Auf der Liste steht sie trotzdem.
Das Vertrauen in die Bindung ist geschichtet, nicht absolut. DNSSEC-signiertes DANE und selbst gehostetes WKD sind stark. Die Governikus-Zertifizierung ist die einzige echte Namens-Bindung. Das klassische Web of Trust trägt praktisch nichts mehr bei, die persönlichen Signaturen auf dieser Identität hängen an einem Schlüssel von 2011.
Post-Quantum: bewusst nicht adressiert. GnuPG 2.4.4 kann kein PQC, und die PQC-Arbeit in OpenPGP steckt noch im Entwurfsstadium mit praktisch keiner Interoperabilität. Curve25519 bietet gegen einen kryptografisch relevanten Quantencomputer keinerlei Widerstand, „heute sammeln, später entschlüsseln“ trifft also auf alles zu, was heute an diesen Schlüssel verschlüsselt wird. Ein hybrider Unterschlüssel lässt sich später ergänzen, ohne den Schlüssel neu zu bauen. Das ist eine der Auszahlungen des Unterschlüssel-Aufbaus von oben.
Was man daraus mitnimmt
Vier Punkte, und die gelten weit über OpenPGP hinaus:
Härtung kann schwächen. Einen verpflichtenden Basisalgorithmus zu entfernen hat die Verhandlung nicht verengt, sondern zerstört. Bevor du etwas abschaltest, das eine Spezifikation vorschreibt, sieh nach, was die Maschinerie drumherum macht, wenn es fehlt.
Prüfe das Artefakt, nicht den Befehl.gpg --batch --gen-key ist mit Rückgabewert 0 durchgelaufen und hat einen wunderschön aussehenden Schlüssel erzeugt. Sichtbar war der Defekt ausschließlich in --list-packets.
Konfiguration und Schlüssel sind nicht unabhängig. Der Schlüssel hat einen Defekt dauerhaft von der Konfiguration geerbt, die in den fünfzehn Sekunden aktiv war, in denen er entstand.
Manche Defekte sieht man nur von außen. Das Symptom erschien auf dem Terminal des Absenders. Teste deinen eigenen Schlüssel so, wie ein Fremder ihn benutzt. Genau dafür ist die Schleife mit mktemp -d und --locate-external-keys weiter oben da.
Und die Kurzfassung für alle, die nur bis hierher gescrollt haben: Ein moderner Schlüssel ist nicht automatisch ein korrekt konfigurierter Schlüssel. Der Unterschied steckt in Signatur-Subpaketen, die dir kein einziges Standardwerkzeug von sich aus zeigt.
Wenn du deinen eigenen Schlüssel gerade nachgeprüft hast und dort etwas anderes steht als erwartet, oder wenn ich mich irgendwo irre, dann dürft ihr mich sehr gerne fragen.
Moin. Seit dem 24. Juli 2026 habe ich einen neuen GPG-Schlüssel. Ed25519, gültig bis 2031, der Hauptschlüssel zertifiziert nur noch und hat für Signieren, Verschlüsseln und Authentisieren je einen eigenen Unterschlüssel. Technisch ein hübsches Ding. Der alte Schlüssel bleibt bis Ende des Jahres gültig und hat den neuen gegengezeichnet, damit der Wechsel keine harte Kante bekommt.
Und dann stand da die Frage, die eigentlich immer die wichtigere ist und trotzdem meistens hintenrunterfällt: Woher soll irgendwer wissen, dass dieser Schlüssel mir gehört? Nicht „wo finde ich den Schlüssel“, das ist gelöst. Sondern: Warum sollte jemand glauben, dass die Person, die diesen Schlüssel kontrolliert, dieselbe ist wie die hinter github.com/Kernel-Error, hinter dem Mastodon-Account und hinter dieser Domain?
Die alten Antworten funktionieren nicht mehr
Die klassische Antwort auf diese Frage heißt Web of Trust. Ich unterschreibe deinen Schlüssel, du unterschreibst meinen, irgendwann spannt sich ein Netz aus Signaturen über die Welt und jeder kann über ein paar Ecken einen Pfad zu jedem finden. Elegante Idee. Nur: Wann warst du zuletzt auf einer Keysigning-Party? Eben. Ich auch nicht.
Dazu kommt ein sehr praktisches Problem, über das kaum jemand redet: keys.openpgp.org, der Keyserver, den heute praktisch alle benutzen, veröffentlicht Fremdsignaturen grundsätzlich nicht. Zuverlässig überleben dort nur Selbstsignaturen, einzige Ausnahme sind eigens attestierte Fremdzertifizierungen, und die benutzt in der Praxis kaum jemand. Das ist als Schutz gegen Signatur-Flooding absolut nachvollziehbar, bedeutet aber eben auch: Das Web of Trust reist nicht mehr mit. Du kannst deinen Schlüssel von hundert Leuten unterschreiben lassen, auf dem meistgenutzten Keyserver sieht ihn trotzdem niemand mit diesen Unterschriften.
Die zweite klassische Antwort waren Zertifizierungsstellen für Personen. CAcert habe ich hier vor Jahren mal vorgestellt, und die Volksverschlüsselung wurde 2025 eingestellt. Übrig geblieben ist bei mir eine Zertifizierung über Governikus, die die Beglaubigung im Auftrag des BSI macht, also eine eID-basierte Bestätigung mit Level 3, die tatsächlich meinen bürgerlichen Namen an den Schlüssel bindet. Das ist eine der letzten echten Klarnamen-Bindungen, die man noch bekommt. Und ausgerechnet die ist eine Fremdsignatur, fliegt auf keys.openpgp.org also raus.
Was schon vorher da war, und was es eben nicht beweist
Beim Thema Erreichbarkeit war ich schon vorher gut aufgestellt. Der Schlüssel liegt im Web Key Directory, sowohl in der advanced als auch in der direct method. Er steht als OPENPGPKEY-Record in einer DNSSEC-signierten Zone, wie ich das hier vor Jahren schon einmal beschrieben habe. Er liegt auf keys.openpgp.org, keyserver.ubuntu.com und pgpkeys.eu. Er ist in der clearsigned security.txt referenziert, die zu meinem Umgang mit Vulnerability Reports gehört. Und er liegt schlicht als Datei zum Direktdownload.
Sieben Kanäle also. Alle beweisen exakt eine Sache: dass man den Schlüssel findet. Die Governikus-Zertifizierung beweist zusätzlich einen Namen. Aber keiner dieser Kanäle beweist, dass derselbe Schlüssel auch das GitHub-Konto kontrolliert, oder den Matrix-Account, oder die Domain als Identität und nicht nur als Hostingort einer Datei. Genau diese Lücke füllen Identity Claims, und genau dafür gibt es Keyoxide.
Der wichtigste Satz zuerst: Keyoxide hat keine Accounts
Das ist das größte Missverständnis, und ich schreibe es deshalb so deutlich wie möglich hin: Es gibt bei Keyoxide keine Registrierung, keinen Login, kein Profil zum Ausfüllen, keine API-Keys und keine Daten auf deren Seite. Man legt dort nichts an. Man kann dort gar nichts anlegen.
keyoxide.org ist ein zustandsloser Betrachter. Wenn jemand ein Profil öffnet, passiert Folgendes:
Der öffentliche Schlüssel wird geholt, per WKD oder von einem Keyserver.
Aus der Selbstsignatur werden die Notationen mit dem Namen proof@ariadne.id ausgelesen.
Jeder Claim wird live gegen den genannten Endpunkt aufgelöst, je nach Claim-Typ direkt im Browser oder über einen Proxy. Ein TXT-Record lässt sich aus einer Webseite heraus nun mal nicht selbst abfragen.
Grüne Haken werden gerendert, und danach vergisst der Dienst alles wieder.
Die Konsequenz daraus ist der eigentliche Clou: Der Schlüssel ist das Profil. Wenn keyoxide.org morgen offline geht, ist nichts kaputt. Die Claims stehen weiter im Schlüssel, die Proofs liegen weiter an ihren Endpunkten, und jeder kann die komplette Prüfung mit dig und curl von Hand nachvollziehen. Weiter unten zeige ich genau das.
Das ist das genaue Gegenmodell zu Keybase, wo die Verknüpfungen auf einem Server einer Firma lagen. Als die Firma verkauft wurde, war der Ärger groß und die Frage berechtigt, was mit den Identitäten passiert. Bei diesem Modell stellt sich die Frage nicht, weil der Betreiber nichts hält, was verloren gehen könnte. Die zugrundeliegende Spezifikation heißt Ariadne Identity, Keyoxide ist nur eine Implementierung davon.
Zwei Hälften, die aufeinander zeigen
Jeder Proof besteht aus zwei Aussagen, die sich gegenseitig referenzieren:
Hälfte
Liegt in
Sagt
Claim
der Selbstsignatur des Schlüssels
„Ich kontrolliere kernel-error.de“
Proof
genau diesem Endpunkt
„Der Schlüssel mit dem Fingerprint 45FC… gehört mir“
Auf den ersten Blick sieht das zirkulär aus. Der Schlüssel sagt, ihm gehöre die Domain, und die Domain sagt, ihr gehöre der Schlüssel. Beweist das nicht einfach gar nichts? Doch, und zwar genau deshalb, weil es zirkulär ist: Nur wer beides kontrolliert, kann beide Hälften zur Deckung bringen.
Spielen wir es durch. Jemand klaut meinen geheimen Schlüssel. Er kann jetzt beliebige Claims hineinschreiben, zum Beispiel „ich kontrolliere example.org“. Aber er kann bei example.org keinen TXT-Record setzen, der auf meinen Fingerprint zeigt. Der Claim bleibt rot. Umgekehrt: Jemand übernimmt eine meiner Domains und setzt dort einen TXT-Record mit meinem Fingerprint. Schön, aber ohne den geheimen Schlüssel kann er den passenden Claim nicht in die Selbstsignatur schreiben. Auch das ergibt keinen grünen Haken. Ein Angreifer mit einer der beiden Hälften bekommt nichts. Er braucht beide.
Warum eine Notation und keine Signatur
Ein Claim ist technisch ein Notation-Subpacket in der Selbstsignatur einer User-ID. Eine Notation ist ein frei definierbares Schlüssel-Wert-Paar, das im Signaturpaket mitsigniert wird. Der Name ist bei Ariadne immer proof@ariadne.id, der Wert beschreibt den Endpunkt.
Und hier schließt sich der Kreis zu dem Keyserver-Problem von oben. Weil die Claims in der Selbstsignatur stehen und nicht in einer Fremdsignatur, kann kein Keyserver sie wegputzen. Meine Governikus-Zertifizierung ist auf keys.openpgp.org verschwunden, weil sie eine Fremdsignatur ist. Die Keyoxide-Claims sind da, weil ein Keyserver eine Selbstsignatur nicht entfernen kann, ohne den Schlüssel dabei zu zerstören. Das ist für mich das stärkste praktische Argument, Notationen zu sammeln statt Unterschriften.
Vier Claims, vier sehr unterschiedliche Qualitäten
Live sind am Ende vier Claims, alle ausschließlich auf der Namens-UID:
Vier Claims, vier grüne Haken. Geprüft wird das erst beim Aufruf, gespeichert ist bei Keyoxide davon nichts. Der Fingerprint darunter ist der einzige Anker.
Das öffentliche Profil dazu liegt unter keyoxide.org/45FCD081ADB54872EA5B06B9893DE0CDDE986DEB. Bevor jetzt jemand vier grüne Haken sieht und denkt, das seien vier gleichwertige Beweise: sind sie nicht, und das gehört ausgesprochen.
Die beiden DNS-Claims sind mit Abstand die stärksten. Sie liegen in meinen eigenen, DNSSEC-signierten Zonen auf meinen eigenen Nameservern. Wer den Record fälschen will, muss meine Domain- oder DNS-Verwaltung übernehmen. Unterwegs fälschen reicht nicht, sofern der Prüfer DNSSEC auch wirklich validiert.
Der Gist-Claim hängt daran, dass GitHub sein URL-Schema beibehält und den Gist nicht wegräumt.
Der Matrix-Claim hängt an matrix.org und daran, dass ein bestimmter Raum weiter existiert und die Nachricht nicht gelöscht wird.
Ein Claim ist nie stärker als der Endpunkt, auf den er zeigt. Vier grüne Haken nebeneinander suggerieren eine Gleichwertigkeit, die es nicht gibt.
Domain-Proof: ein TXT-Record am Apex
Der Proof für eine Domain ist ein TXT-Record. Bei mir in beiden Zonen:
kernel-error.de. IN TXT "openpgp4fpr:45FCD081ADB54872EA5B06B9893DE0CDDE986DEB"
kernel-error.com. IN TXT "openpgp4fpr:45FCD081ADB54872EA5B06B9893DE0CDDE986DEB"
Am Apex, nicht unter www, und das ist eine bewusste Entscheidung. Der Claim behauptet etwas über die Identität der Domain, nicht über einen Webserver-Hostnamen. www ist nur ein Host darunter. Keyoxide rendert den Claim außerdem als den Domainnamen, und kernel-error.de liest sich als Identität, während www.kernel-error.de sich als Webseite liest. Kleiner Unterschied, aber genau der Punkt, um den es hier geht.
Beide TLDs bekommen den Record, weil sie bei mir verschiedene Rollen haben. Die .com ist die Mail-Identität mit WKD und DANE, die .de ist die Webpräsenz mit der security.txt. Beides bin ich, also gehört beides an den Schlüssel gebunden.
Beide Zonen sind DNSSEC-signiert und laufen mit inline-signing. Für so eine Bearbeitung heißt das: einfrieren, editieren, prüfen, wieder auftauen. Ein rndc reload ist hier der falsche Befehl.
Angenehm unspektakulär war dabei, dass mehrere TXT-Records am selben Namen problemlos nebeneinander leben. Der neue Record ist einfach neben den SPF-Record und zwei Verifizierungs-Records gerutscht, ohne dass ich irgendetwas anfassen musste.
Einen Bluesky-Claim habe ich bewusst weggelassen. Meine did:plc:... hängt ohnehin schon über einen DNS-TXT-Record und /.well-known/atproto-did an der Domain. Wer den Domain-Claim prüft, hat Bluesky damit transitiv mit erledigt. Noch ein Claim mehr wäre nur mehr Fläche ohne mehr Aussage.
GitHub: ein öffentlicher Gist namens proof.md
Für GitHub braucht es einen öffentlichen Gist. Die Datei darin mussproof.md heißen, der Inhalt ist eine einzige Zeile:
Eine Abgrenzung, über die regelmäßig jemand stolpert, deshalb explizit: Einen GPG-Schlüssel im GitHub-Profil zu hinterlegen, damit signierte Commits als „Verified“ angezeigt werden, ist eine völlig andere Sache. Anderer Mechanismus, anderer Zweck, und mit dem Keyoxide-Proof hat das nichts zu tun. Der hinterlegte Schlüssel wird von GitHub automatisch für die Commit-Verifikation benutzt. Einen Schalter „diesen Schlüssel als Signing Key verwenden“ gibt es für GPG-Schlüssel übrigens gar nicht, den gibt es nur für SSH-Schlüssel.
Matrix: der Proof ist eine Nachricht
Bei Matrix hat der Proof wieder eine ganz andere Form. Er ist weder eine Datei noch ein DNS-Record, sondern eine Nachricht in einem öffentlichen Raum. Man postet in #doipver:matrix.org schlicht:
Und zeigt dann in der Notation auf genau diese Nachricht. Beim Formatieren gibt es drei Kleinigkeiten, die man garantiert einmal falsch macht: Beim User wird das führende @ weggelassen, bei der Raum-ID das führende ! und bei der Event-ID das führende $. Aus @kernel-error:kernel-error.com wird also u/kernel-error:kernel-error.com, aus !dBfQZxCoGVmSTujfiv:matrix.org wird der nackte Rest, und genauso bei der Event-ID.
Wichtig ist außerdem, dass der Raum unverschlüsselt ist. Eine verschlüsselte Nachricht kann ein Prüfer nicht lesen, damit wäre der Proof wertlos. #doipver ist genau dafür da und entsprechend offen.
Der ganze Vorgang lässt sich komplett über die Client-Server-API erledigen, also POST /_matrix/client/v3/join/... und danach PUT /_matrix/client/v3/rooms/{roomId}/send/m.room.message/{txnId}. Die Antwort auf das Senden enthält direkt die event_id, also genau das, was die Notation braucht. Das ist elegant, wenn man es scripten will.
Ehrlicherweise muss man dazu aber sagen: Ein Access-Token gibt volle Kontrolle über den Account. Das aus der Hand zu geben, nur um eine einzige öffentliche Zeile abzusetzen, ist ein schlechtes Geschäft. Der Weg über Element von Hand kostet dreißig Sekunden: Nachricht posten, „Teilen“ beziehungsweise Permalink aufrufen, und Raum-ID sowie Event-ID direkt aus der matrix.to-URL ablesen. Wer trotzdem ein Token benutzt, sollte das Gerät danach abmelden.
Die Claims in den Schlüssel schreiben
Das eigentliche Eintragen passiert in gpg --edit-key. Sieht harmlos aus, hat aber gleich in Zeile zwei die erste Falle:
gpg --edit-key 0x893DE0CDDE986DEB
uid 1
notation
proof@ariadne.id=dns:kernel-error.de?type=TXT
save
Entfernen geht über notation und dann den gleichen Ausdruck mit einem führenden Minus, none löscht alle. Und weil man sich auf Anzeigen ungern verlässt, hier der Blick auf das, was wirklich im Schlüssel steht:
Ohne vorher eine UID auszuwählen schreibt gpg die Notation in jede UID. Bei mir also auch in die Foto-UID. Keyoxide prüft dann denselben Claim zweimal und zeigt ihn doppelt an. Die offizielle Dokumentation erwähnt das im Nebensatz, überlesen ist es schnell, und aufräumen darf man es anschließend von Hand.
Falle 2: jede Änderung schreibt die Selbstsignatur neu
Das ist der eigentliche Preis, und davor warnt einen niemand. Ein Claim mehr bedeutet eine neue Selbstsignatur, und damit ist jede veröffentlichte Kopie des Schlüssels veraltet. Konkret hieß das bei mir jedes Mal: neu hochladen zu drei Keyservern, zwei selbst gehostete Dateien überschreiben und noch eine DNSSEC-Zonenbearbeitung für den DANE-Record hinterher.
Wer drei Claims einzeln nacheinander setzt, macht diese Runde dreimal. Also: Claims sammeln und in einem Rutsch eintragen. Nebenbei wächst der Schlüssel auch spürbar. Die DANE-rdata ist im Lauf des Tages von 909 auf 1130 Byte gewachsen, die WKD-Datei von 12703 auf 12940 Byte. Nichts Dramatisches, aber bei einem DNS-Record schaut man da schon hin.
Falle 3: drei Caches, drei vorgetäuschte Fehlschläge an einem Nachmittag
Das ist der Teil, bei dem vermutlich jeder Leser nickt. An einem einzigen Nachmittag haben mir drei verschiedene Caches je einen kaputten Rollout vorgespielt. In allen drei Fällen war das Deployment in Ordnung und die Prüfung hat gelogen.
Der eigene Resolver. Nach dem Setzen des TXT-Records schlug die lokale Prüfung immer weiter fehl. Google, Cloudflare und Quad9 hatten den neuen Record sofort. Der veraltete Eintrag kam ausgerechnet von meinem eigenen DoT/DoH-Resolver, auf den systemd-resolved zeigt, und der hielt die alte Antwort noch etwa zwei Stunden fest. Das sieht exakt aus wie ein kaputtes Deployment und kostet echte Debugging-Zeit.
Der View-Name am Ende ist bei rndc flushname laut Handbuch optional. In meinem Setup mit einer eigenen resolver-View ist er aber Pflicht, sonst passiert schlicht nichts Sichtbares. Die allgemeine Lehre daraus: Wer eigene DNS-Änderungen prüft, fragt immer einen autoritativen Server und einen öffentlichen Resolver, niemals nur den Systemresolver.
Ein HTTP-Cache vor keyserver.ubuntu.com. Der lieferte einen veralteten Schlüssel mit nur der Hälfte der Claims aus. Ein angehängter Cache-Busting-Parameter brachte sofort die korrekte Version, und ein erneuter Upload wurde mit "ignored" quittiert. Der Server war also die ganze Zeit aktuell, nur der Cache davor nicht.
Und die Keyserver selbst. Keyserver mischen Selbstsignaturen, sie ersetzen sie nicht. Nach einem Update trägt die Keyserver-Kopie die alte und die neue Selbstsignatur. Ein naives grep -c notation zählt dann fünf Notationen, während die selbst gehostete Kopie drei zeigt. gpg nimmt beim Import die neueste, praktisch ist das also harmlos, aber jedes Verifikationsskript lügt einen dabei an. Man prüft besser, was ein frischer Import auflöst, nicht was im rohen Blob steht.
Die Moral aus allen drei Fällen ist dieselbe: gegen eine autoritative Quelle prüfen und gegen eine cache-busted Anfrage, niemals nur gegen die eine bequeme Quelle, die gerade zur Hand ist.
Falle 4: die Gist-Raw-URL springt den Host
Ein kurzer Schreckmoment mit einfacher Ursache. Die Adresse https://gist.github.com/<user>/<id>/raw/proof.md antwortet mit einem 301 auf gist.githubusercontent.com. Ein curl ohne -L liefert damit gar nichts zurück, was auf den ersten Blick nach einem kaputten Proof aussieht. Ist es nicht, es fehlt nur ein Buchstabe am Kommando.
Falle 5: –print-dane-records gibt es nicht mehr
In GnuPG 2.4 ist --print-dane-records weg. Der Aufruf bricht mit einem Fehler ab und verweist auf --export-options export-dane. Nur reicht export-minimal allein nicht: Die Foto-UID bleibt drin und bläst einen DANE-Record von rund einem Kilobyte auf etwa zwölf Kilobyte auf. Die kompakte Variante braucht zusätzlich einen Export-Filter:
Der fünfte Claim: Fediverse, und warum er nicht live ist
Jetzt der ehrliche Teil. Ein fünfter Claim war gebaut, ausgerollt, nie verifiziert und noch am selben Tag wieder zurückgebaut. Ich schreibe das hier hin, weil vier aufgeräumte grüne Haken nichts erzählen und dieser eine Fehlschlag mehr über die Technik verrät als der Rest zusammen.
Der Plan war ein Claim auf https://www.kernel-error.de/@kernel-error.de, also den ActivityPub-Actor dieses Blogs, mit dem Proof in den Profil-Metadaten. Beim WordPress-ActivityPub-Plugin werden die „Extra Fields“ als PropertyValue-Attachments an den Actor gehängt. Zwei Entscheidungen dabei waren richtig und bleiben es auch nach dem Rückbau.
Erstens: Die Claim-URL leitet im Browser um, liefert aber unter Accept: application/activity+json sauber den Actor aus. „Mach das doch mal im Browser auf“ ist bei einem maschinenlesbaren Endpunkt schlicht kein gültiger Test. Wer so prüft, verwirft eine funktionierende Adresse.
Zweitens: Die Platzierung war wichtiger als gedacht. Keyoxide akzeptiert den Proof in der Bio, in einem Beitrag oder in den Profil-Metadaten. Bei einem WordPress-Blog-Actor ist die Bio aber der Blog-Slogan, und der steht auf jeder einzelnen Seite der öffentlichen Webseite. Ein Fingerprint im Header, weil man die erstbeste dokumentierte Möglichkeit genommen hat. Die Profil-Metadaten sind genauso gültig und für Leser unsichtbar. Lohnt sich also, erst alle Optionen zu lesen und dann zu wählen.
Ein Implementierungsdetail mit echter Fallhöhe: Das Feld wurde mit $wpdb->insert eingetragen, ausdrücklich nicht mit wp_insert_post(). Letzteres feuert die ActivityPub-Hooks und erzeugt einen Update-Eintrag in der Outbox, der dann als Geisterbeitrag durch das Fediverse geht. Kontrolliert habe ich das über den Zeilenstand der ap_outbox vor und nach dem Eingriff: 130 zu 130, sowohl beim Anlegen als auch beim Löschen. Danach Caches leeren, und zwar zuerst Redis, dann FastCGI, sonst füllt sich der FastCGI-Cache sofort wieder aus den alten Redis-Daten. Wer mehr über das Plugin-Innenleben lesen mag, findet in meinem Beitrag zum eigenen ActivityPub-Plugin für Grav die Mechanik dahinter.
Und dann zeigte Keyoxide trotzdem ein rotes Kreuz.
Ein rotes Kreuz und eine sehr überzeugende falsche Fährte
Der Claim stand auf dem Profil als rotes Kreuz mit der Beschriftung [---], was so viel heißt wie „kein Service Provider hat gematcht“. Die anderen vier waren grün. Das Access-Log sah so aus:
OPTIONS /@kernel-error.de 301
GET /.well-known/nodeinfo 200
GET /wp-json/activitypub/1.0/nodeinfo/2.1 200
GET /@kernel-error.de/api/config 404 (doipjs/2.1.0)
Die naheliegende Lesart ist falsch, und sie ist verführerisch. /api/config ist ein Owncast-Endpunkt. Man denkt sofort: Keyoxide hält meinen Claim für einen Owncast-Server, da ist die Provider-Erkennung kaputt. Stimmt aber nicht. Die beiden NodeInfo-Abfragen darüber stammen aus dem ActivityPub-Postprozessor von doipjs, ActivityPub hatte also gematcht. Die Owncast-Abfrage ist der Fallback, der danach läuft, weil der ActivityPub-Test bereits gescheitert war. Symptom, nicht Ursache.
Die Ursache steht in der ersten Zeile. WordPress beantwortet ein OPTIONS auf die hübsche Actor-URL mit einem 301 auf die Startseite. Und ein umgeleiteter CORS-Preflight ist in allen heute relevanten Browsern ein harter Fehler. Die Fetch-Spezifikation ist an der Stelle inzwischen weniger strikt formuliert, in der Praxis bricht der Browser trotzdem ab, und der Prüfer kommt nie an den Actor heran. Das Ärgerliche daran: Die CORS-Header waren die ganze Zeit korrekt gesetzt, Access-Control-Allow-Origin: * und der ganze Rest. Sie kamen nur mit Status 301, und damit sind sie wertlos.
Eine andere Claim-URL hilft nicht heraus. /@handle und /@handle/ antworten beide mit 301 auf OPTIONS, und die eigentliche Actor-ID /?author=0 beantwortet OPTIONS mit 405 und ganz ohne CORS-Header. Es musste also in den nginx, mit einer Location, die den Preflight selbst beantwortet:
Platziert als erste Regex-Location, damit sie vor den späteren gewinnt, und der Nicht-OPTIONS-Zweig spiegelt exakt das, was auch location / macht, damit normaler Verkehr unverändert durchläuft. Der Preflight ging damit von 301 auf 204.
Und der erste Fix reichte nicht, das Log hat es leise gesagt
Nach dem Reload kam sauber der 204. Aber das anschließende GET /@kernel-error.de tauchte im Access-Log überhaupt nicht auf, und doipjs marschierte direkt wieder zur Owncast-Abfrage weiter. Genau diese Abwesenheit ist die Fehlermeldung: Ein Preflight, der erfolgreich ist und auf den dann gar keine Anfrage folgt, bedeutet, dass der Browser die Antwort auf den Preflight abgelehnt hat.
Die Liste der erlaubten Header war schlicht geraten. Accept, Authorization, Content-Type klingt vernünftig, deckte aber offensichtlich nicht ab, was doipjs tatsächlich schickt. Die Erweiterung auf Access-Control-Allow-Headers: * war die zweite Hälfte des Fixes. Der Wildcard wirkt hier, weil die Anfrage ohne Credentials und ohne HTTP-Auth läuft. Bei einer Anfrage mit Credentials wäre der Stern wirkungslos, und Authorization deckt er ohnehin nie ab, den müsste man immer namentlich nennen. Oben steht schon die korrigierte Fassung.
Noch eine Warnung für alle, die so etwas nachbauen: Access-Control-Max-Age: 86400 heißt, dass der Browser die alte Preflight-Antwort einen Tag lang behält. Ein normales Neuladen leert den CORS-Preflight-Cache nicht. Also im privaten Fenster nachtesten, sonst jagt man ein Gespenst.
Drei Lehren daraus, die weit über Keyoxide hinaus gelten:
Richtige Header auf einem falschen Statuscode sind trotzdem kaputt. Ein 301 mit perfekten CORS-Headern scheitert genauso hart wie gar keine Header.
Ein Log liest man von oben nach unten, nicht von unten nach oben. Die verdächtigste Zeile, der 404, war die Folge. Der langweilige 301 zwei Zeilen darüber war der Fehler.
Eine Anfrage, die im Log fehlt, ist auch ein Befund. Die zweite Runde habe ich ausschließlich aus etwas diagnostiziert, das nicht passiert ist.
Ein Nebeneffekt, der mir erst hinterher aufging: Das war nie ein Keyoxide-Problem. Jeder Cross-Origin-Konsument meines ActivityPub-Actors, der dafür im Browser läuft und einen Preflight braucht, ist an diesem Redirect gescheitert, seit es die Adresse gibt, und es ist niemandem aufgefallen. Serverseitige Abrufer waren nie betroffen, die kennen kein CORS. Der nginx-Fix ist deshalb geblieben, auch nachdem der Claim wieder weg war. Er repariert einen echten Defekt, völlig unabhängig von Keyoxide.
Und trotzdem blieb es rot
Nach dem CORS-Fix zeigte das Log endlich das gewünschte Bild: OPTIONS ... 204, gefolgt von GET /@kernel-error.de 200 6915 "doipjs/2.1.0". Der Actor wurde geholt. Und der Claim blieb rot. Der Reihe nach ausgeschlossen habe ich dann:
Provider-Matching. Das Regex in activitypub.js ist ein Catch-All, und die NodeInfo-Abfragen im Log beweisen ohnehin, dass der zugehörige Postprozessor gelaufen ist.
Feldplatzierung.attachment.value ist eines der drei Felder, die doipjs durchsucht.
Inhalt und Schreibweise. Fingerprint einmal in Großbuchstaben, einmal in Kleinbuchstaben probiert. Beides ohne Treffer. Die anderen vier Claims benutzen alle Großbuchstaben und verifizieren einwandfrei, an der Schreibweise liegt es also nicht.
Bleibt ein Verdacht, den ich ausdrücklich als unbewiesen kennzeichne: Das hier ist ein WordPress-ActivityPub-Blog-Actor. NodeInfo meldet software: wordpress, und der preferredUsername lautet kernel-error.de, ist also ein Benutzername mit Punkten darin, den Mastodon so gar nicht erzeugen könnte. Das ist ein deutlich weniger befahrener Pfad durch doipjs als ein normales Mastodon-Profil. Weiterzukommen hätte bedeutet, fremdes JavaScript zu debuggen, für den fünften von fünf Claims.
Also habe ich aufgehört, und die Entscheidung gehört zur Geschichte dazu. Rückbau komplett: Notation aus dem Schlüssel entfernt, alle sieben Kanäle neu ausgerollt, das Profilfeld gelöscht, Caches geleert. Der Claim ist damit vollständig verschwunden, es bleibt also kein dauerhaftes rotes Kreuz stehen. Das ist übrigens eine erfreuliche Eigenschaft: Ein Claim, der nicht funktioniert, lässt sich sauber zurückziehen. Nur die Historie der Notation liegt für immer auf den Keyservern. Wer die rohen Schlüsseldaten auseinandernimmt, findet die alten Selbstsignaturen samt Claim also weiterhin. Im aktuellen Profil taucht er nicht mehr auf.
Zwei Stolpersteine aus dem Rückbau, die man kennen sollte:
Das Entfernen einer Notation braucht eine Bestätigung. Die Reihenfolge ist uid 1, dann notation, dann der Ausdruck mit führendem Minus, dann ein y, dann erst save. Ohne das y schluckt gpg das nachfolgende save als Antwort auf seine Rückfrage, und das Entfernen passiert stillschweigend gar nicht.
Keyserver behalten jede Generation von Selbstsignaturen. Nach dem Rückbau lagen auf keys.openpgp.org zehn Selbstsignaturen auf dieser UID, mit der Reihe 4, 5, 4, 3, 2, 0 Claims. Harmlos, weil gpg wie openpgp.js die neueste nimmt. Aber --export-options export-clean reduziert das nicht auf eine Selbstsignatur. Wer Notationen über einen so bereinigten Export zählt, zählt zu hoch und hält einen erfolgreichen Rückbau für gescheitert. Besser die Zeitstempel pro Signatur vergleichen.
Falle 6: ein langsamer Matrix-Raum sieht aus wie kaputte Föderation
Zum Schluss noch eine schöne Fehldiagnose. Der erste Versuch, #doipver:matrix.org beizutreten, starb mit ResponseNeverReceived. Das liest sich wie ein glasklarer Föderationsausfall, und ich habe entsprechend angefangen zu suchen. Nur: federationtester.matrix.org meldete FederationOK: true mit gültigen Zertifikaten über IPv4 und IPv6, und blankes TCP wie auch HTTPS vom Server nach matrix.org liefen einwandfrei.
#doipver ist einfach ein großer, stark föderierter Raum. Der Alias löst auf hunderte beteiligte Server auf, und ein make_join gegen einen einzelnen ausgelasteten Server dauert entsprechend. Geholfen haben zwei Dinge: mehrere Server zum Beitreten mitgeben und einen großzügigen Timeout setzen, 180 Sekunden waren genug. Der Parameter heißt in der aktuellen Spezifikation via, ich habe hier noch den älteren Namen server_name benutzt.
POST /_matrix/client/v3/join/%21dBfQZxCoGVmSTujfiv%3Amatrix.org
?server_name=matrix.org&server_name=privacyguides.org&server_name=monero.social
Die übertragbare Lehre, gerade für dieses Publikum: Bevor man aus einer einzigen gescheiterten Anfrage eine Föderationsstörung diagnostiziert, sollte man prüfen, ob die Anfrage nur langsam war. Am selben Tag tauchten übrigens noch zwei 429 Too Many Requests bei Schlüsselabfragen im Log auf, ebenfalls reine Ablenkung.
Selbst nachprüfen, ganz ohne die Webseite
Und hier kommt der Punkt, an dem das ganze Modell überzeugt. Du musst keyoxide.org nicht vertrauen, du kannst die komplette Prüfung selbst machen. Vier Schritte, gegen meinen Schlüssel:
# 1. Schlüssel holen, so wie es ein Fremder tun würde
gpg --auto-key-locate clear,wkd --locate-external-keys kernel-error@kernel-error.com
# 2. Claims aus der Selbstsignatur lesen
gpg --export 0x893DE0CDDE986DEB | gpg --list-packets | grep 'proof@ariadne.id'
# 3. Claims auflösen
dig +short TXT kernel-error.de | grep openpgp4fpr
dig +short TXT kernel-error.com | grep openpgp4fpr
curl -sSL https://gist.github.com/Kernel-Error/b51b81489a0600908cf09aca13765b9c/raw/proof.md
# Matrix: Nachricht im Client öffnen oder das Event per API lesen
# Raum !dBfQZxCoGVmSTujfiv:matrix.org
# Event $q3zhGJTbPWZD5LjUnsnAI9p43YzHXglniNv_SL7dldQ
# 4. Gegen den Fingerprint aus Schritt 1 vergleichen, das ist die gesamte Prüfung
Das ist Keyoxide. Mehr macht die Seite auch nicht, sie macht es nur hübscher und schneller. Wenn du diese vier Schritte einmal von Hand durchgehst, sitzt das Argument „du musst der Webseite nicht vertrauen“ deutlich besser als jede Erklärung.
Was das beweist, und was eben nicht
Damit hier niemand mit falschen Erwartungen rausgeht, die ehrlichen Grenzen:
Claims sind praktisch dauerhaft. Sie stehen in der Selbstsignatur, die auf Keyserver hochgeladen wird, und keys.openpgp.org kennt kein Zurücknehmen. Man sollte also nur Identitäten claimen, die man dauerhaft und öffentlich an den Schlüssel gebunden haben möchte. Genau deshalb habe ich Telegram und Threema bewusst weggelassen.
Xing und LinkedIn habe ich übersprungen, es gibt dort keinen brauchbaren Proof-Mechanismus. Eine Klarnamen-Bindung existiert bei mir ohnehin in stärkerer Form über die eID-Zertifizierung. Telefonnummern, SIP und Postanschrift sind nicht verifizierbar und ohnehin eine schlechte Idee.
Das beweist Kontrolle, nicht Identität. Ein grüner Haken sagt: Dieselbe Instanz kontrolliert diesen Schlüssel und dieses Konto. Er sagt nicht, wer diese Instanz ist. Dafür braucht es weiterhin so etwas wie eine eID-Zertifizierung oder ein persönliches Treffen. Das ist die ehrliche Grenze des ganzen Ansatzes, und sie zu überverkaufen wäre falsch.
Die Prüfung ist nur so stark wie der schwächste Endpunkt. Eine DNSSEC-signierte Zone auf eigenen Nameservern ist stark. Ein Gist auf einer Plattform, die morgen ihr URL-Schema ändern kann, ist es nicht.
Keyoxide selbst ist eine Bequemlichkeitsschicht. Verschwindet Keyoxide, funktionieren die Claims weiter. Verschwindet GitHub, ist genau dieser eine Claim tot.
Nachtrag: der neue Schlüssel hatte noch ein ganz anderes Problem
Beim Durchleuchten des Setups ist mir nebenbei ein echter Defekt am funkelnagelneuen Schlüssel aufgefallen: Er hatte überhaupt keine Algorithmus-Präferenzen. Die Folge war messbar und wirklich unangenehm. Ein Dritter, der an den alten Schlüssel verschlüsselt hat, bekam AES256. Beim neuen Schlüssel wurde daraus stillschweigend AES128. Der neue, modernere Schlüssel wurde also schwächer adressiert als der, den er ablöst. Das ist eine eigene Geschichte und die erzähle ich in einem eigenen Beitrag, denn der Unterschied zwischen „mein Schlüssel ist modern“ und „mein Schlüssel ist korrekt konfiguriert“ verdient mehr als eine Randnotiz.
Fazit
Das Web of Trust ist als Alltagsmechanismus tot, die Zertifizierungsstellen für Privatpersonen sind weitgehend weg, und trotzdem bleibt die Frage bestehen, wem ein Schlüssel gehört. Identity Claims sind darauf eine überraschend saubere Antwort: kein Account, kein Anbieter, der etwas hält, keine Abhängigkeit von einer Firma, die morgen verkauft wird. Nur zwei Aussagen, die aufeinander zeigen, und ein Schlüssel, der sein Profil selbst ist.
Der Aufwand ist überschaubar, wenn man die Claims sammelt und in einem Rutsch einträgt. Die Zeit geht nicht für Keyoxide drauf, sondern für die eigenen Caches und für einen alten Redirect, den nie jemand hinterfragt hat. So gesehen war der gescheiterte fünfte Claim der nützlichste von allen.
Wie immer gilt: Wenn etwas unklar ist, wenn ich mich irgendwo irre oder wenn jemand doch noch weiß, warum ein WordPress-ActivityPub-Actor bei doipjs durchfällt, dann dürft ihr mich sehr gerne fragen.