Найти в Дзене
Я, Golang-инженер

#82. TLS-настройки клиента в Go: повышаем безопасность и скорость сетевых соединений

Это статья об основах программирования на Go. На канале я рассказываю об опыте перехода в IT с нуля, структурирую информацию и делюсь мнением. Хой, джедаи и амазонки! Изучаю TLS и рассуждаю на тему, как можно извлечь максимум пользы из настроек клиента на Go для повышения надёжности и производительности в долгосрочной перспективе. TLS - Transport Layer Security - криптографический протокол для повышения безопасности сетевой передачи данных. Позволяет конфигурировать соединение с различным уровнем защиты и оптимизацией скорости установки защитного соединения. TLS на мой взгляд решает две задачи: Эти две задачи обеспечивают защиту от так называемой MITM-атаки (Man-in-the-Middle - человек по середине). TLS пришёл на замену SSL (Secure Sockets Layer), как более совершенная форма, защищающая от выявленных уязвимостей. Этапы развития: TLS может применяться в различных протоколах, мне интересен прежде всего в связке с http, и как следствие, с TCP. Соответственно, TLS действует между HTTP и T
Оглавление

Это статья об основах программирования на Go. На канале я рассказываю об опыте перехода в IT с нуля, структурирую информацию и делюсь мнением.

Хой, джедаи и амазонки!

Изучаю TLS и рассуждаю на тему, как можно извлечь максимум пользы из настроек клиента на Go для повышения надёжности и производительности в долгосрочной перспективе.

1. Протокол TLS

1.1. Общее описание

TLS - Transport Layer Security - криптографический протокол для повышения безопасности сетевой передачи данных. Позволяет конфигурировать соединение с различным уровнем защиты и оптимизацией скорости установки защитного соединения.

TLS на мой взгляд решает две задачи:

  1. Защищает данные на этапе передачи по сети от прослушивания и незаметного изменения;
  2. Защищает от незаметного перенаправления данных не на тот сайт, на который отправлен запрос.

Эти две задачи обеспечивают защиту от так называемой MITM-атаки (Man-in-the-Middle - человек по середине).

TLS пришёл на замену SSL (Secure Sockets Layer), как более совершенная форма, защищающая от выявленных уязвимостей. Этапы развития:

  1. SSL 1.0 - 1994 г. - не издана из-за большого количества уязвимостей;
  2. SSL 2.0 - 1995 г. - много недостатков типа малого числа шифров;
  3. SSL 3.0 - 1996 г. - минимум известных уязвимостей;
  4. TLS 1.0 - 1999 г. - множество улучшений и закрытие уязвимостей SSL 3.0;
  5. TLS 1.2 - 2008 г. - множество улучшений, повышение безопасности;
  6. TLS 1.3 - 2018 г. - множество оптимизаций, повышение безопасности.

TLS может применяться в различных протоколах, мне интересен прежде всего в связке с http, и как следствие, с TCP. Соответственно, TLS действует между HTTP и TCP - на мой взгляд, это уровень представления модели OSI. Почитать о протоколах можно здесь: Сетевая модель OSI и Internet.

Можно сказать, что HTTPS - это взаимодействие HTTP и TLS:

HTTPS = HTTP + TLS

При установке TLS-соединение происходит "рукопожатие" - так называют два этапа: запрос клиента к серверу и ответ сервера клиенту - это одно рукопожатие.

Рукопожатие происходит несколько раз, чтобы защищённое соединение установилось (условно - в окошке браузера при оплате картой появляется ключик). Для последней на текущий момент версии TLS 1.3 таких рукопожатий нужно 2, для чуть более устаревшей, но также используемой TLS 1.2 - таких рукопожатий нужно 3. Соответственно, вывод: при использовании TLS 1.3 запросы более оптимизированы без потери безопасности за счёт современных алгоритмов шифрования и пр. - ориентировочно экономия в ~100 мс = 0,1 сек за счёт сокращения одного рукопожатия.

Описание взаимодействия клиента и сервера по TLS 1.3:

  1. Клиент -> сервер: клиент отправляет на сервер запрос на защищённое подключение, в котором допустимые версии TLS-протокола, набор допустимых шифров и некоторая другая служебная информация;
  2. Сервер -> клиент: сервер сопоставляет инфо клиента со своими ресурсами и направляет клиенту выбранные параметры, в т.ч. сертификат сервера по стандарту X.509 (обычно - цепочку сертификатов). Это первое рукопожатие.
  3. Клиент -> сервер: клиент проверяет полученные данные в соответствии со своими настройками TLS и если всё в порядке, отправляет условный finished серверу. Защищённое соединение установлено.

