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 опенсслом:
| |
Шаг 2 — … всё. Начиная с Java 9 этот файл — уже готовый keystore, конвертировать ничего не нужно. Если старое приложение требует именно JKS:
| |
| Флаг | Что делает |
|---|---|
-importkeystore | перенос записей между хранилищами (в т.ч. смена формата) |
-srckeystore / -srcstoretype | исходное хранилище и его тип |
-destkeystore / -deststoretype | целевое хранилище и тип (если не существует — создастся) |
Спросит пароль нового хранилища и пароль исходного. Обратное преобразование (JKS → PKCS#12) — та же команда с обменом src/dest.
⚠ совместимость: PKCS#12, созданный OpenSSL 3.x, старые Java 8 могут не открыть
(современное шифрование контейнера). Лечится опцией -legacy при экспорте в openssl —
подробности в первой статье.
Сценарий 2: ключ рождается в keytool
Иногда требуется, чтобы приватный ключ создавался сразу в хранилище (и не существовал отдельным файлом). Генерация пары с самоподписанным сертификатом:
| |
| Флаг | Что делает |
|---|---|
-genkeypair | сгенерировать ключевую пару + самоподписанный сертификат |
-alias | имя записи в хранилище (по нему потом всё: import, export, смена пароля) |
-keyalg / -keysize / -sigalg | RSA-2048 / SHA256withRSA — рабочий стандарт |
-validity | срок самоподписанного сертификата, дней |
-dname | subject одной строкой (иначе спросит поля интерактивно) |
-ext SAN=... | subjectAltName — без него серверный сертификат не примет ни один современный клиент |
Дальше самоподписанный сертификат меняется на подписанный вашим CA. Запрос на подпись:
| |
CSR подписывается в CA (openssl ca — см. первую статью), и подписанный сертификат возвращается в хранилище на тот же alias — но сначала цепочка (см. следующий раздел).
Импорт сертификатов: порядок имеет значение
Правило: сначала доверенные (корневой, промежуточные), потом свой подписанный.
Если импортировать свой сертификат раньше его цепочки, keytool откажется собрать
цепочку и упадёт с Failed to establish chain from reply.
| |
Тонкости, на которые тратятся часы:
- 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:
| |
Помните два «но»: обновление Java может принести новый cacerts (свой CA придётся доимпортировать снова — автоматизируйте), и менять общесистемное доверие ради одного приложения — сомнительная практика: чаще правильнее отдельный truststore приложению.
Просмотр и обслуживание
| |
Про пароли ключей: в JKS у каждого ключа мог быть свой пароль, отличный от пароля
хранилища, — источник вечной путаницы («пароль подходит к хранилищу, но не к ключу»).
В PKCS#12 пароль один на контейнер, и -keypasswd для него не работает — ещё один
довод в пользу PKCS#12. Если у legacy-JKS утерян пароль ключа — данные не достать,
только перевыпуск.
Быстрая диагностика
| |
Типовые ошибки и их причины:
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.