Plank help · updated 2026-09-10

Подпись ключом ЭЦП РК (без NCALayer)

Что на самом деле делает NCALayer, когда государственный портал РК просит подписать, почему это удобство десктопа, а не требование протокола, и как получить ту же подпись из скрипта — вместе с правилами обращения с ключом, которые обязательны всегда.

Agents: fetch the raw markdown of this page at /ru/help/kz-ecp-signing.md

Подпись ключом ЭЦП РК (без NCALayer)

Почти каждый государственный портал Казахстана закрывает действия на запись подписью ЭЦП, и почти каждый просит её через NCALayer — небольшое Java-приложение, которое пользователь ставит себе; оно открывает окно, спрашивает, каким ключом подписать, и возвращает подпись.

NCALayer — это удобство десктопа, а не свойство протокола. Он не хранит секрета, которому доверяет портал, и не выполняет ничего на стороне сервера. Это локальная служба на WebSocket, которая берёт документ, подписывает его файлом ключа, уже имеющимся у пользователя, и отдаёт подпись обратно в страницу. То же самое может сделать что угодно, умеющее читать .p12.

От этого различия зависит, автоматизируем ли портал. «Там нужен NCALayer» — не повод остановиться: именно так Подключение ЭСФ выписывает настоящие счета-фактуры в проде — без NCALayer и без QR на каждую операцию.


1. Схема всегда одна и та же

Любой портал, который подписывает таким способом, работает по одному сценарию:

1. запросить у сервера документ для подписи   →  он возвращает XML
2. подписать этот XML локально ключом          →  enveloped XMLDSig
3. отправить подписанный XML обратно           →  действие зафиксировано

Документ строит сервер, поэтому вы никогда не собираете его сами. Два живых примера:

ПорталШаг 1Шаг 3
Tizilim (тема)POST /api/auth/esign-auth-xmlPOST /api/auth/login-check-esp {xml}
ЭСФ (тема)AuthService.createAuthTicketauthTicketXmlSessionService.createSessionSigned {signedAuthTicket}
egov.kz (тема)POST /v1/<SVC>/xmlPOST /v1/<SVC>/signing/send-eds {signed_xml}

Одно измеренное исключение: у входа на egov.kz шага 1 нет вовсе — документ там константа, которую клиент собирает сам. Двадцать восемь его услуг идут по схеме выше.

Как только эта схема опознана, найти её в новом портале — вопрос поиска по клиентскому бандлу: эндпоинт, который возвращает XML, и эндпоинт, который принимает {xml: …}.

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


2. О чём на самом деле просят NCALayer

Прочитайте запрос, который страница ему отправляет, — это полная спецификация подписи, которую вам нужно воспроизвести. Вот запрос Tizilim, дословно из его бандла:

{
  "module": "kz.gov.pki.knca.basics",
  "method": "sign",
  "args": {
    "allowedStorages": ["AKKaztokenStore", "PKCS12"],
    "format": "xml",
    "data": "<XML, который вернул сервер>",
    "signingParams": {
      "decode": false, "encapsulate": false, "digested": false, "tsaProfile": null
    },
    "signerParams": {
      "extKeyUsageOids": ["1.3.6.1.5.5.7.3.2"],
      "chain": ["-----BEGIN CERTIFICATE----- …корневые НУЦ РК… -----END CERTIFICATE-----"]
    },
    "locale": "ru"
  }
}

В переводе это значит: enveloped XMLDSig по документу как есть — без предварительного base64-декодирования (decode: false), без обёртки (encapsulate: false), без предварительного хеширования (digested: false), без службы штампов времени (tsaProfile: null), ключом, в сертификате которого расширенное назначение 1.3.6.1.5.5.7.3.2, с цепочкой до корневых НУЦ РК, которые страница передаёт сама.

Три вещи, которые стоит знать, когда встречаете такой запрос:

  • Сокет — wss://127.0.0.1:13579/, протокол — JSON-сообщения по WebSocket. Из песочницы туда не достучаться: это loopback пользователя, а не ваш.
  • У этого API три поколения, и страницы откатываются по ним вниз: kz.gov.pki.knca.basics/sign (текущее), kz.gov.pki.knca.commonUtils/signXml (старее, позиционные аргументы [storage, signType, xml, "", ""]) и голое {method: "signXml", args: [storage, "", "SIGNATURE", xml]}. Читайте самое новое — два других это та же подпись с худшим соглашением о вызове.
  • Имена хранилищ, которые встретятся: PKCS12 (файл ключа — обычный случай), AKKaztokenStore, AKKZIDCardStore, AKJaCartaStore, AKAKEYStore, AKEToken5110Store, AKEToken72KStore, JKS. Для автоматизации важен только PKCS12; остальное — аппаратные токены, которые должны быть физически воткнуты.

3. Ключ — теперь один файл, два только у старых

С 30 апреля 2024 года НУЦ РК выдаёт ОДИН ключ. Одно регистрационное свидетельство служит и для аутентификации, и для подписи, на алгоритме СТ РК ГОСТ Р 34.10-2015. До этой даты человек получал два невзаимозаменяемых файла; ранее выпущенные свидетельства действуют до конца срока — но срок этот год, так что сегодня у клиента почти наверняка универсальный ключ. Просите «ключ ЭЦП» в единственном числе, а два файла воспринимайте как исключение, которым они стали.

Порталы при этом не изменились, и им незачем: они по-прежнему фильтруют по расширенному назначению, и OID в запросе говорит, какое назначение требуется.

OIDЗначениеЗапрашивается приЭпоха двух ключей
1.3.6.1.5.5.7.3.2clientAuthвходеAUTH_RSA…, AUTH…
1.3.6.1.5.5.7.3.4emailProtectionподписи документовRSA…, GOST…

egov.kz по-прежнему просит первый на входе и второй на каждой отправке — два явно разных вызова в одном бандле, измерено 10.09.2026 (тема §4).

Очевидный вывод отсюда — что одно универсальное свидетельство обязано нести оба OID — неверен, и в тот же день это было измерено. Настоящее универсальное ГОСТ-свидетельство (ключ первого руководителя ТОО, выпущен в июне 2026) несёт 1.3.6.1.5.5.7.3.4 и казахстанские OID 1.2.398.3.3.4.3.2, 1.2.398.3.3.4.1.2, 1.2.398.3.3.4.1.2.1 — и не несёт 1.3.6.1.5.5.7.3.2. Его подпись логин-документа egov всё равно приняли.

Поэтому читайте extKeyUsageOids как то, чем он является: подборщик ключа для десктопа, а не правило, которое проверяет сервер. Он говорит NCALayer, какой файл на диске предложить; сервер проверяет подпись, а не заявленное назначение сертификата. Универсального ключа хватает даже порталу, который просит clientAuth. Измерено только на входе egov — не считайте, что эндпоинт записи или другой портал так же снисходительны.

EKU всё равно прочитайте — он говорит, ключ какой эпохи у вас в руках:

openssl pkcs12 -info -nokeys -clcerts -in key.p12 -passin file:pw.txt \
  | openssl x509 -noout -text | grep -A1 'Extended Key Usage'

-legacy обязателен, и его отсутствие не выглядит как забытый флаг. Все замеренные здесь .p12 НУЦ РК зашифрованы RC2-40-CBC, который OpenSSL 3 по умолчанию отвергает: вы получаете Algorithm (RC2-40-CBC : 0) … unsupported, и это читается как битый файл или неверный пароль, а не как политика по умолчанию. С -legacy тот же файл открывается. На ГОСТ-сертификате OpenSSL дальше разбирает расширения — строка EKU выше печатается, — но говорит Unable to load Public Key: это ожидаемо и безвредно, казахстанских кривых он не умеет. (Измерено 10.09.2026 на собственных тестовых ключах ЭСФ SDK, OpenSSL 3.0.13.)

Смена алгоритма важнее, и она сужает выбор подписанта до одного. Универсальный ключ — ГОСТ, а ГОСТ не подписывает ни одна стандартная библиотека XMLDSig: нужен провайдер с казахстанскими кривыми. Путь на чистом Python из §4 для любого ключа с апреля 2024 года мёртв; Kalkan (или то, что его в себе несёт) — уже не выбор по умолчанию, а единственная дорога. Алгоритм всё равно подтвердите, а не предполагайте:

# -nokeys обязателен: без него openssl печатает РАСШИФРОВАННЫЙ ПРИВАТНЫЙ КЛЮЧ
# в stdout, а в рабочем пространстве stdout — это история чата. Пароль тоже
# передавайте через -passin file:/env:, а не как -passin pass:… в командной строке.
openssl pkcs12 -info -nokeys -clcerts -in key.p12 -passin file:pw.txt \
  | openssl x509 -noout -text | grep 'Public Key Algorithm'

4. Чем подписывать

Проверенный путь: Kalkan

Kalkan — собственный Java-криптопровайдер НУЦ РК, тот самый, на котором построен и сам NCALayer. Он умеет и RSA, и ГОСТ, не требует нативных библиотек, и Plank уже использует его в проде для ЭСФ. А поскольку универсальный ключ — ГОСТ (§3), для любого ключа после апреля 2024 это не просто выбор по умолчанию, а единственный вариант — и заодно единственный путь в этом репозитории, за которым стоят реальные доказательства.

Вся операция — четыре строки:

Security.addProvider(new KalkanProvider());
KncaXS.loadXMLSecurity();
String signedXml = XMLUtil.createXmlSignature(
    new SigningEntity(privateKey, Arrays.asList(certificate)),   // из .p12
    xmlFromServer,
    Security.getProvider(KalkanProvider.PROVIDER_NAME));

Это тот же самый вызов, которым клиент ЭСФ подписывает свой authTicket. Копируйте EsfClient.java из Подключения ЭСФ, а не выводите заново: загрузка учётных данных, регистрация провайдера и совместимость с Java 8 там уже сделаны правильно.

Практические замечания:

  • Jar-файлы лежат внутри SDK ЭСФ. esf-sdk-*.rar с https://kgd.gov.kz/sites/default/files/ftpdata/ESF/ — публично, ~180 МБ. Если в рабочем пространстве уже есть scripts/esf/sdk/, используйте его и не качайте заново. В песочнице нет unrar; распаковывайте через bsdtar -xf esf-sdk.rar -C <каталог> (libarchive читает RAR), а если bsdtar тоже нет — pip install --quiet patool и patoolib.extract_archive(...); оба варианта описаны в Подключении ЭСФ.
  • В образе песочницы нет JRE. Ставьте tarball Temurin без sudo в ~/.local/jdks/ и переиспользуйте, если он уже там.
  • Для ЭСФ нужна именно JDK 8, потому что её SOAP-клиенту нужен JAX-WS, удалённый в Java 11+. У самой подписи такой зависимости нет, и теперь это измерено: 10.09.2026 Kalkan загрузил ГОСТ-ключ и выдал корректный enveloped XMLDSig (SignatureMethod …xmldsig-more#gost34310-gost34311) на OpenJDK 17, при classpath из lib/ того же SDK. За JDK 8 идите только тогда, когда нужен ещё и SOAP-клиент.
  • Только если ключ клиента RSA — выпущен до апреля 2024 и ещё не истёк — подписант на чистом Python (signxml / xmlsec) вообще применим: без JDK и без 180 МБ. ГОСТ он не умеет, так что на универсальном ключе это не лёгкий путь, а тупик. И тоже не проверен ни на одном из этих порталов. Сверьтесь с §3, прежде чем потратить на это вечер.
  • NCANode — открытый HTTP-сервер подписи ровно для этой задачи, есть образ Docker. Очевидный кандидат, если хочется подпись как сервис, а не в процессе. Здесь тоже не проверен — если будете оценивать, зафиксируйте результат.

Когда ключ должен остаться на машине пользователя

Часть клиентов не отдаст .p12, и это разумная позиция. Скажите об этом прямо, а не уговаривайте: без доступного ключа автоматизация ограничена тем, что портал отдаёт без входа (для Tizilim это по-настоящему полезный публичный API на чтение). Промежуточный вариант — ассистент готовит всё, а пользователь подписывает у себя — предлагается в интеграции с ЭСФ и его стоит предложить; только честно скажите, что он стоит одного взаимодействия на каждое подписанное действие, а именно эту цену автоматизация и должна была убрать.


5. Докажите подпись до того, как строить на ней

Сделайте это первым делом, отдельно, до любой работы над функциональностью. Это один круг запросов, и он закрывает единственный вопрос, который имеет значение.

1. POST на эндпоинт портала «дай XML»       → сохранить XML дословно
2. подписать                                 → сохранить подписанный XML
3. POST на эндпоинт портала «проверь»

Портал, ответивший ролями пользователя, сессией или токеном, подпись принял — и вся интеграция разблокирована. Отказ — сигнал изменить ровно одну вещь и повторить.

Сохраните оба файла. Когда позже что-то сломается, вопрос «изменилась подпись или изменился портал» решается за секунды при наличии заведомо рабочей пары и не решается никак без неё.


6. Виды отказов и что каждый означает

  • Порталы отвечают одним сообщением на любую ошибку подписи. Tizilim отвечает 422 «Ошибка при обработке сертификата» и на кривую подпись, и на неверный тип ключа, и на недоверенную цепочку. Поэтому меняйте по одной переменной за раз: файл ключа, потом OID, потом передаётся ли цепочка, потом канонизация.
  • Не тот ключ из пары — самая вероятная причина отказа с первой попытки. Проверьте OID (§3) прежде, чем подозревать что-то тонкое.
  • Пробелы — часть документа. Подписывайте байты сервера ровно как получили: не форматируйте, не пересериализуйте, не перекодируйте и не убирайте завершающий перевод строки. Одного прохода XML через парсер достаточно, чтобы сломать дайджест.
  • Просроченный или отозванный сертификат. Эти ключи действуют год и заканчиваются тихо. openssl pkcs12 -in key.p12 -nokeys -clcerts -passin file:pw.txt | openssl x509 -noout -enddate скажет вам об этом раньше, чем портал — и здесь -nokeys тоже обязателен.
  • «Принято» не значит «готово». ЭСФ возвращает accepted для счетов-фактур, которые потом переводит в FAILED; считайте, что так может любой портал, и проверяйте статус объекта после отправки, а не доверяйте ответу на отправку.

7. Обращение с ключом

Это не рекомендации, и они действуют с момента, когда .p12 попал в рабочее пространство:

  • Храните ключ и его пароль в защищённых учётных данных рабочего пространства — никогда в заметке, скрипте, коммите, строке лога или истории чата.
  • Знайте, кто ещё его видит. Файл под scripts/ виден каждому участнику общего рабочего пространства; /home/coder приватен для одного пользователя. .gitignore на это никак не влияет, а ключ ЭЦП — это юридическая подпись, а не отзываемый API-токен. В общем рабочем пространстве кладите его осознанно и скажите владельцу, что станет видно. См. Скрипты и интеграции.
  • Добавьте *.p12 и файл учётных данных в .gitignore рабочего пространства до того, как ключ появится, а не после.
  • Никогда не выводите содержимое ключа, пароль или приватную половину сертификата в транскрипт.
  • Никогда не подписывайте без явного подтверждения то, что портал делает публичным или необратимым: опубликованную закупку, поданную заявку, сданный отчёт. Соберите, покажите, спросите, потом подписывайте.
  • Скажите пользователю, как вас отключить: удаление ключа из рабочего пространства мгновенно закрывает доступ. Он должен знать это, не спрашивая.

8. Что проверено

  • Работает в проде: вызов enveloped XMLDSig через Kalkan — против ЭСФ, для ИП: подписанная сессия и реально выписанный счёт-фактура.
  • Прочитано с живого портала: запрос Tizilim к NCALayer из §2, его OID, список хранилищ и откат по трём поколениям API; а также весь REST-контур egov.kz, его константный логин-документ и оба OID (тема).
  • Доказано на egov.kz (10.09.2026): enveloped XMLDSig от Kalkan, сделанный на OpenJDK 17 настоящим универсальным ГОСТ-ключом и без NCALayer, принимается эндпоинтом /identity/v3/auth/eds200 и сессия. Это второй портал, принявший этот подписант, и первый ГОСТ-овый.
  • Также измерено: .p12 НУЦ РК требуют у OpenSSL флага -legacy; универсальное свидетельство не несёт OID clientAuth и всё равно принимается (§3); а собственные тестовые ключи ЭСФ SDK (пин Qwerty12 — публичная тестовая пара, лежащая в самом SDK) прогоняют подписант целиком вообще без ключа клиента. Проверяйте подписант на них, прежде чем просить ключ у клиента.
  • Не проверено: что подпись Kalkan принимает Tizilim; что эндпоинт записи так же снисходителен к EKU, как вход (§3); подписант на чистом Python для RSA; NCANode. Каждое — короткий эксперимент по §5: проведите его и обновите эту страницу.