IT security, FreeBSD, Linux, mail server hardening, post-quantum crypto, DNS, retro computing & hands-on hardware hacks. Privater Tech-Blog seit 2003.

Schlagwort: InfoSec (Seite 1 von 16)

Anthropic Cyber Verification Program: wie ich Claude für Security-Arbeit freigeschaltet habe und für wen sich das lohnt

Heute hat mich mitten in einer Session in Claude Code diese Meldung erwischt:

● Opus 5.5's safeguards stopped the response above · continuing once with that noted

● Opus 5.5's safeguards flagged this session. You may be seeing this for the first time on an Opus model: Opus 5.5 is more capable and has stronger safeguards as a result, which can sometimes flag non-cybersecurity work. We're improving these safeguards to reduce the amount of incorrectly flagged messages. Opus 4.8 is answering instead, or you can edit and retry with Opus 5.5. Send feedback with /feedback or learn more: https://support.claude.com/en/articles/8106465

  Details: `[cyber]`
Anthropic Cyber Verification Program mit gesperrter Claude-Security-Anfrage, Identitätsprüfung und anschließend freigeschaltetem Zugriff für Security-Research.

Übersetzt heißt das: Ein Klassifikator hat meine Anfrage in die Schublade cyber gesteckt, Opus 5.5 hat die Antwort abgebrochen und ein schwächeres Modell hat übernommen. Der Modellwechsel ist ein vorgesehener Schutzmechanismus, auch wenn die Einstufung im Einzelfall ein Fehlalarm sein kann. Anthropic hat für bestimmte Security-Themen einen Riegel vorgeschoben, und wer dadurch bei legitimer defensiver Arbeit ausgebremst wird, kann sich um eine Freischaltung bewerben. Das Ganze heißt Cyber Verification Program, kurz CVP. Ich habe das heute durchgezogen und schreibe hier auf, warum es das gibt, für wen es gedacht ist und wie die Freischaltung abläuft. Weil öhm ja hilft vielleicht jemandem von euch.

Was hinter der Meldung steckt

Opus 5 und Opus 5.5 laufen nicht nackt. Jede Anfrage geht durch automatische Prüfungen, und die schauen nicht nur auf die letzte Nachricht. Laut Anthropics Hilfeseite zum Modellwechsel schauen die Klassifikatoren auf alles, was das Modell liest: Memory, Inhalte aus Connectors, Ergebnisse der Websuche und Dateien. Ein Fallback kann also auch durch etwas ausgelöst werden, das du gar nicht selbst getippt hast. Das könnte erklären, warum es mich in einer Session erwischt hat, in der ich eigentlich gar nichts Offensives vorhatte. Zum Beispiel, als ich diesen PHP, nein FreeBSD-rtld-Bug verfolgt habe, bin ich ein paar Mal an diesen Punkt gekommen.

Schlägt der Cyber-Klassifikator bei Opus 5 oder Opus 5.5 an, kann die Anfrage bei eingeschaltetem automatischem Modellwechsel auf Opus 4.8 zurückfallen. Als Beispiele nennt Anthropic Exploit-Generierung, das Scannen von Binaries nach Schwachstellen und Penetrationstests. Sicheres Programmieren, Quellcode nach Schwachstellen durchsuchen und Security-Issues sortieren soll dagegen weiter auf den großen Modellen laufen. Der Wechsel ist transparent, die Meldung oben ist genau dieser Hinweis. Wer den automatischen Wechsel nicht will, kann ihn in Claude Code unter Config > MODEL & OUTPUT abschalten, dann pausiert die Unterhaltung stattdessen.

Warum ich den Riegel richtig finde

Ich bin immer mal wieder als Security-Researcher unterwegs. Wenn mir irgendwo etwas auffällt, melde ich das nach Responsible Disclosure an die Betreiber. Der eine oder andere Leser erinnert sich vielleicht noch an eine etwas seltsame E-Mail oder einen noch seltsameren Anruf von mir, weil seine IP-Kamera offen im Netz hing :-). Bei genau dieser Arbeit bietet sich eine AI an. Einen Dienst einordnen, eine Konfiguration bewerten, einen Proof of Concept verstehen oder einen sauberen Report schreiben geht damit bei mir deutlich schneller.

Nur ist vieles in der IT-Sicherheit Dual Use. Das Wissen, mit dem ich eine Lücke finde und melde, lässt sich auch nutzen, um sie auszunutzen. Ein Modell, das einem Pentester beim Exploit hilft, kann ebenso jemandem helfen, der in fremde Netze einbrechen will. Dass so etwas nicht einfach jedem offensteht, der damit womöglich Mist baut, finde ich absolut richtig. Ich hätte mir eher Sorgen gemacht, wenn es diesen Riegel nicht gäbe.

Zwei Schubladen: verboten und Dual Use

Anthropic unterscheidet in der Beschreibung der Cyber-Safeguards zwei Kategorien, der Unterschied ist für das Verständnis wichtig:

  • Prohibited use: Dinge, die fast nur böswillig genutzt werden und kaum einen defensiven Zweck haben, etwa massenhafte Datenexfiltration oder die Entwicklung von Ransomware. Das bleibt gesperrt, daran ändert auch das CVP nichts. Das brauchst du so aber auch eher nicht, das zielt ja in der Regel auf Masse und das Ausnutzen.
  • High Risk Dual Use: Tätigkeiten mit einem legitimen defensiven Zweck, zum Beispiel das Ausnutzen von Schwachstellen im Rahmen eines Tests oder die Entwicklung offensiver Security-Werkzeuge. Standardmäßig gesperrt, aber genau hier setzt das CVP an. Das brauch ich und öhm du vielleicht auch?!

Das Programm ist also kein Generalschlüssel. Es verschiebt die Grenze für Leute, bei denen Anthropic nachvollziehen kann, wer sie sind und wofür sie das brauchen.

Für wen das Programm gedacht ist

Das Portal nennt als Anwendungsfälle des Programms ganz nüchtern Basic Pentesting, Red Teaming, Bug Bounty use cases. Gemeint sind also Pentester, Red Teams, Bug-Bounty-Jäger und alle, die Schwachstellen suchen, um sie zu schließen oder zu melden. Das Programm ist kostenlos. Wer dagegen nur sicheren Code schreiben oder den eigenen Quellcode prüfen will, braucht es in der Regel nicht, das läuft auch ohne.

Ein paar Einschränkungen solltest du vorher kennen:

  • Die Freigabe hängt an einer Organisation, nicht an deiner Person. Bewirbst du dich mit deinem privaten Konto, gilt sie nicht automatisch im Team-Konto deines Arbeitgebers und umgekehrt.
  • Laut Hilfeseite können nur autorisierte Admins der jeweiligen Organisation die Bewerbung im Portal aufrufen. Bei meinem privaten Konto war ich das selbst, im Firmenkonto muss das also der Admin erledigen.
  • Organisationen mit Zero Data Retention sind derzeit ausgeschlossen. Das passt dazu, dass bei mir eine Datenspeicherung Bedingung für das aktive Programm ist (dazu gleich mehr).
  • Wer Claude über Anthropic direkt nutzt, also claude.ai, Claude Code oder die API, bewirbt sich über das Verification Portal. Für Microsoft Foundry, Claude auf AWS und Google Cloud gibt es eigene Wege, auf Amazon Bedrock gibt es das Programm derzeit nicht. Bei Drittanbietern, die Claude in ihre Produkte einbauen, musst du den jeweiligen Anbieter fragen.

So läuft die Bewerbung ab

Los geht es im Verification Portal unter portal.anthropic.com/programs/cvp. Dort meldest du dich mit deinem Anthropic-Konto an und wählst das Cyber Verification Program aus. Bei mir sah der Ablauf so aus:

  1. Identität bestätigen: Zuerst musst du dich mit einem amtlichen Lichtbildausweis ausweisen, im Portal heißt das Identitätsprüfung (KYC). Details dazu im nächsten Abschnitt.
  2. Anwendungsfall beschreiben: Danach folgt ein Formular mit ein paar Fragen dazu, was du machst und wofür du die Freischaltung brauchst.
  3. Belege angeben: Dazu gehören ein paar öffentlich nachprüfbare Dinge, die zeigen, dass du dich für das Programm qualifizierst. Bei mir waren das unter anderem dieser Blog und meine öffentlichen Profile. Mein Tipp: Beschreibe deine Tätigkeit möglichst nachvollziehbar und nenne Belege, die jeder selbst prüfen kann.
  4. Prüfung abwarten: Anschließend liegt alles beim Safeguards-Team von Anthropic. Das Team prüft die Bewerbung, und wird sie genehmigt, ist das Programm für deine Organisation aktiv. Die Entscheidung kommt per E-Mail.
Verification Portal von Anthropic mit dem Cyber Verification Program im Status In Prüfung. Freigeschaltet werden sollen Basic Pentesting, Red Teaming und Bug Bounty, als Voraussetzungen sind Anwendungsfall-Formular und Identitätsprüfung abgehakt.
Bewerbung abgeschickt: Anwendungsfall und Identitätsprüfung erledigt, jetzt ist das Safeguards-Team dran.

Anthropic peilt laut eigener Aussage eine Entscheidung innerhalb von zwei Werktagen an. Bei mir ging es deutlich schneller, zwischen den beiden Screenshots in diesem Beitrag liegt nicht einmal eine halbe Stunde. Darauf würde ich mich aber nicht verlassen.

Die Identitätsprüfung: was mit deinem Ausweis passiert

Den Ausweis irgendwo hochzuladen ist nichts, was ich leichtfertig mache. Deshalb habe ich mir angeschaut, was Anthropic dazu auf der Seite zur Identitätsprüfung schreibt. Die Prüfung macht der Dienstleister Persona. Du brauchst den echten, physischen Ausweis in der Hand und ein Handy mit Kamera, gegebenenfalls für ein Live-Selfie. Akzeptiert werden gültige amtliche Lichtbildausweise, als Beispiele nennt Anthropic Reisepass, Personalausweis und Führerschein. Kopien, Scans, Fotos von Fotos und digitale Ausweise werden nicht akzeptiert.

Ausweisbild und Selfie liegen laut Anthropic bei Persona und nicht auf den eigenen Systemen. Anthropic greift nur bei Bedarf darauf zu, etwa bei einem Einspruch, und kopiert die Bilder nicht. Die Daten dienen laut Anthropic der Verifizierung, der Betrugsprävention und rechtlichen sowie sicherheitsbezogenen Pflichten. Zum Training der Modelle werden sie nicht verwendet, und außer Anthropic und den Verifizierungspartnern bekommt sie niemand, es sei denn, es gibt eine gesetzliche Pflicht zur Herausgabe. Ob man dem vertraut, muss jeder selbst entscheiden. Für mich war das in Ordnung, schließlich will ich ja gerade, dass Anthropic weiß, wer da Exploits bespricht.

Die Bedingungen, damit es aktiv bleibt

Nach der Freigabe steht das Programm im Portal auf Aktiv. Darunter stehen zwei Bedingungen, die laut Portal fortlaufend durchgesetzt werden:

  • 30-tägige Datenspeicherung für die Organisation
  • Nutzungsüberwachung und Sicherheitsklassifikatoren
Verification Portal von Anthropic mit dem Cyber Verification Program im Status Aktiv. Unter Dieses Programm aktiv halten sind die 30-tägige Datenspeicherung für die Organisation sowie Nutzungsüberwachung und Sicherheitsklassifikatoren als aktiviert markiert.
Freigegeben: Das Programm ist aktiv, solange Datenspeicherung und Überwachung eingeschaltet bleiben.

Fällt eine davon weg, ruht der Zugang, bis das behoben ist. Das ist der eigentliche Deal hinter dem Programm: Du bekommst mehr Spielraum, dafür akzeptierst du für deine Organisation eine 30-tägige Datenspeicherung sowie Nutzungsüberwachung und Klassifikatoren. Zusammen mit der Identitätsprüfung macht das Missbrauch deutlich besser nachvollziehbar. Ich finde diesen Tausch fair, für meine Arbeit waren die Bedingungen kein Problem.

Was das Programm nicht kann

Ein Punkt hat mich beim Nachlesen überrascht, und er betrifft genau die Meldung vom Anfang. Opus 5.5 ist derzeit noch nicht Teil des Cyber Verification Program. Anthropic schreibt, dass das Programm bald auf Opus 5.5 ausgeweitet werden soll. Bis dahin kann es dir auch mit aktivem CVP passieren, dass Opus 5.5 bei einer Cyber-Anfrage auf Opus 4.8 zurückfällt. Der Unterschied ist, dass die Dual-Use-Sperren auf Opus 4.8 dann gelockert sind. Laut Anthropic steht Organisationen, die Opus 4.8 über das CVP nutzen, außerdem Opus 5 mit weniger Cyber-Einschränkungen zur Verfügung.

Außerdem gilt weiter:

  • Die verbotene Kategorie bleibt verboten, auch mit Freigabe.
  • Auch freigeschaltete Nutzer können bei legitimer Arbeit noch geblockt werden. Anthropic schreibt das offen und arbeitet nach eigener Aussage daran.
  • Wirst du trotz Freigabe geblockt, prüfe zuerst, ob du in der richtigen Organisation angemeldet bist. Die Organisations-ID steht in der Bestätigungsmail. Hilft das nicht, gibt es ein Formular für Fehlalarme und Einsprüche, das auch nach einer Ablehnung der Bewerbung der richtige Weg ist.

Und bei OpenAI?

OpenAI geht für ChatGPT und Codex einen ähnlichen Weg. Die Security-Angebote dort laufen unter dem Namen Daybreak, Einzelpersonen stellen einen Antrag auf Trusted Access, den Einstieg dafür findest du unter chatgpt.com/cyber. Auch dort gehört eine Identitätsprüfung dazu, und auch dort garantiert sie keine Zulassung. Als ich mir das angeschaut habe, war für die Validierung allerdings ein größerer Account nötig. Bei Anthropic habe ich weder in der Doku noch in der Bewerbung eine Vorgabe zu einem bestimmten Abo gefunden, bei mir ging es ohne großen Account weiter. Das finde ich besser geregelt, denn gerade wer nebenbei Lücken sucht und meldet, hat nicht unbedingt das teuerste Paket.

Mein Fazit

Wer Sicherheitsforschung, Pentests oder Bug Bounty macht und dafür Claude nutzt, kann jederzeit über so eine Meldung stolpern. Dann lohnt sich das CVP. Die Bewerbung hat mich eine gute Viertelstunde gekostet, der Ausweis war das Aufwendigste daran. Im Gegenzug akzeptierst du Datenspeicherung und Überwachung, und genau so sollte das bei Werkzeugen dieser Klasse aus meiner Sicht auch sein. Wer diese Bedingungen nicht will oder nicht erfüllen kann, sollte sich ehrlich fragen, wofür er die Freischaltung eigentlich braucht.

Siehe auch:

Wenn ihr Fragen habt, dann dürft ihr mich sehr gerne fragen.

RFC 9989 löst RFC 7489 ab: was sich für DMARC wirklich ändert

Beitragsbild zu RFC 9989 und DMARC, Vergleich mit RFC 7489, neuen DMARC-Tags und E-Mail-Sicherheitsdarstellung

Stefan Haun von cyberscale.io hat mir per Mail einen Hinweis geschickt: mein alter Beitrag zu DMARC bezieht sich auf RFC 7489, das seit Mai 2026 durch RFC 9989 abgelöst ist. Der Hinweis kam, so fühlt es sich für mich an, ggf. im Rahmen von Linkbuilding, mit Verweis auf seinen eigenen Guide zu DKIM und DMARC. Das schmälert die fachliche Korrektheit des Hinweises nicht, deshalb hier der ehrliche Dank und der längst überfällige Nachtrag. Mitbekommen hatte ich die neue RFC schon vor einer Weile, geschrieben aber noch nichts, weil es bei einem frischen RFC erstmal eine Zeit braucht, bis sich das in der Praxis herumspricht und Implementierungen nachziehen. Genau dieser Verzug taucht unten beim Praxis-Check nochmal auf.

Was RFC 9989 überhaupt ist

In der Community läuft das Projekt unter dem Namen DMARCbis. Anders als der Name vermuten lässt, ist daraus keine einzelne neue RFC geworden, sondern drei:

  • RFC 9989, die Kernspezifikation: Policy, Alignment, die Tags im Record.
  • RFC 9990, die aggregierten Reports, also die täglichen XML-Berichte.
  • RFC 9991, die Failure Reports, das ARF-Format pro Nachricht.

Alle drei tragen offiziell „Obsoletes: 7489“ im Kopf, RFC 9989 zusätzlich noch „Obsoletes: 9091“ (der 2021 veröffentlichte, experimentelle Vorläufer, der np und die Public-Suffix-Domain-Idee einführte und dessen Inhalt RFC 9989 jetzt vollständig integriert und ersetzt). Dazu ein Reifegrad-Sprung, der eine eigene Erwähnung wert ist: RFC 7489 war „Informational“, RFC 9989 ist „Standards Track“, also auf dem offiziellen Weg zum Internet Standard. DMARC verlässt damit den Status der informellen Beschreibung eines etablierten Verfahrens und wird zu einer echten IETF-Spezifikation.

Die Tags: drei raus, drei gegenüber RFC 7489 neu

Der eigentliche technische Kern steckt in der Menge der Policy-Tags im Record. Drei Tags sind aus der aktiven Spezifikation gestrichen (in der IANA-Registry stehen sie weiterhin, nur als historic markiert), drei sind gegenüber RFC 7489 neu, wobei np davon eigentlich ein Wiedersehen ist:

Entfernt (RFC 7489)Neu gegenüber RFC 7489 (RFC 9989)
pct, Prozent-Rollout der Policyt, expliziter Test-Modus
rf, Report-Format, quasi nie genutztnp, Policy für nicht existierende Subdomains, importiert aus RFC 9091
ri, Report-Intervall, von Empfängern ignoriertpsd, Public-Suffix-Domain-Flag

rf und ri werden von einem RFC-9989-konformen Empfänger nicht mehr ausgewertet, RFC 9989 selbst nennt für die Streichung keine ausführlichere Begründung als das Fehlen praktischer Relevanz. pct ist der interessantere Fall, dazu gleich mehr.

t: der Testmodus, der pct ersetzt

t=n ist der Default und bedeutet normale Durchsetzung. t=y setzt die in p, sp oder np konfigurierte Policy eine Stufe herab: aus reject wird quarantine, aus quarantine wird none. Reports laufen dabei unverändert weiter, man sieht also weiterhin, wer durchfallen würde. RFC 9989 selbst nennt t ausdrücklich nur einen Ersatz für einen Teil der alten pct-Funktionalität, nämlich für die beiden Randwerte: t=n und t=y sollen sich bei Empfängern und Zwischenstationen analog zu pct=100 und pct=0 verhalten, nicht zu jedem beliebigen Prozentwert dazwischen. pct=0 unter RFC 7489 führte nach Abschnitt 6.6.4 bei reject ebenfalls schon zu quarantine statt zu gar keiner Reaktion, t=y übernimmt diesen einen Spezialfall, nur ohne Prozentrechnung. Für einen echten Teil-Rollout mit Werten zwischen 0 und 100 gibt es dagegen keinen direkten Nachfolger, dazu unten mehr.

np: eine eigene Stufe für nicht existierende Subdomains

Unter RFC 7489 galt für Mail von einer Subdomain, egal ob sie einen eigenen DNS-Eintrag hat oder nicht, immer dieselbe Regel: sp, falls gesetzt, sonst p. Eine Subdomain, die gar nicht existiert, zum Beispiel irgendwas.example.com, wo irgendwas nie angelegt wurde, bekam also exakt dieselbe Policy wie eine echte, konfigurierte Subdomain. Genau das ist ein bekanntes Spoofing-Einfallstor, weil sich beliebige, nie existierende Namen fälschen lassen, für die es nie eine eigene Policy gab. np schafft jetzt erstmals eine eigene, von sp und p unterscheidbare Stufe genau für diesen Fall. Neu erfunden ist das Tag dabei nicht: RFC 9989 übernimmt es wörtlich aus dem 2021 veröffentlichten, experimentellen RFC 9091, das mit RFC 9989 ebenfalls obsolet wird, und macht daraus einen festen Bestandteil der Hauptspezifikation. Fehlt np weiterhin, fällt die Regel auf sp zurück, falls gesetzt, sonst auf p, genau wie in RFC 9989 Abschnitt 4.7 definiert. Wer schon vorher ein striktes sp=reject gesetzt hatte, bekommt also weiterhin exakt das gleiche Verhalten wie zuvor, nur jetzt mit der Möglichkeit, es bei Bedarf davon zu lösen.

DNS-Tree-Walk statt Public Suffix List, psd markiert die Grenze

Die eigentliche Änderung ist strukturell: RFC 9989 löst die Bestimmung der Organizational Domain von der statischen, extern gepflegten Public Suffix List und ersetzt sie durch einen DNS-Tree-Walk-Algorithmus. psd (Werte y, n, u, Default u) liefert dafür die expliziten Grenzmarkierungen: psd=y setzt ein Public Suffix Operator auf seinem eigenen Record, um zu sagen, dass darunter fremde, eigenständige Organizational Domains beginnen, psd=n sagt umgekehrt, dass genau diese Domain die Organizational Domain für sich und ihre Subdomains ist. Relevant ist das vor allem für Registries und Hoster, die selbst öffentlich registrierbare Second-Level-Domains betreiben, also Strukturen wie bei .co.uk. RFC 9989 weist ausdrücklich darauf hin, dass Public-Suffix-List-Auflösung und Tree-Walk in Einzelfällen zu unterschiedlichen Ergebnissen kommen können. Für eine gewöhnliche Domain wie diese hier, ohne PSO-Rolle, ändert sich am eigenen Verhalten dadurch nichts.

Macht das bestehende Records ungültig? Nein, by design, mit einer Ausnahme in beide Richtungen

DMARC trägt seit RFC 7489 die Regel, dass unbekannte Tags ignoriert werden müssen. RFC 9989 übernimmt das wörtlich und baut die ganze Abwärtskompatibilität darauf auf. Ein alter, nur RFC-7489-konformer Validator, der t oder np nicht kennt, überliest sie einfach und wendet weiterhin p und sp an. Er stürzt dabei nicht ab und verhält sich nicht falsch, er kennt nur die feinere neue Steuerung noch nicht. Das hat aber eine Kehrseite, die man beim Einsatz von t=y kennen sollte: genau derselbe alte Validator ignoriert auch t=y und wendet bei p=reject weiterhin reject in voller Härte an. t=y schützt also nur gegenüber Empfängern, die RFC 9989 bereits verstehen, nicht gegenüber alten Implementierungen. Umgekehrt ignoriert ein RFC-9989-konformer Validator ein vorhandenes pct, weil das Tag schlicht nicht mehr definiert ist.

Genau an dieser Stelle steckt die Nuance, die wehtun kann: wer noch mit pct zwischen 1 und 99 einen echten Teil-Rollout fährt, das klassische schrittweise Hochfahren auf reject mit einem Bruchteil der Nachrichten, bekommt bei einem RFC-9989-konformen Empfänger keine reduzierte Durchsetzung mehr. Das Tag wird ignoriert, die volle in p konfigurierte Policy greift sofort. Einen direkten Nachfolger für genau diesen Zwischenbereich gibt es nicht, t kennt nur an und aus. Wer so einen Teil-Rollout fährt, muss stattdessen auf eine echte Stufenmigration ausweichen, etwa p=none, dann p=quarantine, dann p=reject, mit t=y jeweils als zusätzliche Bremse innerhalb einer Stufe. Nur der Randfall pct=0 hat mit t=y einen echten, direkten Ersatz. Für alle anderen gilt: bestehende v=DMARC1-Records bleiben syntaktisch gültig, niemand muss etwas ändern, um funktionsfähig zu bleiben. Weitgehend kompatibel ist die richtige Beschreibung, additiv dagegen nicht ganz, weil eben doch drei Tags aus der aktiven Auswertung verschwinden und die Organizational-Domain-Bestimmung sich strukturell ändert.

Mailinglisten und die p=reject-Warnung

Abschnitt 7.4 rät Domains, deren Nutzer möglicherweise auf öffentlichen Mailinglisten schreiben, ausdrücklich von p=reject ab. Der Grund ist alt und in RFC 7960 im Detail beschrieben: Listen-Reflektoren brechen die SPF-Alignment, weil die Mail über einen fremden Server läuft, und selbst DKIM-Signaturen überleben nicht jede Listen-Software unverändert. Der konkrete, im RFC-Text selbst genannte Weg ist eine gestufte Migration: zuerst mindestens einen Monat p=none, dann mindestens ebenso lange p=quarantine, jeweils mit Auswertung der Reports, bevor überhaupt über p=reject nachgedacht wird. t=y wird an dieser Stelle im RFC nicht namentlich erwähnt, würde sich als zusätzliche Bremse innerhalb einer Stufe aber inhaltlich genauso eignen wie der frühere pct-Ansatz, das ist an dieser Stelle meine eigene Einordnung, keine RFC-Vorgabe. Wichtig zusätzlich, weil es die Wucht von p=reject etwas relativiert: RFC 9989 verpflichtet Empfänger, nicht allein aufgrund eines p=reject abzulehnen, und schreibt vor, einen DMARC-Fail ohne weitere eigene Erkenntnisse wie quarantine zu behandeln.

RFC 9990: DKIM-Ergebnisse und der Selector werden verbindlicher

In den regelmäßigen, in der Praxis meist täglichen aggregierten XML-Reports war das DKIM-Auth-Ergebnis unter RFC 7489 eher beiläufig spezifiziert. RFC 9990 macht daraus eine klare Pflicht, allerdings nur bedingt: wurde für eine Nachricht überhaupt eine DKIM-Signatur validiert, muss das Ergebnis im Report stehen, eine komplett unsignierte Nachricht braucht weiterhin kein eigenes DKIM-Element. Neu und für die Praxis besonders nützlich ist zusätzlich, dass der verwendete Selector jetzt ebenfalls verpflichtend im Report steht, unter RFC 7489 war er optional. Damit lässt sich aus den Reports direkt ablesen, welcher Selector bei welchem berichtenden Mail-Receiver (nicht) verifiziert, was Diagnosen bei DKIM-Key-Rotationen deutlich erleichtert.

