Skip to content

Соответствие приказам МИИ РК №522/НҚ и №500/НҚ (в силе с сентября 2026) - #17

Merged
eudj1n merged 5 commits into
v4from
feature/rules-2026-compliance
Sep 14, 2026
Merged

eudj1n merged 5 commits into
v4from
feature/rules-2026-compliance

Conversation

@eudj1n

@eudj1n eudj1n commented Sep 7, 2026 •

Copy link
Copy Markdown

Сверка v4 с двумя новыми приказами и приведение кода в соответствие.

  • №522/НҚ от 28.08.2026 (Минюст 31.08.2026 №39747, в силе с 11.09.2026) — Правила выдачи, хранения и отзыва сертификатов открытого ключа ЭЦП. Регулирует удостоверяющие центры, к нам применимы только профили сертификатов, СОС, OCSP- и TSP-сертификатов. Разбор — rules-2026-compliance.md.
  • №500/НҚ от 21.08.2026 (Минюст 24.08.2026 №39672) — Правила формирования и проверки подлинности ЭЦП. Профильный для сервиса документ: описывает саму процедуру. Разбор — rules-2026-verification-compliance.md.

По существу мы обоим соответствовали: профили кодифицируют то, что НУЦ уже выпускает (сверено с ключами NCA SDK 2.0 из репозитория), а костяк процедуры проверки был на месте. Ниже — то, что расходилось.

Профили сертификатов (№522/НҚ)

  • Разностный СОС мог подменить полный. Профили структур 13/14 маркируют delta расширением freshestCRL 2.5.29.46, critical вместо deltaCRLIndicator. Расширение внесено в allowlist критичных (указатель охват списка не сужает), а base теперь ищется только среди списков не с delta-эндпоинта: иначе delta без индикатора выигрывала бы отбор по CRLNumber (на бою 57 725 против 1 346), и всё, отозванное только в полном списке, вернулось бы как ACTIVE. Провенанс определяется каталогом кэша, поэтому скачанное по freshestCRL лежит в отдельном crl/<type>/ondemand-delta — иначе та же дыра открывалась бы через on-demand путь.
  • Ответ-ошибка OCSP больше не UNKNOWN, а UNAVAILABLE. Он не подписан и о сертификате ничего не сообщает (RFC 6960 §4.2.1), тогда как UNKNOWN в isValid фатален — tryLater от одного из двух обязательных теперь респондеров объявлял валидную подпись недействительной. Там же закрыт NPE на пустом теле ответа: он уходил мимо всех catch и давал 500 на весь verify.
  • Зеркала (ocsp1, crl1) больше не удваивают работу. OCSP прекращает обход на первом авторитетном ответе; адреса CRL группируются по точкам распространения (внутри точки это один и тот же список, RFC 5280 §4.2.1.13) и качается первый сработавший.
  • freshestCRL теперь читается — адрес delta НУЦ публикует именно там, и для издателя вне конфигурации мы работали на одном полном списке.
  • Шаблоны «цифровая система» физлица и юрлица и «Казначейство — Клиент» добавлены в CertificateKeyUser; UID (OID цифровой системы), businessCategory (код клиента Казначейства) и DC (роль) — в CertificateSubject. Все они есть на тестовых ключах и молча выпадали. Попутно чинилось отчество: X500Principal.toString() печатает его как GIVENNAME, а разбор ждал только G, поэтому subject.lastName был пуст на всех сертификатах НУЦ.
  • Дефолты: оба адреса респондеров НУЦ; NCANODE_CA_CRL_TTL 1440 → 720 (КУЦ обновляет свой СОС не реже раза в 24 часа).

