Apple привнесла боль в публикацию Electron-приложений на macOS

Илья, JavaScript-разработчик Fora Soft

В новых версиях macOS, начиная с Mojave 10.14, Apple привнесла серьезные изменения в публикацию приложений. Они связаны с подписанием и нотаризацией приложений. Если ваше приложение “плохое” по мнению Apple, оно выдаст всяческие угрожающие предупреждения пользователям при первом запуске, мотивируя их как можно быстрее переместить его в корзину и забыть о нем, как о страшном сне. Не самая приятная ситуация для заказчиков приложений, не правда ли?

Автор сам является разработчиком приложения на ElectronJS, и примерно год назад столкнулся с тем, что помимо привычного подписания приложения сертификатом, его необходимо нотаризовать на серверах Apple, то есть проверить на наличие потенциально вредоносного кода. “Рецепт” нотаризации до крайности прост: отправляем приложение, ждем 5-10 минут, пока автоматизированная утилита Apple проверит код, получаем результаты и радуемся (у меня же не было цели написать вирусное ПО для macOS, правда?). Для только погружающихся в эту область, подготовил отдельную вводную статью, в которой рассказал об основных аспектах публикации приложения на macOS.

Я добавил в общий workflow сборки macOS-приложения процесс нотаризации. И с первого раза получил священный “Package Approved” от Apple, а Electron-приложение благополучно перестало выводить сообщение о возможном наличии вирусного ПО при первом запуске.

Все было хорошо примерно до недавнего времени, когда я заметил, что такая ситуация повторяется. “Ну что ж… - подумал я, - ... сейчас нотаризируем приложение повторно, ничего сложного”. Однако в результате статус нотаризации был “Package Invalid”. Apple приложила лог нотаризации, в котором было примерно 100500 ошибок нотаризации. Например, невалидный сертификат для некоторых файлов приложения (“а может сертификат устарел?”), отсутствие hardened runtime (“что?”), и даже отсутствие сертификатов для исполняемых файлов библиотек из NPM (“вы серьезно??? я не имею к ним прямого отношения, отпусти”).

Звучит страшно, не правда ли? Да-да, Apple постепенно “закручивают гайки”, делая свою операционную систему более безопасной и не допуская возможности опубликовать хоть сколько бы небезопасное, с их точки зрения, приложение.

Я ушёл на некоторое время в запой в исследование путей решения для указанных ошибок. В результате удалось решить все проблемы и лучше разобраться в “новых правилах игры” с Apple. К слову, в интернете есть решения на все проблемы, но, к сожалению, они достаточно сильно разбросаны по просторам всемирной паутины. Я потратил достаточно много времени, чтобы разобраться со всем, а сейчас хотел бы собрать весь накопленный опыт в статью, чтобы помочь другим разработчикам быстрее разобраться в некоторых проблемах в процессе нотаризации Electron-приложения.

Стоит также отметить, что на момент, когда я столкнулся с проблемами, приложение использовало библиотеку ElectronJS версии 5.0.0 (да, разработка приложения началась достаточно давно), для сборки приложения использовался electron-packager версии 13.1.1, а для подписания – библиотека electron-osx-sign версии 0.4.15.

Для подписания приложения у нас есть сгенерированный на сайте разработчиков Apple сертификат вида “Developer ID Application: <TeamName> (<UniqueID>)”. Сертификат такого вида позволяет распространять приложения через любые интернет-сервисы. Для этого, кстати, мы используем уже написанный релизный сервер Electron (electron-release-server). Для публикации приложения в Mac App Store нужен другой сертификат.

А что там с болью? Пока не ощутили…

Перейдем теперь к основной боли. Как уже сказал в начале, я столкнулся с некоторым набором ошибок, который был представлен в логе нотаризации от Apple.

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

Претензии от Apple

The binary is not signed. Отсутствие сертификатов и временных меток для внутренних исполняемых файлов приложения