TLS не идеален, и описаны случаи его взлома, по крайней мере на TLS 1.2 и более младших версиях.

Cценарий, когда сервер возвращает свой TLS-сертификат клиенту - не единственный. Помимо этого клиент может отправлять серверу свой TLS-сертификат. Тогда обоюдно, и клиент, и сервер будут проверять - доверять ли запросу с такого-то домена. Это дополнительный уровень безопасности. Если сервер отдаёт новости или что-то в таком духе - тут такой уровень безопасности передачи данных вроде бы ни к чему. А вот если речь об общении между юрлицами - вполне может быть годным такой сценарий обоюдной проверки сертификатов.

Отдельно стоит сказать о сертификатах.

1.2. TLS-сертификаты

1.2.1. Общие сведения

TLS-сертификат - это электронный документ для аутентификации хоста (сервера и/или клиента) при защищённой передаче данных.

В интернете для сертификатов используется международный стандарт X.509. Графическое представление содержимого сертификата формата X.509:

Сертификат X.509: Википедия
Сертификат X.509: Википедия

Появился формат X.509 в 1988 г.

Сертификаты выдают центры сертификации, есть пять основных центров на долю которых приходится большая часть выданных сертификатов, см. справку на 2024 г. ниже:

Центры сертификации: Wikipedia
Центры сертификации: Wikipedia

Три из этих компании в США, по одной в Великобритании и Бельгии.

Такие крупные центры выдают свои сертификаты дочерним или партнёрским центрам, а те в свою очередь - своим партнёрам или уже напрямую серверам. Есть конечно и более мелкие центры, их сертификаты тоже являются достоверными.

Данные о достоверных центрах сертификации хранятся в сетевых операционных системах, таких как Linux (в т.ч. на Android), Windows, MacOS, iOS и пр., а также в специальных хранилищах некоторых программ, таких как браузеры. Есть центры, которые выдают сертификаты бесплатно, другие - платно. В любом случае, чтобы сертификату такого центра попасть в сетевую операционную систему или ПО типа браузера - нужно очень постараться. Причём, это не специальные серверные ОС, а в т.ч. обычные ОС на наших смартфонах и ноутбуках.

По сути, функционал TLS-протокола, который позволяет исключить атаку MITM, - позволяет проводить такую атаку центрам сертификации. Это компромиссное решение, когда защищаемся от одних другими, которым безоговорочно верим. Но от этих "других" не ожидаем целенаправленной атаки на конкретного пользователя.

В 2022-м году международные центры сертификации прекратили выдавать сертификаты российским компаниям под санкциями. Некоторое время при переходе на такие сайты высвечивалось соответствующее предупреждение. После этого Минцифры разработал и стал выдавать собственный сертификат, став таким образом центром сертификации. Получить такой сертификат можно через госуслуги. Проблема в том, что такой сертификат не появляется в большинстве иностранных ОС или браузерах - только в отечественных ОС (на базе Linux, например) и отечественных браузерах типа Яндекса.

Пример информации о сертификате, полученной из браузера:

Пример DV-сертификата
Пример DV-сертификата

Это DV-сертификат, т.е. сертификат, подтверждающий только домен. Какие ещё бывают сертификаты помимо DV и в чём особенность расскажу далее.

Ещё следует сказать, что сертификаты могут быть отозваны центрами сертификации по различным причинам, одна из которых - компрометирование сертификата.

1.2.2. Цепочки сертификатов

Сертификаты образуют цепочки, и передаются в TLS в виде массива сертификатов. Первый элемент массива - всегда листовой (см. ниже), очерёдность прочих сертификатов в цепочке не гарантирует точную последовательность для TLS 1.3.

Сертификат конечного сервера называют листовым сертификатом (лист дерева), сертификат исходного центра сертификации - корневым сертификатом. Пример цепочки сертификатов:

1. Корневой CA --> 2. Промежуточный CA --> 3. Сертификат сервера

Здесь CA - Certificate Authority, центр сертификации.

Каждый последующий сертификат в качестве подписи ссылается на предыдущий сертификат через поле Issuer формы X.509:

  • TLS ищет на какой сертификат ссылается листовой сертификат и использует публичный ключ найденного сертификата для проверки, валидности подписи листового. И так далее по цепочке, пока не дойдёт до сертификата, который есть в ОС - т.е. доверенного сертификата - это не обязательно корневой сертификат, т.к. "дочки" корневого центра сертификации тоже могут быть в ОС.
  • Если доверенного сертификата нет - TLS-рукопожатие не выполняется (если конфиг TLS не сломан) и соединение прерывается.

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

1.2.3. Виды сертификатов

