[Перевод] Современный TLS/SSL в Windows 3.11

9cyf3wuaz5g-3t6opyjjwulmhaa.png


В последнее время происходит ренессанс новых программ для ретро-компьютеров — для них пишут клиенты Slack, клоны Wordle, клиенты Mastodon. Однако большинству этих программ при подключении к Интернету требуется запущенный на современном компьютере прокси для работы с SSL/TLS, которых требуют сегодня практически все API. Но заставлять Gateway 4DX2–66 с установленной Windows 3.11 for Workgroups использовать для подключения к Интернету современную машину — это довольно грустное решение, поэтому я решил изменить статус-кво.

Нельзя сказать, что Windows 3.1 не поддерживала защищённые соединения; например, в Internet Explorer 2 была поддержка SSL. Но со временем и клиенты, и серверы перешли на новые версии протокола и алгоритмов SSL (теперь называемого TLS), и отказались от поддержки старых версий, потому что в них обнаружены уязвимости наподобие POODLE.
Обычно для обеспечения поддержки современного TLS программы можно обновить до более новой версии библиотеки TLS (например, OpenSSL), но одно из самых больших препятствий заключается в том, что Windows 3.1 — это 16-битная операционная система; сегодня библиотеки TLS обычно поддерживают 32-битные ОС, и иногда 16-битные ОС встраиваемых систем, но никогда саму Windows 3.1.[1]

Источником моего вдохновения стала статья Йео Кхенг Менга, потому что он столкнулся с такими же сложностями при разработке вышеупомянутого клиента Slack — казалось, невозможно модифицировать качественную библиотеку TLS и заставить её компилироваться и работать в среде Windows 3.1. Поэтому я захотел попытать счастья, чтобы снова дать своему Gateway и другим компьютерам с Windows 3.1 возможность подключаться к большей части Интернета.

▍ Как будет выглядеть успех?


Для подключения к большинству современных серверов нам необходимо научиться общаться с TLS 1.2. За последние годы серверы в основном отказались от поддержки старых версий TLS. TLS 1.3 — это самая новая версия TLS, стандартизированная в 2018, поэтому её поддержка будет бонусом, хотя эту версию пока поддерживают не все серверы.

Из статьи Википедии про Transport Layer Security:


Также нам необходима поддержка набора современных наборов криптографических алгоритмов, то есть алгоритмов, которые протокол TLS использует для обмена ключами и шифрования данных. При подключении TLS-клиент сообщает серверу в сообщении «Client Hello», какие наборы криптографических алгоритмов он поддерживает; если сервер не поддерживает ни один из них, то он отклоняет подключение с ошибкой «No common ciphers». Для TLS 1.2 существует 37 наборов алгоритмов, поэтому наш клиент должен поддерживать хотя бы самые распространённые (то есть наименее уязвимые).

В своей реализации мы откажемся от стремления к безопасности. Учитывая, что я уже отбросил заботы о безопасности, используя в Интернете тридцатилетнее ПО, я решил, что при создании рабочей реализации вполне можно пойти на некоторые компромиссы. Самыми большими ограничениями стали использование поддельного генератора случайных чисел и отсутствие проверки сертификатов.

▍ Подбираем подходящую библиотеку TLS


Учитывая приведённые выше критерии успеха, я рассмотрел несколько библиотек TLS, которые бы могли мне подойти: OpenSSL, BearSSL, Mbed TLS и WolfSSL. Среди прочих выделялась WolfSSL, потому что имела заявленную поддержку 16-битных компиляторов при полной функциональности. Также она имеет широкий спектр поддерживающего кода для всевозможного оборудования с различными ограничениями, в том числе небольшой объём памяти, благодаря чему у меня было множество примеров для изучения.

Также мне требовался работающий стек TCP/IP для Windows. Сейчас в это трудно поверить, но в Windows 3.x не было встроенной поддержки TCP/IP. Пользователи могут выбрать один из множества вариантов, включая популярный shareware-стек TCP/IP Trumpet Winsock. Для тестирования я решил выбрать собственную реализацию TCP/IP компании Microsoft под названием TCP/IP-32, которую нужно скачивать отдельно. Она предоставляет реализацию TCP/IP, в которой есть интерфейс передачи данных через Winsock (Windows Sockets API) — старую версию того же API, который используется даже сегодня в Windows 11, но моё решение должно работать и с любой другой реализацией.

▍ Среда разработки


Мой план заключался в компиляции WolfSSL в DLL, которую затем можно было бы использовать из любой программы. Так как WolfSSL написана на C, мне требовалась среда C, способная создавать DLL для 16-битных Windows. Я решил использовать Open Watcom v2, которая обеспечивает феноменальную поддержку программ и DLL 16-битных Windows и кросс-компиляцию в том числе и в 64-битной Windows 11.

В отличие от ситуации с Windle, чтобы упростить себе жизнь, я выполнял основную часть разработки и тестирования в Windows 11 с папкой, расшаренной в виртуальную машину VirtualBox с Windows 2000 (которая, в отличие от Windows 11, способна запускать 16-битные программы); при этом я периодически проверял, чтобы всё по-прежнему работало в Windows 3.11 for Workgroups.

▍ Ад DLL


Первой трудностью стали сборка и использование DLL в Windows 3.x. Для решения этой проблемы мне необходимо было понять разницу между дальними и ближними указателями.

Сегодня мы обычно воспринимаем указатели на память как абсолютные адреса; однако при программировании для 16-битных Windows нужно учитывать архитектуру с сегментной защиты памяти x86, уходящую корнями к процессорам Intel 8088 и 8086.

Примечание: я излагаю своё понимание темы после прочтения информации. Я пытался представить простое объяснение, не вдаваясь в подробности, например, в различия между реальным и защищённым режимом. Если что-то окажется в принципе неверным, то напишите мне об этом.

При сегментации память разделяется на сегменты, каждый из которых имеет размер до 64 КБ. Сегмент может представлять код (команды) программы, данные (глобальные и статические локальные переменные) или стек программы. Адреса в памяти состоят из двух частей: сегмент, в котором находится память, и смещение внутри этого сегмента. Указатель на этот полный адрес из двух частей называется дальним указателем.

nndess3daiiynqfowjypbyiddbe.png

Пример дальнего указателя[2]

Система имеет набор регистров, в том числе Code Segment (CS), Data Segment (DS) и Stack Segment (SS), которые отслеживают сегменты, применимые к текущей выполняемой программе. При передаче указателя на функцию внутри той же программы обычно передаётся только смещение до переменной, на которую ссылаются, а сегмент явно не указывается. Такой тип указателя называется ближним указателем и для экономии памяти используется чаще всего.

При вызове DLL из программы ситуация немного отличается. Во-первых, команды для DLL находятся в сегменте, отдельном от команд программы, поэтому в вызове функции необходимо изменить регистр CS на регистр DLL. Если мы используем перед именем функции нестандартное ключевое слово __far, то компилятор C делает это самостоятельно, то есть компилятор генерирует «дальний вызов».

Во-вторых, при передаче указатель в функции DLL мы должны использовать дальние указатели; в работе с DLL есть свои тонкости — при выполнении кода в них используются Code Segment и Data Segment DLL, однако Stack Segment вызывающей программы. Применение дальних указателей гарантирует, что код не будет делать ошибочных допущений о том, на что указывают указатели сегментов.

Также для DLL часто используется ещё одно нестандартное ключевое слово C __pascal. Оно указывает, что функция использует стандарт вызова Pascal. Стандарт вызова определяет способ передачи параметров функции в памяти и очистки стека после возврата из функции. (При стандарте Pascal параметры записываются в стек слева направо, а вызываемая функция удаляет их из стека при возврате). В Windows 3.x применяется стандарт вызовов, применявшийся в Borland Pascal (и Delphi 1.0), отсюда и название стандарта, однако последующие версии Windows перешли на стандарт stdcall.

Это значит, что все функции в WolfSSL, которые будут вызываться из внешней программы, должны быть определены как __far __pascal. Например, функция для инициализации WolfSSL определяется следующим образом:

    int __far __pascal wolfSSL_Init(void);


Всё вышесказанное в общем случае применимо и к вызовам Windows API; ведь в конечном итоге Windows API реализован как вызовы DLL, являющихся частью Windows.

▍ Слишком большой сегмент


Самое серьёзное ограничение, с которым я столкнулся при портировании WolfSSL на 16-битную Windows, стал максимальный размер сегментов — каждый Code Segment и Data Segment должен быть не более 64 КБ. Это представляет сложность при компиляции библиотеки TLS с поддержкой десятков наборов криптографических алгоритмов и различных протоколов, и распространённых, и редко используемых.

К счастью, наша DLL не ограничена общим размером кода или данных в 64 КБ. 16-битные компиляторы имеют концепцию «моделей памяти», определённых как Small, Medium, Compact, Large и Huge. Все компиляторы для платформы, написанные Microsoft, Borland и Open Watcom, поддерживают эти модели как флаг компиляции; модели меняют способ использования компилятором дальних/ближних указателей и список библиотек, используемых в генерируемых командах. Я решил использовать модель памяти «Large», которая позволяет использовать в программе или DLL множественные Code Segment и Data Segment.


Таблица из книги «Programming Windows 3.1, Third Edition» Чарльза Петцольда

Для экономии памяти в целом я начал с отключения максимального количества флагов компонентов WolfSSL времени компиляции, без которых я не подвергал опасности подключение к большинству современных TLS-серверов. Сюда относятся и компоненты, используемые небезопасными наборами криптографических алгоритмов (например, MD5, DES, RC4), а также другие компоненты, например, DTLS и поддержка серверов, которые я не буду использовать для простого TLS-клиента.

Вскоре я обнаружил, что даже этого недостаточно, потому что мы ограничены 64 килобайтами кода в каждом объектном файле. Каждый раз, когда объектный файл превышал это ограничение, компиляция аварийно завершалась с сообщением «Segment too large». В частности, файл internal.c библиотеки WolfSSL имеет огромный размер в 1,25 МБ (а это слишком много даже для рендеринга в GitHub), и сборка завершалась аварийно каждый раз, когда я включал нужный мне компонент.

Мне удалось уменьшить этот файл до размера, помещающегося в два сегмента, раскидав код по двум файлам, internal.c и internal2.c — я знаю, изящное решение! Это была самая мучительная и долгая часть всего процесса; как белка в колесе, я тестировал код, чтобы проверить, достаточно ли в нём компонентов, чтобы подключиться к серверу, включал компонент, видел сообщение «Segment too large», перемещал ещё кусок кода, стараясь при этом не разрушить сложные зависимости между компонентами или ненамеренно не изменить значение вложенных макросов ifdef в коде. В конце концов, всё скомпилировалось.

ken9emtsjy4fqp79hqrqewrwx-g.png


Но даже скомпилировав WOLFSSL.DLL, я сталкивался со странной ошибкой «Access Denied» при попытке загрузить DLL вызовом LoadLibrary Windows API. Наконец, после множества мучений я разобрался, что в опциях компилятора нужно отключить в генерируемом коде отладочную информацию и переключить целевой процессор со стандартного (Intel 8086) на 80286.

gi9ypx2lbiyad7t3vdqbedx8tgk.png


▍ Заработало!


Заставив всё компилироваться, я быстренько набросал тестовую программу для связи с тестером возможностей браузера Qualys SSL Lab и скачивания результатов в файл.

Отладка происходящего (особенно отсутствующих возможностей) была возможна благодаря использованию Wireshark для декодирования сообщения «Client Hello». Это сообщение, которое TLS-клиент отправляет серверу при подключении, чтобы рассказать о своих возможностях.

kciz6qxfwkxu-evkdyae2emgrog.png


В моих первоначальных сборках мне не хватало расширения Server Name Indication (SNI), которое сегодня требуется, чтобы позволить серверам хостить несколько TLS-сайтов на одном IP-адресе с разными сертификатами. Добавив расширение и переместив ещё немного кода в internal2.c, я добился своего.

Мой код скачал результаты Qualys в Windows 3.11 for Workgroups. Я открыл получившийся файл в Internet Explorer 3, и вуаля:

rjz7vmoozd7yje7es6t7juz47so.png


Мы видим, что наша сборка WolfSSL поддерживает приличное количество (два) отборных наборов криптографических алгоритмов, а также множество менее безопасных, и этого более чем достаточно, чтобы с нами могло общаться большинство веб-сайтов!

▍ Бонус: TLS 1.3


Для большинства ситуаций поддержки TLS 1.2 было бы вполне достаточно, но в WolfSSL есть встроенный TLS 1.3, и было бы замечательно, если бы его могли использовать наши программы для Windows 3.1. По сравнению с TLS 1.2 в версии TLS 1.3 есть существенные изменения; кроме того, список из 37 наборов криптографических алгоритмов урезан всего до пяти безопасных.

Поэкспериментировав с флагами компилятора и переместив ещё больше кода, чтобы избежать ошибок «segment too large», я, наконец, заставил свою сборку WolfSSL компилироваться со включённым TLS 1.3. Однако я столкнулся с проблемой: код думает, что полученные криптографические данные «слишком велики», потому что занимают 1 байт. Пока я закомментировал эту проверку, и, похоже, в реальных ситуациях всё работает, но мне бы хотелось понять, почему это происходит.

tdmedssvzunpe9ftlavl7pvjicg.png


Вот скриншот страницы Qualys, на которой видно, что TLS 1.3 работает и используется набор алгоритмов TLS_AES_128_GCM_SHA256:

9cyf3wuaz5g-3t6opyjjwulmhaa.png


tl17u1hcvcuhssbokig8v3j9t2q.png


enhtr8z0devkyuix4jiys_osvjy.png


▍ WinGPT


В чём смысл библиотеки, если её нельзя использовать? Попробуйте ИИ-помощник для Windows 3.1 WinGPT, который пользуется портом WolfSSL для прямого подключения к серверам OpenAI API.

▍ Файлы


Ниже представлен модифицированный исходный код WolfSSL с лицензией GNU General Public License (GPL) v2, под которой лицензирована WolfSSL. Как говорилось выше, этот код со внесёнными мной модификациями небезопасен, ненадёжен и не даёт никаких гарантий. Его определённо не стоит использовать ни для чего, кроме как тестирования и развлечений. Полную лицензию GPL v2 можно найти здесь.

Также вы увидите, что я так и не нашёл способа интеграции с системами сборки (например, CMake), которые использует остальная часть WolfSSL, и внёс изменения, которые непросто будет интегрировать обратно в основную ветвь исходников. Если кто-то захочет сделать 16-битную Windows поддерживаемой архитектурой WolfSSL, то это было бы замечательно; к сожалению, я этого пока не добился.

WolfSSL для 16-битной Windows + исходный код WinGPT 1.0 (14 МБ)

▍ Примечания


  • [1] Самым близким к этому оказалась работа Дидье Ноора по переносу
    mbed TLS в Windows NT 3.x и 95, однако Windows NT 3.x, как и Windows 95 — это 32-битная операционная система.
  • [2] Эта модель применима в реальном режиме, а в защищённом режиме используется таблица дескрипторов и разные размеры для адресов. Подробнее см. в статье Википедии.


Выиграй телескоп и другие призы в космическом квизе от RUVDS. Поехали?

© Habrahabr.ru