Vom PEM zum laufenden Stack – Teil 2: Der Keystore, den Java nicht lesen wollte

OpenSSL · keytool · Docker · PKCS#12

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.

Your keystore contains 0 entries

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.


CASE #1DER LEERE KEYSTORE

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.

CASE #2„PASSWORT FALSCH" – MIT DEM RICHTIGEN PASSWORT

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

  1. „0 entries" trotz erfolgreicher Erstellung = das Zertifikat hat keinen Alias. Nutze keytool -importcert mit -alias.
  2. „Passwort falsch", obwohl das Passwort stimmt = in Wahrheit ein Algorithmus-Problem. OpenSSL 3 schreibt AES-256, altes Java kann es nicht lesen.
  3. Java braucht das Legacy-Format pbeWithSHA1And3-KeyTripleDES-CBC / MAC: sha1.
  4. keytool + -J-D-Optionen erzeugen gefüllt und Legacy auf einmal.
  5. Verifiziere immer mit keytool, nicht nur mit OpenSSL – nur das zeigt dir die Java-Sicht.
  6. 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.

Kommentare (0)

Noch keine Kommentare. Sei der Erste!

Kommentar schreiben