Skip to content
111 changes: 108 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
improvements'ами (CRL cache, OCSP parallel, CAdES-T fixes, request log,
health indicator). Сохранена для возможности PR'а в upstream
malikzh/NCANode. v4 в upstream не пойдёт (другой язык).
- **Состояние v4:** functional + 467 тестов / **90% coverage**.
- **Состояние v4:** functional + 504 теста (+15 эталонов NCALayer, когда
боевая PKI отвечает) / **90% coverage**.
CI/CD обновлён под Java 25 + actions из demo-pki-center.
Batch endpoints (issue #212) реализованы для всех сервисов.

Expand Down Expand Up @@ -205,7 +206,8 @@ JWT/PDF/X509/PKCS12) начнёт возвращать `valid=false` из-за `
NCA SDK 2.0 test pack, заменить p12 в `p12/`, сверить новый период валидности,
обновить эту дату.

467 тестов / **90% line coverage**.
504 теста (+15 эталонов NCALayer, когда боевая PKI отвечает) /
**90% line coverage**.

## test.pki.gov.kz — официальная тестовая PKI

Expand All @@ -232,7 +234,7 @@ REVOKED-ветка покрывается через mock'нутый `CrlIndex`,

```bash
./gradlew bootJar # сборка
./gradlew test # 467 тестов + JaCoCo report
./gradlew test # 504 теста + JaCoCo report
./gradlew test jacocoTestReport # явно

java -jar build/libs/NCANode-4.0.0-SNAPSHOT.jar # запуск приложения
Expand Down Expand Up @@ -1135,6 +1137,109 @@ DER-ридер CRL на обрезанных и не-DER входах; повт
за кэш; ветки WSSE, где сертификат подписанта не достаётся из
`SecurityTokenReference`.

### 45. Приказ №522/НҚ (в силе с 11.09.2026): что он поменял у нас
Полная сверка — `rules-2026-compliance.md`. Приказ регулирует УЦ, а не нас;
применимы только профили сертификатов/СОС/OCSP/TSP. По существу мы им
соответствовали — профили кодифицируют то, что НУЦ уже выпускает (сверено с
ключами SDK 2.0 в репо). Важное, что всплыло:

- **Разностный СОС в профиле помечен `freshestCRL 2.5.29.46, critical`**, а не
`deltaCRLIndicator`. Отсюда два изменения в `CrlService`: расширение внесено в
allowlist критичных (указатель охват не сужает), а base теперь ищется только
среди файлов **не с delta-эндпоинта** (`UsableCrl.fromDeltaEndpoint`). Без
второго delta без индикатора выигрывала бы отбор base по `CRLNumber`
(57 725 против 1 346) и вердикт считался бы по ней одной — всё, отозванное
только в полном списке, вернулось бы ACTIVE.
- **Ответ-ошибка OCSP (`status != 0`) теперь UNAVAILABLE, а не UNKNOWN.**
Он не подписан и о сертификате не говорит ничего (RFC 6960 §4.2.1), а
UNKNOWN в `isValid` фатален — `tryLater` от одного из двух обязательных
теперь респондеров ронял валидную подпись. Пустое тело ответа — там же:
раньше `OCSPResp(ByteArray(0))` давал NPE мимо всех catch → 500.
- **Зеркала.** Профили дают по два адреса и в AIA, и в CDP. OCSP: обход
прекращается на первом авторитетном ответе. CRL: `crlDistributionPoints`
группирует адреса по точкам распространения (внутри точки это один и тот же
список, RFC 5280 §4.2.1.13), качается первый сработавший — иначе в кэше
оказывались две копии 20-МБ СОС.
- **`freshestCRL` теперь читается** (`freshestCrlDistributionPoints`): адрес
delta НУЦ публикует именно там, и для издателя вне конфигурации мы работали
на одном полном списке. Скачанное по нему кладётся в ОТДЕЛЬНЫЙ каталог
`crl/<type>/ondemand-delta`: каталог — единственный носитель провенанса, и
без разделения delta без индикатора выиграла бы отбор base уже через
on-demand путь (та же дыра, что закрыта для конфигурационного). Потолок
`NCANODE_CRL_ONDEMAND_MAX` общий на оба on-demand каталога.
- **Шаблоны «цифровая система»** (`1.2.398.3.3.4.1.1.1`,
`1.2.398.3.3.4.1.2.6`) и «Казначейство — Клиент» (`1.2.398.5.19.1.2.2.1`)
добавлены в `CertificateKeyUser`; `UID` (OID цифровой системы),
`businessCategory` (код клиента Казначейства) и `DC` (роль) — в
`CertificateSubject`. Все они есть на наших же тестовых ключах
(`legal_infosystem_valid.p12`, `legal_treasury_valid.p12`), то есть занижали
вывод `/x509/info` уже сегодня. Попутно чинилось отчество: `X500Principal
.toString()` печатает его как `GIVENNAME`, а разбор ждал только `G`, поэтому
`subject.lastName` был пуст на ВСЕХ сертификатах НУЦ. `businessCategory`
приходит там же как `OID.2.5.4.15` — keyword'а у него нет.
⚠️ Фикстуры на «цифровую систему физического лица» (`1.2.398.3.3.4.1.1.1`)
в тест-паке нет — `INDIVIDUAL_DIGITAL_SYSTEM` не покрыт.
- **Дефолты**: `ncanode.ocsp.url` — оба адреса НУЦ; `NCANODE_CA_CRL_TTL`
1440 → 720 (КУЦ обновляет свой СОС не реже раза в 24 ч).
- ⏰ **Операционное**: сертификат на файловом носителе теперь действует
**1 год** (3 года — токен/облако/удостоверение личности и оба шаблона
«цифровая система»). Для серверного подписанта правильный шаблон —
«цифровая система юридического лица».
- Сама процедура проверки подписи описана **другим** приказом («Правила
формирования и проверки подлинности ЭЦП», пп. 4 п. 1 ст. 55 Цифрового
кодекса) — его нужно сверять отдельно.


### 46. Приказ №500/НҚ: Правила проверки подлинности — профильный для нас документ
Сверка — `rules-2026-verification-compliance.md`. В отличие от №522/НҚ (он про
УЦ), этот приказ описывает саму процедуру формирования и проверки подписи, то
есть работу сервиса. Что изменилось:

- **Срок действия проверяется у всей цепочки** (п. 16.1), а не только у
непосредственного издателя: `isChainDateValid` обходит уже проставленные
`issuerCertificate` (лишних поисков нет) и останавливается на самоподписанном
либо там, где издателя в бандле нет — всё из `NCANODE_CA_URL` считается
настроенным якорем доверия. Корень `CaService` ссылает сам на себя, поэтому
без защиты от петли обход зациклился бы.
- **`CrlResult.EXPIRED`** (п. 18.5): CRL за пределами `nextUpdate` больше не даёт
положительного вердикта. Нефатален ровно тогда, когда есть авторитетный ACTIVE
от OCSP — п. 16.2 разрешает проверять отзыв «посредством OCSP **либо** CRL».
REVOKED из протухшего списка остаётся в силе. ⚠️ CRL-фикстуры в репозитории
протухли (nextUpdate — октябрь 2025), поэтому юнит-тесты на них ждут EXPIRED,
а ACTIVE проверяется на mock'ах и живых списках.
- **`permitsSignature`** (п. 16.4): `keyUsage` должен разрешать digitalSignature
либо nonRepudiation; отсутствие расширения — не ограничение. ⚠️ Требование
относится к сертификату ПОДПИСАНТА, поэтому включается флагом
`isValid(..., requireSigningKeyUsage = true)` — его передают Cms/Xml/Pdf/Wsse/
Jws/Tsp, SBA-verify и pre-sign gate. Без флага (`/x509/info`, `/pkcs12/info`,
`verifyCerts`) проверки нет: у CA-сертификатов НУЦ `keyUsage` = keyCertSign +
cRLSign, и глобальная проверка делала их `valid:false` на info-эндпойнтах.
Номер политики НЕ enforce'им (условия задаёт УЦ, из сертификата не выводимы)
— вместо этого публикуем `certificates[].policies`.
- **`signingCertificateV2` сверяется и в `/cms/verify`** (п. 8), не только на
AdES-пути: `CadesInspector.signingCertificateMatches` стал публичным.

