От HTTP до HTTPS - 1
Содержание:

Вступление

В современном мире без веб-приложений никак. И начнём мы с небольшого эксперимента. В детстве я помню, как во всех ларьках продавалась такая газета, как "Аргументы и факты". Вспомнил я о них потому, что по моему личному восприятию из детства, эти газеты выглядели всегда странно. И решил, а не зайти ли нам на их сайт:
От HTTP до HTTPS - 2
Если перейти в справку Google Chrome, то мы прочитаем, что данный сайт не использует защищённое соединение и информация, которой вы обмениваетесь с сайтом, может быть доступна посторонним. Давайте проверим какие-нибудь другие новости, например новости Санкт-Петербурга от "Фонтанки", электронного СМИ:
От HTTP до HTTPS - 3
Как видно, у сайта Фонтанки с безопасностью по этим данным проблем нет. Получается, что веб-ресурсы могут быть безопасными, а могут и не быть. Так же видим, что обращение к не защищённым ресурсам происходит по протоколу HTTP. А если ресурс защищён, то обмен данными осуществляется по протоколу HTTPS, где S на конце обозначает "Secure". Протокол HTTPS описан в спецификации rfc2818: "HTTP Over TLS". Давайте попробуем создать своё веб-приложение и сами увидеть, как это работает. И попутно будем разбираясь в терминах.
От HTTP до HTTPS - 4

Веб-приложение на 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();
}
Если мы выполним код, то мы сможем перейти на следующий веб-ресурс:
От HTTP до HTTPS - 5
Работает это просто. Благодаря Undertow Builder API мы добавляем HTTP слушателя на адрес localhost и порт 8080. Этот слушатель получает запросы от веб-браузера и возвращает в ответ строку "Hello World". Отличное веб-приложение. Но как мы видим, мы используем протокол HTTP, т.е. такой обмен данными небезопасен. Давайте разбираться, как же осуществляют обмен по протоколу HTTPS.
От HTTP до HTTPS - 6

Требования для 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 — это тема отдельных статей и обзоров, поэтому я просто укажу материалы, которые считаю интересными: Если очень коротко, то основа HTTPS — это TLS handshake и проверка "Server Identity" (т.е. идентификация сервера) по его цифровому сертификату. Это важно. Запомним это, т.к. позже мы вернёмся к этому факту. Итак, ранее мы использовали HttpListener, чтобы рассказать серверу как работать по протоколу HTTP. Если в примере выше мы добавили HttpListener для работы по HTTP, то для работы по HTTPS нам нужно добавить HttpsListener:
От HTTP до HTTPS - 7
Но чтобы его добавить нам нужен 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:
От HTTP до HTTPS - 8
Теперь нам понятно, что 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.
От HTTP до HTTPS - 9

KeyManager

KeyManager — это менеджер ключей. Он отвечает за то, какой "authentication credential" предоставить тому, кто к нам обратиться. Credential можно перевести, как удостоверение. Удостоверение нужно, чтобы клиент был уверен что сервер тот, за кого себя выдаёт и ему можно доверять. Что будет использовано в качестве удостоверения? Как мы помним, Server Identity проверяется по цифровому сертификату сервера. Этот процесс можно представить следующим образом:
От HTTP до HTTPS - 10
Кроме того, в "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
  • keyalg: Алгоритм шифрования ключей. Выберем алгоритм RSA, который является по сути стандартным решением для нашей цели.
  • keysize: Размер ключа (в битах). Минимальный рекомендуемый размер 2048, т.к. размер меньше уже взламывался. Подробнее можно прочитать здесь: "a ssl certificate in 2048 bit".
  • dname: Distinguished Name, отличительное имя.
