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

Kategorie: Linux & BSD (Seite 1 von 9)

Anleitungen und Erfahrungsberichte rund um Linux-Distributionen und FreeBSD — vom Desktop bis zum Server.

Kein SMART für SD-Karten: sieben Linux-Werkzeuge, die trotzdem verraten, wie gesund eine Karte ist

Transcend-8-GB-microSD-Karte im SD-Adapter und USB-Kartenleser neben Linux-Werkzeugen zur Prüfung von Kapazität, Datenintegrität und Schreibleistung ohne SMART.

Eine alte 8-GB-microSD-Karte lag noch in der Schublade, entbehrlich genug für einen Härtetest. Die Ausgangsfrage war simpel: ist die Karte noch gut, oder produziert sie im Hintergrund längst stille Fehler? Bei einer SSD würde ich smartctl -a tippen und hätte binnen Sekunden Health-Prozent, Reallocated Sectors und Power-On-Hours auf dem Schirm. Bei einer SD-Karte geht genau das nicht, und der Grund dafür ist interessanter als die fehlende Zahl selbst.

Warum SMART hier nicht funktioniert

ATA- und NVMe-Laufwerke haben eine standardisierte SMART-Schnittstelle: fest definierte Attribute, die die Firmware selbst pflegt und die jedes Betriebssystem auf die gleiche Weise abfragen kann. SD-Karten haben nichts Vergleichbares. Der Flash-Controller auf der Karte übernimmt zwar Wear-Leveling und Bad-Block-Mapping, aber wie er das im Detail tut und was er darüber nach außen preisgibt, ist herstellerspezifisch und komplett verschlossen. Es gibt zwar eine SD Health Status Extension, CMD56-basiert, die unterstützt aber so gut wie kein Consumer-Werkzeug, und sie setzt einen nativen MMC-Host voraus, keinen USB-Kartenleser.

Ein Wort noch zu TRIM, bevor der Eindruck entsteht, SD-Karten hätten damit überhaupt nichts zu tun: der Linux-MMC-Stack unterstützt discard-artige Operationen für native mmcblk-Geräte durchaus. Nur bringt das über einen USB-Kartenleser nichts, weil die USB-Massenspeicher-Übersetzung diesen Pfad gar nicht durchreicht, das habe ich in diesem Test empirisch bestätigt. Und selbst auf einem nativen Host wäre das nur ein Erase-Hinweis an den Controller, keine standardisierte Health-Rückmeldung wie bei SSD-TRIM zusammen mit SMART.

Testkandidat und Aufbau

Für den Test kam eine Transcend Premium 8GB microSDHC zum Einsatz, Class 10, UHS-Speed-Class U1, in einem Samsung-Adapter auf volle SD-Größe gesteckt und über einen UGREEN-USB-Kartenleser ausgelesen. Der Leser meldet sich per USB als Genesys-Logic-Chip (05e3:0748), ein generischer Combo-Reader, wie er auch in vielen Laptops und günstigen USB-Dongles verbaut ist. Host war ein Linux Mint 22.3 mit Kernel 7.0.0-28-generic, Ubuntu-24.04-Unterbau, alle Befehle liefen als root.

MicroSD-Karte im Samsung-Adapter, gesteckt in einen schwarzen USB-Kartenleser auf einer Küchenarbeitsplatte
So kam die Karte beim Testsystem an: microSD im Samsung-Adapter, gesteckt in den UGREEN-USB-Kartenleser.

Werkzeuge installieren

Bis auf badblocks, smartctl, hdparm und dd, die auf den meisten Systemen ohnehin vorhanden sind, kommt der Rest aus den Standard-Paketquellen, keine Drittanbieter-PPA nötig:

apt-get install -y f3 mmc-utils sdparm flashbench
apt-get install -y fio   # nachinstalliert, sobald der Schritt feststand

Installiert wurden f3 8.0, mmc-utils als Git-Snapshot von 2022, sdparm 1.12, flashbench 62 und fio 3.36, alles aus dem Ubuntu-Noble-Universe-Repository. Getestet auf Linux Mint 22.3, aber auf einem reinen Ubuntu 24.04 sollten Paketnamen und Versionen identisch sein.

Schritt 1: Identifikation, warum SMART eine Sackgasse ist

$ smartctl -a /dev/sdc
/dev/sdc: Unknown USB bridge [0x05e3:0x0748 (0x1209)]
Please specify device type with the -d option.

$ smartctl -a -d sat /dev/sdc
Read Device Identity failed: scsi error unsupported scsi opcode
A mandatory SMART command failed: exiting.

$ smartctl --scan
/dev/nvme0 -d nvme
/dev/nvme1 -d nvme
# sdc taucht gar nicht erst auf, smartd hält es für keinen unterstützten Gerätetyp

$ sdparm -a /dev/sdc
MODE SENSE(10): Malformed SCSI command
    /dev/sdc: Generic   MassStorageClass  1209

$ sdparm -i /dev/sdc
    /dev/sdc: Generic   MassStorageClass  1209
Device identification VPD page:
  Addressed logical unit:
    designator type: T10 vendor identification,  code set: ASCII
      vendor id: Generic
      vendor specific: STORAGE DEVICE

smartctl erkennt die Genesys-Logic-USB-Bridge gar nicht erst als unterstützten Gerätetyp. Erzwingt man SAT, also ATA-Kommandos über SCSI getunnelt, scheitert das vollständig, weil die Bridge-Firmware das Kommando schlicht nicht implementiert. Selbst der Geräte-Scan von smartd listet /dev/sdc gar nicht auf. sdparm kommt einen Schritt weiter, eine simple SCSI-INQUIRY funktioniert, aber die gemeldete Identität bleibt vollständig generisch: „Generic MassStorageClass“, „STORAGE DEVICE“. Weder Hersteller-ID noch Seriennummer noch irgendeine Form von Wear- oder Health-Daten sind über diesen Pfad erreichbar. Das ist der konkrete, reproduzierbare Beleg dafür, dass es für SD-Karten hinter einem USB-Leser kein SMART-Äquivalent gibt.

$ lsusb -v -d 05e3:0748
Bus 002 Device 007: ID 05e3:0748 Genesys Logic, Inc. All-in-One Cardreader
  bcdUSB               3.10
  idVendor           0x05e3 Genesys Logic, Inc.
  idProduct          0x0748 All-in-One Cardreader
  iSerial                 5 000000001209
  bInterfaceClass         8 Mass Storage
  bInterfaceSubClass      6 SCSI
  bInterfaceProtocol     80 Bulk-Only
  SuperSpeed USB Device Capability:
    wSpeedsSupported   0x000e  (Full/High/SuperSpeed, bis zu 5Gbps Link)

Der Leser meldet sich als „All-in-One Cardreader“ von Genesys Logic, ein verbreiteter generischer Combo-Chip, USB-3.1-fähig, spricht auf der SCSI-Ebene aber Bulk-Only Transport (BOT), nicht UAS. Wichtig für die Kausalität: BOT statt UAS ist für sich genommen nicht der Grund, warum SAT-Passthrough scheitert. Es gibt genug BOT-Bridges, die SAT unterstützen, und genug, die es nicht tun, unabhängig vom Transportprotokoll. Belegt ist hier nur, dass ausgerechnet diese Genesys-Bridge keine ATA- beziehungsweise SAT-Kommandos durchreicht, nicht dass USB-Bulk-Only-Bridges das grundsätzlich nicht könnten.

Transcend Premium 8GB microSDHC Class 10 U1 im Samsung-Adapter, liegend auf einem UGREEN-USB-Kartenleser mit blau leuchtender Status-LED
Der Aufdruck verrät mehr als jedes Software-Tool: Transcend Premium, 8GB, Class 10, UHS-Speed-Class U1.

Schritt 2: Durchsatz mit dd und hdparm

hdparm -t allein ergab rund 90 MB/s gepuffertes Lesen, plausibel, aber es lohnt sich, mit echtem O_DIRECT-I/O gegenzuprüfen, damit keine Bridge- oder Seiten-Cache-Effekte den Wert verfälschen.

$ hdparm -Tt /dev/sdc
 Timing cached reads:   24182 MB in  2.00 seconds = 12113.48 MB/sec
 Timing buffered disk reads: 272 MB in  3.00 seconds =  90.66 MB/sec

# 512MB Zufallsnutzlast zuerst im tmpfs erzeugt, damit die CPU-Last von
# /dev/urandom die eigentliche Messung nicht verfälscht
$ dd if=/dev/urandom of=/root/sdtest/payload.bin bs=1M count=512
536870912 Bytes (537 MB, 512 MiB) kopiert, 1,74754 s, 307 MB/s

# SCHREIBEN: Direct I/O direkt auf das Rohgerät, kein Seiten-Cache beteiligt
$ dd if=/root/sdtest/payload.bin of=/dev/sdc bs=1M count=512 oflag=direct,sync
536870912 Bytes (537 MB, 512 MiB) kopiert, 22,8654 s, 23,5 MB/s

# LESEN: erst flushen, dann Direct I/O der gleichen 512MB zurück
$ blockdev --flushbufs /dev/sdc
$ dd if=/dev/sdc of=/root/sdtest/readback.bin bs=1M count=512 iflag=direct
536870912 Bytes (537 MB, 512 MiB) kopiert, 6,28156 s, 85,5 MB/s

# Integritätscheck
$ cmp /root/sdtest/payload.bin /root/sdtest/readback.bin
MATCH: identical

Ergebnis: rund 23,5 MB/s sequenzielles Schreiben, rund 85,5 MB/s sequenzielles Lesen, mit byteidentischem Rücklese-Ergebnis. Die Asymmetrie zwischen Schreiben und Lesen ist normal und erwartbar, Schreiben braucht auf Flash-Ebene ein Erase-before-Program, Lesen nicht. 23,5 MB/s reißt die Class-10- und U1-Mindestangabe von 10 MB/s locker, für U3/V30 mit 30 MB/s Minimum würde es nicht reichen, was exakt zum aufgedruckten Rating der Karte passt: Class 10, U1, keine U3- oder V30-Kennzeichnung.

Schritt 3: f3probe, ist die Kapazität echt?

Bevor der große, langsame Test kommt, lohnt sich der schnelle: f3 (Fight Flash Fraud) bringt mit f3probe einen Kapazitätsbetrugs-Check, der binnen weniger Minuten läuft, statt die ganze Karte zu beschreiben.

$ f3probe --time-ops /dev/sdc

Probe finished, recovering blocks... Done