Существуют три типа сертификатов:

  • DV (Domain Validated) - подтверждает право на управление доменом.
  • OV (Organization Validated) - для юрлиц.
  • EV (Extended Validation) - технически, требует глубокой проверки компании и может выдавать «зелёную строку» в браузерах.

Для выдачи DV-сертификата условно нужно подтвердить право владения доменом. Это может быть подтверждение на e-mail, указанный для связи с владельцем домена с сайта DNC, размещение на сайте какой-то информации или прочего. Отсюда вывод: DV-сертификат может получить относительно просто, в т.ч. его может получить недобросовестный сайт. Но при этом будет заветный ключик в строке браузера при https. Кроме того, именно такие домены некоторые центры сертификации предоставляют бесплатно в рамках концепции безопасного интернета.

Главное отличие DV от OV/EV - в поле Subject формы X.509 имеется название организации и другая служебная информация. Соответственно, сертификаты OV/EV более надёжные в плане защиты от мошеннического сайта, чем DV-сертификат. Хотя множество даже крупных компаний используют DV-сертификат, или не используют TLS вовсе, т.е. сетевые запросы по голому HTTP.

Именно поэтому на мой взгляд ориентирование на "зелёный ключик" - не гарантия защиты от недобросовестного сайта. Хотя если так рассуждать, то и OV/EV сертификаты может получить чуть ли не любая компания при очень большом желании и возможностях.

В параграфе выше был скриншот сведений о DV-сертификате. Вот пример для OV-сертификата с указанием организации:

Пример описания OV-сертификата
Пример описания OV-сертификата

А вот, по-видимому, EV-сертификат:

Пример описания EV-сертификата
Пример описания EV-сертификата

Почему этот EV считаю - в имени центра сертификации присутствует аббревиатура EV. Других вариантов внешне нет. Если копнуть чуть глубже, то в разделе Subject для EV-сертификата можно найти примерно в 2 раза больше информации, чем в OV-сертификате (хотя не обязательно так всегда происходит). В DV-сертификате в разделе Subject может быть только одно поле - CN или CommonName.

Пример доп инфо в разделе Subject, характерной для EV-сертификатов:

Детали о EV-сертификате
Детали о EV-сертификате

Ещё существует самоподписанный сертификат. Технически его можно сделать любым: DV/OV/EV - т.к. его подписывает не доверенный центр сертификации, а сам сервер, и в поля сертификата может записать что угодно. Кроме того, корневой сертификат - также самоподписанный.

Разница между самоподписанным сертификатом "сделанном на коленке в гараже" и самоподписанным сертификатом центра сертификации в том, что второй находится в сетевой ОС (почти) каждого компьютера и браузера, и настройки TLS ему будут доверять. Кроме того конфиг TLS можно "сломать", чтобы он пропускал самоподписанные сертификаты, сделанные "на коленке". Но толку от такого TLS в этом случае немного - разве что защита от прослушивания трафика. Есть некоторые сценарии использования таких пользовательских самоподписанных сертификатов, но они специфичны.

Пожалуй, это всё по теории. Переходим к практике.

2. TLS в Go

В Golang есть три стандартные библиотеки для работы с TLS:

  • crypto/tls - всевозможные конфигурации соединения и создание самих tls-соединений, шифры и прочее, что нужно для tls-протокола;
  • crypto/x509 - для работы с сертификатами X.509 и их цепочками - парсить чужие и создавать свои, загружать готовые, проверять;
  • net/http - косвенно, интегрирован с crypto/tls - создание клиентов и серверов с tls-конфигами.

Рассмотрим crypto/tls, а конкретно - его структуру Config. Даже если при создании клиентов не включать конкретно эту структуру, некоторые значения будут включены по-умолчанию. Рассмотрим структуру:

Код из официальной документации
Код из официальной документации

Я убрал все комментари к полям для компактности. Полей довольно много - и всё это можно настраивать... Разберём некоторые из полей, которые на мой взгляд самые важные. Версия Go 1.23.5.

2.1. InsecureSkipVerify

InsecureSkipVerify - это переключатель вкл./выкл. для TLS. Это ключевая настройка TLS, которую не нужно трогать, но о которой нужно знать. По-умолчанию false, т.к. TLS-проверки осуществляются по-умолчанию. Если установить true, tls не будет проверять сертификат противоположной стороны. Конечно, если не используется поле для кастомных TLS-проверок VerifyPeerCertificate или что-то подобное - об этом далее.

Комментарий разработчиков с переводом:

Стандартная документация + перевод для поля InsecureSkipVerify tls-конфига
Стандартная документация + перевод для поля InsecureSkipVerify tls-конфига