RFC 9991: Failure Reports und die Datenschutzfrage

Das ARF-Format für die einzelnen Failure Reports ist nicht neu, das nutzte schon RFC 7489. Neu ist, dass RFC 9991 dieses Thema aus der Kernspezifikation herauslöst, in eine eigene RFC packt und dabei ein altbekanntes Problem konkreter adressiert: Failure Reports können personenbezogene Daten enthalten, Empfängeradressen und im schlechtesten Fall Teile des Mail-Inhalts. Genau deshalb liefern nach eigener Beobachtung viele große Empfänger schon seit Jahren kaum bis gar keine ruf-Reports aus, RFC 9991 selbst schreibt, dass die Datenschutzbedenken viele Betreiber dazu gebracht haben, den Einsatz von Failure Reports einzuschränken. RFC 9991 bringt jetzt konkrete Redaction-, Datenminimierungs- und Transport-Security-Empfehlungen mit, formuliert als starke Empfehlung, nicht als Pflicht oder Garantie. Ob das in der Praxis tatsächlich mehr Empfänger dazu bringt, ruf überhaupt zu bedienen, bleibt abzuwarten, ein Schritt in die richtige Richtung ist es trotzdem.

Praxis-Check an den eigenen sechs Domains

Live abgefragt mit dig +short TXT _dmarc.<domain>, Stand heute:

kernel-error.de:        v=DMARC1; p=reject; rua=mailto:postmaster@kernel-error.de; ruf=mailto:postmaster@kernel-error.de; fo=1; sp=reject; aspf=s;
kernel-error.com:       (identisch)
kernel-error.org:       (identisch)
vandemeer.de:           (identisch)
fuchs-meckenheim.de:    (identisch)
heidbreders.de:         (identisch)

Drei Beobachtungen dazu. Erstens: pct=100 stand ursprünglich noch auf allen sechs Records, ein Überbleibsel aus der Zeit vor dem eigentlich gewünschten vollen Rollout, dazu weder rf noch ri. Weil überall ohnehin die volle Durchsetzung gewollt war und kein Teil-Rollout lief, war die neue Bedeutungslosigkeit von pct genau der Anlass, das Tag von allen sechs Records ersatzlos zu streichen, kein Muss, aber ein guter Moment dafür.

Zweitens: heidbreders.de stand bislang als einzige der sechs Domains auf sp=none, die anderen fünf auf sp=reject. Beim Aufräumen gleich vereinheitlicht, jetzt tragen alle sechs sp=reject. np explizit zu setzen würde am eigenen Verhalten trotzdem nichts ändern, weil der Default, der Rückfall auf sp, für alle sechs Domains ohnehin schon reject ist. Es besteht kein Handlungsbedarf, ein expliziterer Record wäre höchstens Geschmackssache.

Drittens, und das ist der Punkt, der zur eingangs erwähnten Adoptionsverzögerung passt: rspamd, hier in Version 4.1.0 der eigene DMARC-Validator für eingehende Mail, unterstützt np nach aktuellem Stand von September 2026 noch nicht. Bekommt der eigene Filter eine gespoofte Mail von einer nicht existierenden Subdomain einer fremden Marke, deren Domain-Owner über np strenger sein wollte als über sp, wendet rspamd aktuell trotzdem die laxere sp-Policy an. Eine kleine, aber reale Lücke auf der Empfänger-Seite, bis rspamd nachzieht. Vier Monate nach Publikation der RFC haben ohnehin die wenigsten Absender-Domains np überhaupt gesetzt, akuten Grund zur Sorge gibt das nicht, einen Blick in die rspamd-Release-Notes aber schon.

Die Frage aus Abschnitt 7.4 ist dabei nicht, ob ich selbst eine Mailingliste betreibe, sondern ob Absender meiner Domains an fremden Mailinglisten oder ähnlichen indirekten Mail-Flows teilnehmen. Bei mir ist das praktisch nicht der Fall, für die eigene Infrastruktur bleibt die gestufte Migration also Theorie. Für Leser mit Nutzern, die auf Mailinglisten schreiben, ist sie es sehr wohl.

Kurzfassung: Kein akuter Handlungsbedarf für Domains mit bestehendem p=reject und pct=100. Wer noch einen Teil-Rollout mit einem pct-Wert zwischen 1 und 99 fährt, findet in t keinen direkten Ersatz und muss auf eine echte Stufenmigration über p ausweichen, nur der Randfall pct=0 lässt sich eins zu eins durch t=y ersetzen. np ist ein sinnvolles Sicherheits-Add-on, dessen Wirkung von der Update-Geschwindigkeit der empfängerseitigen Implementierungen abhängt, ein gutes Beispiel dafür, dass ein neues RFC nicht am Tag der Veröffentlichung live ist, sondern über Monate und Jahre durch Software-Updates bei allen Beteiligten durchsickert.

Siehe auch

Eigene Erfahrungen mit der Umstellung auf RFC 9989, oder Fragen zu DMARC allgemein? Dann darfst du mich sehr gerne fragen.

Keyoxide: der fünfte Claim scheiterte an der Position im Profil, nicht am Beweis

Technische Illustration zum Keyoxide-Claim: Ein OpenPGP-Beweis wird im ActivityPub-JSON von Position fünf auf Position eins verschoben und anschließend erfolgreich verifiziert.

Zwei Keyoxide-Beiträge liegen schon hier, im ersten vier Claims über DNS, GitHub und Matrix, im zweiten die Linux-App und der Matrix-Kanal des Projekts. Was in beiden fehlte, war ein fünfter Claim, der genau dieses Blog beweist, den Fediverse-Actor @kernel-error.de@www.kernel-error.de. Der ist keine Mastodon-Instanz, sondern das WordPress-ActivityPub-Plugin, das aus diesem Blog selbst einen Actor macht. Im August habe ich genau diesen Claim gebaut, ausgerollt und noch am selben Tag wieder zurückgebaut, weil er einfach nicht grün wurde. Das hier ist die Geschichte, warum nicht, und was seit heute anders ist.

Der gescheiterte erste Versuch im August

Der Claim in der Selbstsignatur des Schlüssels: proof@ariadne.id=https://www.kernel-error.de/@kernel-error.de. Der Beweis dazu: ein Extra Field des ActivityPub-Plugins namens OpenPGP mit dem Wert openpgp4fpr:45FC…6DEB, exakt die Platzierung, die Keyoxide selbst als unterstützt dokumentiert. Der Actor lieferte auch wortgetreu {"type":"PropertyValue","name":"OpenPGP","value":"openpgp4fpr:45FC…6DEB"} aus. Trotzdem blieb der Claim rot.

Bei der Fehlersuche im August ist mir dabei ein echter Bug auf der eigenen Seite untergekommen und gleich mitgefixt: WordPress beantwortete OPTIONS-Anfragen auf die Actor-URL mit einem 301 auf die Startseite, ein umgeleiteter CORS-Preflight scheitert damit in jedem Browser hart. Eine eigene nginx-Location, die den Preflight selbst mit 204 beantwortet, hat das behoben, dauerhaft, unabhängig vom Rest dieser Geschichte, weil es jeden browserseitigen Consumer des Actors betrifft. Das steht bereits ausführlich im ersten Keyoxide-Beitrag.

Was im August systematisch ausgeschlossen wurde

Nach dem CORS-Fix ließ sich der Actor sauber abrufen, im Access-Log stand GET /@kernel-error.de 200 … "doipjs/2.1.0", das Werkzeug hinter Keyoxide hatte den Actor also tatsächlich in der Hand. Der Rest wurde einzeln durchgestrichen. Provider-Matching: die ActivityPub-Regel von doipjs ist ein Catch-all für jede https-URL, NodeInfo-Anfragen im Log belegten, dass die ActivityPub-Nachbearbeitung wirklich lief. Feld-Pfad: attachment.value ist einer von drei Pfaden, die doipjs durchsucht, neben summary und content. Groß- und Kleinschreibung des Fingerprints: doipjs kleinschreibt vor dem Vergleich, also irrelevant. Am Ende blieb nur ein Rückbau übrig, Notation raus, Redeploy über alle sieben Schlüssel-Kanäle, Feld gelöscht. Alles sah richtig aus, und trotzdem war es rot. Genau dieser Widerspruch ist der Grund, warum ich das jetzt, im September, zu Ende verfolgt habe.

Die Ursache im Verifizierer: doipjs und runJSON

doipjs ist die Bibliothek hinter keyoxide.org, Quelltext offen auf Codeberg. Die eigentliche Suchfunktion heißt runJSON(proofData, checkPath, params) und läuft einen JSON-Pfad ab. Zwei Zeilen daraus erklären den ganzen Fehler:

// Array: der Reihe nach abarbeiten, kein try/catch pro Element
if (Array.isArray(proofData)) {
  let result = false
  for (let index = 0; index < proofData.length; index++) {
    if (result) { continue }
    result = await runJSON(proofData[index], checkPath, params)
  }
  return result
}

// Objekt ohne den naechsten Schluessel im Pfad: wirft, sofort
if (typeof proofData === 'object' && !(checkPath[0] in proofData)) {
  throw new Error('err_json_structure_incorrect')
}

Steckt der Beweis in einem Array, iteriert runJSON über die Einträge und ruft sich selbst für jedes Element auf, ohne dabei einzelne Fehler abzufangen. Fehlt einem Objekt der nächste erwartete Schlüssel im Pfad, wirft die Funktion sofort. Beides zusammen bedeutet: im ersten Element eines Arrays, dem der gesuchte Schlüssel fehlt, bricht die ganze Suche ab, alles danach wird nie mehr angeschaut. Aufgefangen wird das erst eine Ebene höher, in run(), und zwar pro Zielpfad einzeln, summary, attachment.value, content bekommen je einen eigenen Try-Block. Ein Wurf in attachment.value gibt also nur diesen einen Pfad auf, summary und content laufen trotzdem weiter, finden dort aber naturgemäß nichts, weil der Beweis nur im Attachment-Array steht.

Die andere Hälfte: wie das ActivityPub-Plugin Profilfelder ausliefert

Die zweite Hälfte des Bugs steckt im WordPress-Plugin selbst, in class-extra-fields.php, Funktion fields_to_attachments(). Für jedes einzelne Profilfeld erzeugt die Funktion nicht ein Attachment, sondern zwei: zuerst ein klassisches PropertyValue mit name und value, das ist die Tabelle, die Mastodon rendert. Danach, als Unterstützung für FEP-fb2a, entweder ein Link mit href, wenn der Wert ein einzelner Link ist, oder ein Note mit content, wenn nicht.

Weder Link noch Note hat einen Schlüssel namens value. Im echten Actor sah das im August deshalb so aus:

attachment[0]  PropertyValue  Blog   value=...   geprueft, kein Treffer
attachment[1]  Link           Blog   href=...    kein value-Schluessel, wirft
attachment[2+] ...                               nie erreicht

doipjs prüft auf einem WordPress-Actor also faktisch nur attachment[0]. Die bestehenden Felder standen im August in der Reihenfolge Blog, GitHub, Matrix, Kontakt, das OpenPGP-Feld kam als fünftes dazu und lag damit nie an erster Stelle. Genau das erklärt jede einzelne Beobachtung von damals: erfolgreich abgerufen, korrektes JSON im Feld selbst, trotzdem rot.

Warum das auf Mastodon nie auffällt

Mastodon liefert für seine eigenen Profilfelder ausschließlich PropertyValue-Einträge aus, jedes Element im Array trägt also value, und die Schleife läuft ungestört durch, egal an welcher Position der Beweis steht. Der FEP-fb2a-Zusatz ist eine Erweiterung für genau die Fälle, die Mastodon nicht abdeckt, in diesem Fall anklickbare Links im Profil außerhalb der reinen Tabellenoptik. Niemand hat hier etwas falsch gemacht: das Plugin erweitert sinnvoll über Mastodon hinaus, doipjs ist gegen Mastodons Datenform geschrieben, und beide Annahmen treffen für sich genommen zu. Nur zusammen ergeben sie eine Reihenfolge-Abhängigkeit, die auf keinem der beiden Wege sichtbar wird, wenn man nur die eigene Seite testet.

Beim Gegenlesen dieser Analyse kamen noch zwei Präzisierungen dazu, die den Befund nicht ändern, aber schärfen. Erstens: ein Wurf in attachment.value beendet nicht die gesamte Verifikation, nur diesen einen Pfad, summary und content laufen wie oben beschrieben trotzdem weiter. Zweitens: ein reiner Unit-Test von verifications.run() übergeht den eigentlichen Netzwerk-Abruf, der wirkliche Beweis ist ein kompletter Durchlauf über Claim.match() und verify() gegen den lebenden Actor, dazu unten mehr. Vorschlag aus derselben Runde war außerdem, den Beweis zusätzlich oder stattdessen im Profil-summary zu platzieren, das ist positionsunabhängig. Entscheidung: das Extra Field bleibt, weil es das sauberere Profil ergibt, die Reihenfolge-Abhängigkeit wird dafür hier dokumentiert, summary bleibt die bekannte Rückfalloption.

Reproduziert, bevor irgendetwas am Schlüssel geändert wurde

Bevor der Schlüssel angefasst wurde, habe ich doipjs 2.1.0 lokal aus npm in ein Wegwerf-Verzeichnis installiert und dessen eigene verifications.run() plus die ActivityPub-Provider-Definition direkt gegen das echte, live abgerufene Actor-JSON laufen lassen, mit dem Beweis an unterschiedlichen Positionen eingesetzt:

live actor, unveraendert                             result=false errors=["err_json_structure_incorrect","err_json_structure_incorrect"]
OpenPGP an erster Stelle (Index 0)                   result=true  errors=[]
OpenPGP an letzter Stelle (Index 8), wie im August    result=false errors=["err_json_structure_incorrect","err_json_structure_incorrect"]
Nur PropertyValue (Mastodon-Form), OpenPGP letzte     result=true  errors=[]

Die beiden Fehler in den ersten zwei Zeilen sind der attachment.value-Pfad, dort steht der Link- oder Note-Eintrag ohne value, und der content-Pfad, ein Person-Actor hat kein Top-Level-content-Feld. Die letzte Zeile ist die Kontrollprobe: derselbe Beweis an derselben Position funktioniert, sobald das Array die reine Mastodon-Form hat. Vier Zeilen, die die ganze Geschichte erzählen. Der relative Importpfad war dabei nötig, doipjs/src/… lässt sich nicht als Package-Subpath importieren, die exports-Angabe in dessen package.json blockt das.

Der Fix auf dem Blog

Das Extra Field OpenPGP wurde neu angelegt, mit menu_order = -1, während die bestehenden vier auf 0 bis 3 stehen, Inhalt im selben Absatz-Blockformat, das das Plugin auch für die anderen Felder benutzt, <!-- wp:paragraph --><p>openpgp4fpr:45FC…6DEB</p><!-- /wp:paragraph -->. Eingefügt wurde das per direktem $wpdb->insert(), nicht über wp_insert_post(), weil Letzteres die ActivityPub-Hooks auslöst und ein Update für den Actor in die Outbox stellt, das später als Geister-Post auftaucht, Blog-Titel, Datum 2003. Gemessen: ap_outbox stand vorher und nachher bei genau demselben Wert. Danach zuerst der Redis-Object-Cache geleert, dann der nginx-FastCGI-Cache, die umgekehrte Reihenfolge würde FastCGI aus noch altem Redis-Inhalt neu befüllen. Im lebenden Actor sieht das Ergebnis jetzt so aus:

0 {"type": "PropertyValue", "name": "OpenPGP", "value": "openpgp4fpr:45FCD081ADB54872EA5B06B9893DE0CDDE986DEB"}
1 {"type": "Note", "name": "OpenPGP", "content": "openpgp4fpr:45FCD081ADB54872EA5B06B9893DE0CDDE986DEB"}
2 {"type": "PropertyValue", "name": "Blog", "value": "<a href=...>"}
3 {"type": "Link", "name": "Blog", "href": "https://www.kernel-error.de", ...}

Der reine Text bekommt hier eine Note als Begleitung, kein Link drin, also kein Link-Eintrag. attachment[0] trägt jetzt den Beweis, genau da, wo doipjs zuerst und im Zweifel einzig hinsieht.

Die Gegenprobe vor der Schlüsseländerung

Nur der Reihenfolge-Test allein hätte mir nicht gereicht. Vor der eigentlichen Schlüsseländerung lief deshalb ein kompletter doipjs-Durchlauf, Abruf, Provider-Matching, Verifikation, Nachbearbeitung, gegen die produktive Seite, mit dem Fingerprint direkt übergeben, den Beweis brauchte der Schlüssel dafür noch nicht:

const claim = new Claim('https://www.kernel-error.de/@kernel-error.de', FINGERPRINT)
claim.match()      // ergibt: activitypub, owncast (die URI ist absichtlich mehrdeutig)
await claim.verify()
// ergibt: status 200 (VERIFIED), provider "wordpress" (aus NodeInfo),
//         profile "@kernel-error.de@www.kernel-error.de"

Erst nachdem dieser Test grün war, bekam der Schlüssel die Notation, dieselbe Regel wie schon bei den DNS-Claims: der Schlüssel behauptet nie öffentlich etwas, das noch nicht beweisbar ist.

Schlüssel und Redeploy über sieben Kanäle

Die Notation kam ausschließlich in die Namens-UID, in einer skriptgesteuerten gpg --batch --command-fd 0 --edit-key-Sitzung, sonst bekommt auch die Foto-UID den Claim mit. Danach geprüft: fünf Claims auf UID 1, Algorithmus-Präferenzen und Feature-Flags unverändert, die Governikus-eID-Zertifizierung und die Gegensignatur des alten Schlüssels weiterhin gültig, Ablaufdaten unverändert, Foto-UID unangetastet. Jede Claim-Änderung erzeugt eine neue Selbstsignatur, deshalb musste der aktualisierte Schlüssel auf alle sieben Kanäle: keys.openpgp.org, keyserver.ubuntu.com, pgpkeys.eu, WKD, sowohl über die Advanced-Methode via keys.openpgp.org als auch direkt selbst gehostet, der Download auf dem Blog, und der DANE-OPENPGPKEY-Record per Zone-Freeze/Thaw, DNSSEC-ad-Flag danach an drei öffentlichen Resolvern geprüft.

Eine gute Frage, die während des Rollouts aufkam: wenn WKD auf der eigenen Seite doch reicht, warum überhaupt noch auf die Keyserver hochladen, während gerade erst getestet wurde? Zwei Antworten. Erstens war der Test zu diesem Zeitpunkt schon abgeschlossen, der Ende-zu-Ende-Check lief gegen den lebenden Actor mit dem blanken Fingerprint, bevor der Schlüssel den Claim überhaupt trug, veröffentlicht wurde der Schlüssel erst, als das Ergebnis grün war. Zweitens ist WKD hier gar nicht wirklich nur die eigene Seite: die WKD-Advanced-Methode unter openpgpkey.kernel-error.com ist ein CNAME auf den WKD-Dienst von keys.openpgp.org, und Clients probieren Advanced zuerst. Auch das Keyoxide-Profil per Fingerprint holt sich den Schlüssel von keys.openpgp.org. Ohne das Keyserver-Update hätten die meisten Prüfer also weiterhin den alten Schlüssel mit vier Claims gesehen. Wahr und ebenfalls wichtig: Keyserver behalten jede Generation einer Selbstsignatur, es zählt nur die jüngste. Ein Claim lässt sich mit einer neueren Selbstsignatur zurückziehen, die alte Version verschwindet aber nie, das gehört vor der Veröffentlichung mitentschieden, nicht danach.

Das Ergebnis

Der Schlüssel abgerufen wie ein Fremder ihn abrufen würde, per HKP von keys.openpgp.org und per WKD, und mit doipjs gegen alle fünf Claims geprüft:

OK  200  wordpress  @kernel-error.de@www.kernel-error.de
OK  200  github     Kernel-Error
OK  200  dns        kernel-error.de
OK  200  dns        kernel-error.com
(Matrix braucht auf der Pruefer-Seite einen echten Matrix-Account, auf keyoxide.org gruen, in einem blanken lokalen Test nicht pruefbar)

Der Provider steht dabei als wordpress, doipjs liest den Software-Namen aus NodeInfo aus, bei diesem Blog tatsächlich {"software":{"name":"wordpress","version":"7.1"}}, live nachgeprüft. Die naheliegende erste Reaktion darauf war „müsste da nicht Mastodon stehen?“, die Antwort gehört in diesen Beitrag: der Account ist ein Fediverse-Account, kein Mastodon-Account, Mastodon ist nur eine von mehreren Implementierungen. Ein Account auf mastodon.social würde [mastodon] zeigen, einer auf einer GoToSocial-Instanz [gotosocial]. Man könnte den Namen in NodeInfo fälschen, das wäre aber falsch und nicht nur stilistisch: Server und Clients passen ihr Verhalten am Software-Namen aus, sie würden dann Mastodon-spezifische Endpunkte abfragen, die hier gar nicht existieren, und es wäre eine unwahre Aussage in einem Profil, dessen ganzer Zweck überprüfbare Echtheit ist.

Frische Schlüsselbunde über gpg --auto-key-locate clear,<methode> --locate-external-keys liefern für WKD, DANE und Keyserver jeweils alle fünf Claims. Größen mit fünf Claims: WKD direkt 13178 Byte, der volle Download 17926 Byte, der DANE-Record 1368 Byte, davon 70 Byte allein für diesen neuen Claim. Im Actor liegt attachment[0] jetzt auf PropertyValue OpenPGP, attachment[1] auf dessen Note-Begleitung, der Wert steht als reiner Text ohne umschließendes <p>, weil ein vorhandenes Code-Snippet auf dem Blog genau das aus Extra-Field-Werten entfernt, ursprünglich damit Mastodons rel="me"-Linkprüfung funktioniert, die einen nackten <a> als Wurzelelement erwartet. Zwei verschiedene Prüfer, zwei verschiedene Annahmen über die Form derselben Daten, an dieser einen Stelle trifft es genau zusammen.

Auf Mastodon selbst zeigt sich das neue Profilfeld erst, wenn entfernte Instanzen den Actor erneut abrufen, absichtlich wurde keine Update-Aktivität verschickt, das wäre wieder der Geister-Post-Mechanismus von oben. Keyoxide betrifft das nicht, das Werkzeug holt den Actor bei jeder Prüfung frisch.

Keyoxide-Profil mit fünf grünen Identity Claims, darunter der Fediverse-Actor @kernel-error.de@www.kernel-error.de mit dem Vermerk [wordpress].
Fünf von fünf, seit heute auch der Fediverse-Claim.

Lektionen

  • „Bei mir ist alles richtig“ kann wahr sein und trotzdem scheitern. Entscheidend sind die Annahmen des Verifizierers über die Form der Daten, nicht nur der Inhalt. Den Quelltext des Prüfers lesen, bevor man die eigene Seite zum fünften Mal durchsucht.
  • Eine Plugin-Funktion für bessere Zukunftskompatibilität, hier FEP-fb2a, bricht einen Verbraucher, der gegen Mastodons Datenform geschrieben wurde. Für sich genommen ist keiner der beiden im Unrecht.
  • Eine versteckte Abhängigkeit blieb übrig: der Beweis funktioniert nur, solange das OpenPGP-Feld das erste Profilfeld ist. Wer eigene Extra Fields verwaltet, sollte das im Kopf behalten.

Ein Upstream-Report an doipjs auf Codeberg ist noch nicht eingereicht. Der eigentlich richtige Fix wäre ein Try/Catch pro Array-Element in runJSON, das würde nebenbei auch die Abhängigkeit von der Feldreihenfolge auflösen.

Aufgeklappter Fediverse-Claim auf Keyoxide mit Profil-Link, Proof-Link und dem Hinweis, dass die Prüfung abgeschlossen ist und der Claim durch den Beweis bestätigt wurde.
Aufgeklappt: Profil-Link, Proof-Link, und die Prüfung ist durch.

Siehe auch

Selbst schon mal an einer Reihenfolge-Abhängigkeit zwischen zwei fremden Projekten verzweifelt, oder eigene Erfahrungen mit ActivityPub-Profilfeldern? Dann darfst du mich sehr gerne fragen.

Keyoxide: die Linux-App ist im Flathub, dazu ein Android-Update und ein neuer Matrix-Kanal

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.

Keyoxide-App unter Linux Mint als Flatpak, Startbildschirm mit dem Suchfeld für ein Profil.
Die Keyoxide-App aus dem Flathub, Startbildschirm mit Suchfeld.
Keyoxide-App zeigt das eigene Profil mit vier bestätigten Identity-Claims und dem öffentlichen Schlüssel über WKD.
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.

Keyoxide-App zeigt den Profile-ID-QR-Code mit dem Key-Link zur WKD-Datei des öffentlichen Schlüssels.
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.

Siehe auch

Eigene Erfahrungen mit Keyoxide, der Flatpak- oder der Android-App? Dann darfst du mich sehr gerne fragen.

BIOS erklärt, Teil 4: AES-NI, SGX, TME und ein Schalter der nichts bewirkt

In der letzten Folge ging es um die obere Hälfte der CPU-Seite, also Kerne, Threads und Prefetcher. Die untere Hälfte ist die spannendere, weil dort keine Leistungsschalter mehr stehen, sondern Sicherheitsfunktionen. Und weil mindestens einer davon eine richtig gute Geschichte hat.

Zur Erinnerung für alle die neu dazustossen: ich gehe hier das BIOS meines Supermicro-Serverboards durch, Themenblock für Themenblock, alle Folgen liegen unter BIOS & Firmware.

AES-NI, oder: der Grund warum es eine Karte gab

Aptio Setup, CPU Configuration, mit Hyper-Threading, den fünf Prefetchern und AES-NI
AES-NI                          [Enable]