Процедура проверки (№500/НҚ)

  • Срок действия всей цепочки (п. 16), а не только непосредственного издателя: истечение любого звена — отрицательный результат. Обход идёт по уже проставленным issuerCertificate, поэтому лишних поисков не делает, и останавливается там, где издателя в бандле нет — всё из NCANODE_CA_URL считается настроенным якорем доверия (иначе ломалась бы проверка подписей старой ГОСТ-2004 иерархии).
  • Истёкший CRL не даёт положительного вердикта (п. 18): новый исход EXPIRED в revocations[].result. Раньше список за пределами nextUpdate в режиме revocationCheck: ["CRL"] молча выдавал «не отозван» всё, что опубликовано после этой даты. Не фатален ровно тогда, когда авторитетный ACTIVE уже получен от OCSP — п. 16 разрешает проверять отзыв «посредством сервиса OCSP либо CRL».
  • Назначение ключа должно допускать подпись (п. 16): keyUsage обязан разрешать digitalSignature либо nonRepudiation. Требование относится к сертификату подписывающего лица, поэтому включается параметром isValid(..., requireSigningKeyUsage = true) на путях проверки подписи; /x509/info, /pkcs12/info и verifyCerts описывают сертификат, а не проверяют подпись, и там оно не применяется — у CA-сертификатов НУЦ keyUsage = keyCertSign + cRLSign. Номер политики не enforce'им (условия задаёт УЦ и из сертификата не выводимы) — вместо этого публикуем certificates[].policies.
  • Привязка signingCertificateV2 сверяется и обычным /cms/verify (п. 8), а не только AdES-путём.
  • NCANODE_SIGN_CERT_CHECK — проверка сертификата подписанта перед подписанием (п. 4) во всех sign-путях, отказ 400 на непригодном ключе. Выключено по умолчанию: включение меняет поведение всех sign-эндпойнтов и добавляет OCSP-запрос на каждое подписание.

Сознательные расхождения (обоснования в rules-2026-verification-compliance.md): не требуем совпадения издателя TSA с издателем подписанта (п. 17.2 — иначе отвергали бы боевые RSA-метки НУЦ на ГОСТ-подписях), не требуем EKU OCSPSigning от самого CA (п. 19.5 — RFC 6960 §4.2.2.2 это разрешает, так НУЦ и отвечает), п. 19.6 читаем как «квитанция должна свидетельствовать о моменте проверки» (буквально невыполним).

Проверка

Тесты 467 → 504, coverage 90%. Эталонные подписи NCALayer прогнаны отдельно — 15/15 (боевая PKI отвечает через раз, при её молчании спека пропускается).

Совместимость

Старые эндпойнты работают как раньше. Новые поля аддитивные (policies, uid, businessCategory, domainComponent, EXPIRED, новые значения keyUser), openapi.yml обновлён. Два изменения поведения по умолчанию:

  • истёкший CRL перестаёт давать положительный вердикт — это и есть требование п. 18;
  • revocations[] может содержать меньше записей, чем адресов OCSP в AIA: обход прекращается на первом авторитетном ответе. До появления сертификатов с парой респондеров (с 11.09.2026) видимой разницы нет.

🤖 Generated with Claude Code

https://claude.ai/code/session_01ThqBhEqRNDMuHx39ZRxqXo

eudj1n and others added 5 commits September 7, 2026 11:15
Сверка новых Правил выдачи, хранения и отзыва сертификатов открытого ключа
ЭЦП с v4 — разбор и обоснование решений в rules-2026-compliance.md.

Приказ регулирует удостоверяющие центры, поэтому применимы только профили
сертификатов, СОС, OCSP- и TSP-сертификатов; сама процедура проверки подписи
живёт в другом приказе (п. 27/34 отсылают к «Правилам формирования и проверки
подлинности ЭЦП»). По существу профили кодифицируют то, что НУЦ уже выпускает
— сверено с ключами NCA SDK 2.0 из репозитория. Расхождения:

Разностный СОС в профиле помечен `freshestCRL 2.5.29.46, critical`, а не
`deltaCRLIndicator`. Расширение внесено в allowlist критичных (указатель
охват списка не сужает), а base теперь ищется только среди файлов не с
delta-эндпоинта: иначе delta без индикатора выигрывала бы отбор по CRLNumber
(на бою 57 725 против 1 346), и всё, отозванное только в полном списке,
вернулось бы как ACTIVE.

Ответ-ошибка OCSP (`status != 0`) больше не UNKNOWN, а UNAVAILABLE: он не
подписан и о сертификате ничего не сообщает (RFC 6960 §4.2.1), тогда как
UNKNOWN в isValid фатален — `tryLater` от одного из двух обязательных теперь
респондеров объявлял валидную подпись недействительной. Там же закрыт NPE на
пустом теле ответа: он уходил мимо всех catch и давал 500 на весь verify.

