Java-приложения не читают «россыпь» PEM-файлов, как nginx: ключи и сертификаты им подаются в хранилищах (keystore/truststore), а управляет ими keytool из JDK. Это продолжение статьи про свой CA на OpenSSL — здесь вторая половина пути: как выпущенные сертификаты доставить в Java-приложение.

Главное изменение со времён старых мануалов: начиная с Java 9 формат по умолчанию — PKCS#12, а родной JKS объявлен legacy (keytool прямо пишет об этом warning’ом). PKCS#12 понимают и Java, и OpenSSL, и почти всё остальное — по возможности везде используйте его; JKS остаётся для старых приложений, которые требуют его явно.

Keystore и truststore

Одно и то же по формату, разное по смыслу:

  • keystore — «кто я»: приватный ключ + свой сертификат + цепочка. Приложение предъявляет это при TLS-хендшейке;
  • truststore — «кому я верю»: только чужие CA-сертификаты, без ключей. По нему проверяются сертификаты противоположной стороны.

Типовой продакшен-сетап Java-сервиса с mTLS — два файла: keystore.p12 и truststore.p12, пути и пароли задаются в конфигурации приложения (javax.net.ssl.keyStore* / trustStore* или средствами фреймворка).

Сценарий 1 (основной): ключ и сертификат выпущены в OpenSSL

Самый частый случай: ключ сгенерирован опенсслом, сертификат подписан вашим CA (см. первую статью), и всё это нужно отдать Java-приложению.

Шаг 1 — собрать PKCS#12 опенсслом:

1
2
openssl pkcs12 -export -in svc.crt -inkey svc.key \
  -certfile CAcert.pem -name svc -out keystore.p12

Шаг 2 — … всё. Начиная с Java 9 этот файл — уже готовый keystore, конвертировать ничего не нужно. Если старое приложение требует именно JKS:

1
2
3
keytool -importkeystore \
  -srckeystore keystore.p12 -srcstoretype PKCS12 \
  -destkeystore keystore.jks -deststoretype JKS
ФлагЧто делает
-importkeystoreперенос записей между хранилищами (в т.ч. смена формата)
-srckeystore / -srcstoretypeисходное хранилище и его тип
-destkeystore / -deststoretypeцелевое хранилище и тип (если не существует — создастся)