Ein Schalter und dahinter steckt eine ganze Ära. AES-NI sind sechs zusätzliche Prozessorbefehle, die AES in Hardware rechnen statt in Software. Der Unterschied ist keine Feinheit, er liegt grob bei einer Grössenordnung, und obendrein sind die Hardwarebefehle immun gegen die Cache-Timing-Seitenkanäle, mit denen man Software-AES angreifen kann. Aber vor allem is et dann ers rischtig flott mit der Crypto.

Ob die CPU es kann und darf, sieht man sofort im OS mit:

grep -o ' aes ' /proc/cpuinfo | head -1
#    aes

Warum es diesen Schalter überhaupt gibt, ist eine berechtigte Frage. Man schaltet AES-NI ja nicht ab, weil man gern langsam verschlüsselt. Die ehrliche Antwort ist vermutlich, dass er aus einer Zeit stammt, in der AES-NI ein Unterscheidungsmerkmal zwischen Produktlinien war, und niemand hat ihn seitdem entfernt. Oder hat jemand eine bessere Erklärung?

Über genau diese Zeit habe ich hier schon geschrieben. In meinem Beitrag zur Intel QuickAssist 8950-SCCP steckt eine Karte, die es nur deshalb gab, weil manche CPUs kein AES-NI hatten. Wer 2013 ein NAS mit einer Atom-CPU verschlüsseln wollte, brauchte Hilfe. Heute ist der Schalter eine Formalie, damals war er die Grenze zwischen „geht“ und „geht nicht“.

Virtualisierung und ein Schalter der Vertrauen heisst

Intel Virtualization Technology [Enable]
Enable SMX                      [Disable]

Der erste ist VT-x, im Handbuch etwas altmodisch VMX und in einer noch älteren Fassung sogar Vanderpool Technology genannt, nach dem Codenamen von 2003. Ohne ihn läuft keine Hardware-Virtualisierung, also weder KVM noch VirtualBox mit brauchbarer Geschwindigkeit. Bleibt selbstverständlich an.

Der zweite ist interessanter. SMX steht für Safer Mode Extensions und ist die Grundlage für Intel TXT, Trusted Execution Technology. Vereinfacht gesagt geht es darum, dass eine Maschine beim Start beweisen kann, dass sie genau die Software geladen hat, die sie laden sollte. Ein gemessener Start, verankert in der Hardware, ähnlich wie Measured Boot mit einem TPM, nur eine Ebene tiefer.

Dabei ist mir etwas aufgefallen, das ich nicht restlos erklären kann. Im Setup steht SMX auf Disable. In /proc/cpuinfo steht das Flag smx trotzdem drin. Die naheliegende Deutung ist, dass das Flag die Fähigkeit der CPU beschreibt und der BIOS-Schalter nur die Freigabe, dass CPUID also weiterhin „kann ich“ meldet, während die Firmware „darfst du nicht“ sagt. Belegen kann ich das nicht, ich habe es nur beobachtet. Wer da genauer Bescheid weiss, immer her damit. Ich bin ja auch nur doof.

Speicherverschlüsselung, und warum SGX daran hängt

Total Memory Encryption (TME)   [Disabled]

TME verschlüsselt den kompletten Arbeitsspeicher. Der Schlüssel wird bei jedem Start neu erzeugt, liegt im Speichercontroller und verlässt die CPU nie. Für das Betriebssystem ist das unsichtbar, es merkt schlicht nichts davon.

Wogegen hilft das? Gegen Angriffe, die physisch am Speicher ansetzen. Der Klassiker ist der Cold-Boot-Angriff, bei dem man die Riegel kühlt, aus der laufenden Maschine reisst und in einem anderen Gerät ausliest, weil DRAM seinen Inhalt für Sekunden bis Minuten behält. Dazu kommt alles, was per DMA am Speicher horcht. Bei einem Server im fremden Rechenzentrum ist das kein theoretisches Szenario. Sucht das mal auf YouTube ist so, das sieht immer krass aus. Vielleicht kann ich so etwas selbst irgendwann mal probieren.

Bei mir steht es auf Disabled, und das ist der Werksdefault. Meine Maschine steht in meiner Wohnung, die Systemplatte ist ohnehin mit LUKS verschlüsselt, und das Angriffsmodell, gegen das TME hilft, setzt jemanden voraus der hier steht während die Kiste läuft. Falls das passiert, habe ich grössere Probleme. Reizvoll finde ich es trotzdem, denn die Kosten sind gering: die Verschlüsselung sitzt im Speichercontroller und macht sich in Messungen kaum bemerkbar.

Ein Detail, über das ich beim Lesen des Handbuchs gestolpert bin, erklärt den Aufbau des Menüs:

*If the feature above is set to Enabled, the next five features are displayed*

Erst wenn TME an ist, erscheinen die fünf Zeilen darunter, und dazu gehört auch SGX. Die Enklaven-Technik hängt hier also an der Speicherverschlüsselung. Das ergibt Sinn, denn der geschützte Speicherbereich einer Enklave muss ja gegen genau die Zugriffe abgesichert sein, die TME abdeckt. Im Setup sieht man davon nur eine flache Liste, im Handbuch die Abhängigkeit.

SGX, und wie Intel die UHD-Blu-ray auf dem PC beerdigt hat

SW Guard Extensions (SGX)       [Disabled]
SGX Factory Reset               [Disabled]
SGX Package Info In-Band Access [Disabled]

SGX ist die Idee, dass ein Programm einen Speicherbereich anlegen kann, in den niemand hineinsehen darf. Nicht andere Programme, nicht der Kernel, nicht der Hypervisor, nicht einmal jemand mit Root. Die CPU verschlüsselt den Bereich und gibt ihn nur dem Code frei, der ihn angelegt hat. Diese Enklaven waren als Fundament für vertrauliche Berechnungen in fremden Rechenzentren gedacht.

Die Geschichte drumherum ist allerdings die bessere. SGX war jahrelang in normalen Desktop-Prozessoren drin, und die prominenteste Anwendung war nicht vertrauliches Rechnen, sondern Kopierschutz. Die Wiedergabe einer Ultra-HD-Blu-ray auf dem PC verlangte SGX, weil der Schlüsselaustausch in einer Enklave passierte. Dann hat Intel SGX ab der elften Generation aus den Consumer-CPUs entfernt, und damit war die UHD-Blu-ray-Wiedergabe auf neuen PCs vorbei. Wer eine Scheibe abspielen wollte, brauchte plötzlich einen älteren Prozessor.

Ich finde das aus mehreren Gründen bemerkenswert. Erstens ist es ein schönes Beispiel dafür, wie eine Sicherheitstechnik zweckentfremdet wird und dann an einer Produktentscheidung stirbt, die mit ihrem eigentlichen Zweck nichts zu tun hat. Zweitens hat es Leute getroffen, die für ihre legal gekauften Scheiben plötzlich keinen legalen Abspielweg mehr hatten, während der inoffizielle Weg weiterhin problemlos funktionierte. Kopierschutz eben.

Im Xeon lebt SGX weiter, dort war es nie der Kopierschutz sondern das Verkaufsargument. Bei mir steht es trotzdem aus, weil ich keine Software habe die Enklaven nutzt, und weil es ohnehin nur zusammen mit TME ginge. Der Vollständigkeit halber, im laufenden System taucht folgerichtig nichts davon auf:

grep -c ' sgx' /proc/cpuinfo    # 0
ls /sys/devices/system/cpu/ | grep -c sgx    # 0

46 Bit, wegen Hyper-V

Limit CPU PA to 46 bits         [Enable]

PA steht für Physical Address. Der Schalter begrenzt die Breite der physischen Adressen auf 46 Bit, was 64 Terabyte adressierbarem Speicher entspricht. Bei 256 GB im Rechner ist das reichlich weit weg von jeder Grenze. 46 Bit und 64 Bit, da muss ich direkt an fe80 bei IPv6 denken und das man die Luft gelassen hat, weil MAC Adressen auch irgendwann zu wenige sind. Ok da sind es 48 Bit aber denken muss ich daran.

Die Begründung im Handbuch ist knapp und ziemlich entlarvend:

Use this feature to limit the CPU physical address to 46 bits to support older hyper-v.

Ältere Hyper-V-Versionen kamen mit breiteren Adressen nicht klar, also gibt es im BIOS eines Serverboards von 2021 einen Schalter, der die CPU künstlich einschränkt, damit eine bestimmte Microsoft-Software läuft. Das ist Abwärtskompatibilität in ihrer reinsten Form. Der Effekt lässt sich direkt nachlesen:

grep -m1 'address sizes' /proc/cpuinfo
#   address sizes : 46 bits physical, 57 bits virtual

46 physisch, wie eingestellt. Die 57 virtuellen Bit sind übrigens eine ganz andere Baustelle, das ist Five-Level Paging, ebenfalls neu mit Ice Lake.

Eine Seriennummer im Prozessor

Aptio Setup, untere Hälfte der CPU Configuration, mit TME, SGX, PPIN Control und Extended APIC
PPIN Control                    [Lock/Disable]

PPIN steht für Protected Processor Inventory Number, eine eindeutige Nummer pro physischer CPU. Gedacht ist sie für die Fehleranalyse in grossen Flotten: wenn ein Prozessor Speicherfehler meldet, will man wissen welcher es war, und zwar auch nachdem er längst ausgebaut wurde.

Eine eindeutige, unveränderliche Hardwarenummer ist allerdings genau die Sorte Datenpunkt, an der sich vor Jahren schon einmal die Gemüter erhitzt haben. Der Pentium III hatte eine Seriennummer, der Aufschrei war so gross, dass Intel sie wieder entfernt hat. PPIN ist die gleiche Idee, nur diesmal mit einem Schalter davor und beschränkt auf Serverprozessoren.

Hier ist mir eine Abweichung aufgefallen. Das Handbuch nennt als mögliche Werte:

The options are Unlock/Disable and Unlock/Enable.

Im Setup steht aber Lock/Disable, ein Wert den das Handbuch gar nicht kennt. Passend dazu fehlt im laufenden System das Flag:

grep -c intel_ppin /proc/cpuinfo    # 0

Meine Lesart: Lock bedeutet, dass das Register gesperrt ist und die Nummer nicht ausgelesen werden kann, und das Handbuch ist an dieser Stelle schlicht nicht auf dem Stand der Firmware. Für mich ist das der Wunschzustand, ich brauche keine Seriennummer und niemand sonst braucht sie von mir. Nur schön dokumentiert ist es eben nicht.

Und zum Schluss der Schalter, der nichts bewirkt

Extended APIC                   [Disable]

Das ist mein Lieblingsfund auf dieser Seite, weil er aussieht wie eine vergessene Optimierung. Der APIC ist der Interrupt-Controller. x2APIC ist seine erweiterte Fassung, die mehr adressierbare Prozessoren und schnelleren Zugriff über Register statt über den Speicher erlaubt. Auf Disable denkt man reflexhaft: aha, hier liegt Leistung brach, das schalte ich mal ein.

Nur ist die Funktion längst aktiv. Aus dem Kernel-Log meiner laufenden Maschine:

DMAR-IR: Queued invalidation will be enabled to support x2apic and Intr-remapping.
DMAR-IR: Enabled IRQ remapping in x2apic mode
x2apic enabled
APIC: Switched APIC routing to: cluster x2apic

Der Grund ist eine Abhängigkeit, die man kennen muss: Interrupt-Remapping über VT-d verlangt x2APIC. Sobald der Kernel die IOMMU mit Interrupt-Remapping hochfährt, schaltet er x2APIC selbst ein, unabhängig davon was die Firmware vorher getan hat. Der BIOS-Schalter steuert nur, ob die Firmware das schon vorab macht. B.T.W.: habe ihr euch MMU beim Commodore C128 mal angeschaut?

Ich lasse ihn deshalb bewusst in Ruhe, und ich schreibe das hier so ausführlich auf, weil er genau die Art Schalter ist, an dem man beim nächsten Setup-Besuch wieder hängenbleibt und denkt, man hätte etwas übersehen. Hat man nicht.

Nächste Folge: Speicher. ECC, Patrol Scrub, und die Frage warum mein DDR4-3200 mit 2933 läuft und das völlig in Ordnung ist.

Siehe auch

Weiss jemand genauer, warum das smx-Flag in /proc/cpuinfo auftaucht obwohl der BIOS-Schalter auf Disable steht? Ich habe dazu keine belastbare Quelle gefunden und würde es gern richtig verstehen. Ihr dürft mich jederzeit fragen.

OpenPGP-Karte eingerichtet: Curve 25519 scheitert am Kartenlimit, die Unterschlüssel landen trotzdem in Hardware

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?

OpenPGP-Smartcard mit drei hardwaregebundenen Unterschlüsseln für Signatur, Verschlüsselung und Authentifizierung; der Primärschlüssel bleibt separat offline gespeichert.

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.

Weißer Versandumschlag mit Pinguin-Sticker, darunter die Chipkarte mit sichtbarem Goldchip.
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:

KarteOpenPGP Smart Card V3.4, ID-1 blanko, FLOSS-Shop, Serie 0000D961, Chip von ZeitControl
ReaderREINER SCT cyberJack pinpad(a), Baujahr 2008
Demo-Schlüsselisoliertes GNUPGHOME, UID „OpenPGP Card Demo card-demo@example.invalid“
Warum isoliertScreenshots 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.

REINER SCT cyberJack pinpad(a) Kartenleser mit Zifferntastatur, LEDs und eingesteckter OpenPGP-Karte.
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:

Beiliegende Karteikarte mit Standard-PIN 123456 und Admin-PIN 12345678 sowie dem Hinweis auf die Sperrfolgen.
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-Dialog mit dem Hinweis, den Admin-PIN und den neuen Admin-PIN über das Pinpad des Kartenlesers einzugeben.
Pinentry zeigt nur den Hinweis, das Pinpad des Readers zu benutzen. Der Admin-PIN selbst taucht auf dem Bildschirm nie auf.
Pinentry-Dialog mit dem Hinweis, den PIN und den neuen PIN über das Pinpad des Kartenlesers einzugeben.
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:

Terminalausgabe von gpg --card-status im jungfräulichen Zustand, Schlüsselattribute überall 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:

Terminalausgabe von gpg card-edit key-attr, Auswahl von Curve 25519 als Default endet mit Card error.
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:

Terminalausgabe von gpg card-edit key-attr, Auswahl von Brainpool P-256 für Signatur- und Verschlüsselungsschlüssel läuft sauber durch, daneben der Pinentry-Dialog für den Admin-PIN.
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:

EingabedauerErgebnisFälle
3 bis 5 SekundenSW 9000, Erfolg8 von 8
7 bis 16 SekundenSW 6400, Fehler14 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:

gpg --quick-add-key 7C7A529A359FA9F8A13DF0868AEA158652F8C926 brainpoolP256r1 sign
gpg: Wrong key usage

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:

Terminalausgabe von gpg list-secret-keys with-fingerprint, Ed25519-Hauptschlüssel plus drei Brainpool-P-256-Unterschlüssel für Verschlüsselung, Signatur und Authentifizierung.
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:

Terminalausgabe von gpg --card-status mit allen drei Unterschlüsseln auf der Karte, dazu der Schlüsselring-Abgleich mit ssb-Markierung für kartengebundene Schlüssel.
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:

Terminalausgabe eines clearsign- und verify-Durchlaufs mit der Karte, Ergebnis Good signature im ultimate Vertrauensstatus.
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:

ssh-add -L
ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQ... cardno:0005 0000D961
ssh -F /dev/null -o IdentityAgent=$(gpgconf --list-dirs agent-ssh-socket) kernel@localhost

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.

Siehe auch

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.

Teil 2: Funktastatur unter Linux „hacken“: AES-Key aus dem Pairing ableiten und Tastendrücke entschlüsseln

„Im nächsten Teil wird nicht nur erklärt, sondern gemacht. Mit einem nRF52840-Dongle und Logitacker schneide ich den Kopplungsvorgang einer Unifying-Tastatur mit, leite daraus den Schlüssel ab und lese die Eingaben mit.“ Das habe ich am Ende des letzten Beitrags geschrieben. Jetzt löse ich es ein.

Kurz zur Erinnerung, falls du den ersten Teil nicht gelesen hast: Logitech-Funktastaturen mit Unifying-Empfänger verschlüsseln die Übertragung mit AES, aber die Kopplung selbst, also der kurze Moment, in dem sich Tastatur und Empfänger gegenseitig kennenlernen, hat eine nie gepatchte Design-Lücke. Wer genau in diesem Moment mithört, kann daraus den Schlüssel ableiten. Das ist CVE-2019-13052, seit 2019 öffentlich, von Logitech nie gefixt.

Logitech MX Keys mit Unifying-Empfänger und nRF52840-Dongle beim Mitschneiden einer Funk-Kopplung mit LOGITacker

Im ersten Teil stand das als Zeile in einer CVE-Datenbank und als Absatz in einer Offenlegung von Marcus Mengs. Diesmal nicht. Diesmal sitze ich mit einem Dongle für unter 30 Euro vor meiner eigenen MX Keys, schneide die Kopplung mit, und am Ende tippe ich auf der Tastatur, während eine Konsole live mitliest, was ich gerade drücke.

Vorweg die Grenze des Beitrags, damit du nicht am Ende danach suchst: den abgeleiteten AES-Schlüssel zeige ich nirgends. Warum, erkläre ich weiter unten. Die Kurzform: das war zum Zeitpunkt der Aufnahme ein aktives, funktionierendes Credential für meine echte Tastatur, kein historisches Artefakt.

Der Dongle, und welches Werkzeug ich benutze

Im ersten Teil hatte ich einen GeeekPi-Dongle über einen Amazon-Affiliate-Link verlinkt, als Empfehlung für genau dieses Vorhaben. Ehrlich gesagt bin ich am Ende nicht mit dem losgezogen. Zwischen dem Schreiben des ersten Beitrags und diesem Versuch lag noch eine Bestellung woanders, und am Ende kam das Board, das gerade greifbar war, nicht das verlinkte. In der Hand hatte ich stattdessen ein makerdiary nRF52840-MDK USB Dongle, anderer Hersteller, anderes Layout, aber derselbe Chip: ein Nordic nRF52840, auf dem Board direkt aufgedruckt mit N52840 QIAADO 2330FM. Kein Beinbruch, eher ein guter Aufhänger für den Bezugsquellen-Block weiter unten, den ich mir sonst hätte sparen können.

Verpackung des makerdiary nRF52840-MDK USB Dongle mit aufgedrucktem Board-Layout und Pinbelegung.
Die Verpackung des makerdiary nRF52840-MDK USB Dongle, mit aufgedrucktem Board und den Pin-Bezeichnungen.

Werkzeug ist LOGITacker, ursprünglich von mame82, also Marcus Mengs selbst, gebaut und heute unter RoganDawes auf GitHub aktiv gepflegt. Anders als etwa jackit läuft hier keine Angriffslogik auf dem Laptop, LOGITacker ist ein Standalone-Gerät. Die komplette Software steckt auf dem Dongle, bedient wird sie über eine serielle Konsole per USB, der Laptop selbst sieht am Ende nur ein weiteres USB-Gerät mit ein paar zusätzlichen Schnittstellen. Vier Boards werden offiziell unterstützt: der Nordic pca10059, zwei makerdiary-Boards, darunter genau das hier verwendete MDK Dongle, und ein Board von April Brother. Kein Zufallstreffer also. Das Board war von Anfang an ein sinnvoller Kauf für dieses Vorhaben, auch wenn es nicht das verlinkte war, und ich musste vor dem Kauf nicht einmal lange suchen, die Liste der unterstützten Boards steht direkt im Repository.

Geflasht habe ich logitacker_mdk_dongle.hex aus Release v0.2.3-beta, Stand 17. Januar 2020 und bis heute die aktuellste verfügbare Version. Kein Neubau aus dem Quellcode nötig, an der zugrundeliegenden Lücke hat sich seit 2020 nichts geändert. Pfostenstifte liegen dem Board bei, werden für dieses Experiment aber nicht gebraucht. Flashen und Betrieb laufen komplett über USB, kein Lötkolben in Sicht.

Das makerdiary nRF52840-MDK USB Dongle neben den beiliegenden, losen Pfostenstiftleisten. Auf dem Chip steht der Aufdruck N52840 2330FM.
Das Board selbst, mit lesbarem Chip-Aufdruck N52840 2330FM. Die Pfostenstifte liegen bei, werden für dieses Experiment aber nicht gebraucht.

Flashen, und wie ich den Bootloader falsch eingeschätzt habe

Ich bin mit der Annahme reingegangen dass so ein Nordic-Board ein serielles DFU spricht, das man mit nrfutil bedient. Falsch gedacht. Hält man beim Einstecken den Knopf auf dem Board, meldet es sich stattdessen als schnödes USB-Massenspeichergerät:

$ lsblk -o NAME,SIZE,LABEL,FSTYPE,MOUNTPOINT
sda   32,1M UF2BOOT vfat /media/kernel/UF2BOOT

Ein UF2-Bootloader, keine Nordic-eigene serielle Schnittstelle. LOGITackers Release liefert für dieses Board aber nur eine .hex-Datei, keine fertige .uf2. Also musste ich die Datei erst mit Microsofts eigenem uf2conv.py umwandeln, und zwar mit der richtigen Familien-ID, die ich mir nicht ausgedacht, sondern gegen die mitgelieferte uf2families.json geprüft habe:

$ grep -A2 NRF52840 uf2families.json
    "id": "0xada52840",
    "short_name": "NRF52840",

$ python3 uf2conv.py -f 0xADA52840 -c -o logitacker_mdk_dongle.uf2 logitacker_mdk_dongle.hex
Converted to uf2, output size: 470528, start address: 0x1000
Wrote 470528 bytes to logitacker_mdk_dongle.uf2

$ cp logitacker_mdk_dongle.uf2 /media/kernel/UF2BOOT/

Der Bootloader hat die Datei angenommen und das Laufwerk danach getrennt, das ist normales Verhalten beim Flashen. Was nicht normal lief: das Board hat sich anschließend nicht von selbst mit der neuen Firmware gemeldet. Erst ein einmaliges Aus- und wieder Einstecken hat es zurückgebracht, und dann korrekt:

$ lsusb
Bus 001 Device 021: ID 1915:520c Nordic Semiconductor ASA Logitacker by MaMe82

Vier neue Schnittstellen kamen dazu: seriell, Maus, Tastatur und HID-Rohdaten. Ein nettes Detail am Rand, die USB-Seriennummer trägt die Firmware-Version gleich mit, v0.2.3-beta.

Das nRF52840-MDK-Dongle im USB-Port eines Notebooks, mit durchgehend grün leuchtenden LEDs im Bootloader-Modus.
Im UF2-Bootloader-Modus leuchten die LEDs des Dongles durchgehend grün.

Erstkontakt, und die Adresse stand quasi auf dem Karton

Serielle Konsole auf, 115200 Baud, fertig. Der Standardmodus von Logitacker heißt discover, eine Art passiver Dauerscan, und der bringt schon etwas, bevor ich überhaupt ein Kommando eingetippt habe:

<info> LOGITACKER_PROCESSOR_DISCOVER: DISCOVERY: received valid ESB frame (addr C3:1F:D0:D1:07, len: 15, ch idx 0, raw ch 5, rssi 47)
<info> LOGITACKER_PROCESSOR_DISCOVER: discovered device is Logitech

C3:1F:D0:D1:07. Das ist keine zufällige Adresse. Die USB-Seriennummer meines Tastatur-Empfängers, die sowohl solaar show meldet als auch auf dem Gehäuse aufgedruckt steht, lautet C31FD0D1. Exakt dieselbe Bytefolge, als Präfix der Funkadresse. Ohne jeden Angriff, nur weil das Gerät eingeschaltet ist, lässt sich die Funkadresse eines Unifying-Empfängers aus der Seriennummer ablesen, die außen auf dem Gehäuse steht.

Ich habe kurz gebraucht, um das wirklich zu glauben, und den Empfänger deshalb aus dem Rechner gezogen und die Seriennummer noch einmal mit der Lupe der Handykamera abfotografiert, im Vergleich zur Konsolenausgabe daneben. Kein Tippfehler, keine Verwechslung, dieselben acht Zeichen. Für einen Angriff braucht es das ohnehin nicht, discover läuft passiv und komplett ohne dass ich irgendetwas an Tastatur oder Empfänger anfassen müsste.

Drei Versuche, die nichts brachten, und der eine, der klappte

Diagramm: zwei Kopplungsversuche über die Easy-Switch-Taste blieben ohne Funkanfrage beim Dongle, der dritte Versuch per Aus- und Wiedereinschalten der Tastatur war beim ersten Mal erfolgreich.
Zwei Versuche über die Easy-Switch-Taste brachten nichts, der dritte Versuch nach der dokumentierten Methode klappte sofort.

Um einen echten Kopplungsvorgang mitzuschneiden, muss erst einer stattfinden. Also den Mitschnitt-Modus starten und danach die schon gekoppelte MX Keys trennen, damit sie sich während laufendem Mitschnitt neu koppeln kann:

LOGITacker (discover) $ pair sniff run
<info> LOGITACKER_RADIO: Channel hopping stopped
<info> LOGITACKER_PROCESSOR_PAIR_SNIFF: Sniff pairing on address BB:0A:DC:A5:75
<info> ESB_ILLEGALMOD: Using channel table 'Unifying pairing'
<info> ESB_ILLEGALMOD: New channel table with length 11
$ solaar unpair 1DA452CF
Unpaired 1: MX Keys Keyboard (MX Keys) [408A:1DA452CF]

$ solaar pair C31FD0D1
Pairing: turn your new device on (timing out in 30 seconds).

Erster Versuch: Easy-Switch-Taste „1“ gehalten, laut solaar show genau der Kanal, der für diesen Rechner reserviert ist. Die LED blinkte brav weiter. Beim Dongle kam in den vollen 30 Sekunden, die solaar pair öffnet, nichts an:

<info> LOGITACKER_PROCESSOR_PAIR_SNIFF: dongle on channel 44
... (many more channel hops, receiver beacon tracked continuously) ...
<info> LOGITACKER_PROCESSOR_PAIR_SNIFF: Lost dongle in pairing mode, restart channel hopping

Zweiter Versuch, gleiche Methode, gleiches Ergebnis. solaar pair lief diesmal direkt in einen Timeout:

$ solaar pair C31FD0D1
solaar: error: Exception: pairing failed: device timeout

An der Stelle dachte ich kurz, das Board oder die Firmware hätten ein Problem, und habe testweise sogar den Kanal manuell auf den Standardwert zurückgesetzt und noch einmal von vorne begonnen. Half nichts. Meine Deutung dazu, ausdrücklich eine Deutung und kein gemessener Fakt: das Dauerblinken der Easy-Switch-LED zeigt vermutlich nur an, dass dieser Kanal aktuell keinen Empfänger kennt, keine aktive Funksuche. Analysiert habe ich das interne Verhalten der Tastatur nicht, dafür fehlt mir der Einblick, und ich will hier nichts behaupten, was ich nicht geprüft habe.

Dritter Versuch, diesmal nach der Methode, die die Logitacker-Dokumentation für Unifying-Geräte tatsächlich vorschreibt und die mit der Easy-Switch-Taste nichts zu tun hat: Tastatur ausschalten, Kopplungsfenster am Empfänger öffnen, Tastatur wieder einschalten. Erster Versuch nach dieser Methode, sofort erfolgreich:

<info> LOGITACKER_PROCESSOR_PAIR_SNIFF: PAIR SNIFF data received on channel 5
<info> LOGITACKER_PROCESSOR_PAIR_SNIFF: PAIR SNIFF assigned C3:1F:D0:D1:08 as new sniffing address
<info> LOGITACKER_PAIRING_PARSER: Device name: MX Keys
<info> LOGITACKER_PAIRING_PARSER: Device RF address: C3:1F:D0:D1:08
<info> LOGITACKER_PAIRING_PARSER: Device serial: 1D:A4:52:CF
<info> LOGITACKER_PAIRING_PARSER: Device WPID: 0x408A
<info> LOGITACKER_PROCESSOR_PAIR_SNIFF: device automatically stored to flash
<info> LOGITACKER_PROCESSOR_PAIR_SNIFF: Sniffed full pairing, moving on with passive enumeration for C3:1F:D0:D1:08

solaar show bestätigt unabhängig dieselbe Seriennummer 1DA452CF und denselben WPID 408A für dieselbe Tastatur, wobei die neu ersniffte Funkadresse C3:1F:D0:D1:08 denselben Präfix trägt wie die passive Sichtung von vorhin, nur eine Adresse höher.

Was ich da wirklich in der Hand hatte, und was ich bewusst nicht zeige

Ein erfolgreich mitgeschnittener Kopplungsvorgang liefert bei Logitacker fünf Dinge: Gerätename, Funkadresse, Seriennummer, WPID, und einen 16 Byte langen AES-Schlüssel. Die ersten vier stehen oben, alle unabhängig gegen Solaar geprüft. Der Schlüssel steht hier nicht. Kein einziges Byte davon, auch keine erfundenen Beispielwerte, die nur wie ein Schlüssel aussehen würden.

Der Unterschied zur EK-Zertifikat-Entscheidung im TPM-Beitrag lässt sich in einem Satz sagen: dort ging es um eine dauerhafte Geräte-Kennung, eindeutig, aber für sich genommen nicht ausnutzbar. Hier ging es um ein Credential, das im Moment der Aufnahme aktiv gültig war und echten Zugriff auf meine echte, gerade auf meinem Schreibtisch liegende Tastatur bedeutet hätte. Direkt im Anschluss an den Mitschnitt aus dem nächsten Abschnitt habe ich die Kopplung deshalb noch einmal neu gemacht, um den mitgeschnittenen Schlüssel ungültig zu machen:

$ solaar unpair 1DA452CF
Unpaired 1: MX Keys Keyboard (MX Keys) [408A:1DA452CF]

$ solaar pair C31FD0D1
Pairing: turn your new device on (timing out in 30 seconds).
Paired device 1: MX Keys Keyboard (MX Keys) [408A:1DA452CF]

Wieder die Aus-und-Einschalt-Methode, wieder auf Anhieb erfolgreich. Der Schlüssel aus dem Mitschnitt ist damit nicht mehr der Schlüssel, der heute im Einsatz ist. Falls du dich fragst, ob das den Beweis im nächsten Abschnitt irgendwie entwertet: nein, der wurde vorher aufgezeichnet, die Rotation kam erst danach.

Der Beweis: Tastenanschläge in Echtzeit entschlüsselt

Diagramm: vom Pairing-Handshake über den mitgeschnittenen und abgeleiteten AES-Schlüssel bis zur Live-Entschlüsselung der Tastenanschläge, wobei der Schlüssel selbst nicht gezeigt wird.
Der Ablauf vom mitgeschnittenen Handshake bis zum entschlüsselten Klartext. Der Schlüssel selbst taucht in diesem Beitrag nirgends auf.

Nach dem erfolgreichen Mitschnitt wechselt Logitacker automatisch in einen passiven Mithör-Modus. Ich habe angefangen, auf der frisch wieder gekoppelten MX Keys zu tippen, und die Konsole hat live mitgeschrieben. Vier Tastendrücke aus derselben Sitzung, jeder mit eigenem Rohframe und eigenem entschlüsseltem Ergebnis:

ZählerVerschlüsselter RohframeEntschlüsselter Klartext-ReportErkannte Taste
4693281200 D3 45 B3 46 90 36 E6 B0 93 46 93 28 12 00 00 00 00 00 00 00 ED00 13 00 00 00 00 00 C9P
4693281400 D3 82 5E 0C 2F 89 D7 B2 86 46 93 28 14 00 00 00 00 00 00 00 6500 17 00 00 00 00 00 C9T
4693281600 D3 C7 B6 18 D3 34 82 1E 3D 46 93 28 16 00 00 00 00 00 00 00 9D00 37 00 00 00 00 00 C9.
4693281800 D3 68 E2 F1 7D 3C 6B D6 3D 46 93 28 18 00 00 00 00 00 00 00 A200 28 00 00 00 00 00 C9ENTER

Nacheinander gelesen: P, T, ., ENTER. Kein vollständiges Wort, sondern das Ende eines Satzes, den ich in dem Moment tatsächlich getippt habe, plus Enter. Genau das steht hier auch so, nicht schöngeredet zu mehr, als es ist.

Die Rohframes in der Tabelle sind Chiffretext, unbedenklich zum Zeigen, reines Rauschen ohne den Schlüssel. Was daraus die Klartext-Reports macht, ist der Schlüssel aus dem vorigen Abschnitt, und der taucht hier nicht auf. Ich habe dabei ziemlich bewusst langsam getippt, fast schon ein bisschen albern vor dem eigenen Bildschirm, nur um in der Konsole klar zuordnen zu können, welche Zeile zu welchem Tastendruck gehört. Es hat trotzdem gedauert, bis ich wirklich geglaubt habe, was da steht.

Was das nicht war

Eine kurze, ehrliche Einordnung. Dieser Versuch endet beim Mitlesen. Ich habe keine Einschleusung versucht, obwohl derselbe bekannte Schlüssel das technisch auch für gefälschte, eingeschleuste Tastenanschläge ermöglicht hätte, also für aktiven Angriff statt reinem Zuhören. Das ist die andere Hälfte von CVE-2019-13052, und die habe ich bewusst nicht angefasst. Nicht aus technischer Unfähigkeit, Logitacker kann das nachweislich, sondern weil eine aktive Einschleusung gegen die eigene Tastatur eine andere Kategorie Experiment ist als reines Zuhören, und weil das für den Beleg der Lücke in diesem Beitrag nicht nötig war.

Was das für die Empfehlung aus Beitrag 1 bedeutet

Im ersten Teil stand am Ende eine Rangfolge: Kabel für wirklich Geheimes, Logi Bolt oder sauberes Bluetooth fürs Büro, vorhandenes Unifying behalten und pflegen, No-Name-Funktastaturen ersetzen. An dieser Rangfolge ändert dieser Beitrag nichts. Was sich ändert, ist der Status des Arguments dahinter. Die Kopplungs-Lücke ist jetzt kein Datenbankeintrag mehr, sondern ein Ablauf, den ich mit handelsüblicher Hardware für unter 30 Euro selbst gegen mein eigenes Gerät durchgeführt habe, an einem gewöhnlichen Nachmittag, ohne Speziallabor und ohne Vorwissen, das ich mir nicht selbst aus der Dokumentation hätte holen können. Das praktische Risiko bleibt so klein wie im ersten Teil beschrieben, gebunden an den kurzen, seltenen Moment der Kopplung. Aber „das ist doch nur graue Theorie“ trägt als Einwand jetzt nicht mehr, und genau das war für mich der eigentliche Grund, diesen zweiten Teil überhaupt zu schreiben.

Was offen bleibt

  • Warum die Easy-Switch-Taste keine Kopplungsanfrage auslöst, bleibt meine Deutung des Dauerblinkens, keine bestätigte Analyse des Tastatur-internen Verhaltens. Sicher war ich einfach nur zu dumm die Tasten in der richtigen Reihenfolge zu drücken. gefettfingert
  • Die aktive Einschleusungshälfte von CVE-2019-13052 habe ich mit dem bekannten Schlüssel nicht ausprobiert. Ob und wann daraus noch ein dritter Teil wird, ist offen. Das hängt nun also an euch. Wenn ich sehen wollte, wie ich einfach Text oder Kommandos einschleuse, sagt es mir 🙂
  • Ob sich derselbe Ablauf gegen andere Unifying-Tastaturen als meine MX Keys genauso zuverlässig reproduzieren lässt, habe ich nicht geprüft. Aber hey, wir sind uns nun wohl alle sicher, dass es klappen wird, oder?
  • Die genaue Ursache der beiden ersten Kopplungsversuche, ob Timing, Tastatur-Firmware-Logik oder etwas Drittes, habe ich nicht bis auf die Funkebene nachverfolgt. gefettfingert

Bezugsquellen

Der folgende Link ist ein Affiliate-Link (Werbung). Kaufst du darüber, bekomme ich eine kleine Provision, für dich ändert sich am Preis nichts.

Der Dongle in diesem Beitrag war tatsächlich ein makerdiary nRF52840-MDK USB Dongle, über einen anderen Kanal bezogen. Der verlinkte Dongle trägt denselben Nordic-nRF52840-Chip, ist aber ein anderes Board von einem anderen Hersteller. Ob Logitacker dieses konkrete Modell offiziell unterstützt, habe ich nicht geprüft. Offiziell gelistet sind der Nordic pca10059, zwei makerdiary-Boards und ein Dongle von April Brother.

Siehe auch

Wenn ich irgendwo danebenliege, korrigiert mich gerne, dann lerne ich selbst etwas. Und die Frage an euch zum Schluss: Würdet ihr für ein bisschen mehr Sicherheit auf Logi Bolt oder Kabel umsteigen, oder ist euch das Restrisiko bei einer gepflegten Unifying-Tastatur egal? Wenn ihr mögt, dürft ihr mich dazu sehr gerne fragen.

Bald ist wieder FrOSCon: warum ich seit zwanzig Jahren hinfahre und wen ich dort treffen möchte

Moin.

Konferenz-Namensschild an einem grünen Schlüsselband, Aufschrift FrOSCon 2026, Sankt Augustin, darunter der Name Kernel-Error und ein Stempel mit dabei seit 2007

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.

Drei Konferenzausweise an bunten Schlüsselbändern an einer Wand. Vorne der Ausweis der FrOSCon 2007 mit handgeschriebenem Namen und dem senkrechten Aufdruck Aussteller, darunter ein hellblaues Schild der FrOSCon 2013 mit Frosch-Logo, rechts ein Besucherausweis der OpenRheinRuhr 2010.
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:

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.

Die Maus funkt im Klartext: wie sicher Funktastatur und Funkmaus unter Linux wirklich sind

Vor Kurzem saß ich in einer Arztpraxis im Wartezimmer, und weil ich so bin wie ich bin, habe ich statt der Zeitschriften die Rechner am Empfang angeschaut. An jedem hing eine Funkmaus und eine Funktastatur, dazu der kleine Adapter im USB-Port. Von einem Hersteller, den ich nicht kannte, der Eindruck war sehr günstiges No-Name-Gerät. So etwas nenne ich selbst gern etwas abfällig „Chinaprodukt“, was eigentlich unfair ist, denn die können das inzwischen richtig gut.

Funkmaus und Funktastatur neben einem Linux-Rechner mit 2,4-GHz-Funkanalyse. Die Tastaturverbindung ist verschlüsselt, die Maus überträgt unverschlüsselt.

Im Gespräch mit dem Arzt kam heraus, dass ihm ein Punkt völlig neu war: dass die Tastatureingaben als Funk durch die Luft gehen und dass man Funk mithören kann. An seinem Platz werden Diagnosen, Namen und Medikamente getippt, und die Frage, ob das jemand aus dem Nachbarzimmer mitlesen könnte, hatte sich vorher schlicht nie gestellt. Aus genau dieser Unterhaltung ist dieser Beitrag entstanden, und er wollte ihn selbst lesen. Also, hallo an dieser Stelle, danke für den Anstoß.

Ich will die Praxis dabei nicht an den Pranger stellen. Über die Sicherheit genau dieses No-Name-Geräts kann ich nichts sagen, ich habe es nie in der Hand gehabt. Der Punkt ist ein anderer: fast jeder benutzt so einen Adapter, und kaum jemand hat sich die Frage je gestellt. Bei einer Mausbewegung ist das auch egal. Bei einem Passwort, einer PIN oder einem Login nicht. Also drei Fragen, die den Beitrag tragen. Kann man das wirklich abhören? Kann man sogar eigene Tasten einschleusen? Und wie sieht man, ob das eigene Gerät geschützt ist?

Ich mache das am Beispiel von Logitechs Unifying-Adapter, dem mit dem kleinen orangen Stern, weil ich davon zwei Stück im Rechner habe und weil Logitech einer der Hersteller ist, bei dem man den Unterschied zwischen „kein Krypto“ und „verschlüsselt“ schön sehen kann. Alles, was ich hier auf dem Rechner prüfe, zeige ich mit dem Befehl und der echten Ausgabe. Und wo etwas unklar bleibt, sage ich das auch.

Durch die Luft, und schon sind es zwei Probleme

Bevor es um Logitech geht, muss eine Sache sauber getrennt sein, denn der ganze Beitrag hängt daran. Wer an einem Funkgerät lauscht, will eines von zwei Dingen, und die sind unterschiedlich gefährlich.

  • Mitlesen, passiv. Der Angreifer hört den Funk nur mit. Das ist gefährlich bei allem, was die Tastatur sendet, also Passwörter, PINs, Nachrichten. Bei der Maus dagegen gehen nur Bewegungen und Klicks über die Luft, da ist Mitlesen fast folgenlos.
  • Einschleusen, aktiv. Der Angreifer sendet selbst und gibt sich als euer Gerät aus. Damit tippt er auf eurem Rechner, öffnet ein Terminal, lädt etwas nach. Das ist der unangenehmere Fall, weil er nicht davon abhängt, dass ihr gerade etwas tippt. Der Rechner steht am Empfang, niemand sitzt davor, und trotzdem passiert etwas.

Daraus ergeben sich drei Schutzstufen, und die Reihenfolge ist wichtig.

  1. Kein Schutz. Klartext. Jeder in Funkreichweite liest mit und kann einschleusen.
  2. Verschlüsselung. Der Funk ist chiffriert. Wer mithört, bekommt nur Kauderwelsch. Logitech nimmt dafür bei Tastaturen AES-128.
  3. Authentisierung. Der Empfänger prüft, ob ein Paket wirklich vom gekoppelten Gerät stammt. Erst das schließt das Einschleusen zuverlässig, denn Verschlüsselung allein sagt nur, dass keiner mitliest, nicht, dass keiner sich einschleicht.

Und hier kommt die Design-Entscheidung, die uns später um die Ohren fliegt. Eine Tastatur braucht zwingend Verschlüsselung, weil sie Geheimes sendet. Eine Maus bekam bei Unifying historisch keine, weil sie ja nichts Geheimes sendet. Das klingt vernünftig, und für sich genommen ist es das auch. Zum Einfallstor wurde es trotzdem, weil ein Empfänger, der offene Maus-Pakete akzeptiert, sich dazu bringen ließ, auch gefälschte Tastatur-Pakete zu schlucken. Genau das war MouseJack. Dazu gleich mehr.

Diagramm des Funkwegs: Tastatur MX Keys funkt AES-128-verschlüsselt, Maus M705 im Klartext, beide über den Unifying-Empfänger zum Rechner, ein Angreifer mit Antenne hört mit.
Der Funkweg. Die Tastatur funkt AES-128-verschlüsselt, die Maus im Klartext. Ein Angreifer mit Antenne hört beide Strecken, von der Tastatur bekommt er aber nur Kauderwelsch.

KeySniffer, so sieht es aus, wenn gar nichts da ist

Damit klar wird, wovon wir reden, erst das schlechte Beispiel. 2016 zeigte die Firma Bastille unter dem Namen KeySniffer, dass Funktastaturen von acht Herstellern die Tastenanschläge komplett im Klartext funken, ohne jede Verschlüsselung. Betroffen waren Anker, EagleTec, General Electric, Hewlett-Packard, Insignia, Kensington, Radio Shack und Toshiba. Mitlesbar aus rund 75 Metern, mit Hardware für unter hundert Dollar. Man tippt sein Passwort, und einer im selben Gebäude schreibt es Zeichen für Zeichen mit.

Zwei Dinge daran sind wichtig. Erstens ist das kein „schwaches Krypto“, das ist „kein Krypto“. Diesen Unterschied wirft man gern in einen Topf, aber er ist der ganze Punkt. Zweitens kann man diese Tastaturen nicht per Update retten. Da hilft nur wegwerfen und ersetzen. Und genau das ist der Unterschied zu Logitech, wo ein Empfänger-Update wirklich etwas ändert, wie wir noch sehen werden. Bluetooth-Tastaturen und die höherwertigen Geräte von Logitech, Dell und Lenovo waren von KeySniffer übrigens nicht betroffen. Das ist die Brücke zu Logitech.

Ein Stern, sechs Geräte: wie Unifying funktioniert

Unifying ist der kleine USB-Adapter mit dem orangen Stern, an den sich bis zu sechs Geräte koppeln lassen. Eine Maus, eine Tastatur, vielleicht ein Ziffernblock, alles über einen Stick. Technisch funkt das bei 2,4 GHz auf Nordics nRF24-Technik, nicht Bluetooth. Das ist ein wichtiger Punkt, den ich später beim Abhören noch brauche. Tastaturen werden mit AES-128 verschlüsselt, der Schlüssel wird beim Koppeln festgelegt. Mäuse funken offen.

MouseJack, 2016, und die offene Maus als Hebel

Ebenfalls 2016, wieder Bastille, diesmal Marc Newlin. Über die offene, nicht authentifizierte Maus-Strecke ließen sich gefälschte Tastatur-Pakete in viele Empfänger einschleusen, dazu erzwungenes Koppeln. Betroffen waren Geräte vieler Hersteller, nicht nur Logitech. Der Angreifer musste nicht warten, bis jemand tippt. Er gab sich als neue Tastatur aus und schrieb selbst.