Good news: The device `/dev/sdc' is the real thing

Device geometry:
	         *Usable* size: 7.31 GB (15333376 blocks)
	        Announced size: 7.31 GB (15333376 blocks)
	                Module: 8.00 GB (2^33 Bytes)
	Approximate cache size: 0.00 Byte (0 blocks), need-reset=no
	   Physical block size: 512.00 Byte (2^9 Bytes)

Probe time: 1'46"
 Operation: total time / count = avg time
      Read: 29.32s / 4197116 = 6us
     Write: 1'15" / 4192321 = 18us
     Reset: 0us / 1 = 0us

Verdikt: eine echte Karte, keine Fälschung. Die angekündigte Größe, 8-GB-Modul mit 7,31 GB nutzbar, deckt sich mit der tatsächlich nutzbaren Größe, die f3probe per binärer Suche gefunden hat, also dort, wo Schreibvorgänge aufhören, korrekt anzukommen. Approximate cache size: 0.00 Byte ist ebenfalls ein gutes Zeichen: manche gefälschten Karten täuschen ihre Kapazität vor, indem sie eine kleine Menge echten Flashs als Write-Back-Cache benutzen, der Schreibvorgänge über die tatsächliche Kapazität hinaus vorübergehend „schluckt“ und damit naive Benchmarks täuscht. Diese Karte zeigt diesen Trick nicht. f3probe hat die für die Prüfung benutzten Blöcke danach wiederhergestellt, ohne -n gestartet, die Karte blieb also in ihrem vorherigen, leeren Zustand. Reine Probe-Zeit rund 1:46 Minuten, mit Block-Wiederherstellung insgesamt rund 3 Minuten, deutlich schneller als ein vollständiger Schreib-Lese-Durchlauf, weil f3probe gezielt sucht statt jeden Block anzufassen.

Schritt 4: f3write und f3read, der Haupttest

$ wipefs -a /dev/sdc
$ parted -s /dev/sdc mklabel gpt mkpart primary ext4 0% 100%
$ mkfs.ext4 -F -L SDTEST /dev/sdc1
$ mount /dev/sdc1 /mnt/sdtest
$ df -h /mnt/sdtest
/dev/sdc1       7,2G     24K  6,8G    1% /mnt/sdtest

$ cd /mnt/sdtest && f3write .
Free space: 7.10 GB
Creating file 1.h2w ... OK!
...
Creating file 8.h2w ... OK!
Free space: 16.46 MB
Average writing speed: 22.78 MB/s

$ f3read .
                  SECTORS      ok/corrupted/changed/overwritten
Validating file 1.h2w ... 2097152/        0/      0/      0
...
Validating file 8.h2w ...  176128/        0/      0/      0

  Data OK: 7.08 GB (14856192 sectors)
Data LOST: 0.00 Byte (0 sectors)
	       Corrupted: 0.00 Byte (0 sectors)
	Slightly changed: 0.00 Byte (0 sectors)
	     Overwritten: 0.00 Byte (0 sectors)
Average reading speed: 90.32 MB/s

Verdikt: sauber. Jedes Byte jeder der 8 Testdateien, 7,08 GB insgesamt, bis auf rund 16 MB Restplatz gefüllt, kam exakt so zurück, wie es geschrieben wurde: 0 korrupte, 0 veränderte, 0 überschriebene Sektoren. Die gemessenen Geschwindigkeiten, 22,78 MB/s Schreiben und 90,32 MB/s Lesen, decken sich eng mit dem rohen dd-Direct-I/O-Benchmark aus Schritt 2, eine gute Gegenprobe, dass die Zahlen echt sind und kein Artefakt einer einzelnen Methode.

Schritt 5: badblocks, die Musterprüfung

$ umount /mnt/sdtest
$ badblocks -wsv /dev/sdc

Es wird nach defekten Blöcken gesucht (Lesen+Schreiben-Modus)
Von Block 0 bis 7666687
Es wird getestet Mit Muster 0xaa: ... 100% erledigt
Es wird getestet Mit Muster 0x55: ... 100% erledigt
Es wird getestet Mit Muster 0xff: ... 100% erledigt
Es wird getestet Mit Muster 0x00: ... 100% erledigt
Durchgang beendet, 0 defekte Blöcke gefunden. (0/0/0 Fehler)

Verdikt: 0 defekte Blöcke, über alle 4 Standard-Testmuster hinweg (0xAA/01010101, 0x55/10101010, 0xFF/lauter Einsen, 0x00/lauter Nullen, gewählt, um Stuck-at-0- und Stuck-at-1-Zellfehler ebenso wie Kopplungsfehler zwischen Nachbarbits zu erwischen). Gesamtlaufzeit für den kompletten Schreib-Lese-Vergleichs-Zyklus über alle 4 Muster: rund 26 Minuten 47 Sekunden auf dieser 7,31-GiB-Karte, was zur Schätzung von rund 27 Minuten aus den früheren Durchsatzwerten passt, 4-mal Schreibdurchlauf plus Lesedurchlauf bei rund 23,5/85 MB/s.

badblocks -w ist ein linearer, positionsbasierter Test, Teil von e2fsprogs. In einem Durchgang schreibt er dasselbe Muster auf jeden logischen Block und liest es zurück. Genau das ist der strukturelle Unterschied zu f3write/f3read, und der Grund, warum badblocks kein Ersatz für f3 bei der Kapazitätsbetrugsfrage ist: eine Karte, die Schreibvorgänge still auf einen kleineren physischen Bereich zurückführt, könnte in einem badblocks-Durchgang trotzdem das „richtige“ Muster zurückliefern, weil ohnehin jeder logische Block identischen Inhalt bekommt, das Aliasing bliebe unsichtbar. f3write vermeidet das, indem es positionsabhängige Pseudozufallsdaten schreibt, sodass Wraparound oder Aliasing als Mismatch auffällt. badblocks beantwortet dafür eine engere, ergänzende Frage: versagen bestimmte Blöcke oder Zellen dabei, irgendein Muster zuverlässig zu speichern, was der einmalige, pseudozufällige Schreibdurchgang von f3 pro Karte nicht so systematisch prüft, kein 0xAA/0x55/0xFF/0x00-Stuck-Bit-Sweep.

Schritt 6: flashbench, ein Blick unter die Haube

$ flashbench -f -o /root/sdtest/flashbench.out /dev/sdc
$ cat /root/sdtest/flashbench.out

4MiB    28.5M/s  28.3M/s  26.7M/s  27.6M/s  23.4M/s  28M/s
2MiB    26.6M/s  28.2M/s  26.7M/s  28.1M/s  26.8M/s  28.1M/s
1MiB    26.6M/s  28.8M/s  26.4M/s  28.4M/s  26.6M/s  27.3M/s
512KiB  26.7M/s  28.2M/s  26.9M/s  28.9M/s  26.8M/s  28.3M/s
256KiB  26.7M/s  28.2M/s  26.6M/s  27.8M/s  27M/s    28.8M/s
128KiB  26M/s    28.5M/s  26.7M/s  28.3M/s  26.8M/s  28.3M/s
64KiB   26.9M/s  28.8M/s  26.8M/s  28.3M/s  26.7M/s  27.9M/s
32KiB   12.6M/s  12.7M/s  13.1M/s  12.6M/s  12.9M/s  12.8M/s
16KiB   5.78M/s  5.91M/s  5.87M/s  5.82M/s  5.82M/s  5.86M/s

flashbench -f (find-fat) liest am Ende der ersten paar Erase-Blöcke zunehmend kleinere Häppchen und misst die Zeit dafür, auf der Suche nach dem Punkt, an dem die Lesegeschwindigkeit plötzlich einbricht. Die Grundidee: das deutet auf die interne Lese- beziehungsweise Page-Granularität des Flashs hin, weil das Lesen eines Teils einer internen Page oder eines Blocks genauso viel kosten kann wie das Lesen der ganzen Einheit, sub-granulare Reads verschwenden dann Bandbreite. Hier hält sich das Plateau, rund 26 bis 29 MB/s, passend zum rohen sequenziellen Lese-Benchmark, stabil bis hinunter zu 64 KiB, bricht dann bei 32 KiB auf weniger als die Hälfte ein (rund 12,6 bis 13,1 MB/s) und nochmal auf etwa ein Fünftel bei 16 KiB (rund 5,8 bis 5,9 MB/s). Ein sauberes, reproduzierbares Signal, 6 Wiederholungen pro Zeile, alle konsistent.

Die Schlussfolgerung bleibt bewusst vorsichtig formuliert: das ist ein Performance-Knick bei 64 KiB auf diesem konkreten Reader-Controller-Pfad, konsistent mit einer größeren internen Leseeinheit oder FTL-Gruppierung, aber keine direkte, verifizierte Messung der tatsächlichen physischen NAND-Page-Größe der Karte (typischerweise 8 bis 16 KiB, dafür bräuchte es Herstellerdokumentation oder eine andere Messmethode). Die Daten sind mit einer bestimmten Erklärung konsistent, sie beweisen sie nicht.

Schritt 7: fio, bleibt die Schreibrate konstant?

dd und f3write liefern Durchschnittswerte. fio mit einem Bandbreiten-Log pro Sekunde zeigt dagegen die Form des Schreibvorgangs über die vollen 6 GB, genau das will man sehen, wenn man einen Pseudo-SLC-Cache-Absturz sucht: viele Karten schreiben schnell in einen kleinen SLC-Modus-Puffer, bis der voll ist, und fallen danach auf eine deutlich niedrigere TLC- oder QLC-Dauerschreibrate ab.

$ fio --name=sdcard-write --filename=/dev/sdc --rw=write --bs=1M --size=6G --direct=1 --ioengine=psync --iodepth=1 --write_bw_log=/root/sdtest/fio_write --log_avg_msec=1000 --group_reporting

write: IOPS=26, BW=26.0MiB/s (27.3MB/s)(6144MiB/236179msec)
bw (KiB/s): min=24576, max=27675, per=100.00%, avg=26645.27, stdev=457.38, samples=236

Der Blick ins rohe Sekunden-Log lohnt sich, nicht nur die Zusammenfassung: 236 Einsekunden-Stichproben über den kompletten 6-GB-Schreibvorgang, alle im Bereich von 24576 bis 27675 KiB/s, also rund 24,0 bis 27,0 MB/s. Erste und letzte Stichprobe liegen im selben schmalen Band, kein erhöhter Burst am Anfang, kein Absturz später.

Verdikt: flach, kein erkennbarer SLC-Cache-Absturz auf dieser Karte. Auch hier lohnt sich die vorsichtige Formulierung: das beweist nicht, dass die Karte null Schreibpufferung hat, nur dass ein eventueller Puffer beziehungsweise Absturz innerhalb dieses 6-GB-Testfensters auf einer Karte mit nur rund 7,3 GB nutzbarer Gesamtkapazität nicht sichtbar wurde. Es könnte schlicht nicht genug Karte nach dem Puffer übrig sein, um einen Absturz zu zeigen, oder, wahrscheinlicher bei einer reinen Class-10/U1-Budgetkarte ohne A1/A2- oder „Extreme/Pro“-Einstufung, sie implementiert gar kein dynamisches SLC-Caching. So oder so steht das praktische Ergebnis: die Schreibleistung war über die gesamte Kapazität konsistent und vorhersagbar, nicht nach vorne verlagert.

Was für eMMC gilt, aber nicht für SD

Ein paar Dinge, die in diesem Testlauf nicht zum Einsatz kamen, aber der Vollständigkeit halber dazugehören. mmc extcsd read und seine Lebensdauer-Schätzfelder, das Nächste an einer echten Health-Prozentangabe in diesem Umfeld, sind eine eMMC-Eigenschaft. Eine wechselbare SD- oder microSD-Karte ist kein eMMC, dieser Pfad stand hier ohnehin nicht zur Verfügung, USB-Leser statt nativer MMC-Host. Ein nativer MMC-Host-Slot, etwa der eingebaute SD-Slot eines Laptops, würde dagegen mehr Identitätsdaten offenlegen als ein USB-Leser: /sys/block/mmcblkX/device/ mit cid, csd, scr, serial, manfid, name, oemid, preferred_erase_size und date, der Kernel parst die Identitätsregister der Karte direkt in sysfs, ganz ohne Zusatzwerkzeug. Wer also einen echten SD/MMC-Controller statt einer USB-SCSI-Bridge als Leser hat, bekommt das gratis dazu, in diesem Test war davon nichts erreichbar.

Und noch ein Missverständnis vorweg: der „SD Card Formatter“ der SD Association ist ein Formatierungs- und Reset-Werkzeug, das die werkseitige Partitionierung wiederherstellt, nachdem andere Betriebssysteme sie durcheinandergebracht haben, kein Health-Check-Werkzeug.

Die Reihenfolge, wenn du das nachmachen willst

Vom schnellsten und harmlosesten zum langsamsten und gründlichsten, ungefähr so, wie dieser Test auch abgelaufen ist:

  1. lsusb -v, udevadm info, dmesg. Leser und Karte identifizieren, Schreibschutz-Status vorab prüfen.
  2. smartctl -a -d sat und sdparm -a. Schneller, harmloser Versuch an Health- und Identitätsdaten, meist scheitert das über einen USB-Leser, und genau dieses Scheitern ist der Punkt, den man erklären sollte.
  3. f3probe. Schneller Kapazitätsbetrugs-Check, für 8 GB etwa 2 bis 3 Minuten, minimal destruktiv, stellt die benutzten Blöcke standardmäßig wieder her.
  4. f3write plus f3read. Der Haupttest, vollständige Schreib-Lese-Integritätsprüfung über die gesamte Kapazität, braucht ein Dateisystem.
  5. badblocks -w. Destruktive Vier-Muster-Prüfung auf defekte Blöcke, Rohgerät, kein Dateisystem nötig. Fängt Dinge, die f3 strukturell nicht kann, siehe Schritt 5.
  6. dd mit oflag/iflag=direct und/oder fio mit Bandbreiten-Log. Durchsatzzahlen und, mit fio, die Form der Schreibrate über die Zeit.
  7. flashbench -f -o DATEI. Optional, für die Neugier, Hinweise auf die interne Lese-Granularität.

Fazit

Für diese konkrete Karte: gesund, echt, keine Defekte gefunden, bei jedem Test, der überhaupt in der Lage gewesen wäre, welche zu finden. Keine SMART-Werte erreichbar über den USB-Leser, echte Kapazität ohne Cache-Trickserei, null Datenkorruption über 7,08 GB, null defekte Blöcke über alle 4 Muster, rund 23 bis 27 MB/s Schreiben und rund 85 bis 90 MB/s Lesen, konsistent über vier unabhängige Messmethoden, kein SLC-Cache-Absturz über die volle Kapazität. Die Karte reißt ihre aufgedruckte Class-10/U1-Angabe locker, für U3/V30 würde es nicht reichen, was sie auch gar nicht behauptet.

Der eigentliche Punkt reicht über diese eine Karte hinaus: ohne SMART bleibt nur Verhaltenstestung statt Register-Abfrage. Schreiben, lesen, vergleichen, und der Karte dabei zusehen, statt sie nach einer Zahl zu fragen, die sie gar nicht hat.

Siehe auch

Falls jemand einen zuverlässigeren Weg zur Health-Einschätzung einer SD-Karte unter Linux kennt, immer her damit. Und falls ihr selbst öfter mit fragwürdigen Billigkarten zu tun habt, dürft ihr mich zu dem Thema sehr gerne 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.

sipgate unter Linux: ein Softphone, das Kontakte und Kalender aus der eigenen Nextcloud kennt

Vor zwölf Jahren habe ich hier beschrieben, wie der Familienkalender meiner Frau in die ownCloud gewandert ist. Vorher gab es ein Büchlein in ihrer Tasche und einen großen Kalender an der Wand, und beide waren selten einer Meinung. Seitdem liegen Termine und Kontakte unter eigener Kontrolle, und alles Mögliche greift darauf zu. Nur ein Gerät hat es nie getan: das Telefon am Schreibtisch.

Zwei Karten nebeneinander: links ein Anruf als nackte Rufnummer ohne Adressbuch, rechts derselbe Anruf mit aufgelöstem Namen aus der eigenen Nextcloud.

Das hat mich lange nicht gestört, bis ich es einmal ausprobiert habe. Ein Anruf kommt rein, auf dem Bildschirm steht eine nackte Rufnummer, und ich sitze vor einem Rechner, auf dem 144 Kontakte mit genau dieser Nummer liegen. Das ist albern.

Mein sipgate-Konto ist alt. Wie alt genau, wusste ich selbst nicht mehr. In diesem Blog taucht sipgate erstmals im August 2009 auf, und 2015 habe ich beim Abschalten meiner alten 01801-Nummer geschrieben, ich hätte sie „vor ? 13 ? Jahren“ bekommen. Die Fragezeichen waren schon damals meine. Also habe ich einfach den Support gefragt, ausdrücklich nur aus Neugier. Die Antwort kam am nächsten Morgen: angemeldet am 30. April 2005. Da sipgate 2004 mit den Basis-Accounts gestartet ist, bin ich also aus dem ersten Jahr dabei, gut zwanzig Jahre. Dass sich jemand die Mühe macht, so eine reine Neugier-Frage überhaupt zu beantworten, finde ich bemerkenswert.

Was ich wollte, war jedenfalls nichts Exotisches: ein Softphone unter Linux, das an diesem Konto hängt und beim Klingeln in mein eigenes Adressbuch schaut statt in gar keines. Dazu am besten noch die Kalender, damit ich beim Telefonieren sehe, ob der Termin, über den gerade gesprochen wird, überhaupt frei ist.

Der Weg dahin war überraschend kurz, aber die naheliegenden Kandidaten führen alle in die Irre. Deshalb schreibe ich beides auf.

Warum der naheliegende Weg nicht funktioniert

Der erste Reflex ist, das Softphone des Anbieters zu nehmen. Bei sipgate steht dazu im eigenen Hilfecenter, dass es unter Linux nicht installiert, sondern nur ausgeführt wird, dass es kein 64 Bit kann, also weder auf x86_64 noch auf ARM läuft, und dass der Support in Kürze eingestellt wird und keine Updates mehr kommen. Das ist eine erfrischend ehrliche Ansage, hilft aber nicht weiter.

Der zweite Reflex heißt Linphone, und das ist die Falle, in der ich am längsten gesteckt habe. Linphone kann CardDAV wirklich, die Funktionen stecken vollständig in der Bibliothek liblinphone. Nur hat die Oberfläche der paketierten Version keinen einzigen Schalter dafür, dort gibt es bei den Adressbuchquellen ausschließlich LDAP. In die Oberfläche kam CardDAV erst mit Version 6.0, und die liegt in keinem einzigen Linux-Distributionsrepo. Ein Blick auf Repology zeigt für linphone-desktop gerade einmal AUR bei 6.1.2, alles andere hängt bei 4.x oder 5.x, Debian unstable bei 5.2.6. Der Hersteller selbst liefert für Linux ausschließlich AppImages aus, ohne veröffentlichte Prüfsumme und ohne eingebettete Update-Information. Wer sich nicht um Aktualisierungen kümmern will, ist damit an der falschen Adresse.

Blink fällt aus, weil es zwar ein ausgereifter SIP-Client ist, beim Adressbuch aber nur Google Contacts kennt. GNOME Calls wäre technisch ein Weg, ist aber für Telefone gebaut und als Desktop-Softphone dünn.

Nebenbei: die Frage wird gestellt. Im Nextcloud-Forum steht ein Thread vom August 2025 mit genau diesem Wunsch, und der Fragesteller verweist darin auf einen älteren Thread zum selben Thema, der ohne Ergebnis blieb. Die Antworten waren eine kommerzielle Lösung und ein Umweg über die FRITZ!Box. Was ich jetzt benutze, kam in keinem der beiden vor.

Die Kette, die tatsächlich funktioniert

Der Trick besteht darin, dass das Softphone gar nicht mit der Nextcloud sprechen muss. Unter Linux gibt es dafür längst eine Zwischenschicht, und die heißt evolution-data-server. Sie ist auf so gut wie jedem Desktop installiert, weil Kalender- und Kontaktanwendungen darauf aufsetzen. Und sie spricht CalDAV und CardDAV von Haus aus.

Nextcloud
  |
  +-- GNOME Online Accounts     Provider owncloud, CalDAV und CardDAV
       |
       +-- evolution-data-server
            |
            +-- Softphone (GOnnect, Adressbuchquelle "EDS")

Das Softphone ist damit austauschbar, und die Zugangsdaten der Cloud liegen genau an einer Stelle statt in jeder Anwendung noch einmal. Als Softphone nehme ich GOnnect von der GONICUS GmbH, einem Open-Source-UC-Client, den es als Flatpak auf Flathub gibt. Er kann als Adressbuchquellen LDAP, CardDAV, CSV und eben evolution-data-server.

Dass er über Flathub kommt, war für mich das Ausschlusskriterium gegen alles andere. Ein AppImage müsste ich von Hand nachziehen, ein Flatpak läuft mit flatpak update einfach mit.

Schritt 1: Die Nextcloud in die Online-Konten hängen

Unter Linux Mint findest du das unter „Online-Konten“, dahinter steckt gnome-online-accounts-gtk. Dort legst du ein Konto vom Typ Nextcloud an, trägst die Adresse deiner Instanz, deinen Benutzernamen und ein App-Passwort ein und hakst Kalender und Kontakte an.

Der Dialog Online-Konten unter Linux Mint mit einem eingerichteten Nextcloud-Konto, bei dem Kalender, Kontakte und Dateien eingeschaltet sind.
Ein Konto vom Typ Nextcloud in den Online-Konten, Kalender und Kontakte angehakt. Ab hier kennt der ganze Rechner die Daten, nicht nur ein Programm.

Nimm dafür wirklich ein App-Passwort aus den Sicherheitseinstellungen deiner Nextcloud und nicht dein normales. Falls du später den Zugriff eines einzelnen Rechners zurückziehen willst, geht das damit mit einem Klick, ohne dass du überall sonst neue Zugangsdaten eintragen musst.

Wenn es geklappt hat, sieht die Konfiguration hinterher so aus:

[Account account_1735125820_0]
Provider=owncloud
Uri=https://cloud.example.org/remote.php/webdav
CalendarEnabled=true
CalDavUri=https://cloud.example.org/remote.php/dav
ContactsEnabled=true
CardDavUri=https://cloud.example.org/remote.php/dav
FilesEnabled=true
AcceptSslErrors=false

Auf AcceptSslErrors=false lohnt ein Blick. Steht dort true, akzeptiert die Verbindung kaputte Zertifikate, und dann kannst du dir den ganzen Rest sparen.

Ab hier haben alle Programme auf dem Rechner Zugriff, die evolution-data-server nutzen. Das Softphone ist nur eines davon.

Schritt 2: GOnnect installieren

flatpak install flathub de.gonicus.gonnect

Das war der ganze Schritt. Rund 310 MB, und beim ersten Start liest GOnnect die Kontakte und Kalender aus evolution-data-server ein, ohne dass du irgendwo CardDAV konfigurieren müsstest. Bei mir standen im Protokoll direkt beim ersten Start:

gonnect.app.addressbook: Found 1 active configurations for address book plugin "EDS"
gonnect.app.feeder.EDSAddressBookFeeder: Loaded 144 contact(s) of source "Kontakte"

Ja, und die Kalender ebenfalls, inklusive der Familienkalender aus dem Beitrag von 2014. Die stehen dann rechts im Fenster, unter den Favoriten und neben der Anrufliste.

Das Hauptfenster von GOnnect mit der Anrufliste auf der linken Seite, den Favoriten rechts oben und den Terminen aus der Nextcloud rechts unten.
Links die Anrufliste, rechts die Favoriten und darunter die Termine aus dem Familienkalender. Namen, Nummern und Kontaktbilder sind unkenntlich gemacht.

Und die Suche oben im Fenster greift auf dasselbe Adressbuch zu. Ein paar Buchstaben genügen, dann steht der Treffer da, und darüber die Quelle, aus der er kommt: eds-contacts. Genau darum ging es. Kein Zwischenschritt, kein Export, keine zweite Kontaktverwaltung im Softphone.

Die Kontaktsuche in GOnnect zeigt einen gefundenen Kontakt, darüber steht die Quelle eds-contacts.
Ein paar Buchstaben genügen. Über dem Treffer steht die Quelle: eds-contacts, also das Adressbuch aus der eigenen Nextcloud.

Schritt 3: Das sipgate-Konto eintragen

Hier kommt die Eigenheit von GOnnect, an der man sich einmal stoßen muss: es gibt keinen Einrichtungsassistenten und keinen Einstellungsdialog für das SIP-Konto. Der Client ist dafür gebaut, in Firmen ausgerollt zu werden, und erwartet deshalb eine fertige Konfigurationsdatei. Die liegt hier:

~/.var/app/de.gonicus.gonnect/config/gonnect/99-user.conf

Unter der Haube arbeitet PJSIP, entsprechend sehen die Schlüssel aus. Das hier ist die vollständige Konfiguration, die bei mir mit einem sipgate-Basis-Konto läuft:

[account0]
userUri=sip:1234567e0@sipgate.de
registrarUri=sip:sipgate.de
proxies=sip:sip.sipgate.de:5061;transport=tls
auth=auth0
transport=tls
srtpUse=mandatory
srtpSecureSignaling=1
verifyServer=true
caListFile=/etc/ssl/certs/ca-certificates.crt
contactRewriteMethod=always-update

[auth0]
scheme=Digest
username=1234567e0
realm=sipgate.de
type=digest
data=HIER_DER_MD5_HASH

1234567e0 ist deine SIP-ID aus dem sipgate-Konto, nicht deine Rufnummer.

Wichtig ist die Zeile type=digest. Du kannst dort auch dein Passwort im Klartext hinterlegen, aber das musst du nicht. SIP authentifiziert sich per Digest, und der Hash dafür ist schlicht MD5(Benutzer:Realm:Passwort). Den rechnest du dir selbst aus:

printf '%s' '1234567e0:sipgate.de:DEIN_SIP_PASSWORT' | md5sum

Das Ergebnis kommt hinter data=. Danach setzt du die Datei noch auf chmod 600.

Damit steht dein Passwort nicht mehr wörtlich in einer Konfigurationsdatei. Ehrlich bleiben muss man trotzdem: der Hash ist für diesen Realm genauso viel wert wie das Passwort selbst, wer ihn hat, kann sich anmelden. Der Gewinn ist ein anderer. Falls du dieses Passwort irgendwo sonst auch verwendest, liegt es hier nicht lesbar herum.

Ein Detail, das mich beim Ändern der Datei erwischt hat: GOnnect muss dabei beendet sein. Der Client schreibt die Datei beim Beenden aus dem Speicher zurück und überschreibt deine Änderungen sonst kommentarlos.

Was der Desktop davon merkt

Zwei Dinge sind mir erst im Betrieb aufgefallen, und beide gehören zu der Sorte, die man nicht vermisst, solange man sie nicht kennt.

Wenn ein Anruf reinkommt oder du selbst einen startest, pausiert die laufende Medienwiedergabe von selbst. Das YouTube-Video im Firefox hält an, der Musikplayer ebenso, und nach dem Auflegen läuft beides weiter. Dahinter steckt MPRIS, die Schnittstelle, über die sich Medienplayer unter Linux fernsteuern lassen. Für mich war das der Moment, in dem sich das Ding nicht mehr nach Fremdkörper angefühlt hat, sondern nach Teil des Desktops.

Das zweite: sip:-Links auf Webseiten funktionieren. Ein Klick darauf öffnet die Anwendung und wählt sofort. Der Client trägt sich beim Installieren als Handler für dieses Schema ein, du musst dafür nichts konfigurieren.

Verschlüsselung, und warum eine Zeile davon die wichtigste ist

sipgate kann das seit über zwanzig Jahren. Heise hat am 1. Februar 2006 über „sipgate-Crypto“ berichtet, damals schon mit TLS für die Signalisierung und SRTP für die Sprache bis zum Festnetz-Gateway. Das Problem war seinerzeit, dass kaum ein Endgerät mitspielte, weshalb sipgate passende Hardware gleich mit anbot. Zwanzig Jahre später kann es jedes Gerät, und trotzdem steht es in kaum einer Anleitung.

Eingeschaltet ist es nämlich nicht von allein. Die Standardkonfiguration läuft über UDP und unverschlüsselt, du musst zwei Dinge selbst ändern: sip.sipgate.de als Proxy eintragen und die Signalisierung von UDP auf TLS umstellen.

Interessant ist, was danach passiert. In der sipgate-Dokumentation steht dieser Satz:

Bei verschlüsselter Signalisierung erfordern wir ebenfalls, dass die Sprachdaten verschlüsselt werden. Ist diese Einstellung falsch, so werden Anrufe mit Fehler „488 No Acceptable here“ abgewiesen.

Sobald die Signalisierung verschlüsselt ist, erzwingt sipgate also auch die Sprachverschlüsselung. Das ist die angenehmste Sorte Sicherheitsentscheidung, weil sie einem die Wahl abnimmt. srtpUse=mandatory ist damit nicht meine Strenge, sondern schlicht das, was die Gegenseite ohnehin verlangt. Ein optional würde dir nichts retten, der Anruf käme trotzdem nicht zustande, nur mit einer verwirrenderen Fehlermeldung.

Kleine Randbemerkung, weil es mich gefreut hat: gefunden habe ich diesen Satz nicht durch Klicken im Hilfecenter, sondern hier:

curl https://help.sipgate.de/llms-full.txt

Das ist das komplette Hilfecenter als eine Textdatei, rund 350 KB, gedacht als maschinenlesbare Fassung für Sprachmodelle. Zum Durchsuchen mit grep ist das erheblich angenehmer als jede Suchmaske, und es beantwortet Fragen, die im Menü drei Ebenen tief liegen. Ich habe so etwas seit einer Weile selbst auf diesem Blog liegen, und es ist schön zu sehen, dass es sich langsam herumspricht.

Die eigentlich wichtige Zeile ist eine andere, und die steht in keinem Beispiel, das ich gefunden habe:

verifyServer=true
caListFile=/etc/ssl/certs/ca-certificates.crt

Die Voreinstellung von PJSIP ist verifyServer=false. Übersetzt heißt das: die Verbindung wird verschlüsselt, aber es wird nicht geprüft, mit wem. Jedes beliebige Zertifikat wird angenommen. Gegen jemanden, der auf dem Weg sitzt und die Verbindung übernimmt, ist so eine Verschlüsselung wertlos, weil er einfach sein eigenes Zertifikat vorzeigt. Erst mit diesen zwei Zeilen wird geprüft, und zwar gegen den Zertifikatsspeicher deines Systems.

Nachmessen statt glauben

Dass die Registrierung mit code=200 klappt, sagt über die Verschlüsselung nichts. Also nachsehen. Der einfachste Test zeigt, wohin die Verbindung überhaupt geht:

ss -tnp | grep 5061
ESTAB  [2001:db8::23]:49705 -> [2001:ab7::18]:5061  users:(("gonnect",pid=48721))
ESTAB  [2001:db8::23]:38171 -> [2001:ab7::1a]:5061  users:(("gonnect",pid=48721))

Port 5061, und auf Port 5060 liegt nichts mehr, es läuft also kein Klartext-SIP nebenher. Meine eigene Adresse habe ich ersetzt, die Gegenstellen sind echt. Ein whois auf 2001:ab7::/36 nennt die netzquadrat GmbH, und sip.sipgate.de löst genau auf diese Adressen auf. Das Zertifikat dazu:

openssl s_client -connect sip.sipgate.de:5061 -servername sip.sipgate.de </dev/null | openssl x509 -noout -subject -issuer
subject=CN = sip.sipgate.de
issuer=C = US, O = DigiCert Inc, CN = GeoTrust TLS RSA CA G1

Wenn du schon dabei bist, kannst du auch gleich nachsehen, was die Gegenstelle überhaupt anbietet. Das geht wie bei jedem anderen TLS-Dienst:

nmap -Pn -p 5061 --script ssl-enum-ciphers sip.sipgate.de
TLSv1.2:
  TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (ecdh_x25519) - A
  TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (ecdh_x25519) - A
  ...
TLSv1.3:
  TLS_AKE_WITH_AES_256_GCM_SHA384 (ecdh_x25519) - A
  TLS_AKE_WITH_CHACHA20_POLY1305_SHA256 (ecdh_x25519) - A
  TLS_AKE_WITH_AES_128_GCM_SHA256 (ecdh_x25519) - A
cipher preference: server
least strength: A

Das Ergebnis ist erfreulich unaufgeregt. Es gibt nur TLS 1.2 und 1.3, die alten Versionen habe ich gegengeprüft und beide werden abgelehnt. Alles bewertet mit A, der Server bestimmt die Reihenfolge, und das Zertifikat ist RSA mit 4096 Bit. Ausgehandelt wird in der Praxis TLS 1.3 mit TLS_AES_256_GCM_SHA384 über X25519.

Ein Schönheitsfehler bleibt: unter TLS 1.2 stehen auch noch reine TLS_RSA_WITH_*-Suiten in der Liste, also solche ohne Forward Secrecy. Weil der Server die Reihenfolge vorgibt und ECDHE oben steht, bekommt sie in der Praxis niemand, sie sind der Rückfall für Uraltgeräte. Schöner wäre es trotzdem ohne.

Bleibt die Sprache. Während des Gesprächs zeigt GOnnect oben links ein grünes Schild, das ist die Verschlüsselungsanzeige.

Ein laufendes Gespräch in GOnnect, oben links ein grünes Schild als Anzeige für die Verschlüsselung.
Das grüne Schild oben links ist die Verschlüsselungsanzeige. Was dahinter wirklich passiert, steht erst im Protokoll.

Ein Symbol ist aber nur ein Symbol. Was dahinter passiert, protokolliert der Client erst, wenn du ihn darum bittest:

[logging]
level=6

Danach einmal telefonieren und ins Protokoll schauen. Was du sehen willst, sind zwei Dinge. Erstens die Aushandlung, in der beide Seiten RTP/SAVP sprechen statt RTP/AVP:

m=audio 4000 RTP/SAVP 96 97 98 99 3 0 8 9 100 102 103 101 104 105 106
a=crypto:1 AES_256_CM_HMAC_SHA1_80 inline:S3ybiZ92TCP4JuPe48V...
a=crypto:3 AES_CM_128_HMAC_SHA1_80 inline:r2RXvwQ31ewSR0xEvuZX...

Und zweitens die Bestätigung, welche der angebotenen Suiten die Gegenstelle genommen hat:

srtp0x...  SRTP started, keying=SDES, crypto=AES_256_CM_HMAC_SHA1_80
           SRTP status: Active  Crypto-suite: AES_256_CM_HMAC_SHA1_80

sipgate hat also die stärkste der vier angebotenen Varianten gewählt. Erst damit weiß ich, dass srtpUse=mandatory wirklich gegriffen hat und nicht still auf „aus“ stand. Nebenbei: mehr als diese vier hat der Client gar nicht im Angebot, alle mit AES im Counter-Modus und HMAC-SHA1. AES-GCM kennt diese PJSIP-Variante nicht, an der Stelle ist also nichts zu optimieren.

Und dann dreh den Loglevel wieder runter. In den Zeilen oben stehen die SRTP-Schlüssel im Klartext, die haben in einer Protokolldatei nichts verloren.

Was dabei nicht verschlüsselt ist

Hier muss ich die gute Laune etwas bremsen, denn „alles verschlüsselt“ wäre falsch.

Verschlüsselt ist die Strecke zwischen deinem Rechner und sipgate. Das ist viel wert, es schützt gegen dein WLAN, gegen fremde Netze und gegen deinen Provider, und zwar sowohl den Gesprächsinhalt als auch die Information, wen du wann anrufst.

Es endet aber bei sipgate. Rufst du ein Mobiltelefon an, läuft der Anruf ab dort durch das normale Telefonnetz, und dort gibt es keine Verschlüsselung. Dazu kommt, dass SDES als Schlüsselaustausch bedeutet, dass die Schlüssel im Signalisierungsweg stehen, den sipgate liest. Der ist zwar durch TLS gegen Dritte geschützt, aber sipgate kennt deine Schlüssel und könnte mithören. Etwas anderes steht auch gar nicht zur Wahl: ZRTP und DTLS-SRTP kommen in der gesamten sipgate-Dokumentation kein einziges Mal vor.

Echte Ende-zu-Ende-Verschlüsselung bekommst du nur zwischen zwei Clients, die direkt miteinander eines von beidem sprechen. Mit einem Anbieter dazwischen ist das, was hier läuft, die Obergrenze. Das ist kein Grund, es zu lassen, man sollte es nur nicht mit etwas verwechseln, das es nicht ist.

Kleinkram, der auffällt

Zwei Meldungen im Protokoll haben mich kurz beunruhigt und sind harmlos. Failed to connect to pipewire instance erscheint, weil das Flatpak den PulseAudio-Socket hat und auf pipewire-pulse zurückfällt. Die Geräte werden danach sauber gesetzt, der Ton funktioniert. Die Fehler des JitsiConnector betreffen die Videokonferenz-Anbindung und nicht das Telefonieren.

Was tatsächlich nicht geht: globale Tastenkürzel. Cinnamon bringt das Portal org.freedesktop.portal.GlobalShortcuts nicht mit, Anrufe per Tastendruck annehmen fällt damit aus. Alles andere, auch das Symbol im Systemabschnitt der Kontrollleiste, funktioniert.

Fazit

Der Teil, den ich für den schwierigen hielt, war der einfachste. Ich musste am Softphone überhaupt kein CardDAV einrichten, weil evolution-data-server das längst erledigt hatte. Ein Konto in den Online-Konten, und die Kontakte und Kalender stehen jedem Programm auf dem Rechner zur Verfügung.

Aufwendig war stattdessen, überhaupt einen Client zu finden, der unter Linux gepflegt wird und den man ohne AppImage-Gefummel aktuell hält. Dass ausgerechnet ein Client, der eigentlich für den Rollout in Firmen gedacht ist und deshalb keinen Einrichtungsdialog hat, für einen einzelnen Basis-Account die beste Wahl ist, hätte ich vorher nicht erwartet.

Und der Familienkalender, der 2014 in die ownCloud gewandert ist, steht jetzt neben dem Telefon. Nur zwölf Jahre später.

Siehe auch:

Falls ihr das nachbaut und irgendwo hängen bleibt, oder falls ihr einen besseren Weg kennt, dann dürft ihr 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?

Intel QuickAssist 8950-SCCP: Krypto-Beschleuniger von 2013 gegen eine CPU von heute

Intel-QuickAssist-8950-SCCP-Krypto-Beschleuniger neben einer modernen Server-CPU im technischen Leistungsvergleich.

Manche Bauteile überleben ihren eigenen Sinn. Diese Karte liegt seit Jahren bei mir herum, hat einmal einen echten Job gemacht, und heute ist sie ein Stück Technikgeschichte mit Kühlkörper. Also habe ich sie in meine Workstation gesteckt, einfach um zu sehen, was sie noch kann und wie sie sich gegen eine aktuelle CPU schlägt. Das Ergebnis ist interessanter geworden als erwartet, aber nicht auf die Art, die ich erwartet hatte.

Die Vorgeschichte: irgendwann lief bei mir ein FreeBSD-Server mit einer Intel-Atom-CPU, und diese CPU hatte kein AES-NI. Auf so einer Maschine verschlüsselte Platten zu betreiben ist zäh. Also kam eine Intel QuickAssist Adapter 8950-SCCP rein, eine PCIe-Steckkarte, die genau das in Hardware macht. Und sie hat ihren Job gut gemacht: weniger CPU-Last, mehr Durchsatz am Storage. Später wanderte sie in einen älteren Xeon, immer noch FreeBSD, immer noch GELI. FreeBSD 14 hat sie produktiv nie gesehen.

Intel QuickAssist Adapter 8950-SCCP mit montiertem passiven Kühlkörper, Low-Profile-Karte mit PCIe-Steckkontakten und Slotblech
So hing sie im Server: Low Profile, PCIe x8, ein gerippter Aluklotz über fast der ganzen Platine. Lüfter gibt es keinen, den soll das Rack liefern.

Warum das damals überhaupt ein Problem war

Das ist der Teil, den heute fast keiner mehr auf dem Schirm hat: AES in der CPU war jahrelang keine Selbstverständlichkeit. AES-NI kam 2010 mit Westmere, aber Intel hat den Befehlssatz danach als Segmentierungsmerkmal benutzt. Bei vielen Atom-, Celeron- und Pentium-Modellen fehlte er, und zwar genau in den stromsparenden Storage- und Firewall-Plattformen, in denen man ihn am dringendsten gebraucht hätte. Ohne AES-NI rechnet eine CPU AES über Tabellen-Lookups, also grob eine Größenordnung langsamer, und obendrein mit Cache-Timing-Seitenkanälen, die man erst mit Aufwand wieder zubekommt.

Bei ARM war es lange genauso, und da ist es sogar noch besser zu greifen. ARMv7 hatte überhaupt keine AES-Befehle. Die Cryptography Extensions von ARMv8-A sind bis heute optional, ein Chiphersteller kann sie einfach weglassen. Der Raspberry Pi ist das schönste Beispiel: alle 64-Bit-fähigen Pis bis einschließlich Modell 4 haben keine AES-Beschleunigung, erst der BCM2712 im Pi 5 bringt sie mit. Wer sich mal gefragt hat, warum verschlüsselte Backups auf einem Pi 4 so quälend langsam sind: das ist der Grund, und es ist kein Softwareproblem.

Dazu kommt der zweite Teil der Geschichte. Verschlüsselung war lange die Ausnahme, nicht die Regel. Wer 2010 eine Webseite betrieben hat, hat HTTPS für den Loginbereich angeschaltet und sonst nirgends, weil es teuer war, weil Zertifikate Geld kosteten und weil TLS auf schwacher Hardware wirklich weh tat. Erst mit Snowden und vor allem mit dem Druck, den die Browserhersteller danach aufgebaut haben, kippte das ins Gegenteil. Plötzlich sollte alles verschlüsselt sein, und zwar sofort. In dieser Phase steckten in Loadbalancern und Reverse Proxies routinemäßig Offload-Karten, für Krypto oder für Kompression, weil die Allzweck-CPU dahinter das schlicht nicht mitgemacht hat. Diese Karte kommt aus genau dieser Zeit.

Was das für eine Karte ist

Auf dem Aufkleber steht INTEL(R) QUICK ASSIST ADAPTER 8950-SCCP, Bestellnummer IQA89501G1P5, Intel MM 929848, Herstelldatum 09/2017, Made in Malaysia. Der Chip darauf ist allerdings älter als die Karte: die Generation stammt aus Q4 2013.

Rückseite des Intel QuickAssist Adapter 8950-SCCP mit Typenschild, Bestellnummer IQA89501G1P5, Intel MM 929848 und Herstelldatum 09/2017
Die Rückseite mit dem Typenschild. Der blaue Aufkleber ist ein Echtheitsmerkmal von yottamark, dazu KCC-Zulassung, UL-Nummer und das PCI-Express-Logo.

SCCP steht für Single Coleto Creek PCIe. Coleto Creek ist der interne Codename, der Beschleunigerchip heißt DH895xCC, und Intel hat ihn als Intel Communications Chipset 8955 verkauft. Die PCI-ID ist 8086:0435. Bei Intel ist die Karte längst End of Life, abgekündigt zusammen mit ihrem Nachfolger 8960.

Die Datenblattwerte, also Herstellerangaben und nichts von mir Gemessenes:

AngabeWert
Bulk-Kryptobis 50 Gbit/s
Kompressionbis 24 Gbit/s
Host-InterfacePCIe Gen3 x8
SR-IOV1 Physical Function, 32 Virtual Functions
Leistungsaufnahmemaximal etwa 40 W
Umgebungstemperatur0 bis 55 Grad Celsius

Gebaut wurde das Ding nicht für Workstations, sondern für Telco und Netzwerk: VPN-Konzentratoren, IPsec-Gateways, TLS-Terminierung, Deduplizierungs-Appliances. 50 Gbit/s Bulk-Krypto in einer Low-Profile-Karte war 2013 eine Ansage. Mein Lieblingsdetail aus dem Featureset ist aber ein anderes: die Karte kann Kasumi und Snow 3G in Hardware, also die Mobilfunkchiffren aus 3G und LTE. Das ist so wunderbar spezifisch, dass man sofort weiß, in welchem Blech sie eigentlich stecken sollte, und das ist bestimmt kein Tower unter einem Schreibtisch.

Kühlkörper ab: zwei Chips, nicht einer

Jetzt wird es hübsch. Unter dem Kühlkörper sitzen nämlich nicht ein großer Chip, sondern zwei, und dazu eine Leerstelle.

Platine des Intel QuickAssist Adapter 8950-SCCP ohne Kühlkörper, links der nackte Coleto-Creek-Die, mittig der PLX PEX 8724 als PCIe-Switch, rechts eine unbestückte Lötfläche
Links U5, der Coleto Creek als Flip-Chip ohne Heatspreader. Mittig U1, der PLX PEX 8724. Rechts neben dem Slotblech die leere Fläche, um die es weiter unten geht.

Links auf Position U5 liegt der Coleto Creek als Flip-Chip-BGA ohne Heatspreader. Das nackte Silizium liegt frei, man schaut direkt auf den Die. Das sieht man heute selten und es ist genau der Grund, warum ich den Kühlkörper überhaupt abgeschraubt habe.

Mittig auf U1 sitzt aber ein Chip, den ich dort nicht erwartet hätte: ein PLX Technology PEX8724-CA80BC G, Fertigungscode 1703, also Kalenderwoche 3 in 2017, gefertigt in Taiwan. Das ist ein PCIe-Gen3-Switch mit 24 Lanes und 6 Ports. Auf einer Karte, die nur einen einzigen Beschleunigerchip trägt, ist ein Switch zunächst mal erklärungsbedürftig.

Rechts, direkt neben dem Slotblech, liegt dann noch eine komplett unbestückte BGA-Fläche. Ein vollständiges, leeres Lötpad-Raster in Chipgröße. Da war offensichtlich mal etwas geplant.

Nahaufnahme der unbestückten BGA-Lötfläche neben dem Slotblech des Intel QuickAssist Adapter 8950-SCCP, ein komplettes leeres Pad-Raster in Chipgröße
Der leere Platz aus der Nähe. Ein Platz in Chipgröße, alles drumherum bestückt, nur hier fehlt der Chip.

Wenn man Switch und Leerstelle zusammennimmt, geht eine Rechnung verdächtig glatt auf:

PEX 8724 = 24 Lanes

  x8  Upstream zum Slot
+ x8  zum bestückten Coleto Creek (U5)
+ x8  zu einem weiteren, unbestückten Platz
= 24

Und der Kernel liefert dazu tatsächlich einen passenden Befund: der Downstream-Port c4:08.0 des Switches existiert, hat LnkCap Width x8 und ist untrainiert, also LnkSta Width x0. Da hängt nichts dran.

Und hier muss ich mich selbst bremsen, denn die Versuchung ist groß. Belegt ist nur: in der Switch-Konfiguration sind diesem Port acht Lanes zugewiesen, und es hängt nichts daran. Nicht belegt ist, dass diese acht Lanes zu dem leeren Platz führen. Das könnte genauso ein nie herausgeführter Port sein oder für etwas völlig anderes gedacht gewesen sein. Die Lötfläche macht die Deutung attraktiv, und mehr ist es nicht. Wer es wirklich wissen will, müsste die Lanes am Board durchmessen oder das EEPROM des PEX8724 auslesen und dort in die Port Configuration schauen. Beides habe ich nicht gemacht.

Genauso vorsichtig muss man mit der naheliegenden Erzählung sein, Intel habe hier dieselbe Platine für eine Dual-Chip-Variante vorgesehen. Das passt zur Namenslogik, das S in SCCP steht ja für Single, und es passt zum leeren Platz. Eine offizielle Bestätigung für ein 8950-DCCP oder Ähnliches habe ich trotzdem nicht gefunden. Also: plausible Deutung, kein Faktum.

Am Rand der Platine sitzt außerdem noch ein schwarzer, geschirmter Steckverbinder mit der Bezeichnung J8. Der macht das, was bei einer Grafikkarte der Zusatzstecker macht: Stromversorgung direkt vom Netzteil. Das passt zu den bis zu 40 Watt aus dem Datenblatt, denn ein PCIe-Steckplatz dieser Bauform liefert nach Spezifikation nur 25 Watt. Dazu mehrere Spannungsregler mit Speicherdrosseln, ein 100-MHz-Quarz und Siebdruck-Markierungen für x1, x4 und x8 am Kartenrand.

Einbau: erfreulich langweilig

Die Karte steckt jetzt in einem Slot mit PCIe 4.0 x8 an einem Xeon Gold 5315Y, unter Linux Mint mit Kernel 7.0. Und dann passiert genau das, was man sich von einem In-Kernel-Treiber wünscht: nichts Besonderes.

[   36.915329] dh895xcc 0000:c5:00.0: enabling device (0140 -> 0142)
[   37.744772] dh895xcc 0000:c5:00.0: qat_dev0 started 12 acceleration engines

Kein Paket installiert, keine Firmware nachgeladen, keine Konfigurationsdatei, kein adf_ctl. Die Module qat_dh895xcc und intel_qat laden von allein, die Firmware qat_895xcc.bin.zst lag über linux-firmware längst auf der Platte. Zwölf Acceleration Engines starten, alle Selftests bestehen, der Heartbeat meldet sich gesund. Ein Chip von 2013 auf einer Karte von 2017 in einem Board von 2021 unter einem Kernel von 2026, und es ist ein Nichtereignis. Das ist ein starkes Argument für In-Tree-Treiber, und es wird weiter unten noch bitter kontrastiert.

Was man zum Prüfen braucht:

lspci -nn | grep -i qat
dmesg | grep -i dh895
grep -c qat /proc/crypto
ls /sys/kernel/debug/qat_dh895xcc_0000:c5:00.0/

Das debugfs-Verzeichnis ist die interessanteste Fundstelle der ganzen Karte. Dort liegt in dev_cfg die komplette, vom Treiber selbst generierte Instanzkonfiguration: 16 Krypto-Instanzen, 16 Kompressions-Instanzen, je Instanz 512 gleichzeitige symmetrische oder 128 asymmetrische Requests, dazu Interrupt-Coalescing und ein Heartbeat-Timer. In fw_counters stehen Requests und Responses pro Acceleration Engine, und das ist später mein Beweis, dass die Karte wirklich rechnet und nicht die CPU heimlich einspringt. Dazu heartbeat/status, cnv_errors und 32 Ring-Banks unter transport/.

Ein PCIe-Switch, der Generationen übersetzt

Jetzt löst sich auch auf, warum der PLX-Switch da ist. So sieht der Baum aus:

c2:02.0  Root Port #17          LnkCap Gen4 x8  ->  LnkSta Gen3 x8
  c3:00.0  PLX PEX 8724 Upstream                 ->  LnkSta Gen3 x8
    c4:00.0  Downstream Port #0                  ->  LnkSta Gen2 x8
      c5:00.0  Intel DH895XCC Series QAT   [8086:0435]
    c4:08.0  Downstream Port #8                  ->  LnkSta x0   (leer)

Host-seitig läuft die Karte mit Gen3 x8, also 8 GT/s. Chip-seitig kommen aber nur Gen2 x8 an, also 5 GT/s. Der Coleto Creek ist ein Gen2-Baustein, und damit die Karte am Steckplatz trotzdem als moderne Gen3-x8-Karte auftritt, sitzt der PEX 8724 als Übersetzer dazwischen. Der QAT-Endpunkt behauptet in seiner LnkCap sogar x16, verdrahtet sind aber acht Lanes.

Das ist der erste wirklich belastbare Befund des Tages: das Nadelöhr sitzt auf der Karte, nicht im Steckplatz und nicht in der CPU. Dafür braucht man die Dual-Chip-Spekulation von oben überhaupt nicht.

Zwei weitere Kleinigkeiten aus dem laufenden System, die ich hübsch finde. Die Karte sitzt allein in ihrer IOMMU-Gruppe und unterstützt Function Level Reset, sie lässt sich also ohne ACS-Override-Gebastel per VFIO an eine VM durchreichen. Und sie meldet 32 Virtual Functions, aktiv sind davon null.

Der praktisch wichtigste Nebeneffekt des Einbaus hat aber gar nichts mit Krypto zu tun. Der PLX-Switch belegt vier Busnummern, und dadurch ist meine NVMe von c3:00.0 auf c7:00.0 gewandert. Auf der alten Adresse sitzt jetzt der Upstream-Port des Switches. Ich hatte mir ein setpci-Kommando mit der alten Adresse notiert, und das hätte nach dem Einbau munter in die Register des Switches geschrieben statt in die der SSD. Merke: PCI-Adressen sind nichts, was man sich aufschreibt. Die holt man sich jedes Mal frisch aus lspci. Ist ja jetzt nicht so, als wenn mir das schon mal untergekommen ist bzw. ich so was gefettfingert habe 😛

Die Prioritäten-Tabelle, oder: der Kernel hat schon entschieden

Der Treiber registriert zehn Algorithmen in der Crypto-API des Kernels, alle mit selftest: passed:

xts(aes)                        qat_aes_xts               skcipher   prio=100
ctr(aes)                        qat_aes_ctr               skcipher   prio=100
cbc(aes)                        qat_aes_cbc               skcipher   prio=100
authenc(hmac(sha1),cbc(aes))    qat_aes_cbc_hmac_sha1     aead       prio=100
authenc(hmac(sha256),cbc(aes))  qat_aes_cbc_hmac_sha256   aead       prio=100
authenc(hmac(sha512),cbc(aes))  qat_aes_cbc_hmac_sha512   aead       prio=100
rsa                             qat-rsa                   akcipher   prio=1000
dh                              qat-dh                    kpp        prio=1000
pkcs1(rsa,sha512)               pkcs1(qat-rsa,sha512)     sig        prio=1000
deflate                         qat_deflate               acomp      prio=4001

Diese Prioritäten sind die halbe Geschichte des Beitrags. Die Linux Crypto API wählt bei gleichem Algorithmusnamen immer die Implementierung mit der höchsten Priorität. Wenn man das mit den CPU-Implementierungen auf derselben Maschine vergleicht, wird es sehr deutlich:

AlgorithmusQATbester CPU-Wertwer gewinnt
xts(aes)100xts-aes-vaes-avx2 = 600CPU
cbc(aes)100cbc-aes-aesni höherCPU
authenc(...)100CPU-Kombis höherCPU
rsa1000rsa-generic = 100Karte
dh1000dh-generic = 100Karte
deflate4001deflate-generic = 0Karte

Übersetzt heißt das: für symmetrische Massenverschlüsselung wird diese Karte nie von allein benutzt. Für rsa, dh und deflate dagegen immer. Das ist kein Zufall und kein Konfigurationsfehler, sondern eine Aussage der Kernel-Entwickler darüber, wo Offload bei einer aktuellen CPU überhaupt noch etwas bringt. Man kann diese Tabelle lesen wie ein Urteil über die Karte, und genau so ist sie auch gemeint.

Ein Detail nur mit Vorsicht: pkcs1(qat-rsa,sha512) steht mit Priorität 1000 registriert und hat den Selftest bestanden. Daraus folgt noch nicht, dass Kernel-Modul-Signaturprüfung tatsächlich über die Karte läuft. Die Registrierung macht es möglich, die Priorität spricht dafür, aber um es zu belegen müsste man den Verifikationspfad tracen und dabei die Zähler in fw_counters beobachten. Habe ich nicht gemacht, also behaupte ich es auch nicht.

Bevor die Zahlen kommen: was meine Messung nicht zeigt

Diesen Absatz gibt es, weil die Tabellen danach sonst präziser wirken als sie sind. Wer mir eine Zahl vorhält, soll wissen, wie sie entstanden ist.

  • Alle Kryptomessungen laufen über AF_ALG aus einem Python-Skript mit einem synchronen Request-Loop. Das ist ungefähr der Worst Case für eine asynchrone Offload-Karte: pro Request wird gewartet, statt die Queue tief zu halten, für die die Hardware gebaut wurde. Die Zahlen charakterisieren also diesen Zugangsweg, nicht die Karte an sich.
  • Kein Warmup, keine Wiederholungen, keine Streuung, kein CPU-Pinning. Angaben mit einer Nachkommastelle tragen eine Genauigkeit, die statistisch nicht gedeckt ist. Für belastbare Latenzen bräuchte es Median und P95 über mehrere Läufe.
  • Die Skalierung über Worker nutzt Prozesse, nicht Threads. Kontextwechsel und Speicherkopien gehen also mit in die Zahl ein.
  • cryptsetup benchmark ist selbst nur ein Indikator. Es misst im Speicher über die Crypto-API und ist laut eigener Manpage nicht direkt auf reale Storage-Verschlüsselung übertragbar.
  • Hart sind dagegen die fw_counters. Die kommen aus der Hardware und belegen, dass und wie oft die Karte gerechnet hat. Und die Form der Ergebnisse ist robust: Latenz pro Request hoch, Skalierung über Worker fast linear, CPU bei kleinen Blöcken weit vorn. Nur die absoluten Werte sind weich.

Als Gegner steht bewusst der ungünstigste Fall für die Karte: ein Xeon Gold 5315Y mit acht Kernen und vollem VAES-Befehlssatz. Wenn eine 13 Jahre alte Karte gegen die Rechenwerke einer aktuellen CPU antritt, dann bitte richtig.

Ein Strom, synchron: die Karte verliert deutlich

Zuerst die CPU-Basislinie mit cryptsetup benchmark, damit die Größenordnung steht:

aes-cbc   128b   1168,2 MiB/s enc   3936,0 MiB/s dec
aes-cbc   256b   1004,5 MiB/s enc   3729,8 MiB/s dec
aes-xts   256b   5494,4 MiB/s enc   5510,1 MiB/s dec
aes-xts   512b   5045,2 MiB/s enc   5053,6 MiB/s dec

Und dann der direkte Vergleich über AF_ALG, ein einzelner synchroner Strom:

ImplementierungBlockDurchsatzLatenz pro Operation
xts-aes-vaes-avx24 KiB1157,2 MiB/s3,4 µs
xts-aes-vaes-avx216 KiB2681,2 MiB/s5,8 µs
xts-aes-vaes-avx264 KiB4327,2 MiB/s14,4 µs
qat_aes_xts4 KiB100,3 MiB/s38,9 µs
qat_aes_xts16 KiB244,9 MiB/s63,6 µs
qat_aes_xts64 KiB308,8 MiB/s201,6 µs
cbc-aes-aesni4 KiB583,5 MiB/s6,7 µs
qat_aes_cbc4 KiB79,8 MiB/s48,9 µs

Pro Einzelrequest ist die Karte also grob elfmal langsamer als die CPU, ungefähr 39 gegen 3,4 Mikrosekunden bei 4 KiB. Für ein Bauteil, das mal genau dafür gekauft wurde, ist das eine ernüchternde Zahl.

Nur: das ist nicht die Hardwarelatenz der Karte. Gemessen ist die Latenz dieses Pfades, und der ist lang: Python, sendmsg über AF_ALG, Socket-Puffer, Kernel-Copy, Treiber, PCIe, Karte, und alles wieder zurück, plus Scheduling und Aufwecken des wartenden Prozesses. Ein erheblicher Teil davon kann im Socket-Pfad stecken. Die CPU-Variante läuft durch denselben Overhead, profitiert aber davon, dass sie synchron im selben Kontext rechnet und überhaupt nicht schlafen muss. Belastbar ist deshalb nur die relative Aussage für diesen Zugangsweg und die Form der Kurve. Wer die reine Hardwarelatenz will, braucht ein Frontend ohne Socket-Umweg, also praktisch den Userspace-Stack, und der existiert für diesen Chip nicht mehr. Dazu unten mehr. Das ist eine Messlücke, die ich mit den vorhandenen Mitteln nicht schließen kann, und ich schreibe sie lieber hin als sie zu verstecken.

Viele Ströme: jetzt zeigt sie, wofür sie gebaut wurde

Und dann wird es doch noch spannend. Dieselbe Messung mit parallelen Workern, Blockgröße 16 KiB:

Workerqat_aes_xtsxts-aes-vaes-avx2
1210,7 MiB/s2632,6 MiB/s
2454,1 MiB/s5311,9 MiB/s
4799,3 MiB/s10777,5 MiB/s
81714,5 MiB/s22056,0 MiB/s
162926,7 MiB/s25229,7 MiB/s

Die Karte skaliert von einem auf 16 Worker um fast Faktor 14, also nahezu linear, und bei 16 Workern war sie noch nicht am Ende. Das ist exakt das Verhalten, für das so ein Baustein entworfen wurde: viele gleichzeitige, unabhängige Operationen, keine einzelne schnelle. Eine Karte in einem Loadbalancer sieht nie einen Strom, sie sieht tausende.

Nur skaliert die CPU eben auch. Und sie landet am Ende 8,6 mal höher. Das ist der ganze Punkt dieses Beitrags in einer Zahl. So fertig, feierabend, einpacken!

Dass wirklich die Karte gerechnet hat und nicht heimlich doch die CPU, zeigen die Firmware-Zähler nach dem Lauf:

QAT firmware requests in diesem Lauf: 1.340.416

AE   Requests   Responses
 0:    154181     154181
 1:     94420      94420
 2:     90590      90590
 3:    154177     154177
 ...
11:     90662      90662

Alle zwölf Acceleration Engines haben gearbeitet, Requests und Responses stimmen überall überein, kein verlorener Request. Die Verteilung ist nicht gleichmäßig, sondern fällt in Dreiergruppen von etwa 154k, 94k und 90k. Das kommt von der Zuordnung Ring-Bank zu Engine und ist so ein Detail, an dem man beim Lesen kleben bleibt.

Wo der Deckel liegen könnte, und warum ich mich da nicht festlege

Die 2926,7 MiB/s liegen auffällig nah an der Kapazität des internen Links:

2926,7 MiB/s Nutzdaten = 3069 MB/s
Jedes Byte muss zweimal über den Bus: rein zum Verschlüsseln, raus als Chiffrat.
Gen2 x8 = 5 GT/s x 8 Lanes x 8/10 = 4000 MB/s pro Richtung
3069 / 4000 = 77 % der theoretischen Richtungskapazität

77 Prozent Nutzlast auf einem PCIe-Link sind praktisch der Anschlag, mit dem TLP-Overhead bei 256 Byte MaxPayload liegt das erreichbare Maximum bei etwa 85 bis 90 Prozent. Das sieht also sehr nach einem Bus-Limit aus.

Aber: meine Messung kann das nicht trennen. Sie zeigt, dass dieser Zugangsweg in der Nähe eines Ceilings landet, und sie kann nicht sagen, ob das der PCIe-Link, der Treiber oder AF_ALG ist. Die Rechnung oben ist ein Plausibilitätsargument, keine Messung des Busses. Sauber wäre es, PCIe-Bandwidth-Events über die Uncore-Zähler mitzuschreiben. Das ist der wichtigste offene Messpunkt in diesem Beitrag, weil eine der Hauptaussagen daran hängt.

Noch eine Rechnung, die verlockend glatt aufgeht und bei der man aufpassen muss. Die gemessenen 2926,7 MiB/s sind 24,6 Gbit/s, das Datenblatt sagt 50 Gbit/s, das sind fast genau 49 Prozent. Und verdrahtet sind acht von 16 Lanes, also genau die Hälfte. Der Kurzschluss liegt auf der Zunge, und er ist falsch: „mit x16 wäre man auf dem Datenblattwert“ und „die Platine war für zwei Chips gedacht“ sind zwei verschiedene Hypothesen, nicht eine. In einer Dual-Chip-Bestückung bekäme jeder Chip x8, keiner käme je auf x16, und das Nadelöhr wäre dann der gemeinsame Gen3-x8-Upstream. Rechnerisch landet man auf beiden Wegen bei ungefähr 49 Gbit/s, aber über völlig verschiedene Mechanismen. Wer das zusammenrührt, argumentiert falsch, auch wenn das Ergebnis stimmt. Die Karte selbst hat ja auch nur einen PCIe 8x Anschluss.

Und es gibt eine zweite Lesart, an der der ganze Vergleich mit dem Marketing hängt. Wenn Intels 50 Gbit/s als Summe beider Richtungen gemeint sind, also 25 rein und 25 raus, dann reicht Gen2 x8 dafür aus, und meine gemessenen 24,6 Gbit/s pro Richtung sind aggregiert 49,2 Gbit/s. Dann lautet der richtige Satz nicht „die Karte erreicht die Hälfte“, sondern „die Karte erreicht ihre Spezifikation“. Intel dokumentiert nirgends, wie gezählt wird, also ist das aus den vorliegenden Unterlagen nicht entscheidbar. Die 24,6 Gbit/s stehen fest, nur ihre Deutung hängt an einer undokumentierten Konvention. Natürlich kann ich auch einfach dran vorbei gelesen haben oder ich messe falsch. Korrigiert mich gerne, dann lerne ich selbst etwas \o/

Und jetzt die Enttäuschung: dm-crypt will nicht

Der naheliegendste Anwendungsfall, und ausgerechnet genau der, für den die Karte in meinem FreeBSD-Server gearbeitet hat, funktioniert unter aktuellem Linux nicht:

# cryptsetup plainOpen /dev/ram0 probe --cipher capi:qat_aes_xts-plain64 --key-size 512 --key-file /dev/zero
device-mapper: reload ioctl on probe (252:3) failed: Datei oder Verzeichnis nicht gefunden

Der erste Reflex ist natürlich, dass ich den Namen falsch geschrieben habe. Also Gegenprobe mit derselben capi:-Syntax über sieben Varianten:

Angabe bei --cipherErgebnis
aes-xts-plain64funktioniert
capi:xts(aes)-plain64funktioniert
capi:xts-aes-aesni-plain64funktioniert
capi:xts-aes-vaes-avx2-plain64funktioniert
capi:xts-aes-vaes-avx512-plain64funktioniert
capi:qat_aes_xts-plain64Fehler, ENOENT
capi:qat_aes_cbc-plain64Fehler, ENOENT

dm-crypt kann also sehr wohl einen konkreten Treiber adressieren, sogar xts-aes-vaes-avx512, das mit seiner niedrigen Priorität sonst nie zum Zug käme. Nur die beiden QAT-Treiber fliegen raus. Das ist kein Tippfehler und keine Namensauflösung.

Der Beweis kommt aus dem laufenden Kernel selbst, ausgelesen über die NETLINK_CRYPTO-Schnittstelle. Das ist die belastbare Quelle, denn sie sagt, was dieser Kernel registriert hat, und nicht, was irgendeine Quelldatei im Netz behauptet:

qat_aes_xts              flags=0x00010585   ASYNC | NEED_FALLBACK | TESTED | ALLOCATES_MEMORY
qat_aes_cbc              flags=0x00010485   ASYNC | TESTED | ALLOCATES_MEMORY
qat_aes_ctr              flags=0x00010485   ASYNC | TESTED | ALLOCATES_MEMORY
qat_aes_cbc_hmac_sha256  flags=0x00010483   ASYNC | TESTED | ALLOCATES_MEMORY
qat_deflate              flags=0x0001048a   ASYNC | TESTED | ALLOCATES_MEMORY
qat-rsa                  flags=0x00000406   TESTED
xts-aes-aesni            flags=0x00000405   TESTED
xts-aes-vaes-avx2        flags=0x00000405   TESTED
cbc-aes-aesni            flags=0x00000405   TESTED
deflate-generic          flags=0x0004040a   TESTED

Der Unterschied ist genau ein Bit: 0x00010000, also CRYPTO_ALG_ALLOCATES_MEMORY. Alle symmetrischen QAT-Implementierungen haben es, keine einzige CPU-Implementierung hat es. Die niedrigen Bits sind übrigens kein Flag, sondern der Algorithmustyp.

Und dm-crypt fordert seine Transforms genau mit diesem Bit als Maske an, an drei Stellen im Code:

cc->cipher_tfm.tfms[i]      = crypto_alloc_skcipher(ciphermode, 0, CRYPTO_ALG_ALLOCATES_MEMORY);
cc->cipher_tfm.tfms_aead[0] = crypto_alloc_aead(ciphermode, 0, CRYPTO_ALG_ALLOCATES_MEMORY);
mac                         = crypto_alloc_ahash(mac_alg, 0, CRYPTO_ALG_ALLOCATES_MEMORY);

Die Suche findet damit nichts und liefert ENOENT, und das kommt oben als „Datei oder Verzeichnis nicht gefunden“ an. Kette geschlossen, keine Hypothese mehr. Dass qat-rsa das Flag nicht hat, passt genau ins Bild: RSA läuft über die Karte, weil es kein Block-I/O-Pfad ist.

Die Wendung: das ist eine Leitplanke, kein Bürokratiefehler

An dieser Stelle wollte ich anfangen zu schimpfen. Die Karte kann AES-XTS in Hardware, der Kernel registriert es, und der eine Konsument, für den es gedacht war, weigert sich. Klingt nach Linux, das sich selbst im Weg steht. Ist es aber nicht.

Die Maske stammt von Mikulas Patocka, und die Begründung im Commit ist knapp:

Don’t use crypto drivers that have the flag CRYPTO_ALG_ALLOCATES_MEMORY set. These drivers allocate memory and thus they are unsuitable for block I/O processing.

Der Grund dahinter ist ein Low-Memory-Deadlock. Stell dir vor, es wird auf ein dm-crypt-Gerät geswappt. Der Kernel will Speicher freimachen, schickt die Swap-Out-BIO durch dm-crypt, dm-crypt fragt die Crypto-API, und die fordert daraufhin Speicher an, den es gerade nicht gibt. Ende der Vorstellung.

Und mit QAT ist genau das schon einmal schiefgegangen. Im März 2022 gibt es einen Bericht auf der Kernel-Mailingliste über massive Datenkorruption mit QAT plus dm-crypt plus XFS. Die Diagnose kam von Giovanni Cabiddu, Intel:

The implementations of aead and skcipher in the QAT driver are not properly supporting requests with the CRYPTO_TFM_REQ_MAY_BACKLOG flag set. If the HW queue is full, the driver returns -EBUSY but does not enqueue the request.

Die Folge: dm-crypt wartet endlos auf die Completion eines Requests, der nie an die Hardware gegangen ist. Und das Dateisystem war hinüber. Das ist kein theoretisches Risiko, das ist ein Schadensfall mit Datum.

Die Zeitleiste danach ist lehrreich, weil sie nicht so endet, wie man denkt:

DatumWas passiert
Juli 2020Das Flag CRYPTO_ALG_ALLOCATES_MEMORY wird auf QAT gesetzt
Juli 2020dm-crypt schließt Treiber mit diesem Flag aus (Patocka)
März 2022Der Schadensfall: Datenkorruption mit QAT, dm-crypt und XFS
Mai 2022Intel liefert den eigentlichen Fix, ein Backlog-Mechanismus
Juli 2023Intel will das Flag entfernen, um QAT für dm-crypt zurückzuholen. Nie gemerged.
Juni 2025Stattdessen: Priorität von Skcipher und AEAD wird von 4001 auf 100 gesenkt
August 2026Auf meinem Kernel gemessen: Flag gesetzt, Priorität 100, dm-crypt verweigert

Die Auflösung war also nicht „dm-crypt darf QAT wieder benutzen“, sondern „QAT wird per Priorität aus dem Weg geräumt“. Und die Begründung in diesem Commit ist der stärkste Absatz, den ich zu dieser Karte gelesen habe:

Most kernel applications utilizing the crypto API operate synchronously and on small buffer sizes, therefore do not benefit from QAT acceleration. Reduce the priority of QAT implementations for both skcipher and aead algorithms, allowing more suitable alternatives to be selected by default.

„Synchron und kleine Puffer“ ist exakt das, was ich oben unabhängig gemessen habe, 39 gegen 3,4 Mikrosekunden bei 4 KiB. Der Kernel hat 2025 formal festgestellt, was meine Messung 2026 auf dieser Maschine bestätigt. Und der Patch, der Intels eigene Hardware degradiert, kommt von Intel, mit Acked-by von Eric Biggers. Wenn man es wohlwollend liest, ist das ein Hersteller, der ehrlich ist. Zur Version muss man genau sein: der Commit ging in Mainline 6.17, wurde aber wegen Cc: stable auch in ältere Stable-Zweige zurückportiert, war dort also schon vor dem 6.17-Release wirksam.

Und damit ist die Pointe eine viel bessere als „Linux ist im Weg“: was wie eine willkürliche Blockade aussieht, ist eine Leitplanke aus einem echten Schadensfall. Das erklärt übrigens auch, warum FreeBSD hier lockerer war. Dort gibt es diese Leitplanke nicht, es gibt keine Priority-Hierarchie, die den Beschleuniger aussortiert, und keine Maske, die ihn vom Blockgerät fernhält. Das heißt allerdings nicht, dass das Problem dort nicht existiert. Es heißt nur, dass niemand ein Geländer davor gebaut hat.

Ein Blick auf die FreeBSD-Seite lohnt an dieser Stelle sowieso, denn dort ist der Weg ein struktureller anderer. qat(4) ist ein Treiber für das OpenCrypto-Framework, also für crypto(9). Wer GELI benutzt, muss überhaupt nichts umkonfigurieren: GELI fragt das Framework, und das Framework nimmt den Beschleuniger. Deshalb war die Sache damals auch so unspektakulär, ich habe die Karte gesteckt und die Platten waren schneller.

Ganz so glatt war die Historie allerdings nicht. Im Basissystem gibt es qat(4) erst seit FreeBSD 13.0, und in 14.0 wurde dieser Treiber durch Intels Upstream-Variante ersetzt. Auf dem ersten Server, der noch älter war, kann es diesen Weg also nicht gegeben haben. Die aktuelle Manpage führt die Serie 8925 bis 8955 weiterhin als unterstützte Hardware, dieser Chip ist dort also nicht rausgefallen. Und noch etwas gegen zu viel Nostalgie: im FreeBSD-Forum gibt es einen Thread zur GELI-Performance, in dem QAT zunächst schlechter war als AES-NI. Erst nach dem Umstellen der QAT-Services berichtete der Autor rund 30 bis 34 Prozent Vorsprung bei AES-XTS mit SHA256, und bei anderen Lasten blieben Verluste. Die Karte war also auch damals kein bedingungsloser Gewinn, sondern eine, die zur Konfiguration passen musste.

Der zweite Dämpfer: Intels Userspace hat die Karte fallengelassen

Bleibt der andere große Anwendungsfall: TLS-Terminierung mit nginx oder HAProxy, OpenSSL-Offload. Dafür braucht man qatlib und dazu die QAT-Engine oder den Provider für OpenSSL. Auf meinem System ist davon nichts installierbar, kein Kandidat in den Repos, und OpenSSL 3.0.13 kennt nur den Default-Provider.

Schlimmer als „nicht gepackt“ ist aber der Grund: Intel hat dh895xcc aus qatlib entfernt. Die aktuellen Versionen unterstützen nur noch QAT Gen4, also die 4xxx- und 420xx-Serien. Die frühen Generationen sind nicht mehr dabei. Der Weg wäre der alte Out-of-Tree-Stack, und der baut gegen einen Kernel 7.0 nicht mehr.

Ich formuliere das bewusst nicht als „tot“. Behauptet ist: für diesen Chip ist der heutige Mainstream-Linux-Userspace praktisch abgehängt. Nicht behauptet ist, dass es global unmöglich wäre. Mit altem Kernel, altem Out-of-Tree-Treiber und passender Distribution ließe sich der Stack sicher noch aufbauen, und ältere DPDK-Versionen führten dh895xcc als unterstütztes Gerät. Es ist eine Frage von Aufwand und Kernel-Alter, nicht von Unmöglichkeit.

Die Ironie daran ist schön bitter. Intels eigener Userspace hat die Karte längst aufgegeben, während der Linux-Kernel sie weiter pflegt und beim Booten ohne ein einziges Paket zum Laufen bringt. Wer wissen will, warum In-Tree-Treiber so wertvoll sind: das hier ist der Beweis in einer Karte.

Ein dritter Befund noch, damit ihn niemand für einen Kartenfehler hält: ein einzelnes sendmsg über AF_ALG mit 256 KiB oder mehr blockiert dauerhaft. Aufgefallen ist mir das erst mit der Karte bei 1 MiB, reproduzieren lässt es sich aber mit dem CPU-Cipher genauso. Es ist also nicht die Hardware. Wo genau im AF_ALG-Pfad es hängt, habe ich nicht nachgewiesen, der Socket-Sendepuffer wäre die naheliegende Vermutung, aber eben eine ungeprüfte. Alle Messungen oben sind deshalb auf maximal 64 KiB begrenzt.

Kompression: der einzige Pfad, der von allein läuft, und er schadet

Erinnerst du dich an qat_deflate mit Priorität 4001 gegen deflate-generic mit 0? Jeder Kernel-Konsument, der nach deflate fragt, bekommt die Karte. Realistisch ist das zswap, die komprimierte Auslagerung im RAM.

Also nachgestellt: eine cgroup mit hartem Speicherlimit, 700 MiB gut komprimierbare anonyme Seiten, zswap auf deflate. Und tatsächlich, die Karte komprimiert die Auslagerung, 130.108 Firmware-Requests belegen das. Nur ist der direkte Vergleich bei gleicher Last ziemlich brutal:

zswap-CompressorwallusersysQAT-Requests
lzo (CPU)0,85 s0,11 s0,72 s0
deflate (QAT)9,47 s0,26 s5,13 s130.027

Elfmal langsamer. Und die System-CPU-Zeit steigt sogar von 0,72 auf 5,13 Sekunden, das Offload spart also nicht einmal CPU, es kostet zusätzlich welche. Der Grund ist derselbe wie überall in diesem Beitrag: zswap komprimiert eine 4-KiB-Seite pro Request, und das ist genau der Latenz-Worstcase.

Fair bleiben muss ich hier trotzdem: der Vergleich mischt zwei Effekte, denn Deflate ist algorithmisch auch schlicht teurer als LZO. Sauber wäre deflate auf CPU gegen deflate auf der Karte, und das habe ich nicht gemessen, weil sich deflate-generic bei Priorität 0 nicht ohne Weiteres erzwingen lässt. Die Aussage „QAT-zswap ist keine gute Idee“ trägt der Test, die Aufteilung zwischen Algorithmus und Latenz nicht.

Bleibt eine Frage, die ich hübsch finde: warum steht deflate überhaupt noch auf 4001? In der Datenkorruptions-Diskussion von 2022 schlug Intel vor, die Priorität der betroffenen Algorithmen auf 1 zu senken. Heute stehen Skcipher und AEAD bei 100, die Kompression aber weiter bei 4001. Die naheliegende Deutung: 4001 war ursprünglich die Politik „nimm immer den Beschleuniger“, sie wurde für Krypto nach dem Vorfall zurückgenommen und für Kompression nie. Damit wäre qat_deflate der letzte Überrest dieser alten Haltung, und ausgerechnet der Pfad, der heute noch stillschweigend gewinnt und dabei elfmal langsamer ist. Das ist allerdings meine Deutung, kein Beleg. Die Commit-Historie der Prioritätswerte für die Kompression habe ich nicht nachverfolgt.

Wer das nachbauen will: den Zustand danach wieder zurücksetzen, also enabled=N und compressor=lzo. Ein Dauerbetrieb mit dieser Einstellung wäre eine echte Verschlechterung der Maschine.

Die unvermeidliche Frage: kann man damit Bitcoin oder Monero minen?

Nein. Und zwar aus zwei völlig verschiedenen Gründen, und dieser Unterschied ist der eigentlich interessante Teil.

Monero geht prinzipiell nicht, nicht bloß langsam. Monero nutzt seit 2019 RandomX, und der Algorithmus wurde absichtlich so entworfen, dass Spezialhardware keinen Vorteil hat. Er generiert zur Laufzeit zufällige Programme aus Integer-, Fließkomma- und Branch-Instruktionen und führt sie in einer VM aus, teils JIT-compiliert. Er braucht 2 MiB Scratchpad pro Thread, permanent random-access beschrieben und gelesen. Und im Fast-Mode zusätzlich einen Datensatz von rund 2,08 GiB im RAM, aus dem die VM ständig liest. Das macht RandomX speichergebunden statt rechengebunden, und deshalb sind ASICs dort wirtschaftlich uninteressant.

Die QAT-Karte ist das exakte Gegenteil davon: eine Festfunktions-Pipeline. Sie kann AES, SHA, RSA, DH und Deflate, und nichts sonst. Sie hat keinen Befehlssatz, kein Scratchpad-Konzept und keinen Zugriff auf einen 2-GiB-Arbeitsdatensatz. AES kommt in RandomX zwar vor, aber als eingebetteter Schritt in der VM-Schleife, nicht als abtrennbare Massenoperation, die man an einen Coprozessor auslagern könnte. Die CPU dieser Maschine kann RandomX übrigens sehr wohl. Der Algorithmus ist ja genau für CPUs gemacht.

Bitcoin geht theoretisch, praktisch ist es absurd. Bitcoin ist doppeltes SHA-256 über einen 80 Byte großen Blockheader, und SHA-256 kann die Karte. Nur scheitert es an zwei harten Punkten.

Erstens ist es über diesen Stack nicht einmal ansprechbar. Der In-Kernel-Treiber registriert kein reines SHA-256, sondern nur authenc(hmac(sha256),cbc(aes)), also HMAC-authentifizierte Verschlüsselung. Nackte Hashes bekäme man nur über den Userspace-Stack, und der existiert für diesen Chip nicht mehr. Es gibt also gar keinen Weg, der Karte einen Blockheader zum Hashen zu geben.

Zweitens ist die Größenordnung hoffnungslos, und dafür genügt eine geschenkte Obergrenze. Selbst wenn die Karte ihre volle Datenblattleistung als reines Hashing liefern könnte, landet man bei realistischen 64 bis 128 Byte pro Hash-Operation in der Größenordnung von 10⁷ bis 10⁸ Hashes pro Sekunde, also höchstens einige zehn MH/s. Erreichen wird sie das nie, die gemessenen Latenzen zeigen ja, dass sie bei kleinen Nutzlasten zwei Größenordnungen unter ihrem Sweet Spot arbeitet.

HashrateCharakter der Zahl
QAT 8950höchstens 10⁸ H/sunerreichbare Obergrenze
ein aktueller ASIC-Mineretwa 2 mal 10¹⁴ H/sProduktangabe
Bitcoin-Gesamtnetz, 03.08.20269,3 mal 10²⁰ H/sgemessen, 932 EH/s

Selbst mit der geschenkten Obergrenze liegt ein einzelner ASIC noch um mindestens sechs Größenordnungen über der Karte, und das Gesamtnetz um dreizehn. Der Anteil am Netz wäre günstigstenfalls in der Größenordnung 10⁻¹³, bei zehn Minuten Blockzeit also eine erwartete Wartezeit jenseits jeder sinnvollen Zeitskala, bei 40 Watt Dauerlast. Ich verzichte hier absichtlich auf eine konkrete Jahreszahl. Die klingt zwar gut, wäre aber Scheinpräzision auf einer geschätzten Grundlage. Größenordnungen sind hier die ehrlichere Währung, und sie sind genauso eindrucksvoll.

Die eigentliche Pointe ist aber eine sprachliche. Krypto-Beschleuniger heißt Kryptographie, nicht Kryptowährung. Die Karte ist für das gebaut, was Verbindungen und Platten schützt: TLS-Handshakes, IPsec-Tunnel, AES-XTS auf Blockgeräten. Dass beide Bedeutungen dasselbe Wort benutzen, ist ein Sprachunfall, und ich bin ziemlich sicher, dass er der Kryptographie mehr geschadet hat als der Kryptowährung.

Betrieb: man fliegt thermisch blind

Eine praktische Warnung, falls jemand auf die Idee kommt, so ein Ding in einen Desktop zu stecken: die Karte hat keinen Temperatursensor. sensors zeigt nichts, der BMC kennt sie nicht, in der SDR steht kein Eintrag. Man sieht also nicht, wie warm sie wird.

Und die Randbedingungen sind ungünstig: der Kühlkörper ist rein passiv und für den Querstrom eines Rackgehäuses ausgelegt, spezifiziert sind 0 bis 55 Grad Umgebungstemperatur, es dürfen bis zu 40 Watt Verlustleistung sein, und mein Gehäuse ist auf Ruhe optimiert, mit bewusst langsam drehenden Lüftern. Die Netzwerkkarte im Nachbarslot liegt schon im Leerlauf bei 61 Grad, und die beiden teilen sich denselben schlechten Luftstrom. Wie warm die Karte unter der Last aus diesem Beitrag wirklich geworden ist, weiß ich nicht. Ohne Sensor bräuchte es ein IR-Thermometer oder ein Thermoelement. Steht auf der Liste.

Überflüssig gewordene Technik, und warum mir das trotzdem naheliegt

Diese Karte ist für mich so etwas wie eine Soundkarte. Es gab eine Zeit, da war eine dedizierte Soundkarte selbstverständlich, weil der Rest der Maschine das einfach nicht konnte. Ich hänge immer noch an den alten Creative-Karten, nicht nur an den ISA-Dingern, auch an den späteren. Die hatten eine Wertigkeit, ein Gewicht, eine Bestückung, die man ansehen konnte. Man hat ein Bauteil gekauft, das eine Aufgabe hatte, und das hat man auch gesehen.

Heute steckt in meiner Workstation nicht einmal mehr eine Onboard-Lösung im Einsatz, sondern ein Behringer 302USB, also ein kleines Mischpult mit USB-Audiointerface. Für meinen Fall reicht das absolut. Die Aufgabe ist nicht verschwunden, sie hat nur ihren Platz gewechselt, und die CPU rechnet das nebenbei mit, ohne dass es jemandem auffällt. Genau das ist mit Krypto passiert. AES-NI und VAES haben die Beschleunigerkarte nicht besiegt, sie haben sie aufgesogen.

Immer kleiner, komplexer und leistungsfähiger ist total geil. Ohne diese Entwicklung gäbe es die Hälfte von dem nicht, was wir heute technisch machen. Aber sie macht das Verstehen sehr viel schwieriger. Mein alter C64 und der VC20 davor, die Dinger hat man noch verstanden. Eine CPU mit rund einem Megahertz in einem 40-Pin-Gehäuse, groß genug und langsam genug, dass man mit dem Oszilloskop an einzelne Pins konnte und dabei etwas gesehen hat. Man konnte am Speicher nachmessen. Das war begreifbar im wörtlichen Sinn. An dem Xeon in dieser Maschine messe ich nichts nach. Der Die ist unter einem Heatspreader, die Signale sind differentiell und liegen im Gigahertzbereich, und selbst wenn ich rankäme, wäre mein Oszilloskop zu langsam. Dass bei dieser Karte das nackte Silizium offen liegt, ist ein Zufall der Bauform, und trotzdem freut es mich jedes Mal.

Dafür haben wir heute AI, um uns Dinge erklären zu lassen, und das ist ein echter Gewinn. Ich komme damit an Ecken, an die ich vor zehn Jahren nur mit sehr viel mehr Zeit gekommen wäre. Wir müssen nur alle aufpassen, dass wir dabei nicht aufhören zu verstehen. Der Unterschied zwischen „ich habe es erklärt bekommen“ und „ich habe es verstanden“ ist genau der Unterschied zwischen diesem Beitrag und einer Feature-Liste. Und ein Teil davon passiert ja bereits in der AI-Entwicklung selbst: wir verstehen nicht mehr im Detail, was in diesen Systemen passiert. Stand jetzt halte ich das für ein Problem. Vielleicht ist es aber auch bald einfach das Normale, und ich bin der Typ, der dem Oszilloskop nachtrauert.

Ehrliche Gesamteinschätzung

Als Produktivkomponente ist die Karte in dieser Maschine sinnlos. Eine einzige moderne CPU der Einstiegsklasse schlägt sie bei symmetrischer Krypto um Faktor 8,6, der Anwendungsfall, für den sie gekauft wurde, ist unter Linux versperrt, Intels Userspace hat sie aufgegeben, sie zieht laut Datenblatt bis zu 40 Watt für etwas, das die CPU besser kann, und gekühlt wird sie für einen Luftstrom, den mein Gehäuse nicht liefert.

Als Lehr- und Erzählobjekt ist sie ausgezeichnet. An diesem einen Stück Platine lassen sich Krypto-Offload als Architektur, Latenz gegen Durchsatz, warum Queue-Tiefe alles ist, PCIe-Generationen und Lane-Budgets, PCIe-Switches, SR-IOV, IOMMU-Gruppen und die Prioritätslogik der Linux Crypto API erklären. Ich kenne wenige Bauteile, die auf so kleiner Fläche so viele Anknüpfungspunkte haben. Und sie funktioniert einfach, nach 13 Jahren, ohne ein einziges installiertes Paket.

Genau diese Spannung war der Grund, sie überhaupt noch einmal einzustecken.

Was offen bleibt

  • Die Karte per VFIO an eine FreeBSD-VM durchreichen und dort GELI auf QAT laufen lassen, also die Original-Nutzung rekonstruieren. Sie ist allein in ihrer IOMMU-Gruppe, technisch steht dem nichts im Weg. Das ist der Versuch, auf den ich am meisten Lust habe.
  • SR-IOV aktivieren und sehen, was der Treiber mit 32 Virtual Functions macht. Völlig ungetestet.
  • PCIe-Zähler messen, um zu belegen oder zu widerlegen, dass wirklich der Bus limitiert und nicht der Zugangspfad. Der wichtigste offene Messpunkt.
  • Die Benchmarks methodisch nachziehen: Warmup, mehrere Läufe, Median und P95, CPU-Pinning.
  • Kernel-RSA über die Karte messen. qat-rsa gewinnt per Priorität, aber es gibt keinen AF_ALG-Zugang zu akcipher.
  • Temperatur und echte Leistungsaufnahme, beides nur extern messbar.
  • Das PLX-EEPROM auslesen, um die Lane-Aufteilung zu belegen statt zu vermuten.

Siehe auch

Hast du so eine Karte noch im Einsatz, vielleicht sogar noch produktiv? Dann würde mich das wirklich interessieren, und ihr dürft mich sehr gerne fragen.

Wenn PHP beim Aufräumen stirbt: ein FreeBSD-rtld-Bug hinter posix_spawn

Beitragsbild zu einem FreeBSD-Bug: Laptop mit PHP- und lldb-Debug-Ausgaben, Signal-11-Core-Dump und Diagramm der Kausalkette von Nextcloud über proc_open und posix_spawnp bis zur rtld-Heap-Korruption.

Diese Geschichte fing als PHP-Problem an und endete mehrere Wochen später in einem Bug im FreeBSD-Basissystem, ganz unten im Runtime-Linker. Dazwischen liegen mindestens vier falsche Fährten, ein Crash, der bei jedem Lauf ein anderes Opfer suchte, und die schöne Erkenntnis, dass man eine Heap-Korruption nicht mit einzelnen Watchpoints fängt. Ich schreibe das bewusst mit allen Sackgassen auf, weil genau die der lehrreiche Teil sind. Wer nur die Auflösung will, springt ans Ende.

Das Symptom

PHP 8.4 auf FreeBSD 15 (amd64), im Zusammenspiel mit einer selbst gehosteten Nextcloud. Jeder occ-Aufruf und jeder Cron-Lauf lieferte sein Ergebnis korrekt ab und segfaultete danach. Signal 11, jedes Mal, mit schöner Regelmäßigkeit ein Core-Dump von rund 2,2 GB. Die Ausgabe stand vollständig da, bevor es knallte. Der Crash passierte erst im Module-Shutdown, also beim Aufräumen, nachdem die eigentliche Arbeit längst erledigt war.

Funktional war das harmlos. Ärgerlich war der Rest. Das dmesg füllte sich mit Zeilen der Sorte:

pid 12345 (php), jid 0, uid 80: exited on signal 11 (core dumped)

Die Platte lief mit 2,2-GB-Cores voll, und es gab einen unangenehmen Nebeneffekt: hängende Background-Jobs. Wenn PHP-FPM mitten in einem Nextcloud-Cron-Job segfaultet, wird das reserved_at in der Tabelle oc_jobs nie zurückgesetzt. Der Job gilt damit als dauerhaft in Bearbeitung und läuft nie wieder an. Aus einem kosmetischen Shutdown-Crash wurde so ein echtes Betriebsproblem.

Erste falsche Fährte: OPcache JIT

Ein Segfault in PHP, der frische JIT im Spiel: der erste Verdacht war schnell da. Also habe ich mich durch die JIT-Stufen gearbeitet. Tracing-JIT mit opcache.jit=1255, dann Function-JIT mit 1205, dann JIT komplett aus mit 0. Es crashte durch alle Stufen hindurch unverändert weiter.

JIT war auf FreeBSD 15 zwar tatsächlich für sich genommen kaputt und ist bei mir seitdem aus. Aber die Ursache für den Shutdown-Crash war er nicht. Erste Fährte verworfen.

Die Versions- und Build-Jagd

Nächster Verdacht: ein kaputter Build oder eine ABI-Unstimmigkeit zwischen dem PHP-Core und einer Extension. Also PHP komplett aus den Ports neu gebaut, damit Core und alle Extensions garantiert dieselbe Version tragen. Danach Symbol-Builds fürs Debugging. Und dann durch die Punktversionen gehangelt: 8.4.16, .18, .19, .20, .21, .22. Jede einzelne crashte gleich.

Damit war eine wichtige Sache geklärt: Build, Version und CFLAGS sind nicht der Unterschied. Was sich nicht ändert, wenn man alles daran ändert, liegt woanders.

Eine Lehre am Rande, die mich unnötig Zeit gekostet hat: --enable-debug wechselt das ABI-Verzeichnis der Extensions. Danach laden sämtliche als Paket installierten Extensions nicht mehr, weil sie im falschen Verzeichnis gesucht werden. Wer nur Debug-Symbole will, ohne das ABI zu verbiegen, baut so:

make CFLAGS+=" -g" STRIP=

Die Crash-Site per lldb aus dem Core

Das FreeBSD-Basissystem bringt kein gdb mit, dafür lldb. Aus dem Core kommt man so an den Backtrace:

lldb --batch -o "target create --core <core> <php-binary>" -o "bt all"

Der Stack sah beim ersten Lauf so aus:

_start → __libc_start1 → main → php_module_shutdown → zend_shutdown
  → zend_hash_graceful_reverse_destroy → destroy_zend_class +1228

Die crashende Instruktion war cmpq %rbx, 0x20(%r15). Der Offset 0x20 ist in zend_property_info das Feld ce, der Zeiger auf den Klassen-Eintrag. Das Register r15 stand auf 0x6b588e9c404, unaligned und außerhalb des Heaps. Das riecht nach einem Use-after-Free auf geteilte interne Klassen-Metadaten.

Ein genauerer Walk durch die Strukturen korrigierte meine erste Annahme. Der Offset 0x20 liegt nicht nur in zend_property_info, sondern genauso in zend_class_constant auf dem ce-Feld. Die crashende Schleife lief nicht über die Properties, sondern über die Klassen-Konstanten, also die constants_table. Die crashende Klasse war Pdo\Pgsql, eine der neuen internen Subklassen aus dem PHP-8.4-RFC zu den PDO-treiberspezifischen Subklassen, die von PDO erbt. Mein Verdacht drehte sich damit auf etwas 8.4-Spezifisches: Vererbung von internen Konstanten, vielleicht im Umfeld der Property Hooks.

Der Crash wandert

Und jetzt wurde es unangenehm. Die Crash-Site war nicht stabil. Von Lauf zu Lauf sah ich mal destroy_zend_class, mal zend_type_release, mal zend_interned_strings_dtor. Mal war das Opfer Pdo\Pgsql, mal ein arg_info von RedisCluster, mal ein zend_type, mal ein DateTimeZone.

Das ist das klassische Bild eines einzelnen korrumpierenden Schreibzugriffs mit wechselndem Opfer. Wer getroffen wird, hängt allein am Heap-Layout des jeweiligen Laufs. Das erklärt rückblickend, warum die vermeintlich genaue Klasse jedes Mal anders aussah. Ich hatte die ganze Zeit das Spätsymptom analysiert, nicht die Ursache. Als Beispiel eine ganz andere Crash-Site vom zweiten Rechner:

php_module_shutdown → zend_interned_strings_dtor
  → zend_hash_destroy +310 → _str_dtor → _efree +11

Das Opfer hier war ein permanenter interned String. Das sind die intern deduplizierten, prozessweit nur einmal abgelegten Zeichenketten, die PHP überall wiederverwendet, in diesem Lauf der Redis-Kommandoname zintercard. Sein Header war zerschossen, beim Freigeben faultet der Destruktor auf einem ZendMM-Block, der gar nicht mehr gemappt ist. Wieder ein anderer Tatort, dasselbe Muster: irgendwer schreibt einmal quer, und wer danach als Erstes über die zerstörte Stelle stolpert, nimmt den Fall.

Upstream-Issue GH-21995, und die Richtung dreht sich

An diesem Punkt habe ich das Ganze bei php-src als Issue GH-21995 aufgemacht. Zwei Reaktionen haben die Richtung gedreht.

Zuerst @iliaal, einer der PHP-Maintainer:

Cannot reproduce on Linux (ASAN, Valgrind all clean on 37 extension build), so if this is valid it might be FreeBSD specific.

ASAN und Valgrind sauber auf Linux ist ein starkes Indiz gegen einen klassischen Use-after-Free im Zend-Speichermanager. Ein solcher Fehler würde unter ASAN sofort auffliegen. Wenn er das nicht tut, sitzt das Problem woanders, vermutlich unterhalb von PHP.

Dann bestätigte @CamilleScholtz das Verhalten unabhängig, auf PHP 8.5.6, FreeBSD 15, und ausdrücklich nicht in einem Jail. Damit fielen zwei bequeme Ausreden weg: es war weder meine spezielle Konfiguration noch etwas, das in 8.5 schon behoben gewesen wäre.

Die VM reproduziert nicht, ein Heisenbug

Auf Bare-Metal crashte die unveränderte Paket-Installation praktisch bei jedem Lauf, gefühlt zu hundert Prozent. In einer VM dagegen kam ich auf rund 650 saubere Läufe, ohne einen einzigen Crash. Und sobald ich mit lldb und Watchpoints an das Objekt heranging, das ich für das Opfer hielt, verschob sich das Opfer. Die Beobachtung selbst veränderte das Heap-Layout und damit den Ausgang.

Das ist ein Heisenbug im Lehrbuchsinn. Ein einzelner Watchpoint auf ein einzelnes Objekt bringt hier nichts, weil der nächste Lauf ein anderes Objekt zerstört. Ich brauchte eine Messung, die gegen das Layout robust ist.

Messen statt raten: der Tabellen-Diff

Statt ein einzelnes Objekt zu beobachten, habe ich die ganze Tabelle der permanenten interned Strings an definierten Checkpoints verglichen. Ein eigenes lldb-Python-Skript zieht an jedem Checkpoint einen Snapshot der Tabelle und difft gegen den vorherigen. So ist es egal, welches konkrete Objekt in diesem Lauf getroffen wird, denn ich sehe jede Änderung an der ganzen Region.

Das Ergebnis war der erste harte Datenpunkt seit Wochen. Der korrumpierende Schreibzugriff passiert während des Spawns, genauer im Intervall zwischen posix_spawnp und posix_spawn_file_actions_destroy. Überschrieben wird ein zusammenhängender Block von rund 480 Byte, gefüllt mit 8-Byte-Zeigern. Das sieht aus wie Stack-Frames, die dort hingehören, wo sie nicht hingehören. Damit war klar: das ist keine PHP-interne Speicherverwaltung, das ist der Spawn.

Die Batterie: den Auslöser einkreisen

Jetzt konnte ich gezielt testen. Je 20 Läufe pro Kandidat. Nur proc_open crashte, und zwar 20 von 20. popen, exec, system, shell_exec, fopen, dazu Heap-Churn-Kandidaten wie str_repeat und range: alle 0 von 20. Es ging also nicht um fork und exec im Allgemeinen, auch nicht um Heap-Belastung, sondern spezifisch um proc_open.

Und dann entschied die Form des Aufrufs über Crash oder kein Crash:

AufrufSpawn-PfadCrash
proc_open(["true"], …) (relativ)posix_spawnp__libc_execvpe (PATH-Suche)ja
proc_open(["/usr/bin/true"], …) (absolut)posix_spawnpexecvPe direktnein
proc_open("true", …) (String)posix_spawn (/bin/sh -c) → _execvenein

Nur der relative Befehl ohne Schrägstrich im Namen crasht, weil nur der die PATH-Suche im Kind auslöst. Die Länge des PATH war dabei egal, auch mit einem einzigen Eintrag crashte es. Das grenzt es sauber gegen den alten Long-PATH-Overflow ab: es geht nicht um einen zu langen PATH, sondern um einen intrinsischen Stack-Verbrauch im no-slash-Suchzweig.

Das Minimal-Repro ist entsprechend kurz und kommt ganz ohne Framework aus:

php -r 'proc_open(["date"], [], $pipes);'   # → signal 11

Ein Detail fehlt noch, und es ist wichtig: der Crash braucht den vollen Satz geladener Extensions. Ein Minimalsatz von 17 Extensions reicht, aber das Entfernen irgendeiner einzelnen davon stoppt den Crash. Konkret dieser Satz: session, dom, iconv, imagick, intl, pdo, pgsql, phar, simplexml, sodium, xml, xmlwriter, zip, zlib, memcached, pdo_pgsql, redis. Viele geladene Shared Objects plus ein proc_open: beides zusammen ist nötig, keins allein reicht. Diese Beobachtung war später der Schlüssel zur Ursache, auch wenn ich das zu dem Zeitpunkt noch nicht wusste.

Runter in die libc-Quelle

Der Spawn führte mich in /usr/src/lib/libc/gen/posix_spawn.c. Auf amd64 startet do_posix_spawn das Spawn-Kind so:

rfork_thread(RFSPAWN, stack + stacksz, _posix_spawn_thr, &psa)

Der Stack für dieses Kind ist ein winziger, per malloc geholter Puffer:

#define _RFORK_THREAD_STACK_SIZE  4096
stacksz = 4096 + MAX(3, argc + 2) * sizeof(char *);   /* 16-Byte aligned */
stack   = malloc(stacksz);

Für ein {"true", NULL} sind das rund 4128 Byte. Das Entscheidende an RFSPAWN beziehungsweise rfork_thread: das Kind bekommt bis zum exec einen geteilten Adressraum, ähnlich wie bei vfork. Kind und Eltern arbeiten bis zum exec also auf demselben Speicher. Bei einem relativen Kommando läuft das Kind über __libc_execvpe in die PATH-Suche. Meine Hypothese an dieser Stelle war: das Kind erschöpft seine gut 4 KB Stack und schreibt in den direkt darunter liegenden Heap des Elternprozesses. Das würde exakt zu dem 480-Byte-Block aus Zeigern passen, den der Tabellen-Diff gesehen hatte.

Der Beweis: guardspawn

Eine Hypothese ist nur so gut wie ihr Experiment. Also habe ich guardspawn.c geschrieben, einen kleinen Interposer per LD_PRELOAD, der rfork_thread(RFSPAWN) abfängt und dem Kind einen selbst kontrollierten Stack unterschiebt. Zwei Varianten, zwei klare Antworten:

  • Gebe ich dem Kind 1 MB Stack, fällt der Crash auf 0 von 30. Baseline ohne Interposer war 30 von 30.
  • Gebe ich dem Kind wieder nur gut 4 KB, aber mit einer Guard-Page direkt darunter, stirbt das Spawn-Kind selbst mit SIGSEGV, unabhängig von der genauen Stelle.

Damit war die Kernaussage bewiesen: das Kind erschöpft den knapp 4 KB großen Spawn-Stack. Genauso ehrlich habe ich es aber auch in den Report geschrieben: welcher exakte Frame den Puffer überläuft, war zu dem Zeitpunkt nicht bewiesen. Ein alleinstehendes C-Programm triggerte den Fehler nicht, das Ganze hing an der Last des Prozesses. Mein Verdacht ging Richtung Runtime-Linker, aber das war noch eine Vermutung, kein Beweis.

Eine ehrliche Selbstkorrektur

Zwischendurch hatte ich mich verrannt und einen Stack-Underflow zu bestimmt behauptet. Ein zweiter, kritischer Blick von außen und ein eigener Read der Quelle korrigierten das: execvPe selbst verbraucht deutlich weniger als 4 KB, und absolute Kommandos laufen auch durch execvPe und crashen trotzdem nicht. Der Unterschied liegt also nicht in einem bewiesenen Overflow in execvPe, sondern im no-slash-Zweig der PATH-Suche. Ich habe das im Report deshalb als Lokalisierung formuliert, nicht als bewiesenen Mechanismus.

Dazu gehört auch das ehrliche Eingeständnis, dass alle meine früheren php-src-Hypothesen falsch waren: die Property Hooks, der vermeintliche Use-after-Free auf Klassen-Konstanten, die interned-String-Korruption, die pgsql-Verdächtigungen. Das war alles die wandernde Fault-Site, das Spätsymptom, nie die Ursache. Wer wochenlang das Symptom seziert, baut sich überzeugende Theorien über das Symptom. Das gehört in so einen Bericht hinein, nicht wegretuschiert.

Der Bugreport ans FreeBSD-Basissystem

Mit dieser Lokalisierung habe ich den Bug im FreeBSD-Basissystem eingereicht: Bug 295991. Das php-src-Issue GH-21995 habe ich als kein php-src-Bug geschlossen und beide Seiten miteinander verlinkt.

Wichtig war mir die Abgrenzung zu FreeBSD-SA-20:18 beziehungsweise CVE-2020-7458 von 2020. Das war der Long-PATH-Overflow an genau dieser Code-Stelle, längst behoben. Mein Fall ist die gleiche Gegend im Code, aber unabhängig von der PATH-Länge. Es ist bewusst keine Sicherheitsgeschichte, sondern ein Stabilitätsproblem, ausgelöst von völlig legitimem Code beim Aufräumen.

Praktischer Nebenbefund für alle, die sich an der Anubis-Sperre der FreeBSD-Bugzilla stören: den Status eines Bugs bekommt man ohne Browser bequem per REST:

curl -s "https://bugs.freebsd.org/bugzilla/rest/bug/295991"

Upstream pinnt die Ursache

Jetzt kam der Teil, für den sich die Mühe des sauberen Reports gelohnt hat. @bdrewery, FreeBSD-Committer, bestätigte und reproduzierte den Fehler noch bequemer als ich, direkt über den www/nextcloud-Port mit occ status in einer Schleife:

there is some random corruption that shows up with php on exit when loaded with many extensions. Raising the stack size in posix_spawn avoids the problem.

Zur Ehrlichkeit gehört der Seitenhieb, den ich mir dabei eingefangen habe: den Text meines Reports nannte er einen unreadable AI mess. Inhaltlich hat er den Fall getroffen, die Form hat genervt. Das war eine gute und verdiente Lektion über Report-Stil, auf die ich am Ende noch einmal zurückkomme.

@kevans hat den Mechanismus dann endgültig festgenagelt, und zwar an einer Stelle, an der ich nur einen Verdacht hatte. Nicht execvPe sprengt den Stack, sondern der Runtime-Linker beim Lazy-Binding der Symbole. Der Pfad ist _rtld_bindfind_symdefsymlook_defaultdonelist_init. Und donelist_init macht ein alloca, dessen Größe mit der Zahl der geladenen Shared Objects skaliert:

#define donelist_init(dlp) ((dlp)->objs = alloca(obj_count * sizeof(dlp)->objs[0]), assert((dlp)->objs != NULL), (dlp)->num_alloc = obj_count, (dlp)->num_used = 0)

Genau deshalb triggern schwer gelinkte Prozesse den Fehler und Spielzeug-Programme nicht. obj_count ist bei PHP mit dem vollen Extension-Satz groß, das alloca entsprechend fett, und auf dem gut 4 KB kleinen Spawn-Stack ist dann Schluss. Das deckt sich exakt mit meiner rtld-Vermutung aus dem Report und erklärt auch das 17-Extensions-Minimum: unter einer gewissen Zahl geladener Objekte bleibt das alloca klein genug.

Der Fix

Der Fix kam von @kib als Diff D57908. Die erste Revision regressierte und ließ eine www/onlyoffice-Umgebung crashen, mit ld-elf.so.1-Faults in beam.smp und x2t. Das war ein Multithreading-Problem, das kib noch vor dem Commit behoben hat. Danach ging es nach main:

  • 1e370f0 „rtld: stop using unbound alloca()“ vom 29. Juni 2026. Die alloca-Aufrufe in der DoneList und in map_object wandern in den Heap, sobald sie groß werden. Vermerk MFC after: 1 week.
  • 3de9dc5 vom 30. Juni 2026. Ein libc-Regressionstest, der eine Dummy-Shared-Library mehrfach mappt und mit einer Guard-Page arbeitet, um den Underflow zuverlässig zu triggern.

Beim Schreiben dieses Beitrags steht der MFC nach stable/15 an. Für ein 15.1-RELEASE kommt der Fix mit einem der künftigen 15.x-Patches. Bis dahin ist der Workaround simpel: absolute Pfade in proc_open vermeiden den crashenden no-slash-Zweig. Das ist Symptombekämpfung, kein Fix. Und wer nur das volllaufende Dateisystem im Blick hat, räumt die harmlosen Cores einfach weg.

Warum am Ende alles zusammenpasst

Das Schöne an der Auflösung ist, dass sie jedes einzelne der vielen Rätsel erklärt, die mich wochenlang in die Irre geführt haben:

  • Nur proc_open crasht, weil es das einzige PHP-Konstrukt ist, das posix_spawnp nutzt.
  • Nur relative Kommandos crashen, weil nur sie die PATH-Suche und damit das Lazy-Binding im Kind auslösen.
  • Nur FreeBSD auf amd64, weil der rfork_thread-Pfad mit dem kleinen malloc-Stack amd64- und i386-spezifisch ist.
  • ASAN und Valgrind sauber auf Linux, weil glibc posix_spawn ganz anders baut.
  • Der volle Extension-Satz nötig, weil viele Shared Objects das alloca im rtld erst groß genug für den Überlauf machen. Und die vielen permanenten interned Strings legen zusätzlich die späteren Opfer genau unter den Spawn-Puffer.

Zur Methode, und zum Report-Stil

Zwei Dinge nehme ich technisch mit. Erstens: eine Heap-Korruption mit wanderndem Opfer fängt man nicht mit einzelnen Watchpoints, weil das Beobachten das Layout verschiebt und damit das Opfer. Was funktioniert, sind layout-robuste Tabellen-Diffs an definierten Checkpoints. Nicht ein Objekt anstarren, sondern die ganze Region vorher und nachher vergleichen. Zweitens: ein LD_PRELOAD-Interposer mit Guard-Page ist ein billiges, definitives Ja-oder-Nein-Experiment für die Frage, ob ein Stack-Overflow vorliegt. Ein sauberes Experiment schlägt zehn plausible Theorien.

Und dann die Lektion, die mir @bdrewery verpasst hat. Ein Bugreport, der die ganze Hypothesenkette in den Body kippt, ist für den Leser eine Zumutung, egal wie korrekt die Analyse ist. Die richtige Form sind drei bis vier Sätze Kern ganz oben, das reproduzierbare Minimal-Beispiel gleich dahinter, und der ganze Ermittlungskrimi darunter für die, die ihn brauchen. Der Inhalt hat gestimmt, deshalb wurde der Bug gefixt. Aber die Form hätte den Committern viel Zeit gespart. Nächstes Mal Kern zuerst.

Ähnliche Geschichte im Notebook, im Basissystem festgefahren, oder einfach eine Meinung zum Report-Stil? Dann einfach fragen.

NB-2020-U Fingerabdruckleser: der libfprint-Patch ist upstream gemergt

Beitragsbild zum NB-2020-U Fingerabdruckleser: Notebook mit Fingerabdrucksensor, libfprint-Codeausschnitt mit Product-ID 0x2020 und grünem Merge-Status für den upstream übernommenen Patch.

Anfang März habe ich hier beschrieben, wie ich den NEXT Biometrics NB-2020-U in meinem Fujitsu Notebook unter Linux zum Laufen gebracht habe. Die ganze Arbeit lief am Ende auf eine einzige Product ID hinaus: 0x2020 im bestehenden nb1010 Treiber, weil der NB-2020-U denselben Sensor Die wie der NB-1010-U nutzt. Der Beitrag endete mit dem üblichen Cliffhanger: Merge Request eingereicht, CI grün, warten auf das Review durch die Maintainer.

Das Warten hat ein Ende. MR !569 ist gemergt.

Was der Maintainer gemacht hat

Marco Trevisan, einer der libfprint Maintainer, hat den Patch auf den aktuellen master rebased, die Pipeline noch einmal durchlaufen lassen und ihn am 2. Juli 2026 per Auto-Merge aufgenommen (Commit 0fa670f). Blockierende Review-Kommentare gab es keine. Der Patch war klein und die Beweislage eindeutig: gleicher Sensor, gleiches USB Protokoll, gleicher Treiber, nur eine zusätzliche ID in der Tabelle.

Was das für Betroffene heißt

Für alle mit demselben Fingerabdruckleser im Notebook: Ab der nächsten libfprint Version wird der NB-2020-U out of the box erkannt. Kein eigener Patch mehr, kein Selberbauen. Enrollment und Verifikation über fprintd laufen dann direkt, sobald die Distribution die neue libfprint Version ausliefert. Wer nicht warten möchte, nimmt weiterhin den Patch aus dem ersten Beitrag oder baut direkt vom aktuellen master.

Der zweite Leser aus derselben Familie, der NB-2033-U mit seinem komplett eigenen Protokoll, hat einen eigenen Treiber von Grund auf bekommen. Dieser Merge Request !574 liegt noch beim Review, ist aber frisch auf den neuen master rebased und die Pipeline ist grün. Sobald auch der durch ist, folgt ein weiterer kurzer Nachtrag.

Siehe auch

Denselben Leser im Notebook oder eine ähnliche Baustelle mit libfprint? Dann einfach fragen.

ADS-B-Feeder, Teil 2: der NTP-Bug in fr24feed ist in 1.0.57 gefixt, nur anders als gedacht

Raspberry Pi mit RTL-SDR-Stick und ADS-B-Antenne vor einer Flugradar-Karte. Das Beitragsbild thematisiert die Behebung des NTP-Problems in fr24feed 1.0.57 und die erfolgreiche Wiederanbindung eines Flightradar24-Feeders.

Im ersten Teil dieser kleinen ADS-B-Saga hatte ich am Ende eine Sache offen gelassen und sie sogar fett in die Was-noch-kommt-Liste geschrieben: MLAT aktivieren, sobald Flightradar24 den NTP-Bug fixt. Heute ist es soweit. Der Fix ist da, er kam mit Version 1.0.57, und er kam ganz anders als ich erwartet hätte. Statt den kaputten NTP-Client zu reparieren, hat FR24 ihn einfach rausgeworfen.

Wer den ersten Teil noch nicht kennt, holt das am besten kurz nach: Eigener ADS-B Feeder: Flugzeuge tracken mit Raspberry Pi, RTL-SDR und selbstgebauter Antenne. Dort steht das komplette Setup, die selbstgebaute Antenne und eben die Geschichte mit dem NTP-Bug, der meinen Feeder über Wochen am Online-Gehen gehindert hat. Den Bug selbst erkläre ich hier nur noch in ein paar Sätzen, die lange Version steht drüben.

Worum es ging, ganz kurz

Seit Version 1.0.55 hatte der fr24feed-Daemon einen internen NTP-Client, der schlicht nichts tat. Kein einziges Paket auf Port 123, also keine Zeitsynchronisation, und ohne synchronisierte Zeit lässt FR24 den Feeder nicht online gehen. Man hängt in einer Endlosschleife aus Failed to synchronize fest und kommt nie über dieses Sync-Gate hinaus. Mein Workaround war die letzte funktionierende Version 1.0.54 mit apt-mark hold festzunageln und auf einen Fix zu warten.

Im März hatte ich FR24 einen Bug-Report mit strace- und tcpdump-Belegen geschickt. Die Antwort von Muazzam aus dem Support: auf ihrer Seite nicht reproduzierbar, Verdacht auf eine Regression durchs Build-System und nicht durch eine Änderung am NTP-Client selbst. Ich blieb hartnäckig, lieferte am 6. Juni eine syscall-genaue A/B-Analyse nach, und am 8. Juni kam die erlösende Mail (Ticket #741092): „should be fixed in v 57 which will be released later today“. War es dann auch, noch am selben Tag lag 1.0.57-1 im Repo.

Warum ich nicht einfach apt upgrade tippe

fr24feed ist closed-source, proprietär, kein GitHub, keine Quellen. Ich kann ein Release also nicht am Code beurteilen, sondern nur an seinem Verhalten. Und ein blindes Upgrade auf dem laufenden Produktiv-Feeder kam nicht in Frage. Wenn 1.0.57 genauso kaputt gewesen wäre wie 1.0.56, hätte ich mir den Feeder zerschossen und müsste erst wieder zurückrollen, bevor überhaupt wieder Daten fliessen.

Die saubere Variante: das Binary aus dem .deb extrahieren und als isolierte Wegwerf-Instanz gegen eine Wegwerf-Config unter strace laufen lassen. Eigener Fake-Key, ein toter Receiver-Port, der echte Feeder läuft dabei unberührt weiter. Erst wenn der Testlauf sauber durchkommt, fasse ich die Produktion an.

Der Testaufbau, eine Wegwerf-Instanz unter strace

Die Test-Config ist bewusst minimal gehalten. Sie muss nur weit genug kommen, dass der Feeder die Zeitsynchronisation versucht, alles danach interessiert für diesen Test nicht:

fr24key=0123456789abcdef
receiver=beast-tcp
host=127.0.0.1:39999    # absichtlich toter Port, fuer die NTP-Phase egal
bs=no
raw=no
mlat=no
logmode=0

Dann sehen, ob der Pi die neue Version überhaupt schon sieht, und das Paket herunterladen ohne es zu installieren:

apt-cache policy fr24feed
#   Installed: 1.0.54-0
#   Candidate: 1.0.57-1
#      1.0.57-1 500 https://repo-feed.flightradar24.com flightradar24/raspberrypi-stable arm64

apt-get download fr24feed
dpkg-deb -x fr24feed_1.0.57-1_arm64.deb extract57

Erst die Toolchain vergleichen

Bevor ich überhaupt gestartet habe, ein kurzer Blick in die .comment-Section der ELF-Binaries. Die verrät, mit welchem Compiler gebaut wurde, und genau das war FR24s Verdacht:

readelf -p .comment extract57/usr/bin/fr24feed | grep -i gcc
#   GCC: (Debian 14.2.0-19) 14.2.0                       1.0.57 (und 1.0.56)
readelf -p .comment /usr/bin/fr24feed | grep -i gcc
#   GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0           1.0.54 (funktioniert)

Das ist der interessante Punkt: 1.0.57 ist mit derselben GCC-14-Toolchain gebaut wie das kaputte 1.0.56. „Neu kompiliert“ allein ist also noch kein Fix, sonst wäre 1.0.56 ja schon heil gewesen. Genau das machte den strace-Test erst spannend, denn ich konnte nicht aus der Versionsnummer ableiten, ob sich am Verhalten wirklich etwas geändert hat. Der Sprung von GCC 11 auf 14 plus der Distro-Wechsel von Ubuntu 22.04 auf Debian ist gross. GCC 14 ist deutlich strenger bei Undefined Behaviour und uninitialisierten Daten, und ein latenter Bug im NTP-Transmit-Pfad konnte unter GCC 11 unsichtbar bleiben und unter GCC 14 dann brechen. FR24s Build-System-Theorie war im Nachhinein also gar nicht so abwegig.

Der A/B-Lauf

Beide Versionen, die neue 1.0.57 und die installierte 1.0.54 als Kontrolle, laufen durch denselben Harness, auf derselben Maschine, am selben Tag. Ich tracke nur die Netzwerk-Syscalls, das reicht um zu sehen ob da etwas auf Port 123 geht:

timeout -s INT 125 strace -f -tt -e trace=%network -yy -o v57_today.strace extract57/usr/bin/fr24feed --config-file=test.ini > v57_today.log 2>&1

Das Ergebnis, und es überrascht

Mein Abnahmekriterium war simpel formuliert: sendto auf Port 123 muss wieder feuern, dann ist der NTP-Client repariert. Das Ergebnis war eine kalte Dusche und gleichzeitig die ganze Pointe dieser Geschichte:

1.0.54 (Kontrolle)1.0.56 (kaputt)1.0.57-1 (neu)
NTP sendto auf Port 1233x (eigener Client)0x0x, Client entfernt
Source-Address-Discoveryjajaja (Rest-Code)
Zeitsync-Logoffset +0.001 sFailed to synchronizeconfirmed with timesyncd
Failed-to-synchronize-Loopneinja, endlosnein
Kommt über das Sync-Gate?janeinja
ToolchainGCC 11.4.0GCC 14.2.0GCC 14.2.0

Über den gesamten 125-Sekunden-Lauf von 1.0.57 hinweg gab es kein einziges Paket auf Port 123. Null. Genau wie beim kaputten 1.0.56. Nach meinem ursprünglichen Kriterium hätte ich das Release durchfallen lassen müssen. Und trotzdem war der Bug weg. Der entscheidende Hinweis steht eine Zeile vorher im Log:

[time][i]Time synchronization confirmed with timesyncd
[feed][i]Downloading configuration
[main][i]Feed Network client started
[feed][d]Fetching configuration
[feed][e]Result: failure, message: Not found, check your key!

Der einzige Fehler im ganzen Testlauf ist „check your key!“, und der ist erwartet, weil meine Test-Config absichtlich den Fake-Key 0123… benutzt. Das heisst: der Feeder läuft komplett durch bis zur Feed-Registrierung. Genau vor diesem Punkt hingen 1.0.55 und 1.0.56 endlos in ihrer Sync-Schleife fest. Bug also weg, nur eben nicht so, wie ich gedacht hatte.

Zum Vergleich der Beweis aus dem 1.0.54-Kontrolllauf, wo der eigene NTP-Client noch feuert. Hier sieht man das sendto auf Port 123 schwarz auf weiss:

sendto(5<UDP:[25798]>, "33...", 48, 0,
       {sa_family=AF_INET, sin_port=htons(123),
        sin_addr=inet_addr("85.10.204.50")}, 16) = 48
[time][i]Time synchronized correctly, offset +0.001 seconds

Pragmatischer Workaround statt echtem Fix

Was FR24 gemacht hat, ist kein Reparieren des NTP-Clients, sondern ein Umgehen des Problems auf Architektur-Ebene. Der kaputte interne Client ist raus, übrig geblieben ist nur noch etwas Rest-Code für die Source-Address-Discovery. Die eigentliche Zeitsynchronisation delegiert der Feeder jetzt an systemd-timesyncd, also an den NTP-Dienst des Betriebssystems. Statt selbst Pakete auf Port 123 zu schicken, fragt er das OS einfach: ist deine Zeit synchron? Und wenn ja, geht es weiter.

Ehrlich gesagt finde ich das eine vernünftige Entscheidung. Ein eigener NTP-Client in einer Feeder-Software war ohnehin Reinventing the Wheel, das Betriebssystem kann das besser und macht es sowieso schon. Dass der eigentliche Bug damit nie wirklich gefunden wurde, ist aus Ingenieurssicht ein kleiner Wermutstropfen, aber für den Anwender zählt nur, dass der Feeder läuft. Und das tut er.

Das Upgrade mit Sicherheitsnetz

Erst nachdem der Testlauf sauber durch war, ging es an die Produktion. Vorher noch das alte Paket und die Config wegsichern, damit ein Rollback jederzeit ein Einzeiler bleibt:

cp /var/cache/apt/archives/fr24feed_1.0.54-0_arm64.deb /tmp/fr24test/rollback/
sudo cp /etc/fr24feed.ini /etc/fr24feed.ini.bak-20260608-161113

sudo apt-mark unhold fr24feed
sudo apt-get install -y --only-upgrade fr24feed   # 1.0.54-0 auf 1.0.57-1

# Stolperstein: das Paket STOPPT den Dienst beim Upgrade, startet ihn aber nicht neu
sudo systemctl start fr24feed

# Wieder pinnen, jetzt auf die verifiziert gute Version
sudo apt-mark hold fr24feed

Der Stolperstein mit dem nicht neu gestarteten Dienst ist eine Kleinigkeit, kostet aber Nerven wenn man es nicht weiss und sich wundert warum der Feeder nach dem Upgrade tot ist. Ein systemctl start später lief alles. Die Verifikation kam aus der monitor.json und dem Journal:

"build_version":"1.0.57-1"
"feed_status":"connected"
"feed_num_ac_tracked":"92"

[time][i]Time synchronization confirmed with timesyncd
[reader][i]Timestamp source changed from UNKNOWN to SYSTEM-VALIDATED
[feed][n]connected via UDP (fd 6)
[feed][n]working
[feed][i]sent 46,0 AC

feed_status: connected und 92 getrackte Flugzeuge. Nach Wochen auf der festgenagelten 1.0.54 ist der Feeder endlich wieder auf einer aktuellen Version und kommt sauber über das Sync-Gate. Genau das wollte ich.

Die Kehrseite, eine neue Abhängigkeit

Wer einen eigenen Feeder betreibt, sollte das hier auf dem Schirm haben: 1.0.57 spricht selbst kein NTP mehr, also braucht es jetzt einen laufenden NTP-Dienst im Betriebssystem. Auf dem Standard-Pi24-Image ist das systemd-timesyncd, und damit funktioniert es out of the box. Kurz prüfen schadet trotzdem nicht:

systemctl is-active systemd-timesyncd     # active
timedatectl show -p NTPSynchronized       # NTPSynchronized=yes

Wer timesyncd oder chrony bewusst deaktiviert hat, oder ein abgespecktes Image ganz ohne NTP-Daemon fährt, könnte mit 1.0.57 jetzt ein neues Sync-Problem bekommen. Das ist der Preis des pragmatischen Fixes: FR24 hat die Verantwortung fürs Zeit-Setzen ans OS abgegeben, und damit muss das OS sie auch wahrnehmen.

Bonus-Fund: 1.0.57 bringt native GPS-Unterstützung

Beim Stöbern im neuen Binary ist mir noch etwas aufgefallen, das für die MLAT-Frage aus Teil 1 hochinteressant ist: 1.0.57 bringt einen PositioningNmeaDecoder und eine ganze Reihe neuer gps--Direktiven mit. Das könnte heissen, dass sich der VK-162 endlich für das MLAT-Timing nutzen lässt, das ja bislang auf NOT-PERMITTED stand.

strings /usr/bin/fr24feed | grep -oE 'gps-[a-z-]+' | sort -u
#   gps-altitude gps-antenna-connected gps-base-timestamp gps-ip gps-latitude
#   gps-longitude gps-mode gps-status gps-time ...

# Welcher gps-mode-Wert ist gueltig? Durchprobiert:
#   gps-mode=serial  -> [e]Unsupported gps-mode=serial!
#   gps-mode=nmea    -> akzeptiert (einziger gueltiger Wert)

So weit, so vielversprechend. Mit gps-mode=nmea plus mlat-without-gps=no öffnet das Binary dann aber /dev/ttyACM0 nicht selbst, sondern loggt nur stoisch:

[main][i]Waiting for GPS time

An der Hardware liegt es nicht, die liefert nachweislich einen sauberen Fix mit 9 Satelliten, parallel mitgelesen:

$GPGGA,161727.00,5034.69002,N,00656.93035,E,1,09,0.86,384.0,M,...   # Fix, 9 Sat, 384 m

Meine erste Vermutung war, dass 1.0.57 die NMEA-Daten gepusht erwartet, also über die Beast- und Decoder-Strecke oder über eine Netzwerkquelle per gps-ip statt über ein direktes Serial-Open des Dongles. Statt auf der Produktion herumzuraten habe ich FR24 aber lieber direkt gefragt, welche fr24feed.ini-Schlüssel zu einem seriell angeschlossenen NMEA-GPS gehören, Device-Pfad, Baudrate und so weiter.

Update vom 9. Juni 2026: Die Antwort von Muazzam aus dem Support (weiterhin Ticket #741092) kam am nächsten Tag und war kurz, aber unmissverständlich:

No, a local gps won’t help with mlat. For good mlat you need nano second timestamps that fpga provides. Also, we dont have an support for it.

Damit ist die Frage abschliessend beantwortet, wenn auch anders als erhofft. Ein lokal angeschlossener Serial- oder NMEA-GPS ist für MLAT schlicht keine gültige Timing-Quelle, und fr24feed unterstützt diesen Fall auch gar nicht. Der Grund steckt in der Physik der Multilateration: MLAT rechnet Flugzeugpositionen aus den Laufzeitunterschieden desselben Signals an mehreren Empfängern aus. Damit das aufgeht, müssen die Empfänger ihre Empfangszeitpunkte im Nanosekunden-Bereich stempeln, und solche Zeitstempel liefert nur dedizierte FPGA-Hardware der Radarcape-Klasse. Ein NMEA-GPS über USB-Serial hat dagegen Jitter im Millisekunden-Bereich, aus der USB-Latenz und dem Timing der NMEA-Sätze. Das sind gut sechs Grössenordnungen daneben, und selbst mit einem sauberen PPS-Signal kommt man an die FPGA-Genauigkeit nicht heran.

Das ordnet auch mein gps-mode=nmea-Experiment von oben sauber ein. Die GPS-Direktiven in 1.0.57 dienen faktisch nur der Positionsangabe, nicht dem MLAT-Timing. Das beobachtete [main][i]Waiting for GPS time, ohne dass der Feeder /dev/ttyACM0 überhaupt öffnet, war also kein Konfigurationsfehler meinerseits, sondern schlicht fehlender Support für genau diesen Anwendungsfall.

Für mich heisst das, der GPS-Dongle der seit März für genau diesen Moment bereitliegt, bleibt vorerst in der Schublade. Etwas schade, aber die Begründung ist nachvollziehbar und technisch sauber. Und für alle mit dem gleichen Setup ist die Lehre eindeutig: mit einem reinen RTL-SDR plus USB-GPS lässt sich MLAT bei FR24 nicht aktivieren, egal welche fr24feed.ini-Verdrahtung man probiert. MLAT bleibt dauerhaft auf NOT-PERMITTED. Wer MLAT wirklich will, kommt um Timing-Hardware mit FPGA nicht herum.

Fazit, und die eigentliche Lehre

Die schönste Lektion steckt nicht in der Versionsnummer, sondern in meinem Abnahmekriterium. Ich war so auf den einen Syscall fixiert, dass ich beinahe das richtige Ergebnis als Fehlschlag abgehakt hätte. sendto auf Port 123 war nie das eigentliche Ziel, das war nur die zufällige Art, wie 1.0.54 die Zeit synchronisiert hat. Das richtige Erfolgskriterium war die ganze Zeit ein anderes: kommt der Feeder über das Sync-Gate, ja oder nein. Ein bestimmter Syscall ist Mittel zum Zweck, nicht der Zweck selbst. Wer Verhalten testet statt Implementierung, läuft seltener in so eine Falle.

FR24 bekommt von mir Lob für die schnelle Reaktion am Ende und einen pragmatischen Fix, der das Problem zuverlässig erledigt. Ein kleiner Kritikpunkt bleibt, dass der eigentliche Bug nie gefunden wurde, sondern nur umgangen. Aber Hand aufs Herz: ein funktionierender Feeder ist mir lieber als ein vollständig aufgeklärter, der nicht läuft. Mein Beitrag war am Ende vor allem die Reproduktion auf genau der arm64-Hardware, die FR24 im März nicht zum Fehler bringen konnte. Dass der Fix jetzt auf eben dieser Maschine hält, habe ich dem Support noch einmal zurückgemeldet, damit sie die Regression sauber abschliessen können. Manchmal ist der wertvollste Teil eines Bug-Reports, dass man hartnäckig bleibt und sauber misst.

Siehe auch:

Betreibt ihr selbst einen FR24-Feeder und seid über den NTP-Bug gestolpert, oder lasst ihr MLAT über dedizierte Timing-Hardware mit FPGA laufen? Dann lasst es mich gerne wissen, ihr dürft mich jederzeit fragen.

« Ältere Beiträge

© 2026 -=Kernel-Error=-RSS

Theme von Anders NorénHoch ↑