Стандартная проверка цепочки сертификатов при InsecureSkipVerify: false:

  1. Цепочка сертификатов - проверяются подписи сертификатов в цепочке и ищется достоверный сертификат который подписал сертификат сервера. Достоверный сертификат ищется из имеющихся в ОС, либо в пуле сертификатов через настройку tls-конфига в поле RootCAs. Благодаря этой же проверке будет отклонён самоподписанный сертификат/ы, если он недостоверный, или если сервер вовсе не предоставил сертификатов.
  2. Один из доменов листового сертификата совпадает с доменом сервера, на который отправлен клиентский запрос;
  3. Срок действия сертификата не истёк и уже начал действовать - проверяются все сертификаты в цепочке.

К чему приводит выявление ненадёжного сертификата? Если говорить о браузерах - то пользователю будет самому предложено что делать - условно "продолжить небезопасное соединение" или прекратить. При этом соединение будет по-прежнему недоступно для атаки MITM, если продолжить соединение, но веры сайту с таким недостоверным сертификатом уже нет - по какой-то причине, центры сертификации не выдали этому сайту сертификат доверия.

Для клиентов без участия человека при таких настройках просто будет прекращено соединение и возвращена ошибка сетевого соединения, если в tls-конфиге не настроено иное. А настраивать иное можно только в тестовых сценариях, но не на проде.

2.2. VerifyPeerCertificate

VerifyPeerCertificate позволяет создавать кастомные проверки. В это поле передаётся функция, аргументы для которой - срез сырых сертификатов и срез валидированных сертификатов.

Стандартная документация + перевод для поля VerifyPeerCertificate tls-конфига
Стандартная документация + перевод для поля VerifyPeerCertificate tls-конфига

При этом, если в tls.Config другое поле этой структуры - см. выше - InsecureSkipVerify - установлено в true, то второй аргумент будет nil'ом и соответственно если хотим получить цепочку достоверных сертификатов, нужно будет прописать немало кода, т.к. будем извлекать их из rawCerts.

По поводу типа данных сырых сертификатов, т.е. rawCerts [ ] [ ] byte. Вспомним, что строка - это срез байт. Т.е. у нас здесь что-то вроде среза строк, где нулевой срез - всегда листовой сертификат, а остальные элементы среза могут быть в разном порядке (если они конечно вообще есть). Т.е. вспомним, что по TLS 1.3 цепочка сертификатов в правильном порядке от листового до корневого не гарантирована - гарантировано только, что первым будет листовой сертификат.

Логично, что использовать эти проверки с InsecureSkipVerify: false - повышение требований к серверу.

Варианты, что в VerifyPeerCertificate можно валидировать:

  1. Белый лист доменов серверов, с кем допустимо соединение;
  2. Проверка криптографических параметров служебной информации сертификатов - длина ключа, шифры;
  3. Тип сертификата - DV или OV/EV.
  4. Проверка срока действия сертификата когда требуется добавить условия, например отклонять сертификаты со сроком действия менее 24 часов; также интересно узнать, как учитывается локальное время - видимо, клиент каким-то образом получает доступ к системному времени, в т.ч. к часовому поясу компьютера на котором запущен.

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

2.3. ServerName

В ServerName указывается доменное имя сервера, если в доменах сертификата нет того домена, на который отправлен запрос поверх TLS. Например, для сайта https://www.example.ru/ это example.ru, но если в списке доменов сертификата сервера почему-то такого домена нет, на который был отправлен клиентский запрос, и это корректная ситуация, такой домен располагают сюда:

Стандартная документация + перевод для поля ServerName tls-конфига
Стандартная документация + перевод для поля ServerName tls-конфига

Не пойму для каких ситуаций нужно использовать эту настройку, поскольку она на мой взгляд нелогична. Будто клиент и сервер договорились использовать чужой сертификат, условного гугла или вк, чисто шифровать свои данные. Но не проще ли создать свой самоподписанный?

2.4. MinVersion и MaxVersion

Задают версию TLS. По-умолчанию максимальная 1.3, минимальная 1.2.

Стандартная документация + перевод для полей MinVersion и MaxVersion tls-конфига
Стандартная документация + перевод для полей MinVersion и MaxVersion tls-конфига

Варианты версий:

Стандартная документация - значения для полей MinVersion и MaxVersion в tls-конфиге
Стандартная документация - значения для полей MinVersion и MaxVersion в tls-конфиге

Формат 0х0304 - шестнадцатеричное представление значений, используемое по протоколу TLS.

