Daily MaverickPOLL PERIL: ANC campaigner suffers electric shock and broken leg during poster drivePunchEbola:​‌⁠​‍‌‍⁠⁠‍‌​‍​‍‍‌⁠‌​ NOA warns against self-medicationBollywood HungamaAastha Tiku’s directorial debut Dreamboat to premiere at Dharamshala International Film Festival; first look outRTP DesportoCaitlin Simmers e Molly Picklum na final da prova de elite do surf em PenicheEl ComercioRaúl Castro: “No existe una posibilidad jurídica inmediata” de llevarlo ante la justicia de EE.UU.The Jerusalem PostWith five days until elections, Netanyahu shifts strategies to courting right-wing votes - analysisInquirerOSG welcomes SC ruling, seeks review of budget safeguardsDigital SpyCoronation Street and Emmerdale aren't on ITV1 or ITVX today - here's when they returnZDF heuteAktuelle Pressemitteilungen des ZDFVilaWebEls Mossos investiguen una agressió a un alumne d’onze anys a una escola de CerveraSouth China Morning PostMissing lift fire door frames confined to Taizoom, Hong Kong authorities sayAntara News2026 China Apple Annual Conference and Qingyang Apple Conference Kicks Off
The Daily Newsstand · Free, Always
Thursday, October 22, 2026

ЭЦП в Safari на macOS: как я подружил Госплагин, Контур.Плагин и КриптоПро с Safari Web Extensions

Translate

Если у вас Mac и электронная подпись на Рутокене, вы наверняка знаете ритуал: чтобы войти на Госуслуги по ЭЦП, отчитаться в личном кабинете ФНС или подписать документ в Диадоке, нужно открыть Chrome. Госплагин, Контур.Плагин и КриптоПро ЭЦП Browser plug‑in официально работают только в Chromium‑браузерах и Firefox. У КриптоПро есть старое Safari App Extension, но в актуальных версиях Safari оно даже не появляется в настройках.

Я захотел, чтобы всё это работало в Safari, и в итоге получилось три небольших приложения для macOS с открытым кодом: Gosplugin Safari (Госуслуги/ЕСИА), Kontur Safari (Диадок и сервисы Контура) и CryptoPro Safari (nalog.ru, торговые площадки и всё, что работает через cadesplugin). Ниже — как это устроено и на какие грабли Safari я наступил по дороге.

Почему нельзя просто сконвертировать расширение

