Mein modernes OpenPGP-Setup auf macOS mit zwei YubiKeys
Dieses Tutorial ist sehr lang. Ich habe versucht es in sinnvolle Blöcke aufzuteilen, die jeweils als Lesezeichen gespeichert werden können. Auf einem Tablet / Desktop steht ein Inhaltsverzeichns bereit.
Ich wollte mein bisheriges PGP-Setup einmal grundsätzlich neu aufsetzen. Nicht einfach nur einen neuen Schlüssel erzeugen, sondern eine Architektur bauen, die ich auch in einigen Jahren noch guten Gewissens verwenden kann:
- ein möglichst wenig exponierter Primary Key
- getrennte Schlüssel für Signing, Encryption und Authentication
- Hardware-Schutz durch YubiKeys
- ein zweiter YubiKey als echtes Backup
- getestetes Disaster Recovery
- Git- und GitHub-Signing
- SSH über FIDO2
- automatische Veröffentlichung meines Public Keys über WKD
Das wichtigste vorab: Die Firmware von Yubikeys lässt sich nicht aktualisieren. Die Vorraussetzung für Ellyptic-Curve Algorithmen in den OpenPGP Slots des YubiKey ist Firmware 5.23 oder höher.
Das Ergebnis ist dieses Setup:
Wichtigster Kernpunkt dieses Setup:
Der private Primary Key befindet sich nicht in meinem normalen GnuPG-Keyring. Die drei Subkeys liegen dagegen auf beiden YubiKeys.
Dieser Blogbeitragt ist der (hoffentlich) komplette Weg dorthin – inklusive einiger Fehler, die ich während der Einrichtung gemacht habe und die man sich bei einem sauberen Neuaufbau sparen kann.
Hinweis: „State of the Art“ bedeutet hier für mich: modern, praktisch, interoperabel und mit einem YubiKey 5 nutzbar. Das Setup ist nicht post-quantum-sicher. Neuere GnuPG-Versionen besitzen inzwischen auch PQC-/Hybrid-Verfahren, die OpenPGP-Anwendung eines YubiKey 5 unterstützt diese jedoch nicht. Für ein hardwaregestütztes YubiKey-Setup sind Ed25519 und X25519 daher weiterhin eine sinnvolle Wahl.
Die Schlüsselarchitektur
Ich habe bewusst nicht einen einzelnen Schlüssel mit allen Fähigkeiten erzeugt.
Mein Primary Key besitzt ausschließlich:
- [C] Certification
Die täglichen Operationen laufen über drei Subkeys:
- [S] Signing
- [E] Encryption
- [A] Authentication
Konkret:
| Zweck | Algorithmus | Laufzeit |
|---|---|---|
| Primary / Certification | Ed25519 | 5 Jahre |
| Signing | Ed25519 | 2 Jahre |
| Encryption | cv25519 / X25519 | 2 Jahre |
| Authentication | Ed25519 | 2 Jahre |
Der Primary Key ist damit meine eigentliche kryptografische Identität. Mit ihm kann ich unter anderem neue Subkeys zertifizieren, Ablaufzeiten verlängern oder Identitäten ändern.
Im Alltag brauche ich ihn nicht.
Software
Ich verwende Homebrew, mit diesem Befehl lassen sich alle benötigen Tools installieren:
brew install gnupg pinentry-mac ykman openssh
Für die normale GnuPG-Installation:
mkdir -p ~/.gnupg
chmod 700 ~/.gnupg
Warum ~/.gnupg? Es wäre doch sinnvoll, die meisten Arbeitsschritte direkt auf einem externen Datenträger abzubilden? Das ist richtig. Allerdings hatte ich vielerlei Probleme dieses Setup nutzbar unter macOS zum Laufen zu bekommen. Ich habe es schließlich aufgegeben. Dazu später mehr.
pinentry-mac habe ich als PIN-/Passphrase-Dialog konfiguriert:
echo "pinentry-program $(brew --prefix)/bin/pinentry-mac" >> ~/.gnupg/gpg-agent.conf
gpgconf --kill gpg-agent
gpg-connect-agent /bye
Testen lässt sich das Ganze mit:
gpg-connect-agent 'GETINFO version' /bye
Erwartete Ausgabe (ungefähr):
D 2.x.x
OK
Wichtig: YubiKeys prüfen
Damit dieses Setup funktioniert, brauchen die YubiKeys mindestens Firmware 5.2.3, damit ECC-Unterstüzung vorhanden ist. Nicht jeder YubiKey hat diese Version, wodurch sich insbesondere ältere Keys nicht für dieses Setup eignen. Ich verwende zwei YubiKey 5C NFC. Informationen über diese Keys lassen sich mit ykman info und ykman openpgp info anzeigen.
Ein wichtiger Hinweis, denn es gibt direkt die eine kleine Ernüchterung: Die Firmware eines YubiKey 5 lässt sich nicht aktualisieren.
Sollte also einer oder mehrere Keys eine zu niedrige Firmware aufweisen, ist dieser Key leider nicht für dieses Setup verwendbar.
Den Primary Key erzeugen - ein Kampf gegen MacOS
Ich wollte meinen Primary Key ursprünglich direkt auf einer verschlüsselten externen SSD erzeugen: /Volumes/T7Shield/pgp. Dafür hatte ich GNUPGHOME auf dieses Volume gesetzt.
Das führte unter macOS allerdings zu:
gpg-connect-agent: Kein aktiver gpg-agent
...
can't connect to the gpg-agent
IPC "connect" Aufruf fehlgeschlagen
Ein Versuch mit:
gpgconf --create-socketdir
endete mit:
gpgconf: no /run/user dir
gpgconf: using homedir as fallback
gpgconf: error creating socket directory
Das ist auf macOS nicht überraschend: gpgconf --create-socketdir ist für Socket-Verzeichnisse unter /run/user beziehungsweise /var/run/user gedacht – eine Linux-typische Struktur.
Die robustere macOS-Lösung
Ich verwende daher für Key-Management einen separaten lokalen GnuPG-Keyring:
OFFLINE="$HOME/.gnupg-pgp-root"
mkdir -p "$OFFLINE"
chmod 700 "$OFFLINE"
Pinentry muss auch für dieses separate Home konfiguriert werden:
printf 'pinentry-program %s\n' "$(brew --prefix)/bin/pinentry-mac" > "$OFFLINE/gpg-agent.conf"
chmod 600 "$OFFLINE/gpg-agent.conf"
Danach sollte der gpg-agent gekilled werden, um die neuen Konfigurationen zu laden:
gpgconf --homedir "$OFFLINE" --kill gpg-agent
gpgconf --homedir "$OFFLINE" --launch gpg-agent
Diese Änderungen sollten unbedingt getestet werden, etwa mit:
gpg-connect-agent --homedir "$OFFLINE" 'GETINFO version' /bye
Alternativ reicht meistens bereits der erste GPG-Aufruf, da gpg-agent automatisch gestartet wird.
Sicherheitsrelevante Einschränkung
Ein lokales temporäres Key-Verzeichnis ist auf macOS funktional deutlich robuster als ein GNUPGHOME auf einem externen Volume. Es ist aber wichtig, sich nichts vorzumachen, rm -rf ~/.gnupg-pgp-root ist auf einer modernen SSD mit APFS keine garantierte forensische sichere Löschung.
Für ein besonders hohes Bedrohungsmodell würde ich die Root-Key-Erzeugung daher auf einem dedizierten Offline-System durchführen.
Für mein Setup habe ich zumindest:
- FileVault aktiviert
- den Rechner während der sensiblen Arbeiten vom Netzwerk getrennt
- eine starke Passphrase auf dem Primary Key
- den Keyring anschließend entfernt
- getestete verschlüsselte Offline-Backups angelegt
Die Schlüssel erzeugen
Den Primary Key erzeugen
Hier ist ein Detail extrem wichtig, das mir später tatsächlich Probleme gemacht hat: die OpenPGP-UID. Für einen Primary-Key wird diese in einer korrekten Form benötigt, damit beispielsweise GitHub diese auch korrekt zuordnen kann. Eine UID benötigt NAME und EMAIL.
Sie ist beispielsweise so aufgebaut:
NAME="Max Mustermann"
EMAIL="info@maxmustermann.de"
UID="$NAME <$EMAIL>"
Also am Ende Max Mustermann <info@maxmustermann.de. Die spitzen Klammern sind nicht kosmetisch. Sie werden explizit benötigt. Daher hier noch einmal zum Vergleich:
Korrekt:
Max Mustermann <info@maxmustermann.de>
Nicht:
Max Mustermann info@maxmustermann.de
Ich habe anfangs den schweren Fehler begangen, darauf nicht zu achten, was dazu führte, das ich manuell die UIDs in meinem primären Cert-Key ändern musste, was natürlich einen Rückruf des Keys zur Folge hatte. Seid nicht wie ich.
Wenn alles erledigt und geprüft ist, jetzt den Certification-Key erzeugen:
gpg --homedir "$OFFLINE" --quick-generate-key "$UID" ed25519 cert 5y
cert sorgt dafür, dass der Primary ausschließlich die Certification-Fähigkeit bekommt.
Den Fingerprint des Primary-Keys bestimmen
gpg --homedir "$OFFLINE" --list-secret-keys --keyid-format long --with-subkey-fingerprint
Mein Primary-Fingerprint lautet:
F0EE915E82B3E9CBCFA15121091DEF5773661ED3
Fingerprints sind nicht geheim. Ich speichere ihn für die weiteren Befehle, damit er nicht dauernd kopiert werden muss:
export FPR="F0EE915E82B3E9CBCFA15121091DEF5773661ED3"
Wer diesem Tutorial folgt, ersetzt ihn natürlich durch den eigenen Fingerprint.
Den Signing-Subkey erzeugen
gpg --homedir "$OFFLINE" --quick-add-key "$FPR" ed25519 sign 2y
Bitte den Fingerprint des Signing-Subkeys notieren.
Den Encryption-Subkey erzeugen
gpg --homedir "$OFFLINE" --quick-add-key "$FPR" cv25519 encr 2y
Bitte den Fingerprint des Encryption-Subkeys notieren.
cv25519ist die von GnuPG verwendete OpenPGP-Bezeichnung für den Curve25519-basierten Encryption-Key; auf dem YubiKey landet er als X25519-Decipher-Key.
Den Authentication-Subkey erzeugen
gpg --homedir "$OFFLINE" --quick-add-key "$FPR" ed25519 auth 2y
Bitte den Fingerprint des Authentication-Subkeys notieren.
Die gesamte Struktur kontrollieren
Mit folgendem Befehl lassen sich die erzeugten Keys auflisten:
gpg --homedir "$OFFLINE" --list-secret-keys --keyid-format long --with-subkey-fingerprint
In meinem Fall:
sec ed25519/... [C]
F0EE915E82B3E9CBCFA15121091DEF5773661ED3
uid Florian Schuttkowski <mail@florianschuttkowski.com>
ssb ed25519/... [S]
7A86ADC8D74BE03EE37F085E7F3A023218DB3673
ssb cv25519/... [E]
805E415E16810EA90718A68E15745B3844922823
ssb ed25519/... [A]
84471B2E9D08B5569531F85AC025415EB4BCF1C2
Genau diese Struktur ist das Ziel.
Was ich bezüglich meiner UID verbockt hatte, im Detail
Bei meinem ersten Versuch hatte ich versehentlich diese UID erzeugt:
Florian Schuttkowski mail@florianschuttkowski.com
statt:
Florian Schuttkowski <mail@florianschuttkowski.com>
GnuPG selbst konnte damit arbeiten. GitHub später nicht. GitHub meldete bei signierten Commits:
The email in this signature doesn’t match the committer email.
Der Schlüssel war kryptografisch korrekt – die Mailadresse war in OpenPGP aber nicht als E-Mail-Feld der UID hinterlegt. Deshalb im Tutorial unbedingt von Anfang an UID="$NAME <$EMAIL>" verwenden.
Das Backup – bevor ein YubiKey beschrieben wird
Das ist einer der wichtigsten Punkte des gesamten Setups: Vor keytocard muss ein vollständiges Backup existieren. Denn keytocard kopiert den privaten Schlüssel nicht einfach zusätzlich auf die Smartcard. GnuPG ersetzt das lokale Secret-Key-Material im verwendeten Keyring durch einen Smartcard-Stub.
Ein Backup der Keys
Ich verwende meine verschlüsselte T7 Shield:
BACKUP="/Volumes/T7Shield/pgp-backup"
mkdir -p "$BACKUP"
chmod 700 "$BACKUP"
Vollständiges Secret-Key-Backup:
gpg --homedir "$OFFLINE" --armor --output "$BACKUP/full-secret-key.asc" --export-secret-keys "$FPR"
Public Key:
gpg --homedir "$OFFLINE" --armor --output "$BACKUP/public-key.asc" --export "$FPR"
Das Revocation Certificate erzeugen
Zusätzlich erstelle ich, obwohl GnuPG das bereits tut, bewusst ein separates Widerrufszertifikat:
gpg --homedir "$OFFLINE" --armor --output "$BACKUP/revoke.asc" --generate-revocation "$FPR"
Dieses Zertifikat darf nicht veröffentlicht oder versehentlich importiert werden. Wer es besitzt, kann den Key zwar nicht übernehmen, aber widerrufen.
Wie erwähnt, GnuPG erzeugt bei normalen Key-Generierungsverfahren zusätzlich automatisch Revocation-Material unter openpgp-revocs.d/. Das separate revoke.asc macht meinen Recovery-Satz aber deutlich verständlicher.
Die YubiKeys provisionieren
YubiKey #1 vorbereiten
Ich verwende den YubiKey mit Firmware 5.8.0 als Daily Driver. Es sollte dabei immer nur ein YubiKey gleichzeitig eingesteckt sein. Vor dem Reset unbedingt sicherstellen, dass sich keine wichtigen OpenPGP-Keys auf dem Gerät befinden. Das lässt sich mit ykman openpgp info anzeigen.
Anschließend:
ykman openpgp reset
Dieser Reset betrifft die OpenPGP-Anwendung des YubiKeys. FIDO2, PIV, OATH usw. werden dadurch nicht zurückgesetzt. Danach gelten wieder die OpenPGP-Defaults die YubiCo für die Keys vergibt:
User PIN: 123456
Admin PIN: 12345678
Wichtig: KDF vor einer PIN-Änderung aktivieren
KDF steht für Key Derivation Function und wird in OpenPGP smart cards wie den YubiKeys dafür genutzt, PINs nicht als Klartext sondern als sichere Hashes zu hinterlegen. Dieses Vorgehen schützt die Keys gegen brute-force-Angriffe und Offenlegung. Hier hatte ich einen weiteren Stolperstein. Die saubere Reihenfolge ist:
- OpenPGP-App resetten
- KDF aktivieren
- PINs ändern
- Key Attributes setzen
- Keys übertragen
Nicht KDF erst aktivieren, nachdem bereits Keys auf der Karte liegen. Das hat mich Stunden gekostet. grmll. KDF lässt sich auf folgende Weise aktivieren:
gpg --card-edit
Dann:
gpg/card> admin
gpg/card> kdf-setup
Die PINs der OpenPGP Smartcard ändern
gpg/card> passwd
Dort ändern:
- User PIN
- Admin PIN
Die Admin-PIN sollte mindestens acht Zeichen lang sein. Ich verwende deutlich mehr.
Warum KDF zuerst?
Bei meinem ersten Versuch wurde die Admin-PIN nach Aktivierung von KDF plötzlich nicht mehr akzeptiert. Das ist eine historisch bekannte Fehlerklasse bei GnuPG/OpenPGP-Karten, insbesondere bei problematischen PIN-Längen. Da noch keine Keys auf dem YubiKey lagen, war die Lösung einfach:
ykman openpgp reset
Leider bedeutet das die Provisionierung sauber neu beginnen zu müssen.
Darum:
KDF zuerst, danach eine ausreichend lange Admin-PIN.
Kontrolle:
gpg --card-status
Dort sollte stehen:
KDF setting ......: on
Key Attributes auf Curve25519 umstellen
Ein frisch zurückgesetzter YubiKey zeigt bei mir:
Key attributes ...: rsa2048 rsa2048 rsa2048
Wir wollen aber ed25519 cv25519 ed25519.
Dafür:
gpg --card-edit
gpg/card> admin
gpg/card> key-attr
- Für den Signature Slot: ECC, Curve 25519
- Für den Encryption Slot: ECC, Curve 25519
- Für den Authentication Slot: ECC, Curve 25519
GnuPG/YubiKey interpretieren das anschließend passend zum jeweiligen Slot:
Signature → Ed25519
Encryption → X25519/cv25519
Authentication → Ed25519
Kontrolle:
gpg --card-status
Erwartete Ausgabe:
Key attributes ...: ed25519 cv25519 ed25519
Wichtig: Die Algorithm Attributes sollte man vor dem Import der Keys festlegen. Ein späteres inkompatibles Ändern der Attributes kann vorhandenes Key-Material auf der Karte unbrauchbar machen beziehungsweise löschen.
Nicht aus dem Root-Keyring direkt auf den YubiKey übertragen
Das ist eine weitere wichtige Designentscheidung.
Auf keinen Fall ~/.gnupg-pgp-root für keytocard verwenden. Stattdessen erstelle ich einen temporären Transfer-Keyring.
Warum?
Weil keytocard die lokalen privaten Schlüssel im aktuellen GnuPG-Keyring durch Smartcard-Stubs ersetzt.
Der Root-Keyring soll aber vollständig erhalten bleiben, bis Backup und Recovery final verifiziert sind.
Der Transfer-Keyring für YubiKey #1
Als erstes ein neuen, leeren Keyring erzeugen, der für den Transfer auf den YubiKey genutzt werden soll.
TRANSFER="$HOME/.gnupg-yubikey-transfer-1"
mkdir -p "$TRANSFER"
chmod 700 "$TRANSFER"
Pinentry muss ebenfalls angepasst werden:
printf 'pinentry-program %s\n' "$(brew --prefix)/bin/pinentry-mac" > "$TRANSFER/gpg-agent.conf"
chmod 600 "$TRANSFER/gpg-agent.conf"
Jetzt importiere ich das Backup, nicht den Root-Keyring:
gpg --homedir "$TRANSFER" --import "$BACKUP/full-secret-key.asc"
Kontrollieren lässt sich das mit:
gpg --homedir "$TRANSFER" --list-secret-keys --keyid-format long --with-subkey-fingerprint
Es müssen wieder alle vier privaten Keys vorhanden sein: [C], [S], [E] und [A].
Signing-Key auf YubiKey #1 übertragen
gpg --homedir "$TRANSFER" --edit-key "$FPR"
Dann prüfen, welche Keys, welche Nummer erhalten haben:
gpg> list
Die nachfolgenden Nummern sollte man nicht blind übernehmen, sondern anhand der angezeigten Usage-Flags kontrollieren und entsprechend anpassen.
- Signing auswählen:
gpg> key 1 gpg> keytocard, Auswahl:Signature key- Danach wieder abwählen:
gpg> key 1
Encryption-Key auf YubiKey #1 übertragen
Die nachfolgenden Nummern sollte man nicht blind übernehmen, sondern anhand der angezeigten Usage-Flags kontrollieren und entsprechend anpassen.
- Signing auswählen:
gpg> key 2 gpg> keytocard, Auswahl:Encryption key- Danach wieder abwählen:
gpg> key 2
Authentication-Key auf YubiKey #1 übertragen
Die nachfolgenden Nummern sollte man nicht blind übernehmen, sondern anhand der angezeigten Usage-Flags kontrollieren und entsprechend anpassen.
- Signing auswählen:
gpg> key 3 gpg> keytocard, Auswahl:Authentication key- Danach wieder abwählen:
gpg> key 3
Wurden alle Keys übertragen, abschließend die Änderungen auf die Smartcard schreiben:
gpg> save
Fingerprints auf dem YubiKey kontrollieren
gpg --card-status
Diese Fingerprints müssen exakt mit den drei Subkeys übereinstimmen.
Touch für jede kryptografische Operation aktivieren
Ich möchte nicht, dass Malware im Hintergrund beliebig Signaturen oder Entschlüsselungen über meinen YubiKey durchführen kann. Deshalb muss der YubiKey so konfiguriert werden, dass eine Verwendung eine physische Berührung erfordert:
ykman openpgp keys set-touch sig on
ykman openpgp keys set-touch enc on
ykman openpgp keys set-touch aut on
Kontrolle:
gpg --card-status
Bei UIF setting sollte sinngemäß stehen:
Sign=on Decrypt=on Auth=on
Ich verwende bewusst on, nicht fixed. fixed kann später nicht einfach wieder deaktiviert werden, ohne das jeweilige private Key-Material auf der Karte zu ersetzen.
Prüfen, ob alles funktioniert
Signing testen
Um sicherzustellen, dass alles ordnungsgemäß funktioniert, sollten alle Funktionalitäten einmal geprüft werden. Dafür brauchen wir eine beliebige Testdatei:
echo "OpenPGP YubiKey Test" > /tmp/pgp-test.txt
Diese Datei anschließend mit OpenPGP + YubiKey signieren:
gpg --homedir "$TRANSFER" --armor --detach-sign /tmp/pgp-test.txt
Um zu prüfen ob alles funktioniert hat:
gpg --homedir "$TRANSFER" --verify /tmp/pgp-test.txt.asc /tmp/pgp-test.txt
Idealerweise:
Korrekte Signatur von "Florian Schuttkowski ..."
Im temporären Transfer-Keyring erscheint zunächst zusätzlich:
WARNUNG: Dieser Schlüssel trägt keine vertrauenswürdige Signatur
Das bedeutete nicht, dass die Signatur falsch war. Dem neuen Transfer-Keyring fehlte lediglich das lokale Ownertrust. Das beheben wir später.
Encryption und Decryption testen
AUch dafür brauchen wir eine beliebige Datei:
echo "Secret YubiKey Test" > /tmp/pgp-secret.txt
Verschlüsseln mit:
gpg --homedir "$TRANSFER" --armor --output /tmp/pgp-secret.txt.asc --encrypt --recipient "$FPR" /tmp/pgp-secret.txt
Ascnhließend das Original entfernen:
rm /tmp/pgp-secret.txt
Und nun wieder entschlüsseln:
gpg --homedir "$TRANSFER" --output /tmp/pgp-secret.txt --decrypt /tmp/pgp-secret.txt.asc
Die Datei öffnen und verifizieren, dass ihr Inhalt korrekt ist.
Den Authentication-Subkey testen
Dieses Verhalten lässt sich am einfachsten über SSH testen. Der [A]-Subkey kann von GnuPG als SSH-Key verwendet werden.
Public Key exportieren:
gpg --homedir "$TRANSFER" --export-ssh-key "$FPR"
Um ihn testweise über gpg-agent als SSH-Agent sichtbar zu machen:
echo "enable-ssh-support" >> "$TRANSFER/gpg-agent.conf"
Agent neu starten:
gpgconf --homedir "$TRANSFER" --kill gpg-agent
gpgconf --homedir "$TRANSFER" --launch gpg-agent
SSH-Socket setzen:
export SSH_AUTH_SOCK="$(
gpgconf --homedir "$TRANSFER" --list-dirs agent-ssh-socket
)"
Dann:
ssh-add -L
Damit konnte ich verifizieren, dass mein [A]-Subkey grundsätzlich als SSH-Authentication-Key funktioniert. Für meinen normalen SSH-Betrieb habe ich mich später allerdings bewusst für FIDO2 statt OpenPGP-Auth entschieden.
Mehr dazu weiter unten.
YubiKey #2 als echten Cold Spare aufbauen
Für Redundanz wollte ich nicht einfach einen zweiten eigenen Satz Subkeys erzeugen. Insbesondere bei Encryption wäre das problematisch. Wenn jemand heute an meinen Encryption-Key E1 verschlüsselt, kann nur der passende private E1 diese Daten später entschlüsseln. Deshalb besitzen beide YubiKeys dieselben drei OpenPGP-Subkeys.
YubiKey #1 → S1 / E1 / A1
YubiKey #2 → S1 / E1 / A1
Das ist anders als bei FIDO2, wo ich später zwei unterschiedliche Credentials erzeuge.
Den zweiten YubiKey vorbereiten
Ersten YubiKey abziehen. Zweiten einstecken. Dann dieselbe Vorbereitung:
ykman openpgp reset
Danach:
gpg --card-edit
gpg/card> admin
gpg/card> kdf-setup
gpg/card> passwd
gpg/card> key-attr
Wieder:
Signature → ECC / Curve25519
Encryption → ECC / Curve25519
Authentication → ECC / Curve25519
Kontrolle:
gpg --card-status
KDF setting ......: on
Key attributes ...: ed25519 cv25519 ed25519
Frischen Transfer-Keyring für YubiKey #2 verwenden
Den ersten Transfer-Keyring darf man nicht einfach weiterverwenden, weil keytocard dort bereits Smartcard-Stubs erzeugt hat.
Deshalb:
TRANSFER2="$HOME/.gnupg-yubikey-transfer-2"
mkdir -p "$TRANSFER2"
chmod 700 "$TRANSFER2"
Pinentry:
printf 'pinentry-program %s\n' "$(brew --prefix)/bin/pinentry-mac" \
> "$TRANSFER2/gpg-agent.conf"
chmod 600 "$TRANSFER2/gpg-agent.conf"
Dann erneut das vollständige Backup importieren:
gpg --homedir "$TRANSFER2" \
--import "$BACKUP/full-secret-key.asc"
Und den Key editieren:
gpg --homedir "$TRANSFER2" \
--edit-key "$FPR"
Und wieder:
→ key 1 → keytocard → Signature → key 1
→ key 2 → keytocard → Encryption → key 2
→ key 3 → keytocard → Authentication → key 3
→ save
Fingerprints beider YubiKeys vergleichen und Touch-Policy aktivieren
gpg --card-status
Die drei Fingerprints müssen exakt mit YubiKey #1 übereinstimmen. Danach auch auf YubiKey #2:
ykman openpgp keys set-touch sig on
ykman openpgp keys set-touch enc on
ykman openpgp keys set-touch aut on
Der entscheidende Backup-Test
Die bereits mit YubiKey #1 getestete verschlüsselte Datei muss auch YubiKey #2 entschlüsseln können:
gpg --homedir "$TRANSFER2" \
--decrypt /tmp/pgp-secret.txt.asc
Das funktionierte. Zusätzlich habe ich mit YubiKey #2 noch einmal signiert. Damit ist er nicht nur theoretisch ein Backup, sondern tatsächlich ein getesteter Cold Spare.
Ein wichtiger GnuPG-Haken bei identischen Smartcards
GnuPG speichert in seinen lokalen Smartcard-Stubs auch Informationen darüber, auf welcher Karte sich der jeweilige Schlüssel befindet. Deshalb sollte man zwei identische YubiKeys nicht wie zwei völlig transparente RAID-Platten betrachten. Mein Modell lautet:
- YubiKey #1 = Daily Driver
- YubiKey #2 = Cold Spare
Wenn der Daily Driver kaputtgeht und GnuPG beim Backup-Key weiterhin nach der alten Karten-Seriennummer fragt, muss der lokale Smartcard-Stub neu gelernt werden.
Dazu zuerst:
gpg -K --with-keygrip "$FPR"
Die Keygrips der drei Subkeys notieren. Im normalen ~/.gnupg sind für diese Identität nur Smartcard-Stubs vorhanden. Falls ein Wechsel auf die Backup-Karte nötig wird, können die entsprechenden Stub-Dateien unter ~/.gnupg/private-keys-v1.d/ entfernt werden.
Danach:
gpgconf --kill gpg-agent
Backup-YubiKey einstecken und:
gpg --card-status
ausführen. Dabei sollte GnuPG die Karte neu lernen.
Hierbei sollte man äußerst vorsichtig arbeiten und niemals wahllos Dateien aus
private-keys-v1.dlöschen.
Aufräum-Arbeiten
Den normalen macOS-Keyring einrichten
Nach erfolgreicher Provisionierung wechsel ich zurück auf mein default Verzeichnis ~/.gnupg. Falls GNUPGHOME gesetzt ist:
unset GNUPGHOME
gpgconf --list-dirs homedir
Jetzt nur den Public Key importieren:
gpg --import "$BACKUP/public-key.asc"
Daily-Driver-YubiKey einstecken:
gpg --card-status
gpg --list-secret-keys --keyid-format long --with-subkey-fingerprint
Sinngemäß muss hier stehen:
sec# ed25519 ... [C]
ssb> ed25519 ... [S]
ssb> cv25519 ... [E]
ssb> ed25519 ... [A]
Wichtig dabei: Der private Primary Key ist nicht im normalen Alltags-Keyring vorhanden. Das lässt sich am # vor dem primären Key erkennen.
Backup wirklich wiederherstellen
Jetzt kommt der Test, den ich für wichtiger halte als jede ls-Ausgabe. Einen komplett frischen Restore-Keyring erzeugen:
RESTORE="$HOME/.gnupg-pgp-restore-test"
mkdir -p "$RESTORE"
chmod 700 "$RESTORE"
Backup importieren:
gpg --homedir "$RESTORE" --import "$BACKUP/full-secret-key.asc"
gpg --homedir "$RESTORE" --list-secret-keys --keyid-format long --with-subkey-fingerprint
Jetzt beide YubiKeys abziehen.
Dann:
echo "Offline restore test" > /tmp/offline-restore-test.txt
Und mit dem restaurierten Software-Key signieren:
gpg --homedir "$RESTORE" --armor --detach-sign /tmp/offline-restore-test.txt
Wenn das ohne YubiKey funktioniert, ist bewiesen:
Das Backup enthält echtes privates Schlüsselmaterial und nicht nur Smartcard-Stubs.
Anschließend noch die Signatur prüfen:
gpg --homedir "$RESTORE" --verify /tmp/offline-restore-test.txt.asc /tmp/offline-restore-test.txt
Erst danach den Test-Keyring entfernen:
rm -rf "$RESTORE"
Transfer-Keyrings entfernen
Nachdem beide YubiKeys und das Backup erfolgreich getestet wurden:
rm -rf ~/.gnupg-yubikey-transfer-1
rm -rf ~/.gnupg-yubikey-transfer-2
Auch den temporären Root-Keyring brauche ich im Alltag nicht mehr. Aber erst nachdem mindestens ein vollständiges Backup zuverlässig getestet wurde. Noch besser: Zwei physisch getrennte Offline-Backups besitzen.
Der Alltag
Ownertrust setzen
Im normalen ~/.gnupg kann die eigene UID nach dem Import zunächst als nicht vertrauenswürdig gelten.
Deshalb:
gpg --edit-key "$FPR"
gpg> trust
5 = ultimatives Vertrauen
gpg> quit
Das verändert nur den lokalen Trust-Store. Es veröffentlicht nichts.
Default-Key konfigurieren
~/.gnupg/gpg.conf mit einem Texteditor öffnen und folgendes hinzufügen:
default-key F0EE915E82B3E9CBCFA15121091DEF5773661ED3
Dabei den Fingerprint durch den eigenen Fingerprint des Primary-Keys ersetzen. Damit kann man anschließend einfach:
gpg --armor --detach-sign datei.txt
verwenden. GnuPG wählt automatisch den gültigen [S]-Subkey.
Git-Commits mit dem YubiKey signieren
Git konfigurieren
Jetzt konfigurieren wir Git so, dass Commits standardmäßig OpenPGP-signiert werden.
git config --global gpg.format openpgp
git config --global user.signingkey "$FPR"
git config --global commit.gpgsign true
git config --global tag.gpgSign true
git config --global gpg.program "$(which gpg)"
# Für zsh:
echo 'export GPG_TTY=$(tty)' >> ~/.zshrc
source ~/.zshrc
Kontrolle:
git config --global --get gpg.format
git config --global --get user.signingkey
git config --global --get commit.gpgsign
git config --global --get tag.gpgSign
git config --global --get gpg.program
Git-Signing lokal testen
Am Besten in einem neuen Repository testen:
mkdir /tmp/gpg-git-test
cd /tmp/gpg-git-test
git init
echo "YubiKey Git signing test" > README.md
git add README.md
git commit -m "Test signed commit"
Der YubiKey sollte blinken und einen Touch verlangen.
Kontrolle:
git log --show-signature -1
Bei mir wurde mit dem tatsächlichen [S]-Subkey signiert:
7A86ADC8D74BE03EE37F085E7F3A023218DB3673
GitHub „Verified“
Damit GitHub den Commit als Verified markiert, muss mein Public Key im Account hinterlegt sein.
Export:
gpg --armor --export "$FPR"
Den gesamten Block, der ungefähr so aussieht kopieren:
-----BEGIN PGP PUBLIC KEY BLOCK-----
...
-----END PGP PUBLIC KEY BLOCK-----
habe ich bei GitHub unter Settings → SSH and GPG keys → New GPG key eingetragen.
Danach muss außerdem die Committer-Mailadresse stimmen:
git config --global user.email
Bei mir:
mail@florianschuttkowski.com
Diese Adresse muss:
- als echte E-Mail-Adresse in einer UID des OpenPGP-Keys stehen
- im GitHub-Account verifiziert sein
- mit der Committer-E-Mail des Commits übereinstimmen
Genau hier fiel mein früherer UID-Fehler auf.
Bonus: Eine falsche UID nachträglich reparieren
Wer bereits versehentlich eine falsche UID konfiguriert hat, hat ein Problem, aber auch Glück, denn er muss keinen komplett neuen Schlüssel bauen. Dafür wird der Primary Key aus dem Backup temporär wiederhergestellt:
OFFLINE="$HOME/.gnupg-pgp-maintenance"
mkdir -p "$OFFLINE"
chmod 700 "$OFFLINE"
gpg --homedir "$OFFLINE" --import "$BACKUP/full-secret-key.asc"
Korrekte UID hinzufügen:
gpg --homedir "$OFFLINE" --quick-add-uid "$FPR" 'Max Mustermann <mail@maxmustermann.de>'
Danach muss die alte UID widerrufen werden:
gpg --homedir "$OFFLINE" \
--edit-key "$FPR"
Mit gpg> list die alte falsche UID identifizieren.
Dann beispielsweise:
gpg> uid 1
gpg> revuid
gpg> save
Unbedingt kontrollieren, dass wirklich die falsche UID ausgewählt wurde. Danach Public und Secret Backup neu exportieren:
gpg --homedir "$OFFLINE" --armor --output "$BACKUP/public-key-new.asc" --export "$FPR"
gpg --homedir "$OFFLINE" --armor --output "$BACKUP/full-secret-key-new.asc" --export-secret-keys "$FPR"
Nach Kontrolle die alten Dateien ersetzen. Den neuen Public Key wieder ins normale GnuPG importieren:
gpg --import "$BACKUP/public-key.asc"
und bei GitHub aktualisieren. Die YubiKeys müssen dafür nicht neu provisioniert werden. Ihre drei Subkeys haben sich ja nicht geändert.
OpenSSH
Warum ich für SSH nicht den OpenPGP-Auth-Key verwende
Der [A]-Key funktioniert grundsätzlich für SSH. Der Datenfluss wäre allerdings:
Moderne OpenSSH-Versionen können FIDO2-Security-Keys direkt verwenden:
Das ist einfacher und trennt SSH sauber von meiner OpenPGP-Infrastruktur. Deshalb nutze ich [A] zwar als vorhandene OpenPGP-Fähigkeit, für meine normalen SSH-Zugänge aber FIDO2.
FIDO2-SSH unter macOS
Hier gibt es eine wichtige macOS-Besonderheit: Apples mitgeliefertes OpenSSH ist nicht mit der benötigten libfido2-Unterstützung gebaut. Deshalb sollte man unbedingt, falls nicht bereits geschehen, Homebrew installieren und openssh aktualisieren.
brew install openssh
Anschließend den Homebrew-Pfad ermitteln:
brew --prefix openssh
Dann beispielsweise:
OPENSSH_PREFIX="$(brew --prefix openssh)"
export PATH="$OPENSSH_PREFIX/bin:$PATH"
Für ein dauerhaftes zsh-Setup kann dieser Pfad entsprechend in .zshrc aufgenommen werden.
Kontrolle:
which ssh
which ssh-keygen
ssh -V
Wichtig ist, dass wirklich das Homebrew-OpenSSH verwendet wird.
FIDO2-PIN setzen
Die FIDO2-PIN ist nicht die OpenPGP-PIN. Der YubiKey hat mehrere voneinander getrennte Anwendungen:
- OpenPGP PIN
- FIDO2 PIN
- PIV PIN
FIDO2 prüfen und setzen / ändern:
ykman fido info
ykman fido access change-pin
Resident FIDO2 SSH-Key auf YubiKey #1
Ich verwende folgenden Befehl:
ssh-keygen \
-t ed25519-sk \ # Ed25519 auf einem Security Key
-O resident \ # Credential ist auf dem YubiKey auffindbar
-O verify-required \ # FIDO2 User Verification / PIN erforderlich
-O application=ssh:personal \ # Eindeutige Credential-ID
-C "Florian SSH YubiKey 1" \
-f ~/.ssh/id_ed25519_sk_yubikey1
Lokalen entstehen dadurch:
- ~/.ssh/id_ed25519_sk_yubikey1
- ~/.ssh/id_ed25519_sk_yubikey1.pub
Die Datei ohne .pub ist bei einem FIDO2-Security-Key kein klassischer exportierter Private Key. Sie enthält im Wesentlichen den Handle, über den OpenSSH das Hardware-Credential verwendet. Der eigentliche private Schlüssel verlässt den YubiKey nicht.
Resident FIDO2 SSH-Key auf YubiKey #2
Hier mache ich bewusst nicht dasselbe wie bei OpenPGP.
Bei FIDO2 bekommt YubiKey #2 ein eigenes Credential:
ssh-keygen \
-t ed25519-sk \
-O resident \
-O verify-required \
-O application=ssh:personal-backup \
-C "Florian SSH YubiKey 2" \
-f ~/.ssh/id_ed25519_sk_yubikey2
Auf meinen Servern hinterlege ich beide Public Keys in ~/.ssh/authorized_keys. Damit kann jeder der beiden YubiKeys unabhängig authentifizieren.
Resident Key auf neuem Rechner wiederherstellen
Ein Vorteil von resident: Auf einem neuen Rechner kann das Credential vom YubiKey wieder entdeckt werden.
ssh-keygen -K
Das erzeugt wieder die lokalen Handle-/Public-Key-Dateien. Das ist einer der Gründe, warum ich Resident Credentials für diesen Einsatzzweck bevorzuge.
SSH-Config
Beispielsweise:
Host myserver
HostName example.com
User florian
IdentityFile ~/.ssh/id_ed25519_sk_yubikey1
IdentitiesOnly yes
Für den normalen Betrieb verwende ich den Daily Driver. Der zweite FIDO2-Key bleibt das Backup.
Public Keys über WKD veröffentlichen
Als letzten großen Baustein wollte ich meinen Public Key automatisch über meine E-Mail-Adresse auffindbar machen. Dafür verwende ich WKD – Web Key Directory. Ein OpenPGP-Client kann dadurch aus der Mailadresse automatisch meinen Public Key ermitteln.
Ich verwende die Advanced Method über openpgpkey.florianschuttkowski.com.
WKD-Struktur mit gpg-wks-client erzeugen
Zuerst prüfen ob der wks client installiert ist, falls nicht mit homebrew installieren:
command -v gpg-wks-client
Dann:
WKD="$HOME/wkd"
mkdir -p "$WKD/.well-known/openpgpkey"
Jetzt lasse ich GnuPG die korrekte Hash-/Verzeichnisstruktur erzeugen:
gpg-wks-client --install-key -C "$WKD/.well-known/openpgpkey" "$FPR" "$EMAIL"
Danach:
find "$WKD" -type f
Für die Advanced Method entsteht sinngemäß:
.well-known/
└── openpgpkey/
└── florianschuttkowski.com/
├── policy
└── hu/
└── <WKD-HASH>
Die policy-Datei kann leer sein.
Wichtig: Die Datei unter hu/ ist kein ASCII-armored Public Key, sondern das von WKD erwartete binäre Format. Das übernimmt gpg-wks-client für mich.
WKD-URL prüfen
gpg-wks-client --print-wkd-url mail@florianschuttkowski.com
Damit sehe ich exakt, unter welcher URL GnuPG meinen Key später sucht.
WKD mit Hugo
Meine Website läuft mit Hugo auf Cloudflare Pages.
Hugo kopiert Dateien unter static/ unverändert in das erzeugte Webroot.
Deshalb kopiere ich:
cp -R "$WKD/.well-known" ./static/
Im Repository sieht es anschließend so aus:
static/
└── .well-known/
└── openpgpkey/
└── florianschuttkowski.com/
├── policy
└── hu/
└── <HASH>
Dann:
git add static/.well-known
git commit -m "Add OpenPGP WKD"
git push
Dank des vorherigen Git-Setups ist natürlich auch dieser Commit wieder GPG-signiert.
Cloudflare-Subdomain einrichten
In Cloudflare Pages: Workers & Pages → mein Pages-Projekt → Custom domains → Set up a domain
Dort openpgpkey.zieldomain.com eintragen.
Da mein DNS ebenfalls bei Cloudflare liegt, kann Cloudflare den passenden DNS-Eintrag selbst erzeugen. Ich würde die Subdomain über Pages als Custom Domain registrieren, nicht einfach vorher manuell irgendeinen CNAME anlegen.
Keine Redirects auf dem WKD-Pfad
Wichtig: openpgpkey.florianschuttkowski.com darf nicht durch eine globale Redirect-Regel wieder auf eine der beiden Hauptdomains umgeleitet werden:
- florianschuttkowski.com
- www.florianschuttkowski.com
Das gilt insbesondere für /.well-known/openpgpkey/.
Der konkrete WKD-Key muss direkt mit HTTP 200 erreichbar sein.
WKD testen
Ziel-URL ermitteln:
WKD_URL="$(
gpg-wks-client --print-wkd-url mail@florianschuttkowski.com
)"
Dann:
curl -I "$WKD_URL"
# Erwartet:
HTTP/2 200
Der wichtigere Test erfolgt aber mit einem komplett frischen GnuPG-Keyring:
WKDTEST="$(mktemp -d)"
chmod 700 "$WKDTEST"
Dann:
gpg --homedir "$WKDTEST" --auto-key-locate clear,wkd --locate-keys mail@florianschuttkowski.com
Wenn dein Primary-Fingerprint erscheint funktioniert das WKD Ende-zu-Ende.
Danach:
rm -rf "$WKDTEST"
Wartung und Pflege
Ein solches Setup ist nicht „einmal einrichten und vergessen“. Ich plane folgende Routine.
Alle sechs Monate
Beide YubiKeys testen:
- Signing
- Decryption
- FIDO2 SSH
Zusätzlich:
- GitHub Verified prüfen
- WKD testen
- Backup-Medium kontrollieren
Einmal jährlich
Vollständigen Restore-Test des Secret-Key-Backups durchführen. Also tatsächlich:
- Backup importieren
- YubiKeys abziehen
- Signieren
- Signatur prüfen
Vor Ablauf der Subkeys
Meine Subkeys laufen nach zwei Jahren ab. Den Primary temporär aus dem Offline-Backup wiederherstellen und beispielsweise die Subkeys verlängern:
gpg --homedir "$OFFLINE" \
--quick-set-expire \
"$FPR" \
2y \
7A86ADC8D74BE03EE37F085E7F3A023218DB3673 \
805E415E16810EA90718A68E15745B3844922823 \
84471B2E9D08B5569531F85AC025415EB4BCF1C2
Den Primary selbst kann ich separat verlängern:
gpg --homedir "$OFFLINE" --quick-set-expire "$FPR" 5y
Danach müssen aktualisiert werden:
- Public-Key-Export
- Secret-Key-Backup
- normaler ~/.gnupg-Keyring
- GitHub-Key, falls notwendig
- WKD
Die eigentlichen privaten Subkeys auf den YubiKeys ändern sich durch eine reine Verlängerung nicht.
Was tun, wenn ein YubiKey kaputtgeht?
Wenn der Daily Driver defekt, aber nicht kompromittiert ist:
- Cold-Spare verwenden
- GnuPG gegebenenfalls den Backup-YubiKey neu lernen lassen
- Ersatz-YubiKey beschaffen
- dieselben Subkeys aus dem Offline-Backup auf das Ersatzgerät übertragen
Was tun, wenn ein YubiKey gestohlen wird?
Das ist etwas anderes. Der gestohlene YubiKey besitzt echte Kopien der Keys. Touch und PIN bieten erheblichen Schutz, trotzdem würde ich bei einem tatsächlich verlorenen oder gestohlenen Gerät die betroffenen Subkeys als potenziell kompromittiert betrachten.
Deshalb:
- Offline-Primary wiederherstellen
- alte Subkeys widerrufen
- neue
[S],[E],[A]erzeugen - verbleibenden und neuen YubiKey neu provisionieren
- neuen Public Key veröffentlichen
- WKD aktualisieren
- GitHub aktualisieren
Wichtig: Da beide meiner YubiKeys dieselben OpenPGP-Subkeys tragen, macht ein Widerruf auch die Kopien auf dem Backup-YubiKey ungültig. Der Cold Spare hilft mir also beim unmittelbaren Recovery – bei einem Diebstahl sollte ich anschließend trotzdem auf neue Subkeys rotieren.
Was tun, wenn der Primary Key kompromittiert wird?
Das ist der große Notfall. Dann verwende ich revoke.asc um die komplette OpenPGP-Identität zu widerrufen und erzeuge einen neuen Primary Key.
Genau deshalb liegt das Revocation Certificate separat im Recovery-Backup.
Fazit
Der schwierigste Teil dieses Setups war am Ende gar nicht die Kryptografie.
Ed25519, X25519, Subkeys und YubiKeys sind konzeptionell relativ klar.
Die eigentlichen Fallstricke lagen in den Details:
gpg-agentund Unix-Sockets mit einem externenGNUPGHOMEunter macOS- separate
gpg-agent.conffür temporäre GnuPG-Homes - die korrekte Reihenfolge aus KDF, PIN und Key Attributes
- die Tatsache, dass
keytocardden lokalen Secret Key durch einen Stub ersetzt - ein eigener frischer Transfer-Keyring für jeden Backup-YubiKey
- die exakte OpenPGP-UID mit
<mail@example.org> - GitHubs Abgleich der Committer-E-Mail mit dieser UID
- Apples OpenSSH ohne FIDO2-Unterstützung
- die Unterscheidung zwischen identischen OpenPGP-Subkeys und separaten FIDO2-Credentials
- die exakte WKD-Verzeichnisstruktur
Gerade deshalb bin ich froh, den gesamten Prozess einmal vollständig durchlaufen und – noch wichtiger – die Recovery-Pfade tatsächlich getestet zu haben.
Das Ergebnis ist kein Setup, bei dem ich einfach hoffe, dass mein Backup im Ernstfall funktioniert.
Das Ergebnis ist ein Setup, bei dem ich weiß, dass das Backup im Ernstfall funktioniert.