Что я думаю по этому поводу. Если мы создаём клиента под конкретный веб-сервер или под некоторый небольшой набор таких веб-серверов (а не как браузеры - опрашивают самые разнообразные сервера) - то может быть разумно вынести настройку минимальной версии вовне и тестить с 1.3. Если порядок - то только её и оставить.

Суть в чём - если будут дефолтные настройки tls, то клиент на Go запросит у сервера защищённое подключение и предложит на выбор TLS 1.2 и TLS 1.3. У сервера допустим TLS 1.2 только есть - он его и отправляет. Если есть TLS 1.3 - выберет его, как максимальный.

Но, допустим мы установили кастомный конфиг минимальной версии TLS 1.3 для клиента, и на тестах всё ок от сервера. Через пол года разработчики сервера решили что-то изменить и "случайно" настроили соединение к старому серверу с TLS 1.2. Небольшая потеря в безопасности передачи данных и скорости соединения. В обычных условиях этого скорее всего никто даже не заметит. Но если конфиг у нас будет 1.3 - это быстро выявится в сетевых ошибках в системе мониторинга, и тут три пути - либо сервер оперативно у себя меняет настройки, либо клиент изменяет конфиг на 1.2 чтобы не прекращался трафик и даёт время серверу на правки. Ну или смиряется с 1.2. В любом случае, клиент получает информацию о смене версии TLS сервера.

2.5. RootCAs

RootCAs задаёт настройку клиента, каким CA (Certificate Authority, удостоверяющий центр) клиент доверяет при проверке сертификатов сервера. Если поле пусто, то будут подгружены только сертификаты из ОС для проверки полученных сертификатов от сервера.

Есть смысл заполнять это поле, если сервер предоставляет самоподписанный сертификат - возможно это корпоративный сертификат. В т.ч. пример с сертификатом Минцифры, рассмотренный выше, отсутствующий в ОС, может быть сюда добавлен, если требуется.

Стандартная документация - значения для поля RootCAs в tls-конфиге
Стандартная документация - значения для поля RootCAs в tls-конфиге

2.6. Certificates

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

Стандартная документация - значения для поля Certificates в tls-конфиге
Стандартная документация - значения для поля Certificates в tls-конфиге

2.7. VerifyConnection

VerifyConnection используется для доп проверок соединения. В этом он похож на VerifyPeerCertificate, и насколько я понял, был добавлен в стандартную библиотеку Go позднее, при обнаружении неких проблем:

https://github.com/grpc/grpc-go/issues/3610
https://github.com/grpc/grpc-go/issues/3610

И исходное обращение в авто-переводе, которое, как я понимаю, послужило появлению такого поля в tls-конфиге Go:

https://github.com/golang/go/issues/36736
https://github.com/golang/go/issues/36736

Похоже, что это поле даёт больший контроль, чем VerifyPeerCertificate, но последнее также остаётся для обратной совместимости.

Ниже описание поля VerifyConnection:

Стандартная документация - значения для поля VerifyConnection в tls-конфиге
Стандартная документация - значения для поля VerifyConnection в tls-конфиге

Фрагмент структуры, которая передаётся в VerifyConnection:

Стандартная документация - фрагмент структуры  для поля VerifyConnection в tls-конфиге
Стандартная документация - фрагмент структуры для поля VerifyConnection в tls-конфиге

В общем, если писать какие-то кастомные проверки, можно использовать это поле. Те же проверки на тип сертификата - DV или EV/OV.

2.8. CipherSuites

CipherSuites позволяет настраивать какими шифрами взаимодействовать с сервером. Обычно это поле лучше не трогать, в т.ч. можно устанавливать ему значение nil - равноценный результат: будут использованы штатные безопасные шифры.

Стандартная документация + перевод для поля CipherSuite tls-конфига
Стандартная документация + перевод для поля CipherSuite tls-конфига

В Go предустановлены безопасные шифры, и использовать какие-либо другие в TLS попросту не выйдет. За исключением дополнений в комментариях авторов Go.

Ниже представлен список шифров для TLS в стандартной библиотеке:

Стандартная документация - шифры для TLS
Стандартная документация - шифры для TLS

Есть ещё две интересные настройки - они уже не про клиент, а про сервер.

2.9. ClientAuth и ClientCAs для конфигурации сервера

ClientAuth и ClientCAs используются для конфигурации сервера. Работают в паре, нужны для повышения надёжности, когда сервер проверяет сертификат клиента. Ожидается, что клиент будет передавать свои сертификаты, см. п.2.1.6.

Стандартная документация + перевод для полей ClientAuth и ClientCAs tls-конфига
Стандартная документация + перевод для полей ClientAuth и ClientCAs tls-конфига