У Apple есть xcrun safari-web-extension-converter, который превращает Chrome‑расширение в Xcode‑проект. Расширение из CRX конвертируется за минуту, но работать не будет:

  • Нет Chrome Native Messaging. В Safari runtime.connectNative и sendNativeMessage ведут не к произвольному native host из NativeMessagingHosts/*.json, а только к модулю вашего же приложения (SafariWebExtensionHandler), который живёт в песочнице.

  • Нет localhost. У Госплагина есть запасной режим через WebSocket на 127.0.0.1, но Safari не пускает расширения на localhost.

  • Нет части API. Контур.Плагин проверяет chrome.management, которого в Safari нет, и без патча молча отбрасывает все запросы сайта.

  • Строгий порядок вызовов. Safari выполняет обращения расширения к native‑модулю строго по одному — это сильно влияет на производительность (об этом ниже).

Архитектура

Сайт ──► Safari-расширение (официальное, с патчем)
           │ browser.runtime.sendNativeMessage
           ▼
         модуль расширения (SafariWebExtensionHandler, в песочнице)
           │ HTTP 127.0.0.1:9105 / 9106 / 9107
           ▼
         мост внутри приложения (фоновый процесс <app> --bridge)
           │ stdio, протокол Chrome Native Messaging
           ▼
         Госплагин / Контур.Плагин / nmcades ──► КриптоПро CSP ──► токен

Модулю расширения в песочнице разрешены исходящие соединения (network.client + NSAllowsLocalNetworking), а вот запускать чужие бинарники — нет. Поэтому плагин запускает само приложение: при первом старте оно ставит себя в LaunchAgent с аргументом --bridge и дальше работает без окна, слушая 127.0.0.1. Мост находит официальный манифест native host в /Library/Google/Chrome/NativeMessagingHosts, запускает указанный там бинарник с allowed_origins[0] в аргументах (как это делает Chrome) и общается с ним обычным протоколом: 4 байта длины little‑endian + JSON.

Само расширение — официальное, скачанное из Chrome Web Store, с минимальными патчами: chrome.runtime.connectNative(...) заменён на свою реализацию порта поверх sendNativeMessage.

Как уложить стриминговый порт в запрос‑ответ

Native Messaging в Chrome — это долгоживущий порт: сообщения ходят в обе стороны когда угодно. В Safari есть только «отправил — получил ответ», да ещё и последовательно. Первая версия делала open/send/poll, где poll ждал ответ до 2 секунд, — и Диадок стал заметно тормозить: пока одна вкладка висит в poll, запросы остальных стоят в очереди Safari.

Что в итоге сработало:

  • send сразу ждёт ответа плагина (до 8 с) и возвращает его в том же вызове — одна операция вместо двух;

  • poll запускается только если есть запрос без ответа (например, пользователь вводит PIN), и короткими интервалами по 0,5 с;

  • ответы, приходящие частями (у Контура — isChunked/isFinalChunk, у Госплагина — meta.data.chunk/chunks, у КриптоПро — partial), учитываются, чтобы не прекращать опрос раньше времени.

Безопасность моста

Локальный HTTP‑сервер, который умеет подписывать документы, — очевидная мишень для любого сайта. Защита простая и надёжная:

  • мост слушает только 127.0.0.1;

  • принимает только POST с нестандартным заголовком X-NMA: 1 и без заголовка Origin. Страница в браузере не может отправить такой запрос: нестандартный заголовок требует CORS‑preflight, который мост отклоняет, а браузер всегда добавляет Origin. Модуль расширения ходит через URLSession и Origin не ставит;

  • каждое приложение запускает только свой native host из белого списка;

  • содержимое запросов и подписей не логируется.

КриптоПро и nalog.ru: самое интересное

С КриптоПро пришлось повозиться больше всего. Официальное расширение (MV3) в content.js для Safari использует старый API Safari App Extension (safari.extension.dispatchMessage), которого у Web Extension нет, — пришлось перевести его на обычный runtime.connect.

Но главное — в cadesplugin_api.js, который сайты (включая nalog.ru) подключают к себе. Для Safari он ждёт от расширения адрес скрипта и проверяет его так:

if (!url.match("^(moz|safari)-extension://[a-zA-Z0-9/_-]+/nmcades_plugin_api.js$")) {
    plugin_loaded_error();
    return;
}

У Safari Web Extension схема safari-web-extension://, и проверку она не проходит. Менять код сайтов нельзя, поэтому расширение отдаёт странице условный адрес safari-extension://nma-cryptopro/nmcades_plugin_api.js, а маленький скрипт в контексте страницы подменяет его на настоящий в момент, когда cadesplugin_api.js создаёт тег <script>:

var FAKE = /^safari-extension:\/\/nma-cryptopro\/nmcades_plugin_api\.js$/;
var sa = Element.prototype.setAttribute;
Element.prototype.setAttribute = function (n, v) {
    if (this instanceof HTMLScriptElement && String(n).toLowerCase() === "src" && FAKE.test(v)) v = realUrl;
    return sa.call(this, n, v);
};

После этого вход и подпись на nalog.ru заработали в Safari.

Грабли Safari, которые не найти в документации

  • Восстановленные вкладки. После перезагрузки Safari восстанавливает вкладки раньше, чем просыпается фон расширения, и tabs.sendMessage к ним падает с «Tab not found». Решение — в таком случае рассылать ответ во все вкладки: контент‑скрипт сам отбрасывает чужие сообщения по id.

  • Потерянная связь с вкладкой. Иногда tabs.sendMessage не падает, но сообщение до вкладки не доходит — и это не лечится даже перезагрузкой страницы, только новой вкладкой. Контент‑скрипт Госплагина отвечает на каждое сообщение sendResponse(null), поэтому теперь фон ждёт подтверждение 1,5 с и без него делает ту же рассылку.

  • Мост после входа в систему. LaunchAgent стартует не мгновенно, а Safari уже открывает вкладки — расширение повторяет подключение к мосту до 60 секунд.

  • Обновление приложения при открытом Safari. Если заменить.app, пока Safari запущен, он решает, что расширение удалено, и выключает его. Обновлять — только закрыв Safari.

  • Утечка процессов. Расширения открывают порты на каждую вкладку, а Safari не всегда их закрывает — мост закрывает сессии, к которым никто не обращался больше 2 минут.

Подпись приложения без платного аккаунта

Safari спокойно загружает расширения из приложений, подписанных сертификатом Apple Development, а его можно получить и с бесплатным Apple ID через Xcode. Поэтому релиз собирается без подписи (ad‑hoc, universal), а пользователь один раз подписывает его своим сертификатом:

codesign --force --sign "Apple Development" --preserve-metadata=entitlements --options runtime "Gosplugin Safari.app"/Contents/PlugIns/*.appex
codesign --force --sign "Apple Development" --preserve-metadata=entitlements --options runtime "Gosplugin Safari.app"

Без подписи тоже работает, но через «Разработка → Разрешить неподписанные расширения», и эту галочку Safari сбрасывает при каждом перезапуске.

Итог

Три приложения, официальные плагины и расширения, ничего не уходит с компьютера. Нужны macOS 12+, Safari 16+, КриптоПро CSP с лицензией и соответствующий плагин; Chrome не нужен. Проект не связан с Ростелекомом, СКБ Контур или КриптоПро.

Код, DMG и подробная инструкция: github.com/KLon777/gosuslugi‑diadoc‑safari‑ecp. Буду рад issue и PR — особенно если у вас другой носитель или другой сайт с cadesplugin.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.