?

Log in

No account? Create an account
Previous Entry Share Next Entry
HTTPS сервер на Java, PKCS#12 и серверный сертификат
Я
donz_ru


Хозяйке на заметку и себе, чтобы не забыть, так как все это добро ни разу не логично и не интуитивно понятно.

Итак, нам надо выпустить серверный сертификат.
1. Сначала создаем секретный ключ и запрос на сертификат:

openssl req -utf8 -config openssl.cnf -newkey rsa:2048 -nodes -keyout private/cert_priv.key -out requests/cert.req

В процессе вводим пароль к секретному ключу. Не забываем его записать на стикер и приклеить на монитор.

2. Высылаем запрос специально обученному человеку, который создаст сертификат и подпишет его родительским сертификатом, чтобы клиент (например, браузер) мог выстроить цепочку сертификатов, добравшись до доверенного корневого и убедиться, что все ок, и никакой товарищ майор посередине трафик слушать не будет. Получаем сертификат.

3. Теперь надо создать PKCS#12 контейнер - хранилище ключей, который будет содержать полученный сертификат и секретный ключ. Этот контейнер уже будет использоваться в коде.

openssl pkcs12 -export -out cert_container.p12 -inkey private/cert_priv.key -in cert.cer

Для тех, кто считает, что хорошо знает криптографию, первый вопрос: что за пароль "Enter Export Password:" запросит OpenSSL во время выполнения этой команды?

4. Переходим к яве. Нам надо загрузить это хранилище ключей и проинициализировать SSLContext в яве, где мы делаем сервер с поддержкой HTTPS. Условный, но работающий код:

final char[] password = new char[]{'1', '1', '1', '1'};

final KeyStore ks = KeyStore.getInstance("PKCS12");
final InputStream keystore = SenderActorTest.class.getClassLoader().
        getResourceAsStream("cert_container.p12");
if (keystore == null) {
    throw new RuntimeException("Keystore required!");
}
ks.load(keystore, password);

final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
keyManagerFactory.init(ks, password);

final TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(ks);

final SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
sslContext.init(keyManagerFactory.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());




Казалось бы все. Можно использовать. Почти.
Второй вопрос для тех, кто считает, что хорошо знает криптографию: что за пароль надо указать в строках с ks.load и keyManagerFactory.init? Только чур в исходники не подглядывать!

Третий вопрос для тех, кто считает, что хорошо знает криптографию: а где надо указывать пароль для доступа к секретному ключу из этого хранилища?

Четвертый вопрос уже для тех, кто считает, что хорошо знает явовскую инфраструктуру вокруг HTTPS: а что будет, если пароль для секретного ключа указан неверно?

[Ответы]
1. Ну это довольно просто: пароль для доступа к создаваемому PKCS#12 контейнеру

2. Тоже довольно просто: пароль для доступа к создаваемому PKCS#12 контейнеру. Или нет?

3. Хе-хе. Пароль для доступа к секретному ключу должен совпадать с паролем к PKCS#12 контейнеру! Я немного офигел. Или это действительно где-то в спецификациях прописано? Убедиться, что это так как минимум для алгоритма SunX509 можно, перейдя внутрь keyManagerFactory.init и добравшись до конструктора sun.security.ssl.SunX509KeyManagerImpl#SunX509KeyManagerImpl
И вроде бы это должно означать, что пароль для доступа к контейнеру надо указывать при вызове ks.load, а пароль для секретного ключа при вызове keyManagerFactory.init. Но если эти ключи не совпадает, то последний вызов падает. Додебажить декомпилированный код сегодня уже не могу, но там явно что-то не чисто.
У меня все это добро завести получилось только при одном условии: пароль для контейнера и пароль для секретного ключа должны совпадать.

4. А ничего не будет. Если в keyManagerFactory.init указать пароль не от контейнера, то этот вызов упадет. А если указать пароль от контейнера, но который не совпадает с паролем от секретного ключа, то инициализация пройдет отлично. Просто сервер на все HTTPS запросы будет выдавать херню, так как будет искренне думать, что использует правильный секретный ключ, а не фигню, получившуюся в результате применения неверного пароля при извлечении этого секретного ключа. И, соответственно, после обработки исходящего HTTP сообщения таким секретным ключом там будет белиберда.


Все-таки подозреваю, что я где-то что-то не так делаю. Может кто подскажет?

UPD: все в порядке. Это я в пятницу переработал, похоже. Не знаю, для чего именно я вводил пароль при создании запроса на сертификат, но повторное выполнение этой команды показало, что там никакого пароля не требуется. Возможно до этого копался с чем-то и ввел там, а запомнил, что как-будто вводил при создании секретного ключа и запроса на сертификат. Собственно, созданный секретный ключ с заголовком "RSA PRIVATE KEY" без слова ENCRYPTED тоже намекает, что ключ этот у меня создался без шифрования.
Итог: в приведенном куске кода оба раза надо использовать пароль от контейнера. В уже зашифрованном контейнере секретный ключ еще раз уже не шифруется. Так что все норм, все снова стало логично, и нечего копаться в таких темах по вечерам пятниц.


  • 1
Я уже как-то давно писал про это здесь.

В жаве два пароля: один от JKS-контейнера, второй собственно от закрытого ключа. Они могут совпадать, а могут и не совпадать. Пароль от контейнера должен быть не менее 6ти символов длиной.

Спасибо за линк, завтра буду читать вдумчиво. Но сходу вижу ляп: "Сконвертировать цепочку из PKCS12 в JKS." - это не так.

А, ну и это - пароль от контейнера, как и от секретного ключа, никак не зависит от явы и может быть от нуля до дофига символов.

Переутомление - мать учения

Угу. Довольно много прокопал, пока дошло, что и как :)

  • 1