Logitech reagierte mit mehreren Firmware-Ständen für die Empfänger. Nach Logitechs eigenen Release-Notes sind die Kern-MouseJack-Punkte, also erzwungenes Koppeln, Tastatur-Injektion und gefälschte Maus, ab RQR12.05 beziehungsweise RQR24.03 geschlossen. Eine später gefundene Schwäche, die verschlüsselte Injektion (Bastille-Punkt #13, als CVE-2016-10761 geführt), folgte erst ab RQR12.08 und RQR24.06. Diese zwei Generationsnummern solltet ihr euch merken, sie kommen beim Testgerät gleich wieder.

Die vier Lücken von 2019, und die eine, die blieb

2019 nahm sich Marcus Mengs, im Netz MaMe82, Unifying noch einmal vor. Vier CVEs, und der Unterschied zwischen ihnen ist der eigentliche Lehrwert.

CVEWasFix
CVE-2019-13052Aus dem Mitschnitt der Kopplung den AES-Schlüssel ableiten, danach mitlesen und einschleusenkein Patch, Logitech lehnte ab
CVE-2019-13053Einschleusen in die verschlüsselte Strecke, setzt einmaligen physischen Zugriff vorauskein Patch für die erweiterte Form
CVE-2019-13054Schlüssel aus R500- und Spotlight-Presentern auslesen, physischer ZugriffFix für August 2019 angekündigt
CVE-2019-13055Alle gekoppelten Schlüssel aus einem Empfänger in unter einer Sekunde, physischer ZugriffFix für August 2019 angekündigt

Die spektakuläre Fernübernahme, MouseJack, ist an einem aktuellen Empfänger zu. Was bleibt, ist die erste Zeile: CVE-2019-13052. Wer den Moment der Kopplung mitschneidet, kann daraus den AES-Schlüssel ableiten und danach mitlesen und einschleusen. Diese Lücke wurde nie geschlossen. Logitech hat erklärt, dafür keinen Patch zu liefern, sie steckt im Design.

Das praktische Risiko ist gering, weil man dafür genau dann mithören muss wenn ihr ein Gerät neu koppelt, und weil der Schlüssel danach nicht neu ausgehandelt wird. Koppeln passiert selten und ist schnell vorbei. Aber der Satz „Firmware aktuell, also alles gut“ wäre trotzdem falsch, und genau diese Ehrlichkeit macht für mich den Reiz an der Sache aus. Diese Kopplungs-Lücke ist übrigens auch der Aufhänger für den zweiten Teil.

Zeitleiste: 2016 MouseJack, Firmware-Fix ab RQR12.05 und RQR24.03, 2019 die vier Mengs-CVEs mit der nie gepatchten CVE-2019-13052, und 2021 Logi Bolt mit BLE Security Level 4.
Die Angriffe über die Zeit. MouseJack wurde per Firmware geschlossen, aus der 2019er-Serie blieb CVE-2019-13052 offen, und Logi Bolt zieht 2021 die Konsequenz.

Logi Bolt, der Nachfolger, der es richtig macht

2021 brachte Logitech mit Logi Bolt einen neuen Empfänger heraus, der Sicherheit von Anfang an mitdenkt. Die Kernpunkte, alle belegt:

  • Bolt setzt auf Bluetooth Low Energy im Security Mode 1, Level 4, also Secure Connections Only. Das heißt verschlüsselt und authentifiziert. Genau der Schutz gegen Einschleusen, der Unifying von Haus aus fehlte.
  • Es ist ein geschlossenes System, ein Bolt-Empfänger spricht nur mit Bolt-Geräten.
  • Pro Kopplung eine andere Bluetooth-Adresse und eigene Schlüssel.
  • Koppeln geht nur nach einem ausdrücklichen, langen Knopfdruck von einigen Sekunden. Das schließt das erzwungene Koppeln.
  • Firmware ist zentral ausrollbar, mit Schutz gegen das Zurückrollen auf alte Stände.

Ein Missverständnis will ich gleich einfangen. Bolt ist Bluetooth-basiert, aber es ist ein geschlossenes, gehärtetes System, kein normales Bluetooth-Pairing, wie ihr es vom Kopfhörer kennt. Verwechselt das nicht, das eine ist gehärtet, das andere ist ein bunter Haufen.

Zwei Empfänger, eine Maus, eine Tastatur

Genug Geschichte, jetzt der eigene Rechner. Auf dem Testgerät, einem Notebook mit Linux Mint 22.3, stecken zwei Unifying-Empfänger gleichzeitig. Am einen hängt die Maus, eine Logitech Marathon Mouse M705, am anderen die Tastatur, eine MX Keys. Beide Sticks tragen denselben orangen Stern, und wie sich gleich zeigt, tragen sie trotzdem zwei verschiedene Sicherheitsgeschichten mit sich herum.

Die Logitech Marathon Mouse M705 und ein Unifying-Empfänger mit orangem Stern auf einer Tischplatte.
Die Maus M705 und ein Unifying-Empfänger mit dem orangen Stern.
Nahaufnahme eines Logitech-Unifying-Empfängers, der orange Unifying-Stern ist deutlich lesbar.
Der orange Stern ist das Erkennungszeichen von Unifying.
Die Logitech MX Keys Tastatur in der Gesamtansicht.
Die Tastatur, eine Logitech MX Keys.

Erst einmal schauen, ganz ohne Spezialwerkzeug

Man muss nicht sofort ein Paket installieren, um zu verstehen, was da steckt. Das Betriebssystem verrät schon eine ganze Menge. Erst schaue ich, welche USB-Geräte hängen. Mit lsusb sehe ich beide Empfänger, und interessant ist, dass sich beide als exakt dieselbe Kennung melden.

$ lsusb | grep -i logitech
Bus 001 Device 011: ID 046d:c52b Logitech, Inc. Unifying Receiver
Bus 001 Device 012: ID 046d:c52b Logitech, Inc. Unifying Receiver

046d ist Logitech, c52b ist der Unifying-Empfänger. Zwei gleiche Kennungen, zwei physische Sticks. Als Nächstes die Kernelmodule, die diese Sticks bedienen.

$ lsmod | grep -i logi
hid_logitech_hidpp     69632  0
hid_logitech_dj        36864  0
usbhid                 77824  2 hid_logitech_dj,hid_logitech_hidpp
hid                   282624 10 usbhid,...,hid_logitech_dj,hid_logitech_hidpp

Zwei Module machen die Arbeit, und die Aufgabenteilung lohnt sich zu verstehen. hid_logitech_dj kümmert sich um das Multiplexing des Unifying-Empfängers, also darum, dass ein einziger Stick mehrere Geräte tragen kann. hid_logitech_hidpp spricht das Feature-Protokoll HID++, über das gleich auch Solaar redet. Und über die Geräteknoten /dev/hidraw* reden die Werkzeuge am Ende mit den Sticks. So versteht man die Schichten, bevor überhaupt ein Zusatzprogramm im Spiel ist.

Solaar, das eine Werkzeug, das man wirklich braucht

Solaar ist die grafische Oberfläche und zugleich ein Kommandozeilen-Werkzeug für Unifying und HID++. Damit koppelt man Geräte, sieht den Akkustand, den Verschlüsselungsstatus und kann Tasten umbelegen. Auf dem Testgerät läuft Version 1.1.11. Der eine Befehl, der alles auf einmal ausgibt, ist solaar show. Für den Empfänger der Maus, gekürzt auf das Wesentliche:

Unifying Receiver
  USB id       : 046d:C52B
  Serial       : 3CE0986A
    Firmware   : 12.11.B0032
  Has 1 paired device(s) out of a maximum of 6.

  1: Marathon Mouse M705 (M-R0073)
     Kind         : mouse
     Protocol     : HID++ 4.5
     Battery: 50%, discharging, next level 20%.
Solaar-Empfängeransicht der Maus: USB-ID 046d:C52B, Seriennummer 3CE0986A, Firmware 12.11.B0032, ein gekoppeltes Gerät.
Solaar zeigt den Empfänger der Maus. USB-ID 046d:C52B, Firmware 12.11.B0032, ein gekoppeltes Gerät.

Und für den Empfänger der Tastatur:

Unifying Receiver
  USB id       : 046d:C52B
  Serial       : C31FD0D1
    Firmware   : 24.11.B0036
  Has 1 paired device(s) out of a maximum of 6.

  1: MX Keys Keyboard
     Kind         : keyboard
     Protocol     : HID++ 4.5
Solaar-Empfängeransicht der Tastatur: USB-ID 046d:C52B, Firmware 24.11.B0036, MX Keys gekoppelt.
Der zweite Empfänger, der für die Tastatur. Dieselbe USB-Kennung, aber Firmware 24.11.B0036.

Hier fällt schon auf, was ich gleich groß mache. Zwei Unifying-Sticks, aber zwei Firmware-Linien: RQR12 für die ältere Maus-Generation, RQR24 für die neuere Tastatur. Beide tragen denselben orangen Stern, dahinter stecken aber zwei Baujahre und zwei Geschichten.

Zwei Sterne, ein Unterschied

Das ist das Herzstück, und es ist ein einziger Blick in Solaar. In der Geräteansicht steht bei jedem gekoppelten Gerät eine Zeile „Drahtlose Verbindung“. Für die Maus M705 meldet Solaar dort „nicht verschlüsselt“, mit einem roten Schild. Für die Tastatur MX Keys meldet dieselbe Zeile „verschlüsselt“, mit einem geschlossenen Schloss.

Solaar-Geräteansicht der Marathon Mouse M705: Drahtlose Verbindung nicht verschlüsselt, mit rotem Schild.
Solaar meldet für die Maus M705 eine nicht verschlüsselte Funkverbindung, mit rotem Schild.
Solaar-Geräteansicht der MX Keys: Drahtlose Verbindung verschlüsselt, mit geschlossenem Schloss.
Für die Tastatur MX Keys meldet Solaar dieselbe Übersicht als verschlüsselt, mit geschlossenem Schloss.

Was das heißt, und was nicht. Die Tastatur ist mit AES-128 verschlüsselt, der Schlüssel wurde beim Koppeln festgelegt. Wer die Funkstrecke mithört, bekommt Kauderwelsch. Die Maus funkt offen, was für Bewegungen und Klicks kein Problem ist. Und dann die ehrliche Einschränkung, die uns direkt zum Angriff führt: dass die Maus offen funkt, war historisch genau der Hebel für das Einschleusen, bis die Firmware das abgestellt hat.

Ein Gedanke, der jetzt naheliegt, führt in die Irre: dann schalte ich die Verschlüsselung der Tastatur eben ab und schaue, was durchgeht. Geht nicht. Die AES-Verschlüsselung der Tastatur hat keinen Ausschalter. Sie steckt fest im Unifying-Protokoll, Solaar hat dafür keinen Schalter, das alte ltunify auch nicht. Wer die Klartext-Eingaben der Tastatur sehen will, muss das AES brechen, nicht abschalten.

Was man installieren sollte, und was nicht

Für die reine Frage „bin ich sicher“ braucht man erstaunlich wenig. Zwei Pakete reichen, und beide waren auf dem Testgerät sowieso schon da.

PaketWofür
solaarKopplung, Akku, Verschlüsselungsstatus, Tastenbelegung
fwupdFirmware-Updates über LVFS

Ein paar Werkzeuge, über die man im Netz stolpert, braucht man hier ausdrücklich nicht, und ich schreibe dazu, warum.

PaketWarum nicht
ltunifyältere Unifying-Kommandozeile, von Solaar überholt
libratbag / piperDPI- und Tastenkonfiguration für Gaming-Mäuse, für die M705 unnötig, Solaar deckt sie ab
jackit, nrfutil, dfu-toolAngriffs- und Flash-Werkzeuge für nRF-Hardware, hier nicht installiert. jackit gehört zum Crazyradio-am-Laptop-Weg, in diesem Aufbau macht die Funkarbeit stattdessen der Flipper

Bin ich sicher? Zwei Blicke genügen

Der erste Blick ist der Verschlüsselungsstatus, den Solaar meldet. Tastatur verschlüsselt, Maus nicht. Das ist bei Unifying normal und in Ordnung, solange man weiß, dass die Maus offen funkt und dort nichts Geheimes durchgeht.

Der zweite Blick ist die Firmware. fwupd kennt beide Empfänger über LVFS, und fwupdmgr get-updates sagt, ob etwas ansteht.

$ fwupdmgr get-updates --no-unreported-check
Devices with the latest available firmware version:
 • Unifying Receiver
 • Unifying Receiver

Beide sind auf dem neuesten Stand, den LVFS anbietet, das ist RQR12.11 und RQR24.11. Und jetzt die Verbindung zur Geschichte, die den Abschnitt wertvoll macht. Die MouseJack-Fixes stecken in den Firmware-Generationen ab RQR12.05 beziehungsweise RQR24.03 für die Kern-Punkte, und ab RQR12.08 beziehungsweise RQR24.06 für die verschlüsselte Injektion. Beide Empfänger liegen mit 12.11 und 24.11 deutlich darüber. Die Fernübernahme von 2016 ist an diesem Gerät also zu. Das ehrliche Ergebnis dieses Abschnitts ist „hier ist nichts zu tun“, und das darf man auch so hinschreiben.

Trotzdem der Handgriff für den Fall, dass doch einmal ein Update ansteht, als Verfahren, nicht als Messung. Auf dem Testgerät gab es nichts zu aktualisieren, ich habe die letzte Zeile hier also nicht wirklich ausgeführt.

fwupdmgr refresh
fwupdmgr get-updates
fwupdmgr update

Wie würde ein Angreifer das überhaupt abhören?

Jetzt die andere Seite. Unifying funkt bei 2,4 GHz auf nRF24-Technik mit einer eigenen Paketstruktur, Enhanced ShockBurst nennt Nordic das. Normale SDR-Empfänger und die üblichen Sub-Gigahertz-Geräte sehen das schlicht nicht. Man braucht ein nRF24-Radio, das in einen Mithör-Modus versetzt wird. Und genau hier wird es interessant, was ein Flipper Zero kann und was nicht.

Der Flipper Zero, und warum er allein nicht reicht

Das eingebaute Funkteil des Flipper ist ein CC1101. Das arbeitet unter einem Gigahertz, grob 300 bis 928 MHz. Es kommt physikalisch nicht an die 2,4 GHz von Unifying heran. Aus der Schachtel heraus kann der Flipper diesen Angriff also gar nicht, egal welche Firmware drauf ist.

Diagramm: der Sub-GHz-CC1101 des Flippers sieht die 2,4-GHz-Strecke nicht, ein nRF24-Modul erkennt sie und kann MouseJack, Crazyradio oder nRF52840 mit Logitacker können zusätzlich Schlüssel ableiten.
Welches Radio an 2,4 GHz herankommt. Der eingebaute CC1101 des Flippers arbeitet unter einem Gigahertz und sieht die Strecke nicht. Erst ein nRF24-Radio kommt heran.

Auf meinem Aufbau steckt der Flipper mit der Momentum-Firmware und einem Zusatzmodul, einem AIO Board. Auf dem Board steht die Bestückung selbst drauf: RED NRF24, GREEN ESP32, BLUE CC1101, dazu ein EBYTE-Modul und externe Antennen. Das entscheidende Teil für uns ist das nRF24-Radio. Genau das, was dem Flipper von Haus aus fehlt. Damit werden die Apps NRF24: Sniffer und NRF24: Mouse Jacker nutzbar, beide aus dem Projekt von mothball187 und in Momentum enthalten.

Flipper Zero mit aufgestecktem AIO Board V1.4 und drei Antennen, betriebsbereit. Das Board trägt neben ESP32 und CC1101 das nRF24-Radio.
Flipper Zero mit aufgestecktem AIO Board V1.4. Das Board liefert das nRF24-Radio für 2,4 GHz, neben ESP32 und CC1101, das dem Flipper allein fehlt.

Der Ablauf ist in zwei Schritten schnell erzählt. Erst sucht der Sniffer die Kanäle ab und findet die Funkadresse des Empfängers. Dann bekommt der Mouse Jacker diese Adresse und ein Tastatur-Skript und versucht, Tasten einzuschleusen. Klingt einfach. Der erste Schritt ist es aber schon nicht.

Bildschirm des Flipper-NRF24-Sniffers: eine gefundene Adresse auf einem 2,4-GHz-Kanal.
Der NRF24-Sniffer meldet eine gefundene Adresse auf einem 2,4-GHz-Kanal.

Der nRF24-Treiber der Flipper-App gilt selbst als funktionierend, aber unfertig, und das Sniffen im 2,4-GHz-Band rastet gern auf zufälliges Rauschen ein und meldet Adressen, die es gar nicht gibt. Drei Läufe brachten bei mir drei verschiedene Adressen, einmal sogar mit „Unique 0“, also ohne einen einzigen wiederholten Treffer. Eine echte Logitech-Adresse taucht über mehrere Läufe immer wieder auf, das ist das Erkennungszeichen. Mit etwas Geduld, Maus dabei bewegen und jeden Durchlauf sauber mit OK beenden, damit die App überhaupt speichert, hat sich am Ende die Adresse 086A98E03C als die stabile, echte herauskristallisiert. Das Erkennen klappt also, aber es ist selbst schon eine Hürde und nicht der Sofort-Erfolg, den man sich vorstellt.

Die offene Maus lässt sich mit demselben Radio mitschnüffeln, man sieht rohe Pakete. Passwörter sind da natürlich keine drin, nur Bewegung und Klicks. Genau das ist der greifbare Beleg für „die Maus funkt im Klartext“.

Und jetzt der eigentliche Versuch. Ich habe einen einfachen Texteditor auf dem Notebook geöffnet und ihm den Fokus gegeben. Der Mouse Jacker bekam die ersniffte Adresse 086A98E03C und ein harmloses Skript, das nur den Text MOUSEJACK-TEST-0810 tippen sollte, sonst nichts. Der Flipper zeigte „Running duckyscript“.

Bildschirm des Flipper-Mouse-Jackers mit der Meldung Running duckyscript während des Injection-Versuchs.
Mouse Jacker feuert das Skript gegen die Adresse. Auf dem gepatchten Empfänger landet keine einzige Taste.

Im Editor erschien nichts. Keine einzige Taste kam durch. Das ist kein Misserfolg des Versuchs, sondern das erwartete Verhalten an einem Empfänger, der über dem MouseJack-Fix liegt.

Hier bleibe ich bei der Ursache ehrlich, das ist mir wichtig. „Nichts passiert“ allein trennt nicht sauber, ob die Firmware das Paket abgewiesen hat oder ob die ersniffte Adresse nicht exakt der Empfänger war. Um das isoliert zu beweisen, müsste ein bewusst verwundbarer Vergleichs-Empfänger danebenstehen, an dem derselbe Angriff gelingt, und der stand hier nicht daneben. Sicher ist: die klassische MouseJack-Übernahme aus der Ferne, ohne Anfassen, gelang an diesem aktuellen Setup mit RQR12.11 nicht, und das passt dazu, dass fwupd die Firmware als aktuell und über dem Fix meldet. Mehr behaupte ich an dieser Stelle nicht. Das ist gemessen, die Deutung der Ursache ist plausibel, aber nicht bewiesen.

Nur eine von mehreren Möglichkeiten

Der Flipper ist nur ein Weg, und ehrlich gesagt der bequeme für unterwegs. Damit klar ist, was sonst noch geht:

  • Crazyradio PA, ein nRF24-USB-Stick mit der Bastille-Firmware plus jackit. Das klassische MouseJack-Werkzeug, läuft am Laptop und kann mitschreiben.
  • nRF52840-Dongle mit Logitacker von Marcus Mengs. Das moderne Werkzeug, das auch die 2019er-Lücken kann, inklusive der Kopplungs-Lücke CVE-2019-13052. Genau dieses Gespann kommt im zweiten Teil zum Einsatz.
  • Raspberry Pi mit nRF24-Modul. Möglich, aber deutlich fummeliger, weil das Timing schlechter ist als bei einer dedizierten Crazyradio.

Die Grenze des Flippers ist dabei klar. Er ist gut zum Erkennen und für den MouseJack-Versuch, aber er leitet keine Schlüssel ab und entschlüsselt keine Tastatur. Dafür braucht es Logitacker auf dem nRF52840, und das ist der zweite Teil.

Was ich für sichere Umgebungen empfehle

Kein Wischiwaschi, eine klare Rangfolge.

  1. Kabel. Eine USB-Tastatur funkt gar nicht. Für alles, wo wirklich Geheimes getippt wird, ist das die ruhigste Lösung.
  2. Logi Bolt oder eine sauber gekoppelte Bluetooth-Verbindung mit Secure Connections. Verschlüsselt und authentifiziert, also auch gegen Einschleusen geschützt.
  3. Aktuelles Unifying. Völlig brauchbar, wenn die Empfänger-Firmware gepflegt ist. Die Tastatur ist verschlüsselt, die Fernübernahme ist zu. Das Restrisiko ist die Kopplungs-Lücke.
  4. Alles ohne Verschlüsselung, KeySniffer-Klasse, gehört ersetzt. Da hilft kein Update.

Dazu zwei Handgriffe, die jeder machen kann. Die Firmware der Empfänger aktuell halten, mit fwupdmgr, das ist eine Minute Arbeit. Und Geräte nur in vertrauenswürdiger Umgebung koppeln, nicht im Café und nicht im vollen Großraumbüro, wegen genau dieser Kopplungs-Lücke CVE-2019-13052.

Entscheidungshilfe: für wirklich Geheimes Kabel, fürs Büro Logi Bolt oder BLE Secure Connections, vorhandenes Unifying behalten und die Firmware pflegen, No-Name-Funktastaturen ersetzen.
Eine kurze Entscheidungshilfe für das eigene Funk-Setup.

Was offen bleibt

  • Warum das Einschleusen genau scheiterte. Der Durchlauf ist gemessen, der Sniffer findet 086A98E03C und die Injektion landet nichts, aber ob die Firmware abwies oder die ersniffte Adresse nicht exakt saß, ist ohne einen verwundbaren Vergleichs-Empfänger nicht isoliert nachgewiesen.
  • Die Kopplungs-Lücke CVE-2019-13052 ist beschrieben, aber nicht vorgeführt. Das ist der Kern des zweiten Teils.
  • Ob sich auf genau diesem nRF24-Modul die Adresse zuverlässig mitschneiden lässt, hängt von Kanal und Timing ab und ist nicht erschöpfend getestet.
  • Ob die MX Keys selbst per fwupd ein Firmware-Update bekommen könnte, habe ich nicht geprüft, nur die beiden Empfänger.
  • Reichweite und Störumgebung. Alle Aussagen zum Mithören beziehen sich auf die Laborsituation direkt am Gerät.

Wie es weitergeht

Im nächsten Teil wird nicht nur erklärt, sondern gemacht. Mit einem nRF52840-Dongle und Logitacker schneide ich den Kopplungsvorgang einer Unifying-Tastatur mit, leite daraus den Schlüssel ab und lese die Eingaben mit. Das ist die Design-Lücke CVE-2019-13052, und sie betrifft auch die aktuelle Firmware, an der der Flipper heute noch abgeprallt ist. Aus „die Tastatur ist verschlüsselt“ wird dann „und trotzdem lesbar, wenn ich im richtigen Moment dabei war“. Das ist der interessante Teil, und er kommt.

Bezugsquellen

Die folgenden Links sind Affiliate-Links (Werbung). Kaufst du darüber, bekomme ich eine kleine Provision, für dich ändert sich am Preis nichts.

Siehe auch

Wenn ich irgendwo danebenliege, korrigiert mich gerne, dann lerne ich selbst etwas. Ich bin ja auch nur doof ;-). Und die Frage an euch zum Schluss: Nutzt ihr Funk oder Kabel, und habt ihr je nachgesehen, ob eure Tastatur überhaupt verschlüsselt funkt? Wie das geht, wisst ihr jetzt. Wenn ihr mögt, dürft ihr mich dazu sehr gerne fragen.

Neunzehn Sekunden für einen Schlüssel: was der TPM-Chip unter Linux wirklich kann

TPM-2.0-Chip auf einem Mainboard mit Schlüssel und Stoppuhr, unter Linux als sicherer Hardware-Schlüsselspeicher und für Measured Boot genutzt, aber deutlich langsamer als die CPU bei der RSA-Schlüsselerzeugung.

Es gibt Dinge, die unter Linux in der breiten Masse vernachlässigt werden. Secure Boot ist so ein Beispiel, TPM ist ein anderes. Vielleicht ist es an der anfangs bescheidenen Unterstützung gescheitert, vieleicht sind die Themen technisch einfach unangenehm, am ehesten aber liegt es daran, dass der Mehrwert für die meisten überschaubar bleibt. Heute nehme ich mir den TPM vor.

Aufgefallen ist mir der Chip zweimal. Das erste Mal, als er durch die Medien ging, weil die Angst groß wurde, Microsoft mache damit unsere IT kaputt, und weil jemand anfing, sehr viele Patente einzureichen. Das zweite Mal viele Jahre später, als TPM 2.0 zur harten Voraussetzung für Windows 11 wurde. Dazwischen lagen zwanzig Jahre, in denen der Chip in fast jedes Endgerät gewandert ist, ohne dass sich jemand dafür interessiert hat.

In Notebooks und Fertig-PCs ist er heute meist fest verlötet. Bei Server-Mainboards kauft man ihn dagegen als Option dazu, ein kleines Steckmodul auf einem eigenen Header. Der Chip in meinem Testgerät ist ein Infineon OPTIGA SLB 9670, und genau dieselbe Chip-Familie sitzt auf den steckbaren TPM-Modulen für Supermicro-Boards. Damit schließt der Beitrag an die BIOS-Serie an, in der ich mich gerade Menü für Menü durch ein Serverboard arbeite.

Freigestelltes TPM-Steckmodul für einen Supermicro-Header, Beschriftungsseite mit „TPM2.0 SPI", „Trusted Platform Module 2" und „For Super".
Das nachgerüstete TPM-Modul für den JTPM1-Header, hier freigestellt. Auf der Rückseite steckt ein echter Infineon SLB9670. Bei vielen Server-Boards gehört der Chip nicht zum Lieferumfang.

Erst die Geschichte, dann die Hände in den Chip. Alle Ausgaben hier sind echt und stammen vom 10. August 2026, gemessen auf einem Fujitsu Celsius H780 mit Linux Mint 22.3, Kernel 7.0.0-28-generic und tpm2-tools 5.6. Wo ich etwas nur aus Dokumentation kenne oder wo ich vermute, steht das dabei.

Angefangen hat alles mit einer Seriennummer in der CPU

Mitte der neunziger Jahre wollte Intel eine Identifizierungsfunktion direkt in den Prozessor bringen. Der erste Schritt war 1999 die Prozessor-Seriennummer im Pentium III, eine eindeutige Kennung, die jede Software auslesen konnte. Der öffentliche Widerstand war so heftig, dass Intel zurückzog. Das Vorhaben verschwand aber nicht, es suchte Deckung in einem Konsortium. Dieses Detail ist wichtig, weil es das Muster für alles setzt, was danach kam.

1999 gründeten Compaq, HP, IBM, Intel und Microsoft die Trusted Computing Platform Alliance, kurz TCPA. Im April 2003 wurde daraus die Trusted Computing Group, TCG, getragen von AMD, HP, IBM, Intel und Microsoft. Aus einer Firmenidee war ein Industriestandard geworden.

Der Chip, der nach einem Senator benannt wurde

In der Debatte bekam der TPM den Spottnamen „Fritz-Chip“. Der Name ging auf den US-Senator Fritz Hollings aus South Carolina zurück, der den CBDTPA vorantrieb, ein Gesetzesvorhaben, das Kopierschutz in aller Consumer-Elektronik verpflichtend machen sollte. Der Spottname war also ein politischer Vorwurf und keine technische Bezeichnung. Er hat trotzdem länger gehalten als das Gesetz, aus dem er kam.

Vertrauen, aber nicht dein Vertrauen

Ross Anderson von der Universität Cambridge schrieb 2002 und 2003 die Trusted Computing FAQ. Dieses Dokument hat die Debatte stärker geprägt als jede Herstellerankündigung, und es liegt bis heute unverändert online. Sein Kernargument in einem Satz: bei „Trust“ geht es nicht darum, dass der Nutzer dem Rechner vertraut, sondern darum, dass der Rechner dem Nutzer nicht vertraut.

Richard Stallman legte 2002 mit „Can you trust your computer?“ nach und prägte den Gegenbegriff treacherous computing, verräterisches Rechnen. Die FSF benutzt ihn heute noch. IBM antwortete mit einer Gegendarstellung von David Safford, „Clarifying Misinformation on TCPA“.

Die EFF ging einen dritten Weg, und der ist der interessanteste. Seth Schoen veröffentlichte im Oktober 2003 Trusted Computing: Promise and Risk und schlug ein konkretes Feature vor, den Owner Override. Der Eigentümer des Rechners sollte die Attestierung überstimmen können, seinem Bank-Server also erzählen dürfen, sein Opera sei ein Internet Explorer. Die Kritik hatte damit einen Reparaturvorschlag und war nicht bloß dagegen. Die TCG hat den Owner Override nie übernommen.

Microsoft nannte sein eigenes Vorhaben Palladium und benannte es am 24. Januar 2003 in Next-Generation Secure Computing Base um, NGSCB. Der Name wurde harmloser, die Architektur blieb dieselbe.

Zwei Patente von 2001, und ein Cypherpunk der zurückschoss

Im Dezember 2001 wurden Microsoft zwei Patente erteilt. Das eine ist US 6,330,670, „Digital rights management operating system“, angemeldet im Januar 1999, erteilt am 11. Dezember 2001, als Erfinder Paul England, John DeTreville und Butler Lampson. Das andere ist US 6,327,652, „Loading and identifying a digital rights management operating system“. Zusammen decken sie viele Grundelemente eines vertrauenswürdigen Betriebssystems ab.

Die Sorge dahinter war handfest und nicht abwegig. Wenn Palladium unter dieses Patent fällt, dann verletzt jeder unabhängige Nachbau das Patent. Also jeder einzelne Entwickler, jedes Open-Source-Projekt, jeder Wettbewerber. Ohne Lizenz von Microsoft kein freies Trusted Computing. Kritiker beschrieben das damals als Kombination aus Patentschutz und faktischem Zwang zur Nutzung, mit deutlich wettbewerbsrechtlichem Geschmack.

Der Gegenzug kam im August 2002 von einem Cypherpunk, der unter dem Namen Lucky Green auftrat. Er meldete selbst ein Patent an, und zwar auf Verfahren, mit denen Software auf einer Palladium- oder TCPA-Plattform gegen Kopieren geschützt werden kann. Der Zweck war offen defensiv: wer die Ansprüche hält, kann verhindern, dass genau diese Anwendungen ausgerollt werden. Auf einem Panel der USENIX Security wurde er gefragt, ob Microsoft nicht einfach Prior Art geltend machen könne. Sein Argument: Prior Art müsste unter Eid erklärt werden, und nach Jahren Arbeit am Projekt und einem ausführlichen eigenen DRM-Patent sei kaum vorstellbar, dem Palladium-Team wäre relevante Prior Art entgangen. Im September 2002 hielt er zusätzlich einen Vortrag mit dem Titel „TCPA: the mother(board) of all Big Brothers“.

Was daraus geworden ist

Die Auflösung ist unbequemer, als es beide Lager damals gedacht haben. NGSCB ist gescheitert, aber nicht an den Patenten. Im Mai 2004 stellte Microsoft die vollständige Architektur zurück, und die Begründung war wirtschaftlich: die Hardware-Landschaft war nicht reif, und vor allem waren die unabhängigen Softwarehersteller nicht bereit, ihre Anwendungen auf die nötigen neuen APIs umzuschreiben. Curtained Memory und der Nexus-Kernel fielen weg, die TPM-Unterstützung blieb übrig.

Der Chip selbst wurde offen. Nach TPM 1.2 kam TPM 2.0, veröffentlicht 2014 und als ISO/IEC 11889 internationaler Standard. Die TCG stellt die Referenz-Implementierung unter eine royalty-free Copyright-Lizenz, das Patentregime zwischen den Mitgliedern ist RAND. Damit war der Nachbau möglich und dem Patenthebel die Grundlage entzogen.