Зеркала из профилей (ocsp1 и crl1) больше не удваивают работу: OCSP
прекращает обход на первом авторитетном ответе, а адреса CRL группируются по
точкам распространения (внутри точки это один и тот же список, RFC 5280
§4.2.1.13) и качается первый сработавший.

Расширение freshestCRL теперь читается — адрес delta НУЦ публикует именно
там, и для издателя вне конфигурации мы работали на одном полном списке.

Шаблоны «цифровая система» физического и юридического лица и «Казначейство —
Клиент» добавлены в CertificateKeyUser, UID (OID цифровой системы) — в
CertificateSubject; оба OID есть на тестовых ключах и молча выпадали.

Дефолты: оба адреса респондеров НУЦ, NCANODE_CA_CRL_TTL 1440 → 720 (КУЦ
обновляет свой СОС не реже раза в 24 часа).

Тесты 467 → 486, coverage 90%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThqBhEqRNDMuHx39ZRxqXo
Правила формирования и проверки подлинности ЭЦП (приказ МИИ РК №500/НҚ от
21.08.2026) — тот самый документ, на который ссылаются Правила выдачи
сертификатов, и профильный для сервиса: он описывает саму процедуру, а не
работу удостоверяющего центра. Разбор и обоснования — в
rules-2026-verification-compliance.md.

Срок действия теперь проверяется у всей цепочки сертификации, а не только у
непосредственного издателя (п. 16): истечение любого звена — отрицательный
результат. Обход идёт по уже проставленным ссылкам issuerCertificate, поэтому
дополнительных поисков не делает, и останавливается на самоподписанном
сертификате либо там, где издателя в бандле нет — всё, что настроено в
NCANODE_CA_URL, считается якорем доверия. Иначе проверка подписей старой
GOST-2004 иерархии ломалась бы у всех, у кого снятого с публикации корня в
бандле нет.

Истёкший CRL больше не даёт положительного вердикта (п. 18): появился исход
EXPIRED. Раньше список за пределами nextUpdate использовался как обычный, и в
режиме revocationCheck=[CRL] молча выдавал «не отозван» всё, что издатель
опубликовал после nextUpdate. Нефатален он ровно тогда, когда авторитетный
ACTIVE уже получен от OCSP — п. 16 разрешает проверять отзыв «посредством
сервиса OCSP либо CRL». REVOKED из протухшего списка остаётся в силе.

Назначение ключа обязано допускать подпись (п. 16): keyUsage должен разрешать
digitalSignature либо nonRepudiation, отсутствие расширения ограничением не
считается. Номер политики не enforce'им — его условия задаёт УЦ и из
сертификата не выводимы; вместо этого публикуем certificates[].policies, чтобы
проверяющая сторона применила свои.

Привязка signingCertificateV2 сверяется и обычным /cms/verify (п. 8), а не
только AdES-путём: контейнер, у которого атрибут указывает на другой
сертификат, внутренне противоречив независимо от эндпойнта.

Сознательно оставлены расхождения по п. 17.2 (издатель TSA), п. 19.5 (EKU у
самого CA) и п. 19.6 (буквально невыполним) — обоснования в документе. Не
сделаны проверки сертификата перед подписанием (п. 4): нужен выключенный по
умолчанию режим и решение по дефолту.

Тесты 486 → 495, coverage 90%. Эталоны NCALayer прогнаны отдельно: 15/15.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThqBhEqRNDMuHx39ZRxqXo
Правила формирования и проверки подлинности ЭЦП (приказ МИИ РК №500/НҚ, п. 4)
требуют, чтобы до формирования подписи подписывающая сторона проверила
сертификат: подпись удостоверяющего центра, срок действия, отсутствие отзыва
(OCSP, при его недоступности — CRL) и допустимость назначения ключа. При
подписании через цифровую систему это обязанность её владельца (п. 22), то
есть наша.