Лог нотаризации от Apple для многих внутренних файлов приложения содержал ошибку вида The binary is not signed. Сюда включались все исполняемые файлы, например собранные библиотеки ffmpeg и echoprint-codegen, которые используются в приложении внешним образом; а также некоторые исполняемые файлы библиотек с NPM.

Решение достаточно простое. В процессе подписания приложения необходимо помимо самого .app подписывать этим же сертификатом все файлы, на которые ругается нотаризация Apple.

Сделать это достаточно просто. Если вы напрямую используете утилиту codesign, необходимо помимо основного .app файла указать все эти файлы через пробел. Используя библиотеку electron-osx-sign, задействуйте поле binaries, чтобы задавать массив путей до всех бинарных файлов, которые нуждаются в подписи этим же сертификатом.

Стоит отметить, что это же решение применимо и для ошибки нотаризации “The signature does not include a secure timestamp”. Дело в том, что при подписании конкретного файла, сертификат также проставляет свою временную метку – как раз то, на отсутствие чего ругается нотаризация.

The signature of the binary is invalid. Нарушение путей в общей иерархии Electron-приложения

Я считаю, что ошибка “The signature of the binary is invalid” – недочет со стороны Apple. Дело в том, что когда .app файл приложения отправляется на нотаризацию, оно автоматически упаковывается в .zip архив. В результате этого, по какой-то причине нарушается иерархия некоторых путей приложения, вследствие чего для некоторых файлов приложения сертификат становится невалидным.

Решение этой проблемы – самостоятельно упаковывать файл приложения в .zip архив, используя встроенную в macOS утилиту ditto. Здесь важно использовать специальный флаг --keepParent, который позволяет сохранить иерархию всех путей в процессе упаковывания. Соответственно для упаковывания я использовал вот такую команду:

ditto -c -k --keepParent “<APP_NAME>.app” “<APP_NAME>.zip”

После этого, полученный .zip архив можно отправлять на нотаризацию, и проблема должна исчезнуть.

Стоит отметить здесь один момент. Для нотаризации я использую библиотеку electron-notarize. Когда приложение успешно нотаризировано, библиотека пытается вернуть .zip архив приложения, чтобы в дальнейшем можно было распаковать его и заменить .app на нотаризованный .app файл. Однако я столкнулся с ошибкой о том, что системное средство XCode spectool не умеет работать с архивами… Грустно, но правды ради, проблема не критичная, ибо если приложение успешно прошло нотаризацию, можно использовать оригинальный .app файл из собранного до этого .zip архива, никаких проблем не будет.

The executable does not have the hardened runtime enabled. Включение защищенной среды исполнения для Electron-приложения

На десерт, поговорим о наиболее сложной и не менее интересной проблеме нотаризации. В последних версиях операционной системы macOS (10.15 Catalina и 11.0 Big Sur) Apple предъявляет требование к наличию защищенной среды для запуска у публикуемых приложений (hardened runtime). Такая среда обеспечивает защиту приложения от возможных инъекций нежелательного кода, DLL атак, а также фальсификации пространства памяти, выделяемого под процесс (process memory space tampering). Apple хочет как можно лучше защитить пользователей своей операционной системы, поэтому это требование наиболее важное в нотаризации приложения. Пусть и без выполнения требований выше тоже далеко не уедешь.

Для соблюдения этого требования необходимо при подписании приложения задействовать флаг --options=runtime (если подпись осуществляется с помощью утилиты codesign) или указать поле hardenedRuntime: true (если подпись осуществляется с помощью electron-osx-sign библиотеки). Собственно, именно это действие и предприняли в результате чего обнаружили, что использование этого флага для подписи полностью ломает Electron-приложение: приложение попросту не запускается или падает с системной ошибкой.

С появлением новой проблемы, время моего запоя исследования путей решения проблем незапланированно увеличилось. Так или иначе, в результате многократного обращения к Google, выяснил основной набор требований, выполнение которых поможет собрать и подписать Electron-приложение с использованием hardened runtime.