ClientAuthType - не булевый параметр, а имеет предустановленные значения:

Стандартная документация + перевод вариантов значения поля ClientAuth tls-конфига
Стандартная документация + перевод вариантов значения поля ClientAuth tls-конфига

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

Мы разобрали основные настройки tls-конфига. Я рассмотрел все настройки, и признаться, не полностью их понял. Но большая часть там или устарела и неактуальна, о чём говорится в самих комментариях; либо предназначена для tls менее версии 1.3, что не особо актуально.

Посмотрим, как можно написать в Go простой клиент и сервер и поэкспериментировать tls-настройками.

3. Реализуем HTTPS=HTTP+TLS соединение

HTTP можно рассматривать в трёх контекстах:

  1. HTTP - как протокол передачи данных в клиент-серверной архитектуре;
  2. HTTP - как API, т.е. конкретная реализация клиента или сервера, использующая протокол HTTP за основу.
  3. HTTP/HTTPS - как схема в URL при передачи данных;

Если рассматривать http как схему с его вариацией в https, то эта схема говорит, какой вариант соединения устанавливать - чисто по http или http+tls:

  • http://example.com - говорит клиенту, что нужно использовать "голый" http при обмене данных до уровня TCP-тоннеля;
  • https://example.com - говорит клиенту, что нужно использовать помимо "голого" http ещё и настройки для протокола TLS перед обменом "основных" данных по TCP-туннелю.

Это и имеется ввиду, что https=http+tls.

Можно и через http безопасно передавать данные по сети, например используя уровнем ниже TCP не обычный IP, а IPSec.

Напишем простой сервер и простой клиент, и поэкспериментируем с передачей данных. Структура проекта:

Структура проекта
Структура проекта

Файлы cert и key генерируются сервером при запуске. Проект на GitHub: клик.

3.1. HTTP-сервер с TLS

Будет состоять из двух функций, общая длина 96 строк.

Код сервера 1 из 3
Код сервера 1 из 3

Обратите внимание на функцию ListenAndServeTLS - ранее в опытных проектах я использовал только ListenAndServe, т.к. она обслуживает запросы по http-схеме, а сейчас будет https схема.

Генерация сертификата и ключа:

Код сервера 2 из 3
Код сервера 2 из 3

И сертификат по стандарту X.509 с присвоением значений интересующим нас полям:

Код сервера 3 из 3
Код сервера 3 из 3

Поскольку в поле Subject есть доп информация, в частности - название организации - этот сертификат технически можно считать OV-типом (или EV). Но никак не DV, у которого это поле должно быть пустым.

На логике создания сертификата останавливаться не буду, она нам не особо интересна. Нам интересно, как его можно использовать.

При запуске сервера будет создан файл сертификата и файл приватного ключа.

3.2. Клиент с TLS-конфигом

Напишем клиента сразу со всеми настройками tls, с которыми будем экспериментировать. Основная функция:

Код клиента - часть 1 из 3
Код клиента - часть 1 из 3

Я использовал 5 настроек TLS и добавил логи для информации. По-легенде, настройки конфига считываются откуда-то для каждого клиента в отдельности - например, из yaml-файла, передаются через флаги при запуске клиента, из БД или ещё откуда-то.

Пул безопасных сертификатов - тот сертификат, который создаётся сервером. Таким образом клиент будет считать сертификат сервера достоверным.

Структура tls-конфига, константы и конструктор:

Код клиента - часть 2 из 3
Код клиента - часть 2 из 3
Внимание: для поля CipherSuites нельзя напрямую присваивать значение из конфига (строка 44). Это мы разберём в примерах ниже, когда протестируем как ведёт себя соединение с этой настройкой.

Тип данных для минимальной версии я сделал человеко-читаемым типа стринг, а поле для шифров оставил по-умолчанию как они заданы в Go - в 16-ричном численном формате, что не очень дружелюбно по отношению к человеку. Но это поле скорее всего специфично - должно использоваться очень редко. Поэтому можно не заморачиваться с мапингом значений, как я сделал для версии TLS, иначе плюсов может быть меньше, чем минусов.

В блоке кода ниже функции для определения значений остальных полей TLS-конфига в зависимости от входящих данных:

Код клиента - часть 3 из 3
Код клиента - часть 3 из 3

В методе verifyConnection сделал "кнопку" по проверке сертификата, а не реализовал в ней весь код из метода validateTypeCert, т.к. в теории сюда могут пойти новые проверки-кнопки. И будет удобно их сюда добавлять без значительного увеличения сложности метода, это отчасти связано с KISS.

Это весь код клиента. Запустим и проверим как работает установка соединения по TLS с различными настройками.

