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.