Linux hat es dann einfach implementiert. Der Treiber tpm_tis sitzt im Kernel, IBM brachte TrouSerS für TPM 1.2, für TPM 2.0 entstand mit Beteiligung von Intel und TCG der tpm2-tss-Stack samt tpm2-tools. Seit Linux 4.12 gibt es den Resource Manager im Kernel unter /dev/tpmrm0, wodurch der frühere Userspace-Daemon tpm2-abrmd für die meisten Fälle überflüssig wurde. Nichts davon brauchte eine Lizenz von Microsoft.

Die befürchtete Abriegelung kam trotzdem, nur von woanders. Kein einziger der Horror-Fälle aus der TCPA-FAQ wurde durch den TPM Realität. Wo Nutzer heute tatsächlich ausgesperrt werden, steckt eine andere Technik dahinter: Widevine beim Streaming, Play Integrity und früher SafetyNet bei Android, verriegelte Bootloader bei Telefonen, die Secure Enclave bei Apple, Attestierung bei Spielkonsolen. Der TPM wurde stattdessen ein langweiliger Schlüsselspeicher.

Ein Teil der Angst hat sich dann doch erfüllt, nur in anderer Form. Windows 11 macht TPM 2.0 zur harten Voraussetzung, und damit gilt funktionierende Hardware plötzlich als nicht unterstützt. Das ist keine DRM-Geschichte, sondern eine Elektroschrott- und Lebensdauer-Geschichte. Der Mechanismus ist aber derselbe: eine Hardware-Eigenschaft entscheidet, was auf dem Gerät laufen darf.

Und dann kam der Kreis zurück. 2023 schlugen Chrome-Entwickler bei Google die Web Environment Integrity API vor. Der Browser sollte sich von einer Vertrauensinstanz einen Nachweis über seine Umgebung ausstellen lassen, den Webseiten dann prüfen können. Das ist praktisch Wort für Wort das Szenario, vor dem Anderson zwanzig Jahre früher gewarnt hatte, nur ohne TPM. Nach massivem Widerstand zog Google den Vorschlag im November 2023 für Chromium auf dem Desktop zurück.

Hat die Patentanmeldung von damals also geholfen?

Das ist die Frage, mit der ich in die Recherche gegangen bin, und die Antwort ist zweiteilig. Die defensive Patentanmeldung hat, soweit öffentlich nachvollziehbar, nichts bewirkt. Es gibt kein erteiltes Patent, das man heute als den Grund benennen könnte, warum TCPA-DRM nicht kam. Der Gegenzug war eine wirksame Geste, aber kein wirksames Rechtsmittel. Gewirkt hat etwas anderes: erstens die Wirtschaftlichkeit, weil die Softwarehersteller nicht umschreiben wollten und es damit kein Produkt gab, zweitens die Öffnung der Spezifikation und die ISO-Standardisierung, die den Nachbau erlaubten.

Der zweite Teil verdient Anerkennung. Die Kampagne selbst hat gewirkt, nur nicht juristisch, sondern politisch. Der Lärm um TCPA hat plausibel dazu beigetragen, dass die TCG die Spezifikation öffnete, die aggressivsten Ideen fallen ließ und dass Fernattestierung zwanzig Jahre lang aus dem Consumer-Web draußen blieb. Als der Versuch 2023 mit Web Environment Integrity wiederkam, war das Muster des Widerstands eingeübt und der Vorschlag nach Monaten weg. Wichtig dabei: das ist meine Deutung. Die Kausalkette von der Kampagne 2002 zur offenen Spezifikation 2014 ist nirgends dokumentiert, das ist ein Plausibilitätsargument und keine Messung.

Bleibt der Eindruck: die Angst war berechtigt und falsch adressiert. Berechtigt, weil die Mechanik der Fernattestierung tatsächlich genau das kann, was befürchtet wurde. Falsch adressiert, weil der TPM davon der harmloseste Teil war und am Ende zu dem wurde, was jetzt kommt. Ein langsamer, sturer kleiner Chip, der Schlüssel nicht herausgibt.

Erst suchen, ganz ohne TPM-Werkzeug

Bevor ihr irgendetwas installiert: der Kernel weiß längst, ob da ein Chip sitzt. Er sagt es beim Start.

# dmesg | grep -i tpm
[    0.000000] efi: ACPI=0x7990e000 ACPI 2.0=0x7990e014 TPMFinalLog=0x713bb000 SMBIOS=0x7035e000 SMBIOS 3.0=0x7035b000 ESRT=0x70358c98 MEMATTR=0x5f5c2018 MOKvar=0x70323000 INITRD=0x592eda98 RNG=0x798ce018 TPMEventLog=0x798c0018
[    0.010528] ACPI: SSDT 0x0000000079905000 000554 (v01 INTEL  Tpm2Tabl 00001000 INTL 20160422)
[    0.010531] ACPI: TPM2 0x0000000079904000 000034 (v04 FUJ    PC       01170000 FUJ  00000001)
[    0.010607] ACPI: Reserving TPM2 table memory at [mem 0x79904000-0x79904033]
[    0.569145] tpm_tis MSFT0101:00: 2.0 TPM (device-id 0x1B, rev-id 16)

Drei Dinge stehen da drin. In der ersten Zeile übergibt UEFI die Adresse des Event-Logs, TPMEventLog=0x798c0018, also der Liste aller Messungen aus dem Startvorgang. Dann die ACPI-Tabelle TPM2, über die die Firmware mitteilt, wo und wie der Chip ansprechbar ist. Und in der letzten Zeile das Ergebnis: der Treiber tpm_tis hat unter der ACPI-Kennung MSFT0101 ein TPM der Version 2.0 gefunden.

Danach existieren zwei Gerätedateien, und der Unterschied zwischen ihnen ist wichtig.

# ls -la /dev/tpm*
crw-rw---- 1 tss root  10,   224 Aug 10 09:31 /dev/tpm0
crw-rw---- 1 tss tss  252, 65536 Aug 10 09:31 /dev/tpmrm0

/dev/tpm0 ist der rohe Zugang zum Chip. Daran kann genau ein Prozess gleichzeitig arbeiten, und wer die Ressourcen im Chip verwaltet, ist selbst dafür verantwortlich. /dev/tpmrm0 geht über den Resource Manager im Kernel, der das Ein- und Auslagern der Schlüssel-Kontexte übernimmt und mehrere Nutzer nebeneinander erlaubt. In der Praxis nimmt man immer /dev/tpmrm0, und die tpm2-tools tun das von sich aus.

Die Version steht ebenfalls im sysfs, und die solltet ihr prüfen, bevor ihr weiterlest.

# ls /sys/class/tpm/tpm0/
dev  device  pcr-sha1  pcr-sha256  power  ppi  subsystem  tpm_version_major  uevent

# cat /sys/class/tpm/tpm0/tpm_version_major
2

Eine 2 heißt TPM 2.0 und alles in diesem Beitrag gilt. Eine 1 heißt TPM 1.2, und dann gilt fast nichts davon, weil es ein anderer Standard mit einem anderen Software-Stack ist. Nebenbei verrät das Verzeichnis noch etwas: es gibt eine pcr-sha1– und eine pcr-sha256-Bank, der Chip führt die Messregister also in zwei Hash-Verfahren parallel.

Und weil der Kernel das ohnehin bereitstellt, kommt hier der erste kleine Aha-Moment, noch ganz ohne installiertes Paket. Die Messwerte des Startvorgangs liegen als Dateien herum.

# cat /sys/class/tpm/tpm0/pcr-sha256/0
7DE4ABE4ED8D8291D9B58D71F3B2988F5DA81EBE58D4AE518012E877FA1E04F1
# cat /sys/class/tpm/tpm0/pcr-sha256/1
41CBF90AFF5CEEFAA9E9C52E7A3D5B4C8518EA65861C753AF8DB005C5330F53F
# cat /sys/class/tpm/tpm0/pcr-sha256/7
F5841722D3DCC8886F14E92CA2D7304932CF17E2247A85D3B7BB41FE2C02CB4E

Das sind drei der Platform Configuration Register, kurz PCR. Merkt euch den Wert von PCR 7, der taucht später mehrfach auf.

Die Falle mit lsmod

Das ist eine häufige Fehldiagnose, deshalb steht sie hier als eigener Punkt. Auf meinem Testsystem liefert lsmod | grep tpm nämlich schlicht nichts. Kein Modul, keine Ausgabe. Der Chip arbeitet trotzdem einwandfrei, weil tpm_tis in diesen Kernel fest eingebaut ist und deshalb in der Modulliste gar nicht erscheinen kann. Wer nach lsmod urteilt, hält ein funktionierendes TPM für abwesend. Fragt stattdessen nach der Treiberbindung.

# ls -l /sys/class/tpm/tpm0/device/driver
lrwxrwxrwx 1 root root 0 Aug 10 09:31 /sys/class/tpm/tpm0/device/driver -> ../../../bus/platform/drivers/tpm_tis

# cat /sys/class/tpm/tpm0/device/modalias
acpi:MSFT0101:MSFT0101:

Der Symlink zeigt, welcher Treiber das Gerät tatsächlich bedient. Das ist die Antwort auf die Frage, die lsmod nicht beantworten kann.

Wenn nichts auftaucht

Bleibt dmesg stumm und existiert kein /dev/tpm0, dann gibt es drei realistische Ursachen, in dieser Reihenfolge.

  • Im Firmware-Setup abgeschaltet. Die Option heißt je nach Hersteller „Security Device Support“, „TPM State“, „Trusted Computing“, „PTT“ oder „fTPM“. Wo diese Menüs bei einem Serverboard liegen und wie man sich dort zurechtfindet, habe ich in der BIOS-Serie ausführlich aufgeschrieben.
  • Physisch nicht vorhanden. Bei Server-Boards ist der TPM oft ein steckbares Modul auf einem eigenen Header, bei Supermicro etwa die AOM-TPM-Module auf dem Anschluss JTPM1. Wer ihn nicht mitbestellt hat, hat einen leeren Steckplatz. Bei Notebooks ist der Chip dagegen fast immer verlötet.
  • Vorhanden, aber als fTPM in der CPU statt als eigener Chip. Woran man das unterscheidet, steht weiter unten, und man liest es an einer einzigen Zeile ab.

Zwei Details aus dem Firmware-Setup, die hier gut passen. Erstens sind SHA-1-Bank und SHA-256-Bank oft getrennt schaltbar. Dass mein Testgerät beide Banks führt, ist also keine Selbstverständlichkeit, sondern eine Einstellung. Zweitens steht in demselben Menü meist ein TPM Clear. Finger weg, solange ihr nicht genau wisst, was daran hängt. Ein Clear wirft die Hierarchien im Chip weg, und damit ist alles, was gegen diesen Chip versiegelt war, unwiederbringlich verloren. Auch der BitLocker-Schlüssel eines parallel installierten Windows.

Die Schichten, bevor das erste Paket installiert wird

Es hilft, den Stack einmal von unten nach oben gesehen zu haben. Sonst installiert man ein Paket und wundert sich später, welche der vielen Bibliotheken wofür zuständig ist.

Schichtmodell vom Infineon SLB 9670 über den Kerneltreiber tpm_tis und die Gerätedateien bis zu tpm2-tools, tpm2-openssl und tpm2-pkcs11
Der Weg eines Kommandos vom Werkzeug bis in den Chip. Über /dev/tpm0 arbeitet genau ein Prozess, über /dev/tpmrm0 mehrere.

Ganz unten der Chip, darüber der Kerneltreiber, darüber die beiden Gerätedateien. Erst dann kommt Userspace: die TCTI-Schicht kümmert sich um den Transport zur Gerätedatei, die ESAPI-Schicht um Kommandos, Sessions und HMAC-Absicherung. Ganz oben die drei Werkzeuge, die man tatsächlich in die Hand nimmt. Für die Pflichtausstattung reichen zwei Pakete.

PaketWofür
tpm2-toolsdie tpm2_*-Kommandozeile, alles in diesem Beitrag läuft darüber
libtss2-*der TSS2-Stack darunter, meist schon über systemd installiert

Dazu zwei optionale, die ich beide benutze, weil sie den Chip an Software anschließen, die von TPM nichts wissen muss.

PaketWofür
tpm2-opensslProvider für OpenSSL 3, Schlüssel im Chip über die gewohnten openssl-Aufrufe
libtpm2-pkcs11-1 und libtpm2-pkcs11-toolsPKCS#11-Schnittstelle, damit SSH und Browser den Chip nutzen können

Und zwei, die ich ausdrücklich nicht installiere.

PaketWarum nicht
tpm2-abrmdResource Manager im Userspace mit D-Bus-Schnittstelle. Seit Linux 4.12 macht der Kernel das über /dev/tpmrm0 selbst. Das Projekt ist nicht tot, es wird weiter gepflegt und ist für Simulatoren, Tests und manche Integrationen nach wie vor sinnvoll. Beides parallel zu betreiben führt aber zu Konflikten, deshalb hier bewusst nur der Kernel-Weg.
clevis, clevis-tpm2binden ausschließlich LUKS2-Header. Ohne LUKS nutzlos, und auf diesem Gerät gibt es kein LUKS.
# apt-get install -y tpm2-tools tpm2-openssl libtpm2-pkcs11-1 libtpm2-pkcs11-tools

Auf meinem Mint 22.3 sind damit tpm2-tools 5.6, tpm2-openssl 1.2.0 und tpm2-pkcs11 1.9.0 gelandet. Der TSS2-Stack war schon da, den bringt systemd mit. Auf Fedora und RHEL heißt das Hauptpaket ebenfalls tpm2-tools, dazu tpm2-tss, unter Arch genauso tpm2-tools mit tpm2-tss als Abhängigkeit.

Eine Kleinigkeit, über die jeder beim ersten Mal stolpert: /dev/tpmrm0 gehört tss:tss. Entweder arbeitet ihr mit sudo, oder ihr nehmt euren Benutzer in die Gruppe tss auf. Die Rechte in der Ausgabe von ls -la /dev/tpm* weiter oben sagen genau das, crw-rw---- und keine Rechte für alle anderen.

Spricht der Chip?

Es gibt drei Stufen, aufsteigend im Aussagewert. Die erste ist der Lebenstest.

# tpm2_getrandom --hex 16

Kommen 16 Bytes zurück, dann funktioniert die ganze Kette von der Bibliothek über die Gerätedatei bis in die Hardware. Der Zufall kommt dabei wirklich aus dem Chip, und er ist gemächlich, dazu unten die Zahlen. Stufe zwei ist tpm2_getcap properties-fixed, da sagt der Chip, wer er ist. Stufe drei ist tpm2_getcap properties-variable, da sagt er, in welchem Zustand er ist. Beide sind gleich interessant, nur aus unterschiedlichen Gründen.

Wer ist dieser Chip

# tpm2_getcap properties-fixed
TPM2_PT_FAMILY_INDICATOR:
  raw: 0x322E3000
  value: "2.0"
TPM2_PT_LEVEL:
  raw: 0
TPM2_PT_REVISION:
  raw: 0x74
  value: 1.16
TPM2_PT_DAY_OF_YEAR:
  raw: 0x109
TPM2_PT_YEAR:
  raw: 0x7E0
TPM2_PT_MANUFACTURER:
  raw: 0x49465800
  value: "IFX"
TPM2_PT_VENDOR_STRING_1:
  raw: 0x534C4239
  value: "SLB9"
TPM2_PT_VENDOR_STRING_2:
  raw: 0x36373000
  value: "670"
TPM2_PT_VENDOR_TPM_TYPE:
  raw: 0x0
TPM2_PT_FIRMWARE_VERSION_1:
  raw: 0x7003F
TPM2_PT_FIRMWARE_VERSION_2:
  raw: 0xD1900
TPM2_PT_INPUT_BUFFER:
  raw: 0x400
TPM2_PT_HR_TRANSIENT_MIN:
  raw: 0x3
TPM2_PT_HR_PERSISTENT_MIN:
  raw: 0x7
TPM2_PT_ACTIVE_SESSIONS_MAX:
  raw: 0x40
TPM2_PT_PCR_COUNT:
  raw: 0x18
TPM2_PT_NV_COUNTERS_MAX:
  raw: 0x8
TPM2_PT_NV_INDEX_MAX:
  raw: 0x680

Rohe Hex-Werte, die plötzlich Sinn ergeben, wenn man sie einmal übersetzt. Das ist einer meiner Lieblingsmomente an dem ganzen Thema.

  • TPM2_PT_MANUFACTURER: 0x49465800 ist ASCII IFX, also Infineon.
  • VENDOR_STRING_1 und _2 ergeben zusammengesetzt SLB9 und 670, also ein Infineon OPTIGA SLB 9670.
  • TPM2_PT_REVISION: 0x74 ist Spezifikations-Revision 1.16, datiert auf Tag 265 des Jahres 2016.
  • FIRMWARE_VERSION_1: 0x7003F ist Firmware 7.63.
  • TPM2_PT_PCR_COUNT: 0x18 sind 24 PCRs.
  • TPM2_PT_HR_PERSISTENT_MIN: 0x7 heißt, dass nur sieben persistente Schlüsselplätze garantiert sind. Diese Zahl wird später richtig wichtig.
  • TPM2_PT_NV_COUNTERS_MAX: 0x8 sind genau acht monotone Zähler.

Und dann TPM2_PT_NV_INDEX_MAX: 0x680, also 1664. Hier lauert eine Fehldeutung, die in vielen Texten steht: das ist nicht der gesamte NV-Speicher des Chips, sondern die maximale Größe eines einzelnen NV-Index-Datenbereichs. Wie viel NV insgesamt frei ist, sagt diese Eigenschaft überhaupt nicht. Das steht im Datenblatt des Herstellers und liegt bei diesem Chip im Bereich einiger Kilobyte. Ich habe es nicht ermittelt, also behaupte ich hier auch keine Gesamtgröße.

Diskreter Chip oder fTPM in der CPU

Diese Unterscheidung ist keine Nebensächlichkeit, und man liest sie direkt am Hersteller-String ab.

Hersteller-StringWas es ist
IFX, NTC, STM, IBMdiskreter Chip auf dem Board oder auf einem Steckmodul
INTCIntel Platform Trust Technology, läuft in der Management Engine
AMDAMD fTPM, läuft im Platform Security Processor

Ein diskreter Chip hängt an einem physischen Bus, den man anzapfen kann. Ein fTPM hat diesen Bus nicht, dafür hat er andere Probleme. Beides kommt im Angriffs-Abschnitt zurück.

Für mein Testsystem ist die Sache klar: IFX, also ein diskreter Infineon SLB 9670. Aus Linux-Sicht hängt er als memory-mapped TIS an der ACPI-Kennung MSFT0101 und wird von tpm_tis bedient. Ob die physische Leitung dahinter LPC oder SPI ist, lässt sich aus dem Betriebssystem allein nicht entscheiden, weil der PCH einen SPI-TPM auch als MMIO-TIS durchreichen kann. Das ist also plausibel, aber nicht gemessen. Die einzige echte Antwort wäre ein Foto vom offenen Gerät, und dafür müsste das Notebook auseinander.

32 Fehlversuche, und deshalb reicht eine kurze PIN

Jetzt der Zustand, und das ist der wichtigste einzelne Abschnitt des ganzen Beitrags, weil er später alles trägt.

# tpm2_getcap properties-variable
TPM2_PT_PERMANENT:
  ownerAuthSet:              0
  endorsementAuthSet:        0
TPM2_PT_STARTUP_CLEAR:
  phEnable:                  1
  shEnable:                  1
TPM2_PT_LOCKOUT_COUNTER: 0x0
TPM2_PT_MAX_AUTH_FAIL: 0x20
TPM2_PT_LOCKOUT_INTERVAL: 0x1C20
TPM2_PT_LOCKOUT_RECOVERY: 0x15180

Diese drei Werte werden fast immer verwechselt, also einmal sauber.

  • MAX_AUTH_FAIL: 0x20 sind 32 fehlgeschlagene Authentifizierungsversuche, danach sperrt der Chip.
  • LOCKOUT_INTERVAL: 0x1C20 sind 7200 Sekunden, also 2 Stunden. Nach dieser Zeit wird der Fehlerzähler um genau eins verringert.
  • LOCKOUT_RECOVERY: 0x15180 sind 86400 Sekunden, also 24 Stunden. Das ist aber nicht die Erholung des normalen Zählers, sondern die Wartezeit, bevor nach einem fehlgeschlagenen Versuch mit lockoutAuth erneut mit lockoutAuth gearbeitet werden darf.

Einmal vorgerechnet, weil die Zahlen so viel deutlicher sind: ein einzelner Fehlversuch altert nach 2 Stunden aus. Von 32 verbrauchten Versuchen zurück auf null dauert also 64 Stunden und nicht 24.

Und das ist der eigentliche Mehrwert des ganzen Bauteils. Der Chip zählt Fehlversuche und sperrt. Deshalb reicht bei einem TPM eine sechsstellige PIN, wo ein Passwort-Hash auf der Platte eine Passphrase mit hoher Entropie bräuchte. Wer die Platte hat, probiert Millionen Passphrasen pro Sekunde durch. Wer vor dem TPM sitzt, bekommt 32 Versuche und darf dann zwei Stunden warten, um einen einzigen zurückzubekommen. Die Stärke kommt nicht aus dem Geheimnis, sie kommt aus der Hardware, die das Raten unterbindet. Fast jeder Text über TPM lässt diesen Punkt weg, und dann klingt die kurze PIN nach Leichtsinn.

Das Geburtszertifikat des Chips

Jetzt kommt der Teil, den ich in keinem der üblichen TPM-Howtos gesehen habe. In dem Chip liegt ab Werk ein Zertifikat, ausgestellt vom Hersteller auf den Endorsement Key. Erst die Liste der belegten NV-Indizes, dann das Zertifikat selbst.

# tpm2_getcap handles-nv-index
- 0x1410001
- 0x1410002
- 0x1410003
- 0x1800100
- 0x1810008
- 0x1820002
- 0x1880001
- 0x1880011
- 0x1C00002
- 0x1C0000A
- 0x1C10102
- 0x1C10103
- 0x1C10104
- 0x1C10105

# tpm2_nvreadpublic 0x1C00002
0x1c00002:
  name: 000bcfecbb34d24d7356b39b8a0fb0c82c7b052a0335fc8ca2fa602cf1395e5ff804
  hash algorithm:
    friendly: sha256
    value: 0xB
  attributes:
    friendly: ppwrite|writedefine|ppread|ownerread|authread|no_da|written|platformcreate
    value: 0x62072001
  size: 1184

0x1C00002 ist der TCG-Standardindex für das RSA-EK-Zertifikat, 0x1C0000A derselbe für ECC. 1184 Bytes, das passt zu einem X.509-Zertifikat. Also raus damit und mit gewohnten Werkzeugen anschauen.

# tpm2_nvread 0x1C00002 -o ek_rsa.der
WARN: Reading full size of the NV index

# openssl x509 -inform der -in ek_rsa.der -noout -subject -issuer -dates
subject=
issuer=C = DE, O = Infineon Technologies AG, OU = OPTIGA(TM) TPM2.0, CN = Infineon OPTIGA(TM) RSA Manufacturing CA 035
notBefore=Aug 21 14:09:40 2019 GMT
notAfter=Aug 21 14:09:40 2034 GMT

Zwei Dinge fallen sofort auf. Der Aussteller ist eine echte Hersteller-CA, „Infineon OPTIGA(TM) RSA Manufacturing CA 035“, und Infineon hat diesen Chip am 21. August 2019 signiert. Und der Subject ist leer. Ein TPM hat keinen Namen. Die Identität steckt woanders, nämlich in den Erweiterungen.

# openssl x509 -inform der -in ek_rsa.der -noout -text
            X509v3 Key Usage: critical
                Key Encipherment
            X509v3 Subject Alternative Name: critical
                DirName:/2.23.133.2.1=id:49465800/2.23.133.2.2=SLB 9670 TPM2.0/2.23.133.2.3=id:073f
            X509v3 Basic Constraints: critical
                CA:FALSE
            X509v3 CRL Distribution Points:
                Full Name:
                  URI:http://pki.infineon.com/OptigaRsaMfrCA035/OptigaRsaMfrCA035.crl
            X509v3 Certificate Policies:
                Policy: 1.2.276.0.68.1.20.1
            X509v3 Extended Key Usage:
                2.23.133.8.1

Der Subject Alternative Name trägt drei TCG-OIDs, und die decken sich exakt mit dem, was der Chip vorher selbst über sich gesagt hat. 2.23.133.2.1 ist der Hersteller, id:49465800, also wieder IFX. 2.23.133.2.2 ist das Modell, SLB 9670 TPM2.0. 2.23.133.2.3 ist die Firmware, id:073f, was genau die 7.63 von oben bestätigt. Die Extended Key Usage 2.23.133.8.1 ist tcg-kp-EKCertificate, dieses Zertifikat darf also nichts anderes sein als der Nachweis eines Endorsement Keys. Und Infineon veröffentlicht dazu eine Sperrliste.

Was ich hier bewusst weglasse, sind die Zertifikats-Seriennummer und der öffentliche EK-Schlüssel. Beide kennzeichnen dauerhaft und eindeutig genau dieses eine Notebook, und die TCG behandelt den EK aus genau diesem Grund als datenschutzrelevant. Damit erklärt sich beiläufig, warum es überhaupt Attestierungsschlüssel gibt: damit man für eine Attestierung nicht jedes Mal die dauerhafte Chip-Identität vorzeigen muss.

Das hier ist die Wurzel, auf der Fernattestierung aufsetzt. Ein Prüfer kann feststellen, dass in dieser Maschine ein echter, von Infineon signierter TPM sitzt, ohne dem Betriebssystem eine einzige Zeile zu glauben. Genau formuliert beweist das Zertifikat aber nur die Echtheit des Chips. Dass ein bestimmter Attestierungsschlüssel wirklich in diesem Chip liegt, ist ein zusätzlicher Schritt, und den nehme ich mir im Server-Teil vor.

Drei Dinge, alles andere ist Kombination