Как собрать и подписать Electron-приложение?

Требование номер раз. Забудем про Electron версии 5

Да, как бы прискорбно это ни звучало, в процессе поддержки уже имеющегося Electron-приложения придется окончательно забыть про пятую версию Electron и обновить его как минимум до шестой. Думаю, что разработка и сборка нового приложения произойдёт на версии поновее и проблем не возникнет. Как гласит источник (Electron 6.0 Release notes) поддержка hardened runtime появилась только в шестой версии Electron, и все попытки включить это в предшественнике приведут к неудаче. Помимо этого, если вы так же, как и я, используете для сборки electron-packager, вам стоит обновить его до версии 14.0.4 для корректной сборки.

Тестировщики приложения заплачут горькими слезами, но что поделать, таковы правила игры с Apple в данный момент, и придется делать полный регресс-тест приложения с обновленной версией Electron.

Требование номер два. Не забываем создавать файл параметров “entitlements.plist”

Такая практика распространена в разработке приложений в среде XCode и редко применяется в разработке и сборке Electron-приложения, где Electron-сборщик автоматически подключит файл по умолчанию со стандартным набором параметров. Файл представляет из себя XML-код, в котором описаны необходимые системные разрешения / запреты на использование тех или иных системных ресурсов операционной системы. Сейчас такой файл необходим с целью указания двух важных параметров.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
...
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
...
</dict>
</plist>

Первый (allow-unsigned-executable-memory) разрешает приложению использовать неразмеченную память, а второй (allow-dyld-environment-variables) разрешает использование переменных окружения. Переменные окружения знакомы каждому JS-разработчику, но это не совсем те переменные, которые мы используем в своем приложении. Параметр разрешает фреймворку Electron “под капотом” использовать нужные ему для правильной работы переменные окружения, например, путь до системной библиотеки libffmpeg.dylib. При отсутствии этого параметра в файле entitlements.plist, собранное ранее приложение при первом запуске упадёт с ошибкой внезапной пропажи библиотеки libffmpeg.dylib, хотя она лежит внутри .app приложения.

Требование номер три. Правильно подключаем файл параметров “entitlements.plist”

Файл entitlements.plist необходимо подключить в процессе подписания сертификатом уже собранного Electron-приложения. При использовании утилиты codesign напрямую нужно прописать флаг --entitlements, после которого через пробел необходимо указать путь до файла с параметрами. Однако, с первого раза может не получиться, и codesign выдаст ошибку “unrecognized option” для этого флага. Чтобы устранить это, нужно использовать утилиту codesign не из XCode Developer Tools, а из полной среды XCode, где представлена более расширенная версия этой утилиты. Для этого нужно выполнить следующие команды:

xcode-select -print-path
xcode-select -switch /path/to/SDK

Есть способ проще: использовать ранее упомянутую библиотеку electron-osx-sign. Здесь есть один важный момент, связанный с тем, что подключить файл с параметрами необходимо как в поле entitlements, так и в поле entitlements-inherit, а иначе ничего не сработает. Дело в том, что параметр entitlements-inherit отвечает за параметры, указываемые для фреймворков дистрибутива, а наше приложение как раз работает на основе фреймворка Electron, которому обязательно нужны переменные окружения с путями до системных файлов (говорили об этом выше на примере библиотеки libffmpeg.dylib).

Ну что, стало легче?

Конечно, если вы разрабатываете десктопные приложения для macOS на XCode, то скорее всего с такими трудностями вы не столкнетесь, Apple стремительно адаптирует свою среду под быстро меняющиеся требования. Но, как оказалось, Electron-приложения тоже вполне имеют свое право на жизнь, проблемы с нотаризацией могут быть решены, а пользователи могут быть счастливы, не увидев предупреждения о потенциально опасном программном обеспечении.

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

1744 views·1 share
1744 views