Содержание:
Вступление
В современном мире без веб-приложений никак. И начнём мы с небольшого эксперимента. В детстве я помню, как во всех ларьках продавалась такая газета, как "Аргументы и факты". Вспомнил я о них потому, что по моему личному восприятию из детства, эти газеты выглядели всегда странно. И решил, а не зайти ли нам на их сайт:Если перейти в справку Google Chrome, то мы прочитаем, что данный сайт не использует защищённое соединение и информация, которой вы обмениваетесь с сайтом, может быть доступна посторонним.
Давайте проверим какие-нибудь другие новости, например новости Санкт-Петербурга от "Фонтанки", электронного СМИ:
Как видно, у сайта Фонтанки с безопасностью по этим данным проблем нет.
Получается, что веб-ресурсы могут быть безопасными, а могут и не быть. Так же видим, что обращение к не защищённым ресурсам происходит по протоколу HTTP. А если ресурс защищён, то обмен данными осуществляется по протоколу HTTPS, где S на конце обозначает "Secure".
Протокол HTTPS описан в спецификации rfc2818: "HTTP Over TLS".
Давайте попробуем создать своё веб-приложение и сами увидеть, как это работает. И попутно будем разбираясь в терминах.
Веб-приложение на Java
Итак, нам нужно создать самое простое веб-приложение на Java. Для начала, нам нужно само приложение на Java. Для этого воспользуемся системой автоматической сборки проекта Gradle. Это нам позволит не создавать вручную нужную структуру каталогов + Gradle за нас будет управлять всеми необходимыми для проекта библиотеками и обеспечивать, чтобы они были доступны при выполнении кода. Подробнее про Gradle можно прочитать в небольшом обзоре: "Краткое знакомство с Gradle". Воспользуемся Gradle Init Plugin'ом и выполним команду:
gradle init --type java-application
После этого откроем билд скрипт build.gradle
, в котором описано, из каких библиотек состоит наш проект, которые Gradle нам предоставит.
Добавим туда зависимость от веб-сервера, на котором мы будем экспериментировать:
dependencies {
// Web server
implementation 'io.undertow:undertow-core:2.0.20.Final'
// Use JUnit test framework
testImplementation 'junit:junit:4.12'
}
Чтобы веб-приложение работало, нам обязательно нужен веб-сервер, где будет размещено наше приложение. Веб серверов существует огромное множество, но основные это: Tomcat, Jetty, Undertow. Мы с Вами выберем на этот раз Undertow.
Чтобы понять, как нам работать с этим нашим веб-сервером перейдём на официальный сайт Undertow и перейдём в раздел документации.
Мы с Вами подключили зависимость от Undertow Core, поэтому нам интересует раздел про этот самый Core, то есть ядро, основу веб-сервера.
Самым простым способом является использование Builder API для Undertow:
public static void main(String[] args) {
Undertow server = Undertow.builder()
.addHttpListener(8080, "localhost")
.setHandler(new HttpHandler() {
@Override
public void handleRequest(final HttpServerExchange exchange) throws Exception {
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain");
exchange.getResponseSender().send("Hello World");
}
}).build();
server.start();
}
Если мы выполним код, то мы сможем перейти на следующий веб-ресурс:
Работает это просто. Благодаря Undertow Builder API мы добавляем HTTP слушателя на адрес localhost и порт 8080.
Этот слушатель получает запросы от веб-браузера и возвращает в ответ строку "Hello World".
Отличное веб-приложение. Но как мы видим, мы используем протокол HTTP, т.е. такой обмен данными небезопасен. Давайте разбираться, как же осуществляют обмен по протоколу HTTPS.
Требования для HTTPS
Чтобы понять, как подключить HTTPS вернёмся к спецификации HTTPS: "RFC-2818: HTTP Over TLS". Согласно спецификации, данные в протоколе HTTPS передаются поверх криптографических протоколов SSL или TLS. Часто людей вводит в заблуждение, что есть SSL и TLS. На самом деле, SSL развивался и менял свои версии. Позже, очередным витком в развитии протокола SSL стал TLS. То есть TLS — это просто новая версия SSL. В спецификации так и сказано: "SSL, and its successor TLS". Итак, мы узнали, что есть криптографические протоколы SSL/TLS. SSL — это сокращение от Secure Sockets Layer и переводится как "уровень защищённых cокетов". Сокет (Socket) в переводе с английского — разъём. Участники передачи данных по сети используют сокеты, как программный интерфейс (то есть API) для общения друг с другом по сети. Браузер выступает как клиент и использует клиентский сокет, а сервер принимающий запрос и выдающий ответ использует серверный сокет. И именно между этими сокетами и происходит обмен данными. Поэтому изначально протокол и назван SSL. Но время шло и протокол развивался. И в один прекрасный момент протокол SSL стал протоколом TLS. TLS — это сокращение от Transport Layer Security. TLS-протокол в свою очередь основан на спецификации протокола SSL версии 3.0. Протокол TLS — это тема отдельных статей и обзоров, поэтому я просто укажу материалы, которые считаю интересными:- TLS и SSL: Необходимый минимум
- TLS/SSL Technical Reference
- Что такое TLS
- Ключи, шифры, сообщения: как работает TLS
Но чтобы его добавить нам нужен SSLContext.
Интересно, что SSLContext — это класс не из Undertow, а
javax.net.ssl.SSLContext
.
Класс SSLContext входит в так называемый "Java Secure Socket Extension" (JSSE) — расширение Java для обеспечения безопасности интернет соединения. Данное расширение описано в документе "Java Secure Socket Extension (JSSE) Reference Guide".
Как видно из вступительной части документации, JSSE предоставляет фрэймворк и Java реализацию протоколов SSL и TLS.
Как же нам получить SSLContext? Открываем JavaDoc SSLContext и находим метод getInstance.
Как видно, для получения SSLContext нам нужно указать название "Secure Socket Protocol".
В описании параметров дано указание, что эти названия можно посмотреть в "Java Cryptography Architecture Standard Algorithm Name Documentation".
Поэтому, последуем указанию и идём в документацию. И видим, что мы можем выбрать между SSL и TLS:
Теперь нам понятно, что SSLContext нам надо создать следующим образом:
public SSLContext getSSLContext() {
// 1. Получаем контекст, в рамках которого будем работать по TLS протоколу
SSLContext context = null;
try {
context = SSLContext.getInstance("TLS");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
return context;
}
Создав новый контекст вспоминаем, что SSLContext описывался в "Java Secure Socket Extension (JSSE) Reference Guide".
Читаем и видим, что "A newly created SSLContext should be initialized by calling the init method". То есть создать контекст - мало. Его нужно инициализировать. И это логично, т.к. про безопасность мы рассказали только то, что мы хотим использовать протокол TLS.
Чтобы инициализировать SSLContext нам нужно предоставить три вещи: KeyManager, TrustManager, SecureRandom.
KeyManager
KeyManager — это менеджер ключей. Он отвечает за то, какой "authentication credential" предоставить тому, кто к нам обратиться. Credential можно перевести, как удостоверение. Удостоверение нужно, чтобы клиент был уверен что сервер тот, за кого себя выдаёт и ему можно доверять. Что будет использовано в качестве удостоверения? Как мы помним, Server Identity проверяется по цифровому сертификату сервера. Этот процесс можно представить следующим образом:Кроме того, в "JSSE Reference Guide: How SSL Works" сказано, что SSL использует "asymmetric cryptography", а это значит, что нам требуется пара ключей: public key и private key.
Так как речь зашла о криптографии, то в дело вступает "Java Cryptography Architecture" (JCA). Oracle по данной архитектуре предоставляет отличный документ: "Java Cryptography Architecture (JCA) Reference Guide".
Кроме этого, можно прочитать кратки обзор JCA на JavaRush: "Java Cryptography Architecture: Первое знакомство".
Итак, для инициализации KeyManager нам нужен KeyStore, в котором будет храниться сертификат нашего сервера. Самым распространённым способом создания хранилища ключей и сертификатов является утилита keytool, которая входит в состав JDK. Пример можно увидеть в документации JSSE: "Creating a Keystore to Use with JSSE".
Итак, нам нужно при помощи утилиты KeyTool создать хранилище ключей и записать туда сертификат. Интересно, что раньше генерация ключа задавалось при помощи -genkey, а теперь рекомендуется использовать -genkeypair.
Нам понадобится определить следующие вещи:
- alias: Псевдоним или просто имя, под которым будет сохранена запись в Keystore
- keysize: Размер ключа (в битах). Минимальный рекомендуемый размер 2048, т.к. размер меньше уже взламывался. Подробнее можно прочитать здесь: "a ssl certificate in 2048 bit".
- dname: Distinguished Name, отличительное имя.
- validity: Продолжительность в днях, в течении которых генерируемый сертификат валиден, т.е. действителен.
- ext: Certificate Extension, указанные в "Named Extensions".
- -ext san:critical=dns:localhost,ip:127.0.0.1 > для выполнения subject matching по SubjectAlternativeName
- -ext bc=ca:false > чтобы указать, что данный сертификат не используется для подписи других сертификатов
keytool -genkeypair -alias ssl -keyalg RSA -keysize 2048 -dname "CN=localhost,OU=IT,O=Javarush,L=SaintPetersburg,C=RU,email=contact@email.com" -validity 90 -keystore C:/keystore.jks -storepass passw0rd -keypass passw0rd -ext san:critical=dns:localhost,ip:127.0.0.1 -ext bc=ca:false
Т.к. будет создан файл убедитесь, что у Вас есть все права на создание файла.
Кроме того, скорей всего, Вы увидите совет вроде этого:
Тут нам говорят, что JKS — проприетарный формат. Проприетарный — значит является частной собственностью авторов и предназначен для использования только в Java. При работе со сторонними утилитами может возникнуть конфликт, поэтому нас и предупреждают.
Кроме того, мы можем получить ошибку:
The destination pkcs12 keystore has different storepass and keypass
.
Эта ошибка возникает из-за того, что используются разные пароли от записи в Keystore и от самого keystore. Как сказано в документации к keytool,
"For example, most third-party tools require storepass and keypass in a PKCS #12 keystore to be the same".
Мы можем указать сами ключ (например, -destkeypass entrypassw). Но лучше не нарушать требований и задать одинаковый пароль.
Итак, импорт может выглядеть следующим образом:
keytool -importkeystore -srckeystore C:/keystore.jks -destkeystore C:/keystore.jks -deststoretype pkcs12
Пример успешного выполнения:
Для экспорта сертификата в файл можно выполнить:
keytool -export -alias ssl -storepass passw0rd -file C:/server.cer -keystore C:/keystore.jks
Кроме того, мы можем получить содержимое Keystore следующим образом:
keytool -list -v -keystore C:/keystore.jks -storepass passw0rd
Отлично, теперь у нас есть keystore, в котором есть сертификат. Теперь его можно получать из кода:
public KeyStore getKeyStore() {
// Согласно https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#KeyStore
try(FileInputStream fis = new FileInputStream("C:/keystore.jks")){
KeyStore keyStore = KeyStore.getInstance("pkcs12");
keyStore.load(fis, "passw0rd".toCharArray());
return keyStore;
} catch (IOException ioe) {
throw new IllegalStateException(ioe);
} catch (KeyStoreException | NoSuchAlgorithmException | CertificateException e) {
throw new IllegalStateException(e);
}
}
Если есть KeyStore, то можем инициализировать и KeyManager:
public KeyManager[] getKeyManagers(KeyStore keyStore) {
String keyManagerAlgo = KeyManagerFactory.getDefaultAlgorithm();
KeyManagerFactory keyManagerFactory = null;
try {
keyManagerFactory = KeyManagerFactory.getInstance(keyManagerAlgo);
keyManagerFactory.init(keyStore, "passw0rd".toCharArray());
return keyManagerFactory.getKeyManagers();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
} catch (UnrecoverableKeyException | KeyStoreException e) {
throw new IllegalStateException(e);
}
}
Наша первая цель достигнута. Осталось разобраться, что такое TrustManager.
TrustManager описан в документации JSSE в разделе "The TrustManager Interface".
Он очень похож на KeyManager, но его цель проверить, можно ли доверять тому, кто запрашивает соединение. Если совсем грубо, то это KeyManager наоборот =)
У нас нет необходимости в TrustManager'е, поэтому передадим null. Тогда будет создан TrustManager по умолчанию, не проверяющий конечного пользователя, который выполняет запросы на наш сервер. В документации так и сказано: "default implementation will be used".
Аналогично с SecureRandom. Если мы укажем null, то будет использована реализация по умолчанию. Вспомним только, что SecureRandom - это класс, относящийся к JCA и описанный в документации JCA в разделе "The SecureRandom Class".
Итого, подготовка с учётом всех вышеописанных методов может выглядеть следующим образом:
public static void main(String[] args) {
// 1. Подготавливаем приложение к работе по HTTPS
App app = new App();
SSLContext sslContext = app.getSSLContext();
KeyStore keyStore = app.getKeyStore();
KeyManager[] keyManagers = app.getKeyManagers(keyStore);
try {
sslContext.init(keyManagers, null, null);
} catch (KeyManagementException e) {
throw new IllegalStateException(e);
}
Далее остаётся только запустить сервер:
// 2. Поднимаем сервер
int httpsPort = 443;
Undertow server = Undertow.builder()
.addHttpsListener(httpsPort, "localhost", sslContext)
.setHandler(new HttpHandler() {
@Override
public void handleRequest(final HttpServerExchange exchange) throws Exception {
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain");
exchange.getResponseSender().send("Hello World");
}
}).build();
server.start();
}
На этот раз наш сервер будет доступен по адресу https://localhost:443
Однако, мы по прежнему получим ошибку, что нельзя доверять данному ресурсу:
Давайте разбираться, что не так с сертификатом и что с этим делать.
Управление сертификатами
Итак, наш сервер уже готов работать по HTTPS, но клиент ему не доверяет. Почему? Давайте посмотрим:Причина в том, что данный сертификат является самоподписанным (Self-signed Certificate).
Под самоподписанным SSL сертификатом понимают сертификат открытого ключа, изданный и подписанный тем же лицом, которое он идентифицирует. То есть его не выдавал никакой уважаемый центр сертификации (CA, он же Certificate Authority).
Центр сертификации (Certificate Authority) выступает как доверенное лицо и похож на нотариуса в обычной жизни. Он заверяет, что выданные им сертификаты надёжны. Услуга выдачи сертификатов такими CA является платной, поэтому утеря доверия и репутационные риски никому не нужны.
По умолчанию есть несколько центров сертификации, которым доверяют. Этот список доступен для редактирования. И управление списком центров сертификации в каждой операционной системе свой.
Например, управление данным списком в Windows можно прочитать здесь: "Manage Trusted Root Certificates in Windows".
Давайте добавим сертификат в доверенные, как указано в сообщении об ошибке.
Для этого, сначала, скачаем сертификат:
В OS Windows нажмём Win+R и выполним
mmc
для вызова консоли управления. Далее нажмём Ctrl+M для добавления раздела "Сертификаты" в текущую консоль.
Далее в подразделе "Доверенные корневые центры сертификации" выполним Действия / Все задачи / Импорт
. Выполним импорт файла, скачанного ранее в файл.
Браузер мог запомнить прошлое состояние доверия к сертификату. Поэтому, перед открытием страницы нужно выполнить рестарт браузера. Например, в Google Chrome в адресной строке необходимо выполнить chrome://restart
.
В OS Windows для просмотра сертификатов так же можно воспользоваться утилитой certmgr.msc
:
Если мы всё сделали правильно, то увидим успешное обращение к нашему серверу по HTTPS:
Как видно, сертификат теперь считается действительным, ресурс доступен, ошибок нет.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