Bevor irgendein Anwendungsfall kommt: ein TPM bietet im Kern nur drei Dinge an. Alles, was danach als Feature verkauft wird, ist eine Kombination daraus.

  • Schlüssel, die nicht herauskommen. Ein Schlüssel mit dem Attribut fixedTPM wird im Chip erzeugt und verlässt ihn nie. Man kann ihn benutzen, man kann ihn nicht kopieren.
  • PCRs, Register die nur wachsen. 24 Register, hier mit SHA-1- und SHA-256-Bank. Man kann sie nicht setzen, nur erweitern, und die Rechenvorschrift dafür ist neu = hash(alt || messwert). Bei den Registern, auf die es ankommt, nämlich PCR 0 bis 15 auf einer PC-Client-Plattform, geht es nur über einen Neustart zurück. Deshalb kann man die Boot-Historie nicht nachträglich fälschen. Zwei Ausnahmen gehören dazu, sonst wird es ungenau: PCR 16 und PCR 23 sind zur Laufzeit zurücksetzbar. PCR 16 ist ausdrücklich als Debug-Register vorgesehen, und genau deshalb benutze ich es weiter unten für die Demonstration. Gegen ein zurücksetzbares PCR versiegelt niemand etwas Echtes.
  • Sealing, Geheimnisse an einen Zustand binden. Ein Geheimnis wird so verschlüsselt, dass der Chip es nur herausgibt, wenn eine Policy erfüllt ist, typischerweise ein bestimmter PCR-Zustand.

Dazu kommen als Nebenfunktionen die schon erwähnten monotonen Zähler, ein winziger NV-Speicher, ein Zufallszahlengenerator und die Lockout-Logik von oben.

Und weil es das häufigste Missverständnis überhaupt ist, gleich früh und deutlich: ein TPM ist kein Krypto-Beschleuniger, keine Secure Enclave mit eigener Rechenumgebung und kein Schutz gegen einen kompromittierten laufenden Kernel. Er schützt Schlüssel im Ruhezustand und er misst den Startvorgang. Wenn der laufende Kernel übernommen ist, benutzt der Angreifer den TPM einfach mit, ganz höflich über dieselbe Schnittstelle.

Measured Boot in acht Befehlen

Genug Theorie, jetzt der Beweis. Ich baue eine Policy aus dem aktuellen Zustand von PCR 7, erzeuge einen Primary Key im Chip und versiegle damit ein Geheimnis.

# tpm2_startauthsession -S session.ctx
# tpm2_policypcr -S session.ctx -l sha256:7 -L pcr7.policy
76a916a90c7c0906a930cd5cd3e501cb43007331a8b7444222d8aa4c5475f53f
# tpm2_flushcontext session.ctx

Der Hash ist die Policy, und er hängt am Inhalt von PCR 7. Dann der Schlüssel, und ich messe gleich mit, weil die Zahl später noch gebraucht wird.

# time tpm2_createprimary -C o -g sha256 -G ecc256 -c primary.ctx
  value: aes
  raw: 0x6
sym-mode:
  value: cfb
  raw: 0x43
sym-keybits: 128
x: 2be770ed8755bf6f454cb1fea18ba722526d1ee3e9080ab94edda0480b73529d
y: af840b9c69dcac45c9aa3a04085ced5c750bfef5e70c470e4481592ff3026dc7

real	0m0.537s
# echo -n "geheim-nur-bei-diesem-boot" > secret.txt
# tpm2_create -C primary.ctx -g sha256 -u seal.pub -r seal.priv -L pcr7.policy -i secret.txt
keyedhash: ef1dc0bffd0d7529a6d0b44f79bdd14fb8829995c998ffb14d88936f82d3e9f3
authorization policy: 76a916a90c7c0906a930cd5cd3e501cb43007331a8b7444222d8aa4c5475f53f

# tpm2_load -C primary.ctx -u seal.pub -r seal.priv -c seal.ctx
name: 000b50ac9b197a947de6d2ff47d9d80748d4fa1453754bee5f3a8627e307cf8c84fd

# tpm2_startauthsession --policy-session -S s.ctx
# tpm2_policypcr -S s.ctx -l sha256:7
# tpm2_unseal -p session:s.ctx -c seal.ctx
geheim-nur-bei-diesem-boot

Das Geheimnis kommt zurück, weil PCR 7 noch genau den Wert hat, gegen den versiegelt wurde. Schön, beweist aber noch nichts. Interessant wird es erst, wenn sich die Messung ändert.

Dafür wechsle ich auf PCR 16, das Debug-Register. Der Grund ist praktisch: PCR 16 lässt sich ohne Neustart und ohne Firmware-Änderung erweitern, damit ist die Demonstration in einer einzigen Terminal-Sitzung reproduzierbar. Der Mechanismus ist derselbe wie bei PCR 7.

# tpm2_pcrread sha256:16
  sha256:
    16: 0x0000000000000000000000000000000000000000000000000000000000000000

# tpm2_startauthsession -S t.ctx
# tpm2_policypcr -S t.ctx -l sha256:16 -L pcr16.policy
# tpm2_flushcontext t.ctx
# echo -n "unlock-key-material" > s16.txt
# tpm2_create -C primary.ctx -g sha256 -u s16.pub -r s16.priv -L pcr16.policy -i s16.txt
# tpm2_load -C primary.ctx -u s16.pub -r s16.priv -c s16.ctx
# tpm2_startauthsession --policy-session -S p.ctx
# tpm2_policypcr -S p.ctx -l sha256:16
# tpm2_unseal -p session:p.ctx -c s16.ctx
unlock-key-material
# tpm2_flushcontext p.ctx

Und jetzt ändere ich die Messung. Das folgende Kommando ist genau das, was ein ausgetauschter Bootloader auch täte, nur mit einem selbstgewählten Messwert.

# tpm2_pcrextend 16:sha256=$(echo -n "boese-veraenderung" | sha256sum | cut -d" " -f1)
# tpm2_pcrread sha256:16
  sha256:
    16: 0xC6FE6738E4DDC92E1C8E18E290469CCB6B6D305BC1295C4F72B85D0922C25930

Derselbe Unseal-Aufruf wie zwei Blöcke vorher, Zeichen für Zeichen identisch:

# tpm2_startauthsession --policy-session -S p2.ctx
# tpm2_policypcr -S p2.ctx -l sha256:16
# tpm2_unseal -p session:p2.ctx -c s16.ctx
WARNING:esys:src/tss2-esys/api/Esys_Unseal.c:295:Esys_Unseal_Finish() Received TPM Error
ERROR:esys:src/tss2-esys/api/Esys_Unseal.c:98:Esys_Unseal() Esys Finish ErrorCode (0x0000099d)
ERROR: Esys_Unseal(0x99D) - tpm:session(1):a policy check failed
ERROR: Unable to run tpm2_unseal
exit=1

0x99D ist TPM_RC_POLICY_FAIL. Das Geheimnis liegt noch im Blob, es ist nicht gelöscht, es ist nur unerreichbar, solange PCR 16 diesen Wert trägt. Und jetzt der Punkt, auf den es mir ankommt: das ist keine Zugriffskontrolle im Betriebssystem. Da hat kein Dateisystem eine Berechtigung geprüft und kein Dienst eine Regel angewendet. Das ist eine Weigerung der Hardware. Sudo hilft hier nicht, weil root für diese Entscheidung überhaupt nicht zuständig ist.

Firmware, Bootloader und Kernel messen in PCR 7, PCR 4 und PCR 11, daraus entsteht die Policy, die beim Entsiegeln entweder aufgeht oder mit 0x99D scheitert
Wer was misst, und was am Ende über das Entsiegeln entscheidet.

Und wenn ich das Register einfach zurücksetze?

Das ist die Frage, die jeder aufmerksame Leser an dieser Stelle stellt, und sie ist völlig berechtigt. Bei PCR 16 geht das tatsächlich, ohne Neustart.

# tpm2_pcrread sha256:16
  sha256:
    16: 0xC6FE6738E4DDC92E1C8E18E290469CCB6B6D305BC1295C4F72B85D0922C25930
# tpm2_pcrreset 16
# echo $?
0
# tpm2_pcrread sha256:16
  sha256:
    16: 0x0000000000000000000000000000000000000000000000000000000000000000

Dasselbe Kommando gegen PCR 7 lehnt die Hardware ab.

# tpm2_pcrreset 7
ERROR: Esys_PCR_Reset(0x907) - tpm:warn(2.0): bad locality
ERROR: Could not reset PCR index: 7
ERROR: Unable to run tpm2_pcrreset

0x907 ist TPM_RC_LOCALITY. Der Chip unterscheidet, aus welcher Vertrauensstufe ein Kommando kommt, und root auf dem laufenden System hat die Locality, die zum Zurücksetzen eines Boot-Mess-Registers nötig wäre, gar nicht. Das ist der ganze Unterschied zwischen einem Debug-Register und einem Boot-Mess-Register, ausgedrückt in einer Fehlermeldung.

Terminalausgabe: tpm2_pcrreset 16 setzt das Register auf Null zurück, tpm2_pcrreset 7 scheitert mit 0x907 bad locality
PCR 16 lässt sich zurücksetzen, PCR 7 nicht. Root ändert daran nichts.

Die Platte startet ohne Passphrase, aber nur mit PIN

Der Klassiker am Arbeitsplatz ist LUKS2 zusammen mit systemd-cryptenroll. Der Nutzen ist echt: das Notebook startet ohne Eingabe, und ein ausgebautes Laufwerk bleibt trotzdem verschlüsselt.

# systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7 --tpm2-with-pin=yes /dev/nvme0n1pX

Ehrlichkeit an dieser Stelle: dieses Kommando habe ich nicht ausgeführt, und ich konnte es auch nicht. Die Maschine hat gar kein LUKS, die Wurzel liegt auf ZFS mit der ZFS-eigenen Verschlüsselung, aes-256-gcm. Der Befehl steht hier also als dokumentiertes Verfahren nach Manpage und nicht als Messung.

Das Wichtigste daran ist das --tpm2-with-pin=yes. Ohne PIN entsperrt sich das Gerät für jeden, der es einschaltet, und die Festplattenverschlüsselung schützt dann nur noch gegen den Ausbau der SSD, nicht gegen den Diebstahl des Rechners. Mit PIN greift die Lockout-Logik von weiter oben, und genau dadurch wird eine kurze PIN sicher.

Bei der Auswahl der PCRs gibt es dann einen echten Zielkonflikt, der meist verschwiegen wird.

  • Nur PCR 7 gebunden übersteht Kernel-Updates, misst aber im Kern nur den Secure-Boot-Zustand. Das ist wenig Aussage für viel Bequemlichkeit.
  • PCR 7 plus 11 gebunden, zusammen mit einem Unified Kernel Image, erfasst tatsächlich Kernel und initrd. Dafür bricht jedes Kernel-Update die Bindung, wenn man nicht mit signierter Policy arbeitet. Eine wichtige Präzisierung dazu, sonst erklärt man die Folgen falsch: PCR 11 ist kein reines Kernel-Register. systemd-stub misst dort das UKI hinein, danach schreibt systemd-pcrphase im Verlauf des Startvorgangs weitere Phasen-Messungen in dasselbe Register. Der Wert von PCR 11 hängt also davon ab, in welcher Boot-Phase man ihn liest, und genau darauf beruht der Trick, dass ein Geheimnis nur im initrd aufgeht und im laufenden System nicht mehr.
  • PCR 0 bis 7 gebunden ist die strengste Variante, und dann sperrt euch jedes BIOS-Update aus. Das ist der Grund, warum es systemd-pcrlock gibt, das die erwarteten Messwerte vorab berechnet und die Policy nachzieht. Dieselbe Manpage bezeichnet sich allerdings selbst noch als experimentell.

Wer welches Register wofür benutzt, hat die UAPI Group in einer PCR-Registry für Linux zusammengetragen. Das ist die Tabelle, die man beim Basteln offen haben will.

Eine Lücke betrifft ziemlich viele Linux-Nutzer, deshalb sage ich sie deutlich: systemd-cryptenroll und clevis luks bind schreiben beide in einen LUKS2-Header. Wer seine Wurzel auf ZFS-Native-Encryption hat, hat keinen. Upstream-ZFS bringt keine TPM-Integration mit. Ein TPM-gestütztes Entsperren bräuchte dort ein eigenes tpm2_unseal, das zfs load-key füttert, gebaut ins initramfs. Das ist eine echte offene Baustelle und kein Rezept, das ich hier nebenbei hinschreibe.

Ein SSH-Schlüssel, der keine Datei hat

Das ist mein Lieblings-Anwendungsfall am Arbeitsplatz, weil der Nutzen in einem Satz erklärt ist. Ein Angreifer mit Lesezugriff auf ~/.ssh bekommt nichts Verwendbares. Der Schlüssel kann die Maschine nicht verlassen. Er ist nicht gestohlen, er ist an das Blech gebunden.

# export TPM2_PKCS11_STORE=/root/tpm-demo/pkcs11store
# mkdir -p $TPM2_PKCS11_STORE
# tpm2_ptool init --path=$TPM2_PKCS11_STORE
action: Created
id: 1

# tpm2_ptool addtoken --pid=1 --label=ssh --sopin=sopin123 --userpin=userpin123 --path=$TPM2_PKCS11_STORE
# tpm2_ptool addkey --algorithm=ecc256 --label=ssh --userpin=userpin123 --path=$TPM2_PKCS11_STORE
public:
  CKA_ID: '63316131396137363763316531666439'

Die CKA_ID wird pro Schlüssel erzeugt, bei euch steht dort etwas anderes. Den öffentlichen Teil holt man sich in einem Format, das sshd versteht, direkt über den PKCS#11-Provider.

# ssh-keygen -D /usr/lib/x86_64-linux-gnu/pkcs11/libtpm2_pkcs11.so
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBO+enW3ZwHsSq8vHLKliloTMR/fpDgWPfti+UQuad0YA8rIXGE8Q9adJqjO6PmZO1DCavBiJgttOE24qrO6BX/0=

Diese Zeile wandert wie gewohnt in authorized_keys. Und damit der Nachweis auch etwas wert ist, zeige ich ihn als Vorher und Nachher. Auf dieser Maschine liegt in /root/.ssh nämlich kein einziger privater Schlüssel, es gibt also keine zweite Erklärung für einen erfolgreichen Public-Key-Login.

# ls /root/.ssh/id_*
ls: cannot access '/root/.ssh/id_*': No such file or directory

# ssh -o BatchMode=yes root@localhost true
root@localhost: Permission denied (publickey,password).

Derselbe Login, nur mit dem Provider dazu:

# ssh -o PKCS11Provider=/usr/lib/x86_64-linux-gnu/pkcs11/libtpm2_pkcs11.so root@localhost 'echo LOGIN-OK-VIA-TPM; hostname; id -un'
LOGIN-OK-VIA-TPM
ErrorLap
root

Die PIN habe ich für diesen Test über SSH_ASKPASS nicht interaktiv geliefert, im Alltag fragt ssh einfach danach. Richtig sehenswert wird es aber mit -v, denn da erzählt OpenSSH selbst, mit welchem Bauteil es gerade arbeitet.

debug1: provider /usr/lib/x86_64-linux-gnu/pkcs11/libtpm2_pkcs11.so: manufacturerID <tpm2-software.github.io> cryptokiVersion 2.40 libraryDescription <TPM2.0 Cryptoki> libraryVersion 1.9
debug1: provider /usr/lib/x86_64-linux-gnu/pkcs11/libtpm2_pkcs11.so slot 0: label <ssh> manufacturerID <Infineon> model <SLB9670> serial <0000000000000000> flags 0x40d
debug1: Will attempt key:  ECDSA SHA256:otJuTNX5LzFzJKqbTu9UoYROpZSXMMWIWMZUe02v1D4 token
debug1: Will attempt key: /root/.ssh/id_rsa
debug1: Will attempt key: /root/.ssh/id_ecdsa
debug1: Will attempt key: /root/.ssh/id_ed25519
debug1: Will attempt key: /root/.ssh/id_dsa
debug1: Offering public key:  ECDSA SHA256:otJuTNX5LzFzJKqbTu9UoYROpZSXMMWIWMZUe02v1D4 token
debug1: Server accepts key:  ECDSA SHA256:otJuTNX5LzFzJKqbTu9UoYROpZSXMMWIWMZUe02v1D4 token

Drei Sachen daran finde ich stark. Erstens nennt OpenSSH den Chip beim Namen, manufacturerID <Infineon> model <SLB9670>. Der SSH-Client sagt dir, welches Bauteil da gerade unterschreibt. Zweitens steht der TPM-Schlüssel in der Kandidatenliste mit dem Wort token, wo bei allen anderen ein Dateipfad steht, und diese Dateien existieren nicht einmal. Dieser Kontrast in einem Bildschirm ist das ganze Argument: dieser Schlüssel hat keine Datei. Drittens wird der Token-Schlüssel zuerst probiert, noch vor den nicht existierenden Dateien.

Ausgabe von ssh -v: OpenSSH nennt manufacturerID Infineon und model SLB9670, der Schlüssel steht als token in der Kandidatenliste neben Dateipfaden unter /root/.ssh
OpenSSH nennt Hersteller und Modell des Chips. Der Schlüssel steht als token in der Liste, alle anderen Kandidaten sind Dateien, die es nicht gibt.

Lasst mal kurz etwas heraus zoomen 😀 Meine Frau weist mich grade auf mein RBF hin. Wenn ich so konzentriert am Computer sitze und wie in diesem Fall etwas schreibe, dann schaue ich wohl meinen Monitor an, als wenn ich ihn gleich töten möchte. Tjo, und nun? Nun zoomen wir wieder ins Thema!

Die Zahl, die meine eigene Erwartung widerlegt hat

Meine Annahme vor der Messung war schlicht: eine ECDSA-Signatur kostet im Chip rund 97 Millisekunden, also merkt man beim Login nichts. Gemessen kommt etwas anderes heraus.

Weg5 Loginspro LoginAufschlag
Schlüssel auf der Platte, Referenzwert1,552 setwa 310 msReferenz
PKCS11Provider bei jedem Aufruf8,013 setwa 1603 msetwa 1293 ms
Token einmal im ssh-agent2,806 setwa 561 msetwa 251 ms

Die Ursache ist lehrreich. Bei PKCS11Provider baut ssh bei jedem einzelnen Login die komplette Kette neu auf: Store öffnen, Primary Key im TPM erzeugen, Kindschlüssel laden, signieren. Allein die Erzeugung des Primary Keys habe ich weiter oben mit 0,537 Sekunden gemessen. Die eigentliche Signatur ist damit der kleinste Posten in der Rechnung.

# ssh-add -s /usr/lib/x86_64-linux-gnu/pkcs11/libtpm2_pkcs11.so
Card added: /usr/lib/x86_64-linux-gnu/pkcs11/libtpm2_pkcs11.so
# ssh-add -l
256 SHA256:otJuTNX5LzFzJKqbTu9UoYROpZSXMMWIWMZUe02v1D4 /usr/lib/x86_64-linux-gnu/libtpm2_pkcs11.so.1.9.0 (ECDSA)
# time (for i in 1 2 3 4 5; do ssh root@localhost true; done)
real	0m2.806s

Mit ssh-add -s passiert diese Initialisierung einmal pro Sitzung statt einmal pro Verbindung, und die PIN wird auch nur einmal abgefragt. Zwei Gründe, dieselbe Empfehlung, also ganz klar: nehmt ssh-add -s und nicht PKCS11Provider bei jedem Aufruf.

Zur Messhygiene, wie immer: fünf Logins pro Zeile über Loopback, jeweils mit vollem Verbindungsaufbau und Prozessstart, kleine Stichprobe, keine Streuung berechnet. Das sind Größenordnungen und keine Benchmarks.

Der Nebeneffekt, den niemand erwähnt

Diesen Punkt habe ich selbst erst beim Aufräumen gemerkt, und jeder, der dem SSH-Abschnitt folgt, läuft hinein. tpm2_ptool init legt stillschweigend einen Primary Key an und macht ihn persistent im Chip. Vor der Demo waren drei persistente Handles belegt, danach vier.

# tpm2_getcap handles-persistent
- 0x81000000
- 0x81000001
- 0x81000002
- 0x81010001

Und jetzt zurück zu einer Zahl von weiter oben: TPM2_PT_HR_PERSISTENT_MIN ist auf diesem Chip 0x7. Garantiert sind also nur sieben persistente Plätze, und drei davon waren schon vorher weg. Wer bei jedem Basteln ein Handle liegen lässt, macht den Chip irgendwann voll. Den eigenen Müll findet und räumt man so weg:

# tpm2_evictcontrol -C o -c 0x81000000
persistent-handle: 0x81000000
action: evicted

# tpm2_getcap handles-persistent
- 0x81000001
- 0x81000002
- 0x81010001

Dazu eine Warnung, die ich nicht deutlich genug schreiben kann: räumt niemals Handles weg, die ihr nicht selbst angelegt habt. Die drei anderen lagen auf meinem Gerät schon vorher dort, vermutlich aus der Hersteller-Provisionierung oder aus einem früheren Windows-Leben. Wer einen fremden Primary Key entfernt, vernichtet alles, was darunter versiegelt war. Der sichere Weg führt über den Token-Store, denn dort steht das Handle drin, das zum eigenen Werkzeug gehört. Bei mir war es genau das 0x81000000 aus der Ausgabe oben, und ich habe vor dem Entfernen im Store nachgesehen, statt der Reihenfolge zu vertrauen.

Schlüssel im Chip, benutzt über OpenSSL

Für alles, was kein PKCS#11 spricht, gibt es den OpenSSL-3-Provider. Der schönste Moment daran ist ein PEM-Header.

# openssl list -providers -provider tpm2
Providers:
  tpm2
    name: TPM 2.0 Provider
    version: 1.2.0
    status: active

# openssl genpkey -provider tpm2 -algorithm EC -pkeyopt group:P-256 -out tpmkey.pem
# head -2 tpmkey.pem
-----BEGIN TSS2 PRIVATE KEY-----
MIHPBgZngQUKAQOgAwEBAQIEQAAAAQRYAFYAIwALAAYAcgAAABAAEAADABAAINLL

Diese Datei enthält keinen privaten Schlüssel. Sie enthält einen Blob, den ausschließlich dieser eine Chip auspacken kann. Kopiert die Datei auf eine andere Maschine und sie ist wertlos. Signieren geht im Chip, prüfen geht überall.

# printf "attestiere-mich" > d.bin
# openssl pkeyutl -provider tpm2 -provider default -sign -inkey tpmkey.pem -rawin -digest sha256 -in d.bin -out d.sig
# stat -c%s d.sig
70
# openssl pkey -provider tpm2 -provider default -in tpmkey.pem -pubout -out tpmkey.pub.pem
# openssl pkeyutl -verify -pubin -inkey tpmkey.pub.pem -rawin -digest sha256 -in d.bin -sigfile d.sig
Signature Verified Successfully

Zum Prüfen habe ich nur den Standard-Provider gebraucht. Es ist eine ganz normale ECDSA-Signatur, die Gegenseite muss von einem TPM überhaupt nichts wissen. Deshalb lässt sich das in bestehende PKI einbauen, ohne irgendetwas umzustellen, und damit gehen Client-Zertifikate für VPN, 802.1X, mTLS und auch AWS IAM Roles Anywhere.

Der Zufallsgenerator, ehrlich klein geredet

Auf diesem System ist der TPM tatsächlich der Hardware-Zufallsgenerator, den der Kernel benutzt.

# cat /sys/class/misc/hw_random/rng_available
tpm-rng-0 none
# cat /sys/class/misc/hw_random/rng_current
tpm-rng-0

Der Nutzen davon ist vorhanden, aber klein. Moderne CPUs haben RDRAND, der Kernel hat einen guten Entropiepool, und rund 67 Millisekunden pro tpm2_getrandom-Aufruf sind für nichts geeignet, was Durchsatz braucht. Als zusätzliche, unabhängige Quelle im Pool ist es geschenkt und in Ordnung. Mehr würde ich daraus nicht machen.

Fernattestierung, der eigentliche große Fall

Auf Servern liegt der größere Nutzen, nicht am Arbeitsplatz. Das Problem zuerst: bei einer Kiste im Rechenzentrum oder in der Colo weiß man normalerweise nicht, ob sie noch die Software fährt, die man dort installiert hat. Alles, was man fragen kann, ist der Server selbst. Und wenn er kompromittiert ist, lügt er.

Der TPM löst das, weil er eine Aussage über den Startvorgang signiert, die das Betriebssystem nicht fälschen kann. Zwei Schlüssel braucht es dafür: den Endorsement Key als Chip-Identität und einen Attestierungsschlüssel, mit dem tatsächlich signiert wird.

# tpm2_createek -c ek.ctx -G rsa -u ek.pub
# tpm2_createak -C ek.ctx -c ak.ctx -G rsa -g sha256 -s rsassa -u ak.pub -n ak.name
loaded-key:
  name: 000b76c23ef9e6c736e89085381db5c9303c6aca84dd4b18785cbcd5820666146650
  qualified name: 000bfce15794118492a81739a638c4745d23eeffda268ef1d016f22bc07c7a0b5e65

Der qualified name ist didaktisch schön, weil er die Abstammung des Schlüssels von seinem Elternschlüssel mit einbezieht. Jetzt das Quote, mit einer Nonce, die sich der Prüfer ausdenkt. Ich nehme deadbeef in Hex.

# tpm2_quote -c ak.ctx -l sha256:0,1,4,7 -q 6465616462656566 -m quote.msg -s quote.sig -o pcr.bin -g sha256
quoted: ff54434780180022000bfce15794118492a81739a638c4745d23eeffda268ef1d016f22bc07c7a0b5e650008646561646265656600000002acc0c6460000020500000000010007003f000d190000000001000b0393000000201a782856983a34bde88eb32ea706d6863119ee0309e3e8296f0021d34ec896d5
pcrs:
  sha256:
    0 : 0x7DE4ABE4ED8D8291D9B58D71F3B2988F5DA81EBE58D4AE518012E877FA1E04F1
    1 : 0x41CBF90AFF5CEEFAA9E9C52E7A3D5B4C8518EA65861C753AF8DB005C5330F53F
    4 : 0xCDB9366A1664E6B6AE408E3DF98F2E5FF47BCD6F0EA19ACBABE643DE586853DD
    7 : 0xF5841722D3DCC8886F14E92CA2D7304932CF17E2247A85D3B7BB41FE2C02CB4E