3.3. Тестируем клиент

Напомню, сервер у нас генерирует самоподписанный сертификат и настроен на максимальную версию TLS до 1.3 включительно.

В клиенте и сервере я поэкспериментирую с этими настройками, чтобы посмотреть, как ведут себя сетевые соединения. Клиент в исходном состоянии выглядит так:

Настройки клиента - исходное состояние
Настройки клиента - исходное состояние

Настройка сервера:

Настройка сервера - исходное состояние
Настройка сервера - исходное состояние

Переходим к экспериментам.

3.3.1. Шифры

Эксперимент 1 - шифры
Эксперимент 1 - шифры

Запустили код в исходном состоянии - смотрим:

Видим, что выбран шифр 0x1301. Задавали мы другие шифры в срезе, как же так получилось? И что это вообще за шифр? Смотрим в библиотеке Go:

Шифры для TLS 1.3
Шифры для TLS 1.3

Ага, это один из шифров для TLS версии 1.3. Вспомним, что если соединение устанавливается по версии TLS 1.3, то набор шифров в клиенте не настраивается.

Поправим конфигурацию сервера - снизим максимальную версию до TLS 1.2. Запускаем, смотрим:

Эксперимент 2 - шифры
Эксперимент 2 - шифры

Видим, что соединение установлено по одному из ожидаемых шифров.

Взглянем ещё раз на поле tls-конфига клиента для назначения допустимых шифров:

CipherSuite: []uint16{0xc030, 0xFFFF}

0xc030 - это стандартный шифр, он есть в библиотеке Go и для него определена константа TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:

Код клиента
Код клиента

Второе значение 0xFFFF не задано константой и TLS-конфиг не может с ним работать. Для меня здесь было интересно понять, как будет работать TLS конфиг со значениями для шифров, которые ему неизвестны. Ответ: пропускает их. Вот что будет, если оставить в конфиге только этот 0xFFFF:

Эксперимент 3 - шифры
Эксперимент 3 - шифры

Рукопожатие не удалось - запрошен неизвестный шифр.

В описании к полю CipherSuites разработчики Go пишут, что если значение nil - будет использован безопасный набор шифров.

Если соединение устанавливается по TLS 1.3, т.е. когда и сервер и клиент его поддерживают - выбирается он автоматически, то поле CipherSuite игнорируется - не важно, какие значения мы в него передадим. А вот если мы передадим пустой срез или срез с неизвестными шифрами при соединениях для TLS 1.2 и ниже, соединение не состоится:

Конфиг сервера
Конфиг сервера
Эксперимент 5 - шифры
Эксперимент 5 - шифры

Отсюда вывод - при настройках конфигурации TLS нужно смотреть: если из внешних ресурсов пришли настройки шифров с пустым массивом - нужно обработать его в нил. Сделать это можно, например, так:

Обработка случая при пустом срезе
Обработка случая при пустом срезе

Тогда происходит подбор шифров из числа доступных с авто-выбором:

Эксперимент 6 - шифры
Эксперимент 6 - шифры

3.3.2. Версия TLS

Смоделируем ситуацию, когда сервер с устаревающим TLS 1.2 в качестве максимальной версии:

Настройки сервера
Настройки сервера

А клиента сконфигурируем так, что минимальную версию TLS с которой он работает будет 1.3:

Эксперимент 7 - версии TLS
Эксперимент 7 - версии TLS

Как видим, рукопожатия не прошли.

3.3.3. Самоподписанный сертификат

Установим на клиенте минимальную версию TLS 1.2. В данном случае я просто не буду ничего указывать, она по-умолчанию такой станет. И отключим поле с пулом достоверных сертификатов:

TLS-конфиг клиента
TLS-конфиг клиента

В этом случае клиент будет брать пул достоверных сертификатов из операционной системы. Запускаем код:

Эксперимент 8 - самоподписанный сертификат
Эксперимент 8 - самоподписанный сертификат

Если в клиенте включить небезопасную настройку пропуска проверки сертификата - получаем следующее:

Эксперимент 9 - самоподписанный сертификат
Эксперимент 9 - самоподписанный сертификат

Как видим, проверка достоверности сертификата не выполняется и соединение устанавливается. Оно остаётся защищённым в том плане, что данные при передаче шифруются. Но с каким сервером взаимодействует клиент - непонятно.

3.3.4. Срок действия сертификата

Вернём безопасную настройку в клиент и пул достоверных сертификатов:

TLS-конфиг клиента
TLS-конфиг клиента

Установим начало действия сертификата сервера в будущее на 24 часа, т.е. сертификат ещё не начал действовать:

Эксперимент 10 - сертификат ещё не начал действовать
Эксперимент 10 - сертификат ещё не начал действовать

Видим ошибку.

Смоделируем ситуацию, что срок действия уже истёк:

Эксперимент 11 - срок действия сертификата вышел
Эксперимент 11 - срок действия сертификата вышел

Видим похожую ошибку, отличается деталями.

Для наглядности придадим полям сертификата сервера валидное значение и запустим:

Эксперимент 11 - валидные даты сертификата
Эксперимент 11 - валидные даты сертификата

Как видим, всё в порядке когда даты валидны.

3.3.5. Проверка домена сертификата

Вспомним значения формы X.509 для сертификата сервера:

Настройки X.509 сертификата сервера
Настройки X.509 сертификата сервера

В поле DNSName указаны доменные имена, на которые распространяется сертификат. Удалим второй - реальных хост и посмотрим на результат:

Эксперимент 12 - валидация доменного имени
Эксперимент 12 - валидация доменного имени

Соединение не установлено.

Эта проверка выполняется, если в клиенте установлена настройка InsecureSkipVerify: false. Если изменить эту настройку, соединение установится:

Эксперимент 13 - валидация доменного имени
Эксперимент 13 - валидация доменного имени

Можно добиться соединения и при InsecureSkipVerify: false - если в поле ServerName tls-конфига клиента вписать хост из сертификата сервера. Но оба этих случая - и с InsecureSkipVerify: false и с ServerName - на мой взгляд могут быть только для тестов.

3.3.6. Тип сертификата

Осталась одна настройка - отклонять сертификаты DV-типа. Я не нашёл способа лучше, чем просто проверять наличие поля Organization в сертификате:

Настройка сервера
Настройка сервера

Либо других полей из структуры Subject формы X.509. Как я понимаю, они заполняются только если сертификат OV или EV-типа:

Фрагмент стандартной библиотеки Go - pkix
Фрагмент стандартной библиотеки Go - pkix

Вероятнее всего, только в поле CommonName при DV-сертификате будет какое-то значение, в остальных - не должно.

Если в клиенте включена настройка отклонять DV-сертификаты:

Настройки клиента
Настройки клиента

А в сертификате сервера убрать значение для соответствующего поля организации, т.е. смоделируем DV-сертификат, получим следующее:

Эксперимент 14 - валидация EV/OV-сертификатов
Эксперимент 14 - валидация EV/OV-сертификатов

Хорошо бы, чтобы браузеры тоже давали возможность выбора действий, если устанавливается защищённое подключение по DV-сертификату, например - предупреждение "Внимание, подключение по DV-сертификату. К нему мало доверия!" Но вот в своих клиентах это можно реализовать таким образом, с помощью кастомной функции проверки.

На этом разбор TLS-конфига считаю завершённым.

4. Выводы

Напомню, с чем познакомились в публикации:

  1. Протокол TLS - общая информация;
  2. Выяснили, что при использовании TLS 1.3 экономия во времени при установке соединения ориентировочно 0,1 сек по сравнению с TLS-протоколами предыдущих версий;
  3. Узнали о MITM-атаках, в частности, что используя TLS мы защищаемся от злоумышленников, перехватывающих сетевой трафик, но предоставляем доступ к просмотру информации сертифицированным центрам.
  4. Посмотрели на разницу между http как протоколом, как API, и как схемами http и https (https = http+tls);
  5. Разобрались с ключевыми полями настройки TLS в Go;
  6. Научились применять TLS-конфиг клиента на практике и посмотрели, какие есть сообщения о сетевых ошибках при TLS-рукопожатиях;
  7. И главное, разобрались, что TLS выполняет две функции: 1 - позволяет удостовериться, что соединение устанавливается с сервером, который известен центру сертификации (что не мешает мошенническому или иному небезопасному сайту быть известным центру сертификации) и 2 - обеспечить защищённую передачу данных между клиентом и сервером, чтобы на пути никто не смог подсмотреть или изменить данные, по крайней мере, оставаясь при этом незамеченным.

Из явно не разобранных моментов остаётся опрос сервера-подписанта сертификата (OCSP), не отозван ли предоставленный сертификат сервера. Например, по причине компрометации сертификата.

А с какими настройками TLS вы сталкивались? Поделитесь в комментариях.

На этом у меня всё, благодарю, что дочитали публикацию до конца. Успехов, и будем на связи.

unsplash.com
unsplash.com

Бро, ты уже здесь? 👉 Подпишись на канал для начинающих IT-специалистов «Войти в IT» в Telegram, будем изучать IT вместе 👨‍💻👩‍💻👨‍💻