Спросит пароль нового хранилища и пароль исходного. Обратное преобразование (JKS → PKCS#12) — та же команда с обменом src/dest.

⚠ совместимость: PKCS#12, созданный OpenSSL 3.x, старые Java 8 могут не открыть (современное шифрование контейнера). Лечится опцией -legacy при экспорте в openssl — подробности в первой статье.

Сценарий 2: ключ рождается в keytool

Иногда требуется, чтобы приватный ключ создавался сразу в хранилище (и не существовал отдельным файлом). Генерация пары с самоподписанным сертификатом:

1
2
3
4
keytool -genkeypair -alias svc -keystore keystore.p12 -storetype PKCS12 \
  -keyalg RSA -keysize 2048 -sigalg SHA256withRSA -validity 3652 \
  -dname "CN=svc.example.org, OU=Ops, O=Example Corp, L=Moscow, C=RU" \
  -ext "SAN=DNS:svc.example.org,DNS:svc-test.example.org"
ФлагЧто делает
-genkeypairсгенерировать ключевую пару + самоподписанный сертификат
-aliasимя записи в хранилище (по нему потом всё: import, export, смена пароля)
-keyalg / -keysize / -sigalgRSA-2048 / SHA256withRSA — рабочий стандарт
-validityсрок самоподписанного сертификата, дней
-dnamesubject одной строкой (иначе спросит поля интерактивно)
-ext SAN=...subjectAltName — без него серверный сертификат не примет ни один современный клиент

Дальше самоподписанный сертификат меняется на подписанный вашим CA. Запрос на подпись:

1
keytool -certreq -alias svc -keystore keystore.p12 -file svc.csr

CSR подписывается в CA (openssl ca — см. первую статью), и подписанный сертификат возвращается в хранилище на тот же alias — но сначала цепочка (см. следующий раздел).

Импорт сертификатов: порядок имеет значение

Правило: сначала доверенные (корневой, промежуточные), потом свой подписанный. Если импортировать свой сертификат раньше его цепочки, keytool откажется собрать цепочку и упадёт с Failed to establish chain from reply.

1
2
3
4
5
6
7
8
9
# 1. корневой CA (alias любой свободный)
keytool -importcert -file CAcert.pem -alias rootca -keystore keystore.p12
# спросит: Trust this certificate? [no]: → yes

# 2. промежуточные, если есть
keytool -importcert -file intermediate.pem -alias intca -keystore keystore.p12

# 3. свой подписанный сертификат — строго на alias ключа
keytool -importcert -file svc.crt -alias svc -keystore keystore.p12

Тонкости, на которые тратятся часы:

  • alias решает всё: импорт на alias, где лежит приватный ключ, — это «обновить свой сертификат»; импорт на новый alias — «добавить доверенный». Перепутаете — получите либо ошибку, либо сертификат не там, где ждёте;
  • формат файла — строгий PEM: сертификат должен начинаться со строки -----BEGIN CERTIFICATE----- и заканчиваться -----END CERTIFICATE-----. Если перед BEGIN есть текстовая расшифровка (её добавляет openssl с -text и любят вставлять УЦ) — keytool старых версий подавится: вычистите всё вне BEGIN/END (openssl x509 -in in.pem -out clean.pem);
  • keytool принимает X.509 (v1/v2/v3) и PKCS#7-цепочки.

cacerts: системное доверенное хранилище Java

У каждой JVM есть свой truststore по умолчанию — cacerts ($JAVA_HOME/lib/security/cacerts), с публичными корневыми CA, обновляется вместе с Java. Пароль по умолчанию — changeit.

Добавить туда свой корпоративный CA — и он станет доверенным для всех приложений этой JVM:

1
2
keytool -importcert -file CAcert.pem -alias corp-root \
  -cacerts   # Java 9+; в Java 8: -keystore $JAVA_HOME/jre/lib/security/cacerts

Помните два «но»: обновление Java может принести новый cacerts (свой CA придётся доимпортировать снова — автоматизируйте), и менять общесистемное доверие ради одного приложения — сомнительная практика: чаще правильнее отдельный truststore приложению.

Просмотр и обслуживание

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# список записей: alias, тип (PrivateKeyEntry / trustedCertEntry), отпечатки
keytool -list -keystore keystore.p12

# подробно, с subject/issuer/сроками
keytool -list -v -keystore keystore.p12

# одна запись
keytool -list -v -alias svc -keystore keystore.p12

# удалить запись
keytool -delete -alias oldcert -keystore keystore.p12

# сменить пароль хранилища
keytool -storepasswd -keystore keystore.p12

# сменить пароль ключа (только JKS!)
keytool -keypasswd -alias svc -keystore keystore.jks

Про пароли ключей: в JKS у каждого ключа мог быть свой пароль, отличный от пароля хранилища, — источник вечной путаницы («пароль подходит к хранилищу, но не к ключу»). В PKCS#12 пароль один на контейнер, и -keypasswd для него не работает — ещё один довод в пользу PKCS#12. Если у legacy-JKS утерян пароль ключа — данные не достать, только перевыпуск.

Быстрая диагностика

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# что за файл: JKS или PKCS12?
keytool -list -keystore file.jks 2>&1 | head -3   # сам скажет storetype

# сроки всех сертификатов в хранилище одной строкой
keytool -list -v -keystore keystore.p12 | grep -E 'Alias|until'

# сверить сертификат в хранилище с тем, что отдаёт сервер
keytool -list -v -alias svc -keystore keystore.p12 | grep SHA256
openssl s_client -connect host:8443 </dev/null 2>/dev/null \
  | openssl x509 -noout -fingerprint -sha256

Типовые ошибки и их причины:

  • keytool error: java.io.IOException: keystore password was incorrect — либо пароль, либо формат: JKS открывают как PKCS#12 или наоборот (укажите -storetype явно);
  • Failed to establish chain from reply — при импорте подписанного сертификата в хранилище нет его цепочки: сначала импортируйте CA;
  • unable to find valid certification path to requested target (уже в приложении) — противоположная сторона не в truststore: смотрите, какой truststore реально использует JVM (-Djavax.net.ssl.trustStore), и есть ли там нужный CA.