calcDigest: 1a782856983a34bde88eb32ea706d6863119ee0309e3e8296f0021d34ec896d5

Der quoted-Blob sieht wie Rauschen aus, ist aber lesbar, wenn man weiß wo man hinschaut.

  • ff544347 ist die Konstante TPM_GENERATED_VALUE und markiert die Struktur als im TPM entstanden. Wichtig für das Verständnis: sie allein verhindert keine Fälschung. Der Schutz entsteht daraus, dass ein Attestierungsschlüssel ein restricted Signaturschlüssel ist, der nur Daten unterschreibt, die der Chip selbst erzeugt hat. Diese Konstante ist das Erkennungsmerkmal dafür und nicht der Mechanismus.
  • 8018 ist TPM2_ST_ATTEST_QUOTE, der Strukturtyp.
  • 6465616462656566 ist meine Nonce, unverändert zurück. Genau das schlägt einen Replay.
  • 07003f000d1900 ist die Firmware-Version, die in jedem Quote mitläuft. Das ist wieder die 7.63 von oben.

Geprüft wird offline, und zwar mit nichts als dem öffentlichen Attestierungsschlüssel.

# tpm2_readpublic -c ak.ctx -o ak.pem -f pem
# tpm2_checkquote -u ak.pem -m quote.msg -s quote.sig -f pcr.bin -g sha256 -q 6465616462656566
pcrs:
  sha256:
    0 : 0x7DE4ABE4ED8D8291D9B58D71F3B2988F5DA81EBE58D4AE518012E877FA1E04F1
    1 : 0x41CBF90AFF5CEEFAA9E9C52E7A3D5B4C8518EA65861C753AF8DB005C5330F53F
    4 : 0xCDB9366A1664E6B6AE408E3DF98F2E5FF47BCD6F0EA19ACBABE643DE586853DD
    7 : 0xF5841722D3DCC8886F14E92CA2D7304932CF17E2247A85D3B7BB41FE2C02CB4E
exit=0
Ablaufdiagramm zwischen Verifier, Agent, TPM und der Infineon-CA, von der Nonce über das signierte Quote bis zur Entscheidung vertrauenswürdig oder Quarantäne
Der Ablauf einer Attestierung. Der Prüfer glaubt dem Knoten nichts, er rechnet nach.

Die Lücke, die fast jedes Howto verschweigt

Und hier muss ich mich selbst bremsen, denn die Versuchung ist groß, die beiden vorherigen Abschnitte einfach nebeneinanderzulegen und „echte Hardware bestätigt echten Bootzustand“ daraus zu machen. Das wäre eine Beweiskette mit einem Loch in der Mitte.

tpm2_checkquote mit dem öffentlichen Attestierungsschlüssel beweist genau eine Sache: irgendjemand, der diesen Schlüssel besitzt, hat dieses Quote signiert. Es beweist nicht, dass dieser Schlüssel in demselben Chip steckt, dessen Infineon-Zertifikat ich weiter oben gezeigt habe.

Die fehlende Verbindung heißt Credential Activation. Der Prüfer verschlüsselt dabei ein Geheimnis so, dass es nur ausgepackt werden kann, wenn Endorsement Key und Attestierungsschlüssel im selben TPM liegen, klassisch über TPM2_MakeCredential auf der Prüferseite und TPM2_ActivateCredential auf der Knotenseite. Erst wenn der Knoten das Geheimnis zurückmelden kann, ist bewiesen, dass der Schlüssel zu diesem zertifizierten Chip gehört. Alternativ stellt eine Attestierungs-CA ein echtes AK-Zertifikat aus, nachdem sie diese Prüfung einmal gemacht hat. Wie das im Detail aussieht, steht in RFC 9683. Diesen Schritt habe ich hier nicht durchgeführt. Gemessen sind Quote und Signaturprüfung, die Bindung ist dokumentiertes Verfahren.

Keylime, vier Rollen und ein Prüfer der nicht aufhört

Genau diesen Schritt macht Keylime, und genau deshalb ist Keylime mehr als ein Skript um tpm2_quote herum. Die Architektur hat vier Rollen: einen Agent auf dem Knoten, einen Registrar, einen Verifier und einen Tenant. Der Agent schickt Event-Log, IMA-Hashes und Measured-Boot-Daten, der lokale TPM bürgt für die Echtheit, und über das EK-Zertifikat lässt sich beweisen, dass es überhaupt ein echter TPM ist. Der Verifier prüft dann fortlaufend und nicht nur einmal beim Start.

Damit das nicht nach Bastelei klingt: Keylime läuft in der IBM Cloud als Teil der Compliance-Anforderungen für FedRAMP und HITRUST, und SUSE dokumentiert es offiziell für SUSE Linux Micro. Das ist kein Laborspielzeug.

IMA und EVM, zwei Dinge die ständig verwechselt werden

Beide gehören zur Laufzeit-Integrität, machen aber Verschiedenes.

  • IMA, die Integrity Measurement Architecture, hasht Dateien beim Zugriff. Im Messmodus schreibt sie die Hashes in ein Kernel-Log und erweitert damit PCR 10. Im Appraisal-Modus verweigert sie zusätzlich die Ausführung, wenn Hash oder Signatur nicht passen.
  • EVM schützt die zugehörigen erweiterten Attribute, also die Sicherheits-Metadaten der Datei, mit einem HMAC oder einer Signatur. Ohne EVM könnte ein Angreifer mit Schreibrechten die IMA-Attribute einfach mitfälschen.

Der TPM-Bezug liegt bei IMA und bei PCR 10. Eine veränderte Binärdatei erzeugt eine Messung, die im TPM landet und rückwirkend nicht mehr zu entfernen ist, weil PCRs nur wachsen. Zusammen mit Secure Boot und Measured Boot entsteht so eine Kette von der Firmware bis zur einzelnen ausgeführten Datei. Ohne TPM wäre dieses Logbuch fälschbar, mit TPM nicht.

Verschlüsselte Server, die allein neu starten können

Operativ ist das der handfesteste Nutzen im Rechenzentrum. Ein verschlüsselter Server, der nach einem Reboot auf eine Passphrase wartet, ist ein Betriebsproblem, spätestens um drei Uhr nachts. Mit TPM-Bindung startet er durch. Der Zielkonflikt dahinter ist eine echte Architekturentscheidung.

  • TPM allein. Der Server startet ohne Menschen. Wer den ganzen Server stiehlt, bekommt einen Server, der sich selbst entsperrt.
  • Tang und Clevis, netzgebunden. Entsperrt nur, wenn der Server einen Tang-Server im eigenen Netz erreicht. Aus dem Rack getragen bleibt er zu. Braucht dafür einen erreichbaren Tang-Server, was beim Kaltstart eines ganzen Rechenzentrums ein Henne-Ei-Problem ist.
  • Beides kombiniert über Shamir Secret Sharing in Clevis, also zum Beispiel TPM und Tang zusammen.

Meine Empfehlung, und die ist unspektakulär: im eigenen Rack mit eigenem Netz nehmt Tang plus TPM über Clevis, weil dann beide Bedingungen gleichzeitig gelten müssen. Bei einer einzelnen Kiste in fremder Colo nehmt TPM mit PIN und tippt die eben ein, wenn es tatsächlich einen Kaltstart gibt. Ein Server, der sich ohne jede Bedingung selbst entsperrt, ist Verschlüsselung fürs Protokoll und nicht gegen einen Angreifer.

Maschinenidentität, die man nicht in eine VM klonen kann

Ein Schlüssel der die Maschine nicht verlassen kann ist eine Identität, die man nicht mitkopieren kann. Für Zero-Trust-Architekturen ist das der interessante Teil: SPIRE hat dafür einen TPM-Node-Attestor, und für Kubernetes-Knoten, die sich beim Beitritt beweisen sollen, ist das der saubere Weg. Verglichen mit einem Token in einer Cloud-Init-Datei, das jeder mitlesen und in eine beliebige VM kopieren kann, ist das ein anderes Sicherheitsniveau.

Ein TPM für jede VM, und was er nicht leistet

Ein physischer TPM hat genau eine Instanz und lässt sich nicht sinnvoll auf viele Gäste aufteilen. Die Lösung ist ein virtueller TPM pro VM. swtpm stellt dafür eine TPM-2.0-Implementierung im Userspace bereit, QEMU und libvirt binden sie als Gerät in die VM ein. Der Gast sieht ein ganz normales /dev/tpm0 und braucht keine Anpassung. Der Kernel bringt dazu den vTPM-Proxy-Treiber mit, /dev/vtpmx, über den Container und VMs eigene TPM-Instanzen bekommen. Genau so machen es die Cloud-Anbieter, und genau so kommt Windows 11 in einer VM überhaupt an seine TPM-2.0-Voraussetzung.

Die ehrliche Einordnung gehört dazu, weil das in Cloud-Architekturen regelmäßig falsch verstanden wird. Ein vTPM ist Software. Sein Zustand liegt als Datei auf dem Hypervisor. Wer den Hypervisor kontrolliert, kontrolliert den vTPM, und ein Angreifer mit root auf dem Host kann den Schlüsselzustand kopieren. Ein vTPM löst also das Problem „der Gast braucht ein TPM-Interface“, er liefert aber keine Hardware-Vertrauenswurzel für den Gast. Wer echte Attestierung einer VM will, braucht eine Kette, die beim TPM des Hosts beginnt, oder Confidential-Computing-Technik wie AMD SEV-SNP oder Intel TDX.

Zum Schluss noch ein kleiner Anwendungsfall, der technisch nichts Neues ist und praktisch viel bringt: die Datenbank-Zugangsdaten eines Dienstes so versiegeln, dass sie nur auf dieser Maschine und nur in diesem Bootzustand aufgehen. Derselbe Seal-Mechanismus wie oben, nur mit einem anderen Geheimnis. Ein kopiertes Image oder eine in eine VM gehobene Platte bringt die Zugangsdaten dann nicht mit.

Was geht, aber nichts bringt

Dieser Abschnitt ist mir genauso wichtig wie der Nutzen-Teil, also kürze ich ihn nicht ab. Zuerst das Missverständnis, das sich am leichtesten messen lässt.

1. Der TPM als Krypto-Beschleuniger. Gemessen und erledigt.

OperationWoZeit
RSA-2048-Schlüssel erzeugenTPM19,703 s
RSA-2048-Schlüssel erzeugenCPU, openssl0,220 s
ECC-P-256-Primary erzeugenTPM0,537 s
ECDSA P-256 signieren, 50 malTPM4,862 s
ECDSA P-256 signieren, 50 malCPU, openssl0,286 s
# time tpm2_createprimary -C o -g sha256 -G rsa2048 -c rsa_primary.ctx
real	0m19.703s
user	0m0.039s
sys	0m0.016s

# time openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out /dev/null
real	0m0.220s
user	0m0.210s
sys	0m0.010s

Rund 90 mal langsamer bei der RSA-Schlüsselerzeugung. Ein Detail zur Messhygiene ist mir dabei wichtig: während der 19,7 Sekunden liegen user und sys bei 0,039 und 0,016 Sekunden. Die CPU langweilt sich also, die Zeit vergeht wirklich im Chip. Das ist eine Messung des TPM und nicht des Prozess-Overheads.

Die richtige Schlussfolgerung ist nicht, dass der TPM schlecht wäre, sondern dass er kein Beschleuniger ist und nie einer war. Das ist die interessante Kehrseite zu der Krypto-Beschleunigerkarte, die ich neulich gegen dieselbe Klasse moderner CPU gemessen habe. Dort verlor Spezial-Hardware, die schnell sein wollte. Hier verliert sie noch deutlicher, nur ist das kein Befund, sondern die Bauart. Der TPM wollte nie schnell sein, er wollte stur sein.

2. Massendaten im TPM verschlüsseln. Es gibt keinen Datenpfad dafür, und bei den Zahlen von oben erübrigt sich die Diskussion.

3. LUKS nur am TPM, ohne PIN. Technisch drei Minuten Arbeit. Ergebnis ist ein Notebook, das sich für jeden entsperrt, der den Deckel aufmacht. In Kombination mit dem Bus-Sniffing weiter unten ist das nahe an Sicherheitstheater. Wer die Passphrase-Eingabe loswerden will, nimmt die PIN.

4. Der TPM als Passwortspeicher oder Secret Store. Ein einzelner NV-Index kann auf diesem Chip höchstens 1664 Bytes halten, und es gibt genau 8 NV-Zähler. NV im TPM ist Flash mit begrenzten Schreibzyklen und als Ablage für Inhalte nicht gedacht. Der richtige Weg ist immer derselbe: die Geheimnisse liegen verschlüsselt auf der Platte, und nur der Schlüssel dafür wird im TPM versiegelt. Genau das tun systemd-cryptenroll und Clevis auch.

5. Sealing gegen PCR 0 bis 7 auf einem Gerät, das Firmware-Updates bekommt. Jedes BIOS-Update ändert die Messungen und sperrt euch aus. Ein Fall, in dem die Technik funktioniert und die Betriebspraxis daran zerbricht. Der Ausweg heißt signierte Policies und systemd-pcrlock, und im selben Atemzug gehört dazu, dass sich systemd-pcrlock selbst noch als experimentell bezeichnet.

6. Attestierung ohne unabhängigen Prüfer. Ein Quote, das auf derselben Maschine geprüft wird, beweist nichts. Wenn die Maschine übernommen ist, ist die Prüfung übernommen. Attestierung ergibt nur mit einem separaten Verifier und mit bekannten Referenzwerten Sinn. Sehr viele Aussagen der Art „wir nutzen TPM“ sind bei genauem Hinsehen genau das, eine Selbstprüfung.

7. TPM 1.2 im Jahr 2026. SHA-1 und RSA-2048, kein ECC, keine Algorithmen-Agilität, ein völlig anderer Software-Stack, nämlich TrouSerS statt tpm2-tss. Wer noch einen findet, sollte darauf nichts Neues bauen.

8. Die Erwartung, ein TPM schütze ein laufendes System. Das ist das größte Missverständnis, deshalb steht es hier noch einmal als eigener Punkt. Ein TPM schützt Schlüssel im Ruhezustand und misst den Start. Er tut nichts gegen einen Angreifer, der schon im Kernel sitzt, denn der fragt den Chip einfach ganz normal.

Tabelle mit fünf Vorhaben und der jeweiligen Antwort, von viel Krypto rechnen bis Geheimnisse ablegen
Fünf häufige Vorhaben und die kurze Antwort dazu.

Die Grenzen, und die Angriffe die es wirklich gibt

Ohne diesen Abschnitt wäre der Beitrag Werbung. Der Kern des Problems bei diskreten Chips ist unangenehm einfach: nach dem Entsiegeln gibt der TPM das Geheimnis über den Bus heraus, und klassisch liegt es dort im Klartext. Wer LPC oder SPI anzapft, liest den Schlüssel mit. Für BitLocker ist das seit Jahren praktisch demonstriert, und es ist ausdrücklich nicht auf Windows beschränkt. Securitum hat im September 2024 in einem echten Penetrationstest genau das gegen ein Linux-System mit LUKS und Clevis gezeigt.

Das betrifft mein Testsystem direkt, und ich schreibe das lieber offen hin, als es zu verstecken: der SLB 9670 ist ein diskreter Chip auf dem Board. Der Angriff braucht physischen Zugriff und Löterfahrung, aber er ist kein Papier-Szenario.

Die Gegenmaßnahme kann TPM 2.0 von sich aus: Sessions mit HMAC-Absicherung und Parameter-Verschlüsselung, und systemd benutzt das inzwischen. Die Grenze davon wird oft zu großzügig beschrieben, deshalb genau: das ist keine Vollverschlüsselung des Bus-Protokolls. Parameter Encryption schützt gezielt einzelne sensible Parameter in Kommandos und Antworten, also genau die Stelle, an der ein entsiegelter Schlüssel herauskäme. Kommandocodes, Handles und die übrigen Parameter bleiben sichtbar. Gegen das passive Mitlesen des Schlüssels ist es wirksam, gegen Verkehrsanalyse nicht, und gegen einen aktiven Interposer hilft es nur insofern, als die HMAC-Absicherung Manipulationen auffallen lässt. Der Kernel hat zu dieser Interposer-Frage eine eigene Dokumentationsseite. Und noch eine Einschränkung, die systemd selbst dokumentiert: die Art der Einbindung entwickelt sich weiter, alte Enrollments profitieren nicht rückwirkend. Wer vor Jahren enrolliert hat, sollte das neu machen.

Und dann eine Ironie, die zeigt wie dünn das Eis ist: CVE-2023-1017 saß genau in CryptParameterDecryption, also in dem Code, der diese verschlüsselten Parameter auspackt. Die Schutzfunktion war selbst der Angriffsweg.

Wer jetzt denkt, ein fTPM in der CPU sei automatisch besser, hat nur andere Probleme. faulTPM hat gezeigt, dass sich AMDs fTPM über Spannungs-Glitching am Platform Security Processor angreifen lässt. Und TPM-Fail demonstrierte 2019 Timing-Seitenkanäle, mit denen sich private ECDSA-Schlüssel rekonstruieren ließen, und zwar aus Intel PTT (CVE-2019-11090) und aus dem ST33 von STMicroelectronics (CVE-2019-16863). Zwei Details daran tragen den Punkt: der lokale Angriff auf Intels fTPM brauchte je nach Zugriffsrechten 4 bis 20 Minuten, und der ST33 trug eine Common-Criteria-Zertifizierung nach EAL 4+, die Schutz gegen Seitenkanäle ausdrücklich einschließt. Ein Zertifikat ist also kein Beweis.

Auch die Referenz-Implementierung selbst hatte Löcher. CVE-2023-1017 war ein Out-of-bounds-Write, CVE-2023-1018 ein Out-of-bounds-Read, beide in der TPM-2.0-Module-Library, also im Referenzcode der TCG. Betroffen waren dadurch gleichzeitig Software-TPMs wie libtpms und eine Reihe von Hardware-TPMs, deren Firmware auf diesem Code aufbaut. Gefunden hat sie Quarkslab und meldete sie im November 2022. Zwei Bytes über den Puffer hinaus, im ungünstigen Fall Codeausführung im TPM-Kontext, also genau in dem Bauteil, dessen einzige Aufgabe es ist, eine Vertrauensgrenze zu ziehen.

Was bleibt also unterm Strich: ein TPM hebt die Messlatte von „Platte ausbauen und kopieren“ auf „an das Board löten oder den SoC glitchen“. Das ist ein echter Fortschritt und kein magischer Schild. Mit PIN wird es deutlich stärker, weil dann die Lockout-Logik greift und Bus-Sniffing allein nicht mehr reicht.

Der Zustand dieses Testsystems, unbequem und aufschlussreich

Und jetzt der Teil, der mir selbst am meisten zu denken gibt. Der Chip in diesem Notebook arbeitet einwandfrei, wie alles hier oben zeigt. Benutzt wird er von diesem System für praktisch nichts.

BausteinZustand
Secure Bootaus, und als 00 in PCR 7 gemessen
Wurzel-DateisystemZFS-Native-Encryption, aes-256-gcm, nirgends LUKS
systemd-cryptenrollvorhanden, systemd 255.4, ohne LUKS2 aber nutzlos
clevisnicht installiert, würde hier auch nicht helfen
Unified Kernel Imagenicht im Einsatz
systemd-pcrextend.socketübersprungen, ConditionSecurity=measured-uki nicht erfüllt
systemd-pcrmachine.serviceübersprungen, dieselbe Bedingung
systemd-tpm2-setup-early.serviceübersprungen, dieselbe Bedingung
die elf systemd-pcrlock-*-Unitsalle disabled
TPM als /dev/hwrngaktiv, tpm-rng-0

Die drei übersprungenen Units sind der interessanteste Eintrag in dieser Tabelle. Die Bedingung ConditionSecurity=measured-uki prüft nicht das Dateiformat, sondern den Bootweg. Weil dieses System nicht als gemessenes Unified Kernel Image über systemd-stub gestartet ist, überspringt systemd die halbe TPM-Infrastruktur einfach lautlos, ohne Fehler und ohne Warnung. Das ist genau die These aus der Einleitung, jetzt belegt: der Chip ist da, er funktioniert, und die Distribution benutzt ihn nicht.

Der Event-Log, und die Falle mit ls

Zum Abschluss noch eine Kleinigkeit, über die ich zuerst gestolpert bin. Der Event-Log, dessen Adresse die Firmware ganz oben in der ersten dmesg-Zeile übergeben hat, liegt im securityfs. Und ls behauptet, die Datei sei leer.

# ls -la /sys/kernel/security/tpm0/
total 0
-r--r----- 1 root tss 0 Aug 10 09:30 binary_bios_measurements

# tpm2_eventlog /sys/kernel/security/tpm0/binary_bios_measurements | grep -c EventNum
120

Null Bytes laut ls, und trotzdem 120 Events darin. securityfs gibt für diese Datei einfach keine Größe an. Was in den 120 Einträgen steckt, sortiert sich so:

# tpm2_eventlog /sys/kernel/security/tpm0/binary_bios_measurements | grep EventType | sort | uniq -c | sort -rn
     81   EventType: EV_IPL
     12   EventType: EV_EFI_VARIABLE_BOOT
      8   EventType: EV_SEPARATOR
      5   EventType: EV_EFI_VARIABLE_DRIVER_CONFIG
      3   EventType: EV_EFI_ACTION
      2   EventType: EV_POST_CODE
      2   EventType: EV_EVENT_TAG
      2   EventType: EV_EFI_BOOT_SERVICES_APPLICATION
      1   EventType: EV_S_CRTM_VERSION
      1   EventType: EV_NO_ACTION
      1   EventType: EV_EFI_VARIABLE_AUTHORITY
      1   EventType: EV_EFI_HANDOFF_TABLES
      1   EventType: EV_EFI_GPT_EVENT

Der lehrreichste Eintrag im ganzen Log ist aber die Nummer 2, weil er ein abstraktes Konzept in vier Zeilen greifbar macht.

- EventNum: 2
  PCRIndex: 7
  EventType: EV_EFI_VARIABLE_DRIVER_CONFIG
  DigestCount: 2
  Digests:
  - AlgorithmId: sha1
    Digest: "57cd4dc19442475aa82743484f3b1caa88e142b8"
  - AlgorithmId: sha256
    Digest: "115aa827dbccfb44d216ad9ecfda56bdea620b860a94bed5b7a27bba1c4d02d8"
    UnicodeName: SecureBoot
    VariableData: "00"

Da steht der Secure-Boot-Zustand meiner Maschine als 00 im Log, und gehasht wandert genau dieser Wert in PCR 7. Das ist die Brücke zwischen dem Firmware-Setup und dem Chip: was ihr im BIOS umschaltet, wird gemessen und landet im TPM. Schaltet Secure Boot ein, und PCR 7 ändert sich. Alles, was gegen PCR 7 versiegelt war, geht danach nicht mehr auf. Wer den Zusammenhang einmal so gesehen hat, versteht auch, warum die Wahl der PCRs weiter oben ein Zielkonflikt ist und keine Geschmacksfrage.

Eine Nebenbemerkung, weil sie so schön zeigt, wie Secure-Boot-PKI in der Praxis funktioniert: im Log steckt auch der Platform Key des Boards, ein „Fujitsu ODM Quanta BIOS PK Certificate“, gültig von August 2012 bis August 2022. Es ist also seit Jahren abgelaufen. Die Firmware interessiert sich nicht dafür.

Was offen bleibt

  • Ob der SLB 9670 auf diesem Board über LPC oder über SPI angebunden ist. Aus dem Betriebssystem heraus nicht entscheidbar.
  • Warum der Login über PKCS11Provider rund 1,3 Sekunden Aufschlag kostet, ist plausibel erklärt, aber nicht einzeln nachgemessen. Ein strace oder ein Trace über tpm2_ptool würde es belegen.
  • systemd-cryptenroll mit LUKS2 konnte ich nicht testen, weil hier kein LUKS existiert.
  • Keylime habe ich nicht aufgesetzt. Die Beschreibung stützt sich auf Dokumentation von SUSE, Red Hat und dem Keylime-Projekt.
  • Die Bindung des Attestierungsschlüssels an den zertifizierten EK, also TPM2_MakeCredential und TPM2_ActivateCredential, habe ich nicht durchgeführt. Gemessen sind nur Quote und Signaturprüfung.
  • Wie viel NV-Speicher der SLB 9670 insgesamt hat, habe ich nicht ermittelt. NV_INDEX_MAX beantwortet diese Frage nicht.
  • Kein vTPM aufgesetzt. Der Abschnitt dazu ist dokumentiert und nicht gemessen.
  • Ob verschlüsselte TPM-Sessions Bus-Sniffing auf diesem konkreten Chip vollständig verhindern, habe ich nicht überprüft. Dafür bräuchte es einen Logic Analyzer.
  • Ein TPM-gestütztes Entsperren einer ZFS-Native-Encryption-Wurzel habe ich nicht gebaut.
  • Wie sich PCR 7 auf diesem Gerät nach dem Aktivieren von Secure Boot verändert. Das wäre ein naheliegender zweiter Teil.

Siehe auch

Wenn ich hier etwas falsch dargestellt habe, oder wenn ihr einen der offenen Punkte schon gelöst habt, dann korrigiert mich gerne. Dann lerne ich selbst etwas. Ihr dürft mich dazu jederzeit fragen.

Und weil mich das wirklich interessiert: setzt ihr den TPM-Chip selbst ein, ignoriert ihr ihn bisher, oder nutzt ihr ihn eher zufällig?

« Ältere Beiträge

© 2026 -=Kernel-Error=- — RSS

Theme von Anders Norén — Hoch ↑