Vom PEM zum laufenden Stack – Teil 2: Der Keystore, den Java nicht lesen wollte
Teil 2 der Serie. In Teil 1 haben wir die Zertifikatsformate sortiert. Jetzt bauen wir daraus einen PKCS#12-Truststore – und stoßen auf zwei Hürden, die selbst dann zuschlagen, wenn die Datei eigentlich korrekt ist.
Keycloak (ein Java-basierter Authentifizierungsserver) wollte einen PKCS#12-Truststore: eine Datei, die das öffentliche CA-Zertifikat enthält, dem der Dienst vertrauen soll. Ein Truststore enthält bewusst keinen privaten Schlüssel – nur das öffentliche Zertifikat einer CA. Damit sagt man dem Dienst: „Allem, was von dieser CA signiert ist, darfst du vertrauen."
Mein sauberes CA-Zertifikat lag bereit. Der naheliegende Befehl:
openssl pkcs12 -export -nokeys -in ca.crt -out root.p12 -passout pass:<TRUSTSTORE_PW>
-nokeys, weil ein Truststore keinen Schlüssel braucht. Sollte funktionieren. Tat es auch – scheinbar.
Die Datei wurde erzeugt, hatte das richtige Passwort, ließ sich mit OpenSSL öffnen. Aber Java meldete, der Truststore sei leer. Der Beweis mit Javas eigenem Werkzeug keytool:
keytool -list -keystore root.p12 -storetype PKCS12 -storepass <TRUSTSTORE_PW> # -> Your keystore contains 0 entries
BEWEIS: Null Einträge – obwohl OpenSSL beim Erstellen nicht gemeckert hatte und beim Inspizieren brav ein Certificate bag zeigte.
Die Ursache ist subtil: Wenn man mit openssl pkcs12 -export -nokeys ein reines Zertifikat ohne zugehörigen privaten Schlüssel verpackt, landet es als sogenanntes bag ohne Alias (auch „friendlyName" genannt) im Container. Und Java/keytool listet Einträge ohne Alias schlicht nicht als nutzbare Einträge auf. Aus Javas Sicht: leer.
Die Lösung ist, den Truststore mit Javas eigenem Werkzeug zu bauen, denn keytool -importcert ist genau dafür gemacht, ein vertrauenswürdiges Zertifikat mit Alias zu importieren:
keytool -importcert -noprompt \ -alias corp-ca \ -file ca.crt \ -keystore root.p12 \ -storetype PKCS12 \ -storepass <TRUSTSTORE_PW>
Jetzt zeigt keytool -list brav 1 entry mit trustedCertEntry. Java sieht das Zertifikat.
URTEIL: Für Java-Truststores ist keytool die richtige Wahl, nicht der OpenSSL-Export. Was OpenSSL als gültig ansieht, muss für die Java-Welt noch lange nicht nutzbar sein.
Kaum war der leere Keystore gelöst, kam der nächste Klassiker:
ERROR: keystore password was incorrect javax.crypto.BadPaddingException: Given final block not properly padded
Das Tückische: Ich konnte dieselbe Datei mit OpenSSL und genau diesem Passwort problemlos öffnen:
openssl pkcs12 -info -in root.p12 -passin pass:<TRUSTSTORE_PW> -noout # runs through cleanly
BEWEIS: Wenn OpenSSL die Datei mit dem Passwort öffnet, Java aber „Passwort falsch" sagt, liegt es nicht am Passwort, sondern am Verschlüsselungsalgorithmus.
Der Hintergrund: OpenSSL 3.x verpackt PKCS#12-Dateien standardmäßig mit modernem PBES2 / AES-256 / SHA-256. Ältere Java-Versionen (wie das JDK 17 in vielen Container-Images) verstehen diesen Algorithmus beim Entschlüsseln nicht – und melden das irreführenderweise als „password was incorrect", obwohl das Passwort völlig korrekt ist.
Du erkennst das moderne Format an der OpenSSL-Ausgabe:
openssl pkcs12 -info -in root.p12 -passin pass:<TRUSTSTORE_PW> -noout # PKCS7 Encrypted data: PBES2, PBKDF2, AES-256-CBC ... <- Java doesn't like this # MAC: sha256
Java will dagegen das ältere Legacy-Format:
PKCS7 Encrypted data: pbeWithSHA1And3-KeyTripleDES-CBC ... <- Java understands this MAC: sha1
URTEIL: „Passwort falsch" von Java, wenn OpenSSL die Datei problemlos öffnet, bedeutet fast immer den falschen PKCS#12-Verschlüsselungsalgorithmus – nicht das falsche Passwort.
Die Zwickmühle – und ihre Auflösung
Damit steckte ich in einer Falle, die sich durch das ganze Projekt zog:
- OpenSSL-Export im Legacy-Format → Java-lesbar, aber leerer Keystore (kein Alias).
- keytool-Import → gefüllter Keystore mit Alias, aber modernes AES-Format, das Java ablehnt.
Ich brauchte beides gleichzeitig: einen Eintrag mit Alias und das Legacy-Format.
Die Lösung war, keytool zum Bauen zu nutzen (das löst das Alias-Problem) und es gleichzeitig per JDK-Systemeigenschaften zum Legacy-Format zu zwingen:
keytool -importcert -noprompt -alias corp-ca \ -file ca.crt \ -keystore root.p12 -storetype PKCS12 -storepass <TRUSTSTORE_PW> \ -J-Dkeystore.pkcs12.certProtectionAlgorithm=PBEWithSHA1AndDESede \ -J-Dkeystore.pkcs12.certPbeIterationCount=2048 \ -J-Dkeystore.pkcs12.macAlgorithm=HmacPBESHA1 \ -J-Dkeystore.pkcs12.macIterationCount=2048
Die vier -J-D-Optionen reichen die Anweisung „nimm das alte 3DES/SHA1-Format" an die JVM durch, die keytool ausführt. Ergebnis: ein Eintrag mit Alias, im Java-lesbaren Format. Beide Bedingungen auf einen Schlag erfüllt.
Wenn die Datei im Container gebaut werden muss
Ein praktischer Stolperstein am Rande: Welches keytool man benutzt, ist nicht egal. Die zuverlässigste Methode ist, den Keystore mit genau dem Java zu bauen, das ihn später lesen soll – also im Container selbst. Dann ist garantiert, dass das erzeugte Format kompatibel ist.
Weil das Zertifikatsverzeichnis im Container oft schreibgeschützt (read-only) gemountet ist, baut man die Datei in /tmp und holt sie per Stream heraus:
docker compose run --rm -v /host/certs:/out:ro --entrypoint sh keycloak -c '
keytool -importcert -noprompt -alias corp-ca \
-file /out/ca.crt \
-keystore /tmp/root.p12 -storetype PKCS12 -storepass <TRUSTSTORE_PW> \
-J-Dkeystore.pkcs12.certProtectionAlgorithm=PBEWithSHA1AndDESede \
-J-Dkeystore.pkcs12.macAlgorithm=HmacPBESHA1 && cat /tmp/root.p12
' > /host/certs/root.p12
Der Trick: keytool schreibt nach /tmp (immer beschreibbar), gibt das Ergebnis mit cat auf die Standardausgabe, und die Shell auf dem Host lenkt es per > in die Zieldatei – wo du Schreibrechte hast, nicht der Container.
Was mich Teil 2 gelehrt hat
- „0 entries" trotz erfolgreicher Erstellung = das Zertifikat hat keinen Alias. Nutze
keytool -importcertmit-alias. - „Passwort falsch", obwohl das Passwort stimmt = in Wahrheit ein Algorithmus-Problem. OpenSSL 3 schreibt AES-256, altes Java kann es nicht lesen.
- Java braucht das Legacy-Format
pbeWithSHA1And3-KeyTripleDES-CBC/MAC: sha1. keytool+-J-D-Optionen erzeugen gefüllt und Legacy auf einmal.- Verifiziere immer mit
keytool, nicht nur mit OpenSSL – nur das zeigt dir die Java-Sicht. - Baue Keystores im Zielcontainer, dann passt das Format garantiert.
Im nächsten Teil wird es organisatorisch tückisch: Was passiert, wenn mehrere Dienste unterschiedliche Passwörter für dieselbe Datei erwarten – und wie man drei verschiedene Passwort-Variablen auseinanderhält.