Важно понимать, что запрашиваемый ресурс (например, https://localhost) будет сравниваться по нему. Это называется "subject cn matching".
  • validity: Продолжительность в днях, в течении которых генерируемый сертификат валиден, т.е. действителен.
  • ext: Certificate Extension, указанные в "Named Extensions".
Для Self-signed Certificates (т.е. для сертификатов, созданных самостоятельно) необходимо указать следующие расширения:
  • -ext san:critical=dns:localhost,ip:127.0.0.1 > для выполнения subject matching по SubjectAlternativeName
  • -ext bc=ca:false > чтобы указать, что данным сертификат не используется для подписи других сертификатов
Выполним команду (пример для ОС Windows):
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
Т.к. будет создан файл убедитесь, что у Вас есть все права на создание файла. Кроме того, скорей всего, Вы увидите совет вроде этого:
От HTTP до HTTPS - 11
Тут нам говорят, что 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
Пример успешного выполнения:
От HTTP до HTTPS - 12
Для экспорта сертификата в файл можно выполнить:
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 Однако, мы по прежнему получим ошибку, что нельзя доверять данному ресурсу:
От HTTP до HTTPS - 13
Давайте разбираться, что не так с сертификатом и что с этим делать.
От HTTP до HTTPS - 14

Управление сертификатами

Итак, наш сервер уже готов работать по HTTPS, но клиент ему не доверяет. Почему? Давайте посмотрим:
От HTTP до HTTPS - 15
Причина в том, что данный сертификат является самоподписанным (Self-signed Certificate). Под самоподписанным SSL сертификатом понимают сертификат открытого ключа, изданный и подписанный тем же лицом, которое он идентифицирует. То есть его не выдавал никакой уважаемый центр сертификации (CA, он же Certificate Authority). Центр сертификации (Certificate Authority) выступает как доверенное лицо и похож на нотариуса в обычной жизни. Он заверяет, что выданные им сертификаты надёжны. Услуга выдачи сертификатов такими CA является платной, поэтому утеря доверия и репутационные риски никому не нужны. По умолчанию есть несколько центров сертификации, которым доверяют. Этот список доступен для редактирования. И управление списком центров сертификации в каждой операционной системе свой. Например, управление данным списком в Windows можно прочитать здесь: "Manage Trusted Root Certificates in Windows". Давайте добавим сертификат в доверенные, как указано в сообщении об ошибке. Для этого, сначала, скачаем сертификат:
От HTTP до HTTPS - 16
В OS Windows нажмём Win+R и выполним mmc для вызова консоли управления. Далее нажмём Ctrl+M для добавления раздела "Сертификаты" в текущую консоль. Далее в подразделе "Доверенные корневые центры сертификации" выполним Действия / Все задачи / Импорт. Выполним импорт файла, скачанного ранее в файл. Браузер мог запомнить прошлое состояние доверия к сертификату. Поэтому, перед открытием страницы нужно выполнить рестарт браузера. Например, в Google Chrome в адресной строке необходимо выполнить chrome://restart. В OS Windows для просмотра сертификатов так же можно воспользоваться утилитой certmgr.msc:
От HTTP до HTTPS - 17
Если мы всё сделали правильно, то увидим успешное обращение к нашему серверу по HTTPS:
От HTTP до HTTPS - 18
Как видно, сертификат теперь считается действительным, ресурс доступен, ошибок нет.
От HTTP до HTTPS - 19

Итог

Вот мы с Вами и разобрали, как примерно выглядит схема включения протокола HTTPS на веб-сервере и что для этого нужно. Надеюсь, на данном этапе стало понятно, что поддержка обеспечена взаимодействием Java Cryptography Architecture (JCA), отвечающей за криптографию, и Java Secure Socket Extension (JSSE), обеспечивающей реализацию TLS на стороне Java. Мы увидели, как используется входящая в состав JDK утилита keytool для работы с хранилищем ключей и сертификатов KeyStore. Кроме того, мы поняли, что HTTPS использует протоколы SSL/TLS для обеспечения безопасности. Для закрепления советую прочитать отличные статьи на эту тему: Надеюсь, после этого небольшого обзора HTTPS станет чуть более прозрачным. А в случае необходимости включения HTTPS Вы сможете легко понимать термины из документации ваших серверов приложений и фрэймворков. #Viacheslav