`CertificateService.ensureSignerCertificateUsable` вызывается во всех
sign-путях: /cms, /xml, /pdf, /wsse, /jwt, /jws, /x509/sign, /cades, /xades,
/pades. Непригодный ключ получает 400 с субъектом сертификата и статусами
отзыва вместо подписи, которая всё равно не прошла бы проверку.

Вердикт выносится теми же средствами, что и верификация (attachValidationData
+ isValid), поэтому «подписали — проверили» не расходится. Криптопроверку
сертификата ключом УЦ даёт сам поиск издателя: getRootCertificateFor принимает
издателя, только если подпись сертификата сходится с его ключом.

Выключено по умолчанию: включение меняет поведение всех sign-эндпойнтов —
просроченный или отозванный ключ перестаёт подписывать — и добавляет обращение
к OCSP на каждое подписание. Развёртывание, обязанное соответствовать
Правилам, включает NCANODE_SIGN_CERT_CHECK=true.

Тесты 495 → 499, coverage 90%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThqBhEqRNDMuHx39ZRxqXo
…Казначейства

Тестовые ключи НУЦ показали три поля, которые до сих пор не доходили до
ответа.

Отчество не парсилось вообще: `X500Principal.toString()` печатает его как
GIVENNAME, а разбор ждал только "G", поэтому `subject.lastName` был пуст на
всех сертификатах НУЦ. Тест это не ловил — он лишь упоминал поле в
комментарии.

businessCategory (2.5.4.15) и DC — обязательные поля шаблона «участник
цифровой системы "Казначейство – Клиент"» (приказ №522/НҚ, приложение 3,
структура 6): код клиента вида KS01234 и роль вида ROLE01. Первое приходит в
имени от JDK как `OID.2.5.4.15` — keyword'а у него нет, поэтому нужен явный
разбор.

Тест на UID переведён с синтетического сертификата на настоящий
`legal_infosystem_valid.p12`: у него в Subject лежит UID=1.2.398.6.10.1.1, то
есть OID цифровой системы, ради которого поле и добавлялось.

Фикстуры на «цифровую систему физического лица» (1.2.398.3.3.4.1.1.1) в
тест-паке нет — эта ветка `CertificateKeyUser` остаётся непокрытой.

Тесты 493 (+15 эталонов NCALayer, прогнаны в этом заходе — 15/15).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThqBhEqRNDMuHx39ZRxqXo
…yUsage

Оба замечания подтвердились по коду.

Разностный список, скачанный по freshestCRL, попадал в общий on-demand
каталог, а провенанс (`fromDeltaEndpoint`) выводится из каталога — то есть
защита отбора base, добавленная в этом же PR для конфигурационного пути,
обходилась через путь, ради которого чтение freshestCRL и появилось. Теперь
такие списки лежат в `crl/<type>/ondemand-delta`; имя файла осталось
`sha1(url)`, поэтому дедуп с конфигурационным delta-кэшем работает как
раньше. Потолок NCANODE_CRL_ONDEMAND_MAX общий на оба каталога — это один
кэш, разложенный по происхождению, — и прогрев тоже видит новый каталог.

Требование keyUsage перенесено с общей валидности на путь проверки подписи:
`isValid(..., requireSigningKeyUsage = true)` передают Cms, Xml, Pdf, Wsse,
Jws, TspService, SBA-verify и проверка перед подписанием. У CA-сертификатов
НУЦ keyUsage = keyCertSign + cRLSign, поэтому глобальная проверка объявляла
их недействительными в /x509/info, /pkcs12/info и verifyCerts — эндпойнтах,
которые описывают сертификат, а не проверяют подпись им. Освобождать
CA:TRUE не стали: это сняло бы проверку и с подписанта, а п. 16 Правил
№500/НҚ требует её именно там.

Заодно восстановлены семь тестов, которые предыдущий коммит удалил
непреднамеренно: правка вырезала текст от заменяемого теста до конца файла
вместо одного блока. Тесты 493 → 504 (в том числе +1 регрессия на
CA-сертификат в info-пути, +1 на провенанс on-demand delta, +1 на общий
потолок двух каталогов).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThqBhEqRNDMuHx39ZRxqXo
@eudj1n
eudj1n merged commit 00af98f into v4 Sep 14, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant