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

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

Одной из таких компаний является КриптоПро.Это не реклама, выбор продуктов этой фирмы был сделан задолго до моего прихода в компанию, так что я всего лишь пользователь, не более. Так вот, этот производитель выпускает криптографические продукты и библиотеки, которые позволяют подписывать и шифровать документы с использованием ГОСТ-алгоритмов. Есть версии ПО, которые можно использовать под Windows, под Linux, есть также специальные продукты для использования с Java. То есть, покрыты практически все потребности, которые только могут возникнуть у разработчика.

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

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

Итак, исходные данные. Имеется ПО, разработанное в где-то в 2007-2008 годах, написанное на Delphi (версии 2005), использующее системную библиотеку CAPICOM от Microsoft. Новое программное обеспечение пишется на Java, ожидается, что оно сможет работать и под управлением Windows, и под управлением Linux, и для работы с криптографией используется КриптоПро Java CSP. Надо отметить, что старое ПО работает с "отделяемой" подписью, таким образом, новое ПО должно работать в таком же режиме. Как видно, зверинец получается знатный.

На самом деле, я не буду включать код, который пришлось писать, да и разбирать. Лишь отмечу этапы, которые потребовалось пройти на пути к достижению цели. Начнем...

Шаг №1. Написан код на Java, который подписывает файл с использованием нужного сертификата и ключа. Тут почти не возникло никаких сложностей, благо у КриптоПро есть ряд примеров, которые здорово помогают в понимании того, что и как можно (и нужно) использовать. После получения подписи, она была успешно проверена с помощью сайта Гос. Услуг, что вселило некоторую уверенность в том, что задача решена. Святая простота! Я бы даже сказал, наивность. Попытка проверить подпись программным обеспечением, написанным на Delphi бесславно провалилась.

Шаг №2. Предстоит понять, какое ПО "чудит". Хотя, если честно, я подозревал, какое именно. Используя маленькие хитрости, выгружаю из программы на Delphi подписанный файл и его подпись (просто так выгрузить нельзя, программа удаляет файл подписи после проверки). Произвожу проверку с помощью сайта Гос. Услуг - подпись проверку не проходит.

Шаг №3 - понять, что же именно приводит к таким разным результатам, вроде бы, одного и того же действия. И тут на помощь пришел наработанный ранее опыт. Дело в том, что некоторое время назад решалась очень похожая задача - обеспечить совместимость подписи, устанавливаемой при помощи нашего ПО, написанного на Delphi, с требованиями сайта Гос. Услуг. Проблема была очень похожей, но дело облегчалось наличием исходных текстов. Довольно быстро стало понятно, что пресловутая несовместимость обусловлена взаимодействием Delphi и CAPICOM. При написании софта использовалась Delphi 6 (да, ПО довольно возрастное), строки в этой версии - однобайтовые: один символ - один байт. CAPICOM же работает с UNICODE строками: один символ - два байта. И, хотя, в CAPICOM предусмотрена специальная функция, позволяющая преобразовать "правильным" образом однобайтовую строку в двухбайтовую (
Utilities.ByteArrayToBinaryString), наше ПО эту функцию не использовало. Ну а раз так, то преобразование строки Delphi в строку CAPICOM происходило методом "по умолчанию" - каждый байт превращался в два. При использовании же упомянутого метода, он, скорее всего, "упаковывал" бы два символа строки Delphi в один символ UNICODE строки. Таким образом, скложилась интересная ситуация - в зависимости от того, использовалась, или нет, функция Utilities.ByteArrayToBinaryString подписывалась разная информация. И, надо сказать, что при использовании метода CAPICOM, подписывалась "истинные" данные, а вот при преобразовании "по умолчанию" подписывалась слегка измененная версия первоначального документа. Все эти умозаключения довольно быстро нашли свое практическое доказательство - как только добавили вызов метода Utilities.ByteArrayToBinaryString, подпись стала проходить проверку на сайте Гос. Услуг. (Справедливости ради надо сказать, что вызов метода Utilities.ByteArrayToBinaryString в нашем ПО все-таки присутствовал, но зависил он от значения флага, который должен был обеспечить обратную совместимость с более ранними версиями, в котором этого вызова не было).

В решаемой же сейчас задаче, проблема, скорее всего, кроется в том же самом противоречии: Delphi 2005 все так же использует однобайтные символы и строки, а вот Java работает со строками, каждый символ которых (внутренне) представлен в кодировке UTF-16. То есть, для большинства обычно используемых символов алфавитов, это два байта (хотя возможны ситуации, когда один символ может быть представлен двумя "блоками" по два байта).

Шаг №4 - надо каким-то способом на Java провести преобразование данных таким образом, чтобы конечный результат совпал с тем, что мы имеем при работе с Delphi и CAPICOM. И тут как раз и начались трудности. Эти трудности были связаны с тем, что, как оказалось, я не совсем представлял себе процесс преобразования однобайтовых символов Delphi в двухбайтовые символы CAPICOM. А без этого преобразование не повторить!

Первая моя ошибка заключалась в том, что, пытаясь понять, что именно происходит, я попытался подписать данные, полностью состоящие из букв английского алфавита и цифр. Просмотривая получившуюся неотделяемую подпись (а эксперименты я проводил именно с неотделяемой подписью, так как в ней содержатся и сами подписанные данные), я обнаружил, что а процессе преобразования к каждому символу, представленному в исходных данных одним байтом, добавился еще один байт, содержащий нуль-символ (симвод с кодом 0x00). При этом, в результирующих (подписанных) данных нуль-символ следовал за парным ему значащим символом. Таким образом, подписанные данные оказались в кодировке UTF-16 Little-Endian, что для Windows, в принципе, нормально. На самом деле, надо понимать, что порядок байт, скорее всего, зависит от платформы, на которой выполняется преобразование. Так вот, не долго думая я написал код на Java, который удваивал каждый байт нуль-символом, и затем превращал этот "удвоенный" массив в строку в кодировке UTF-16LE. И проверка подписи стала проходить...

И проходила ровно до тех пор, пока на вход подписания не был подан текст, содержащий кириллицу. Проверка подписи на таких данных вновь провалилась. Я вернулся к экспериментам с неотделяемой подписью, которые показали, что символы кириллицы в качестве второго байта получают не нуль-символ, а символ с кодом 0x04. И этому есть вполне логичное объяснение: именно такую кодировку и имеют символы кириллицы при отображении кодовой страницы windows-1251 в UTF-16. Символы английского алфавита сохраняют предшествующий нуль-символ, а у символов русского алфавита первый байт имеет код 0x04. Например, английская буква 'A' имеет код 0x0041, а русская буква 'А' - код 0x0410. В общем, в Wikipedia все это есть.

Итак, стала вырисовываться система: преобразуя однобайтовые символы строк Delphi в двухбайтовые символы для CAPICOM, Windows, скорее всего, опирается на текущую (используемую) кодовую страницу. Какую страницу, ведь в Windows уже давно используется UNICODE?! Да, но... давайте не будем забывать про настройку кодовой страницы для приложений, не использующих UNICODE. Именно ее мы указываем тут:

ctrlpnlregion

Если так, то все довольно просто: надо рассматривать подписываемые байты как строку символов в кодировке windows-1251, которую надо преобразовать в строку UTF-16LE. Дело за малым: пишем код и проверяем его работоспособность - подписываем текст, содержащий символы русского и английского алфавита. Все работает!

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

Что ж, вновь начинается анализ неотделяемых подписей, которые установлены программой, заведомо правильно (с точки зрения ПО, с которым требуется обеспечить совместимость) подписывающей данные. И довольно быстро обнаруживается источник проблемы. Встречайте: символ с шестнадцатеричным кодом 0x98. То есть, он такой в исходном файле. А в преобразованном по разработанному алгоритму, на его месте стоит 0xFDFF. Эталонный же софт выдает 0x0098. Не забываем про Little Endian: в исходном файле лежит 0x9800, а в "подготовленном" 0xFFFD. Что же такое произошло?!

И Wikipedia, собственно, подсказывает, что именно происходит. Во-первых, в таблице символов кодировки windows-1251 символ с кодом 0x98 помечен, как неиспользуемый. Уже один этот факт вызвал у меня недоумение: что, не нашлось символа, который нужно отображать? А во-вторых, в UTF-16 есть коды, которые называются "noncharacter", и именно к ним относится код 0xFDFF, используемый как раз для представления "неиспользуемых" символов. Теперь все становится на свои места. Хотя, конечно, вопросы остаются.

Но, для начала, надо убедиться, что предположение верно. Добавляю в процедуру "подготовки" данных для подписи замену всех вхождений комбинации байтов 0xFFFD на 0x9800 (с учетом кодировки), подписываю результат, проверяю... Барабанная дробь, все дела... Ура! Все здорово, проверка прошла успешно. Фанфары, шапки в воздух, занавес.

В чем же мораль сей басни? На мой взгляд, она (мораль) заключается в том, что надо использовать механизмы, предоставляемые инструментом: вот есть у CAPICOM метод Utilities.ByteArrayToBinaryString, значит и надо его использовать. Если не использовать, то можно получить проблемы. И проблемы, которые не так-то просто решить. Ведь не смотря на то, что мне удалось добиться проверки подписи, проэмулировав преобразование данных, сомнения в том, что это теперь будет работать всегда и везде, лично у меня - остаются. Что будет, если, например, на двух компьютерах - на первом, на котором создается электронная подпись, и на втором, на котором подпись проверяется - будут разные региональные настройки? Пройдет проверка? Сомневаюсь. Ведь я преобразовываю данные исходя из определенных предположений (что региональные настройки установлены определенным образом), а в случае, если эти предположения не верны, скорее всего, что-нибудь пойдет не так и вся "стройная" констркуция рухнет.