[Перевод] Современный TLS/SSL в Windows 3.11
В последнее время происходит ренессанс новых программ для ретро-компьютеров — для них пишут клиенты 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 КБ. Сегмент может представлять код (команды) программы, данные (глобальные и статические локальные переменные) или стек программы. Адреса в памяти состоят из двух частей: сегмент, в котором находится память, и смещение внутри этого сегмента. Указатель на этот полный адрес из двух частей называется дальним указателем.
Пример дальнего указателя[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
в коде. В конце концов, всё скомпилировалось.
Но даже скомпилировав WOLFSSL.DLL, я сталкивался со странной ошибкой «Access Denied» при попытке загрузить DLL вызовом LoadLibrary
Windows API. Наконец, после множества мучений я разобрался, что в опциях компилятора нужно отключить в генерируемом коде отладочную информацию и переключить целевой процессор со стандартного (Intel 8086) на 80286.
▍ Заработало!
Заставив всё компилироваться, я быстренько набросал тестовую программу для связи с тестером возможностей браузера Qualys SSL Lab и скачивания результатов в файл.
Отладка происходящего (особенно отсутствующих возможностей) была возможна благодаря использованию Wireshark для декодирования сообщения «Client Hello». Это сообщение, которое TLS-клиент отправляет серверу при подключении, чтобы рассказать о своих возможностях.
В моих первоначальных сборках мне не хватало расширения Server Name Indication (SNI), которое сегодня требуется, чтобы позволить серверам хостить несколько TLS-сайтов на одном IP-адресе с разными сертификатами. Добавив расширение и переместив ещё немного кода в internal2.c
, я добился своего.
Мой код скачал результаты Qualys в Windows 3.11 for Workgroups. Я открыл получившийся файл в Internet Explorer 3, и вуаля:
Мы видим, что наша сборка 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 байт. Пока я закомментировал эту проверку, и, похоже, в реальных ситуациях всё работает, но мне бы хотелось понять, почему это происходит.
Вот скриншот страницы Qualys, на которой видно, что TLS 1.3 работает и используется набор алгоритмов TLS_AES_128_GCM_SHA256
:
▍ 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. Поехали?