Сознательные расхождения (обоснования — в документе): не требуем совпадения
издателя TSA с издателем подписанта (п. 17.2 — иначе отвергали бы боевые
RSA-метки НУЦ на GOST-подписях, quirk #25); не требуем EKU OCSPSigning от
самого CA (п. 19.5 — RFC 6960 §4.2.2.2 разрешает CA подписывать ответы своим
ключом, так НУЦ и делает); п. 19.6 читаем как «квитанция должна свидетельствовать
о моменте проверки» (`authoritativeAt`), буквально он невыполним.

- **Проверка сертификата ПЕРЕД подписанием** (п. 4) —
`CertificateService.ensureSignerCertificateUsable` во всех sign-путях (10 мест),
отказ 400 на непригодном ключе. **Выключена по умолчанию**
(`NCANODE_SIGN_CERT_CHECK`): включение меняет поведение всех sign-эндпойнтов и
добавляет OCSP-запрос на каждое подписание. Вердикт тот же, что у верификации,
поэтому «подписали — проверили» не расходится. Криптопроверку сертификата
ключом УЦ даёт сам поиск издателя (`getRootCertificateFor` сверяет подпись).

Ещё наблюдение: `AdesReferenceCompatibilityTest` проверяет доступность
`pki.gov.kz` один раз на старте спеки. Если боевая PKI замолкает **в середине**
прогона, краснеют все 15 эталонов при исправном коде (наблюдалось; повторный
прогон — 15/15 зелёные).


## Что не покрыто тестами (≈441 line)

| Пакет | % | Что осталось |
Expand Down
32 changes: 30 additions & 2 deletions docs/_tabs/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ docker run -p 14579:14579 -v ncanode_cache:/app/cache -d malikzh/ncanode
* `NCANODE_PROXY_URL` - Прокси для HTTP-клиента. Через этот прокси будут происходить все запросы из NCANode.
* `NCANODE_PROXY_USERNAME` - Имя пользователя в прокси
* `NCANODE_PROXY_PASSWORD` - Пароль прокси
* `NCANODE_OCSP_URL` - OCSP-сервер куда будут происходить запросы. По умолчанию: http://ocsp.pki.gov.kz/
* `NCANODE_SIGN_CERT_CHECK` - Проверять сертификат подписанта **перед** формированием подписи — п. 4 Правил формирования и проверки подлинности ЭЦП (приказ МИИ РК №500/НҚ от 21.08.2026): подпись удостоверяющего центра, срок действия, отсутствие отзыва (OCSP, при его недоступности — CRL) и допустимость назначения ключа. Если `true`, подписание непригодным ключом отклоняется с кодом 400 и причиной; вердикт выносится теми же средствами, что и верификация, поэтому совпадает с тем, что потом скажет `/{cms,xml,pdf}/verify` о готовой подписи. По умолчанию: `false` — включение меняет поведение всех sign-эндпойнтов и добавляет обращение к OCSP на каждое подписание. Развёртыванию, которое обязано соответствовать Правилам, флаг нужен.
* `NCANODE_OCSP_URL` - OCSP-серверы куда будут происходить запросы, через пробел. Опрашиваются по очереди до первого авторитетного ответа. По умолчанию: http://ocsp.pki.gov.kz/ http://ocsp1.pki.gov.kz/
* `NCANODE_OCSP_STRICT` - Строгий режим OCSP (SSRF-защита). Если `true`, OCSP-проверка игнорирует адрес responder'а из AIA сертификата и ходит только на сконфигурированный `NCANODE_OCSP_URL`. По умолчанию: `false` (AIA-first, config-fallback).
* `NCANODE_CA_URL` - URL-ы корневых сертификатов. Они скачиваются автоматически при запуске NCANode. По умолчанию:
* https://pki.gov.kz/cert/nca_rsa.crt
Expand Down Expand Up @@ -130,6 +131,14 @@ docker run -p 14579:14579 -v ncanode_cache:/app/cache -d malikzh/ncanode
* **CRL издателя нет в кэше** (CA не публикует CRL, или он не сконфигурирован) —
CRL-канал помечается `"result": "UNAVAILABLE"`; это не мешает валидности при
живом OCSP, но такой CRL не может служить fallback-источником.
* **CRL найден, но его период действия истёк** (`nextUpdate` в прошлом либо
отсутствует) — канал помечается `"result": "EXPIRED"`, и это отрицательный
результат проверки отзыва (п. 18 Правил формирования и проверки подлинности
ЭЦП). Отсутствие серийника в протухшем списке ничего не доказывает: всё, что
издатель опубликовал после `nextUpdate`, в него не попало. Не фатально это
ровно тогда, когда авторитетный ответ уже получен от OCSP — Правила
разрешают проверять отзыв «посредством сервиса OCSP **либо** CRL». Запись об
отзыве из такого списка остаётся в силе.

CRL-проверка учитывает delta-CRL по RFC 5280 §5.2.4: если для издателя есть
и полный (base), и применимая к нему delta (`NCANODE_CRL_DELTA_URL`), они
Expand All @@ -139,10 +148,29 @@ CRL-проверка учитывает delta-CRL по RFC 5280 §5.2.4: есл
чем по одному полному CRL.

Каждый элемент `revocations[]` в ответе несёт поле `result`
(`ACTIVE` / `REVOKED` / `UNKNOWN` / `UNAVAILABLE`). Клиент, которому нужна
(`ACTIVE` / `REVOKED` / `UNKNOWN` / `UNAVAILABLE` / `EXPIRED`). Клиент, которому нужна
строгая политика «оба канала обязаны ответить», может реализовать её сам:
`valid && revocations.none { result == "UNAVAILABLE" }`.

## Проверка сертификата перед подписанием

Правила формирования и проверки подлинности ЭЦП (приказ МИИ РК №500/НҚ)
требуют, чтобы **до** формирования подписи подписывающая сторона проверила
сертификат: подпись удостоверяющего центра, срок действия, отсутствие отзыва
(OCSP, при его недоступности — CRL) и допустимость назначения ключа. При
подписании через цифровую систему это обязанность её владельца.

Включается флагом `NCANODE_SIGN_CERT_CHECK=true` и действует на все
sign-эндпойнты (`/cms`, `/xml`, `/pdf`, `/wsse`, `/jwt`, `/jws`, `/x509/sign`,
а также `/cades`, `/xades`, `/pades`). Непригодный ключ получает отказ **400**
с указанием субъекта сертификата и статусов отзыва; причина отказа пишется в
лог. Вердикт выносится теми же средствами, что и верификация, поэтому
совпадает с тем, что потом скажет `/{cms,xml,pdf}/verify` о готовой подписи.

По умолчанию выключено: включение меняет поведение всех sign-эндпойнтов
(просроченный или отозванный ключ перестаёт подписывать) и добавляет обращение
к OCSP на каждое подписание.

## Подписи AdES (CAdES / XAdES / PAdES)

Помимо привычных `/cms`, `/xml` и `/pdf` есть три семейства эндпойнтов,
Expand Down
Loading