{"id":50840969,"url":"https://github.com/bivex/dstu_4163_2020","last_synced_at":"2026-06-14T06:34:25.306Z","repository":{"id":364612401,"uuid":"1268580511","full_name":"bivex/dstu_4163_2020","owner":"bivex","description":null,"archived":false,"fork":false,"pushed_at":"2026-06-13T19:20:42.000Z","size":1082,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-13T19:22:59.426Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bivex.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-13T17:43:19.000Z","updated_at":"2026-06-13T19:20:46.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/bivex/dstu_4163_2020","commit_stats":null,"previous_names":["bivex/dstu_4163_2020"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/bivex/dstu_4163_2020","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bivex%2Fdstu_4163_2020","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bivex%2Fdstu_4163_2020/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bivex%2Fdstu_4163_2020/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bivex%2Fdstu_4163_2020/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bivex","download_url":"https://codeload.github.com/bivex/dstu_4163_2020/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bivex%2Fdstu_4163_2020/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34312071,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-14T02:00:07.365Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-14T06:34:24.498Z","updated_at":"2026-06-14T06:34:25.301Z","avatar_url":"https://github.com/bivex.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Dilovod4\n\nРушій перевірки оформлення організаційно-розпорядчих документів на відповідність\n**ДСТУ 4163:2020**. Python-реалізація формальної Catala-специфікації\n(`dstu_4163_2020.catala_en`) у чистій (гексагональній) архітектурі.\n\n## Що це робить\n\nПриймає опис документа (реквізити, геометрія, типографіка, метадані), застосовує\n13 правил відповідності (по одному на параграф ДСТУ) і повертає структурований\nзвіт: загальний підсумок + перелік конкретних порушень із посиланням на параграф.\n\n## Архітектура\n\nЧотири шари, залежності спрямовані всередину (детальніше — `docs/adr/0001-*`):\n\n```\npresentation (CLI)  -\u003e  application (use-cases, DTO)  -\u003e  domain (модель, правила, порти)\n                              ^\ninfrastructure (адаптери: config, repo, rule set, mapper) -- реалізують доменні порти\n```\n\n- `domain/` — чистий Python: value objects з інваріантами, агрегат `Document`,\n  13 правил (`ConformanceRule`), порти. Без БД/мережі/UI/фреймворків.\n- `application/` — use-case `ValidateDocument`, DTO. Залежить лише від портів (DIP).\n- `infrastructure/` — адаптери: `AppConfig` (env), `DefaultRuleSetProvider`,\n  `InMemoryDocumentRepository`, JSON-мапер (анти-корупційний шар).\n- `presentation/` — CLI + рендерери (text/JSON). Композиційний корінь.\n\nКожне правило 1:1 відображає Catala-scope. Карта — у `docs/requirements.md`.\n\n## Запуск\n\n```bash\n# перевірити документ (text-звіт)\nPYTHONPATH=src python3 -m dilovod4.presentation.cli samples/conformant.json\n\n# JSON-вивід зі stdin\ncat samples/non_conformant.json | PYTHONPATH=src python3 -m dilovod4.presentation.cli --format json -\n\n# або після інсталяції пакета\npip install -e .\ndilovod4 samples/conformant.json\n```\n\nКод повернення: `0` — документ відповідає, `1` — є порушення норми, `2` — помилка\nвхідних даних.\n\n## Генерація .docx\n\nДвигун уміє не лише перевіряти, а й **створювати** документи, що фізично\nреалізують оформлення ДСТУ (поля, Times New Roman, кеглі, інтервал, нумерація\nсторінок). Це окремий вихідний адаптер (порт `DocumentWriter`).\n\n```bash\npip install -e \".[docx]\"          # потрібен python-docx\nPYTHONPATH=src python3 scripts/generate_samples.py samples docx\n```\n\nСкрипт збирає три реалістичні документи (наказ, лист, протокол), генерує .docx і\nперевіряє кожен на відповідність нормі. Готові зразки — у `samples/docx/`.\n\nПрограмно:\n\n```python\nfrom dilovod4.application.generate_document import GenerateDocument\nfrom dilovod4.infrastructure.docx_writer import DocxDocumentWriter\nfrom dilovod4.infrastructure.rule_set_provider import DefaultRuleSetProvider\n\nuse_case = GenerateDocument(writer=DocxDocumentWriter(), rule_set=DefaultRuleSetProvider())\nresult = use_case.execute(document, content, \"out.docx\")  # перевіряє + пише\n```\n\n`Document` несе ПАРАМЕТРИ оформлення, `DocumentContent` — фактичний ТЕКСТ\nреквізитів. Розділення дозволяє перевіряти оформлення окремо від наповнення.\n\n## Генерація .pdf\n\nPDF-адаптер реалізує **той самий** порт `DocumentWriter` (LSP) — взаємозамінний\nз docx без зміни use-case. Будується на reportlab; кирилицю забезпечує системний\nTTF Times New Roman.\n\n```bash\npip install -e \".[pdf]\"           # потрібен reportlab\nPYTHONPATH=src python3 scripts/generate_samples.py samples pdf\n# обидва формати одразу (типово):\nPYTHONPATH=src python3 scripts/generate_samples.py samples\n```\n\n```python\nfrom dilovod4.infrastructure.pdf_writer import PdfDocumentWriter\nuse_case = GenerateDocument(writer=PdfDocumentWriter(), rule_set=DefaultRuleSetProvider())\nresult = use_case.execute(document, content, \"out.pdf\")\n```\n\nШрифт шукається автоматично (macOS/Linux). Перевизначити шлях — через оточення:\n\n| Env | Призначення |\n|---|---|\n| `DILOVOD4_FONT_REGULAR` | шлях до TTF звичайного накреслення |\n| `DILOVOD4_FONT_BOLD` | шлях до TTF напівжирного (необовʼязково) |\n\nЯкщо Times New Roman відсутній — використовуйте метрично сумісну Liberation Serif\nабо вкажіть власний TTF через env. Готові зразки — у `samples/pdf/`.\n\n### Відмітка про електронний підпис (КЕП)\n\nДля електронного документа (`Document.is_electronic = True`) із заданою\n`DocumentContent.e_signature` PDF-адаптер замість рукописного реквізиту 22 малює\n**відмітку про електронний підпис, побудовану за даними сертифіката** — рамку з\nпідписувачем, серійним номером, видавцем, строком дії та позначкою часу.\n\nЦе стик двох норм: §4.4 реквізит 22 ДСТУ 4163:2020 (для е-документів підпис =\nелектронний підпис/печатка) ↔ Закон 2155-VIII Art.18 (КЕП) і Art.24 (чинність\nсертифіката). Якщо сертифікат нечинний за Art.24 (скасований/заблокований, строк\nвийшов або сертифікат видавця нечинний) — відмітка позначається як **НЕДІЙСНИЙ**.\n\n```python\nfrom dilovod4.domain.model import ElectronicSignatureMark, CertificateStatus\n\ncontent = DocumentContent(\n    ...,\n    e_signature=ElectronicSignatureMark(\n        signer=\"ПЕТРЕНКО Олександр Іванович\",\n        certificate_serial=\"58E2D9C1F0A4B7E3\",\n        issuer=\"КН ЕДП «Дія»\",\n        valid_from=\"01.01.2026\", valid_to=\"01.01.2028\",\n        timestamp=\"13.06.2026 16:42:05 EET\",\n        status=CertificateStatus.ACTIVE,\n    ),\n)\n```\n\nГотовий зразок — `samples/pdf/enakaz.pdf` (електронний наказ із КЕП-відміткою).\n\n### QR-код (§5.10 / §5.31)\n\nДля електронного документа з `e_signature` PDF-адаптер додає **QR-код 21×21 мм**\nу верхньому правому куті. QR кодує дані про КЕП/печатку + кваліфіковану позначку\nчасу (крос-лінк заголовка dstu-файлу: §5.10 ↔ Закон 2155-VIII). Навантаження\nбудує домен (`build_signature_qr_payload`), растеризацію — segno.\n\nФормат (версія 2) — компактний позиційний ASCII-рядок:\n\n```\nDSTU4163;2;QES;58E2D9C1F0A4B7E3;01.01.2026;01.01.2028;13.06.2026 16:42:05 EET;V\n```\n\nполя: маркер схеми, версія, тип (QES/AES), серійник, чинний від/до, позначка\nчасу, статус (`V`/`X` за Art.24).\n\nЩільність — критична: норма жорстко вимагає 21 мм, тож навантаження тримаємо\nASCII і коротким. Підписувач та видавець (кирилиця) у QR НЕ кодуються — вони вже\nє у видимій текстовій відмітці; інакше UTF-8 роздув би QR до версії 9 з модулем\n\u003c0.4 мм, який телефон не зчитує. Поточний модуль ~0.57 мм + тиха зона навколо\nсимволу — впевнено сканується.\n\n## Реальне підписання через UAPKI\n\nДля справжнього (а не декларативного) КЕП є інфраструктурний адаптер\n`infrastructure/uapki.py` — Python-обгортка (ctypes) над нативною бібліотекою\n[UAPKI](external/UAPKI) (підсабмодуль). Підписує файли ключем за українськими\nстандартами (ДСТУ 4145 тощо) і піднімає результат у доменну\n`ElectronicSignatureMark` (стик §4.4(22) ДСТУ ↔ Art.18/24 Закону 2155-VIII).\n\nУся взаємодія — через єдину C-функцію `process(jsonRequest)` + `json_free`\n(так само, як офіційні .NET/Java/Node.js інтеграції UAPKI). Lifecycle:\nINIT → OPEN → SELECT_KEY → SIGN → CLOSE.\n\nЗбірка нативної бібліотеки (одноразово):\n\n```bash\ncd external/UAPKI/library \u0026\u0026 bash build-uapki.sh macos-arm64\n# у build/out створіть симлінки major-версій (libuapki.dylib, libuapkif.2.dylib …)\n```\n\nШлях до бібліотеки — автопошук у `external/UAPKI/library/build/out` або через\n`DILOVOD4_UAPKI_LIB`. Приклад:\n\n```python\nfrom dilovod4.infrastructure.uapki import sign_file_pkcs12\n\nres = sign_file_pkcs12(\n    file_path=\"out.pdf\",\n    pkcs12_path=\"key.p12\", password=\"…\",\n    cert_cache_dir=\"certs\", crl_cache_dir=\"crls\",\n    signature_format=\"CMS\",          # RAW / CMS / CAdES-BES/T/C/XL/A\n)\nmark = res.to_signature_mark(signer=\"…\", issuer=\"…\",\n                             valid_from=\"…\", valid_to=\"…\")\n```\n\nUAPKI підключається лише за потреби; без зібраної бібліотеки тести\nсамопропускаються (`UapkiLibraryNotFound`). Збірочні артефакти лишаються всередині\nпідсабмодуля і не комітяться у головний репозиторій.\n\n### Верифікація підпису\n\n`verify_signature(container, ...)` перевіряє підпис через UAPKI VERIFY — і сам\nкриптопідпис, і цілісність даних окремо:\n\n```python\nfrom dilovod4.infrastructure.uapki import verify_signature\n\nres = verify_signature(\n    container=open(\"out.pdf.p7s\", \"rb\").read(),\n    cert_cache_dir=\"certs\", crl_cache_dir=\"crls\",\n    content=open(\"out.pdf\", \"rb\").read(),   # для detached-підпису\n)\nres.is_valid                 # True лише при TOTAL-VALID\nres.status                   # TOTAL-VALID / TOTAL-FAILED / INDETERMINATE\nres.status_signature         # VALID / INVALID — криптопідпис\nres.status_message_digest    # VALID / INVALID — цілісність даних\n```\n\nПідміна вмісту дає `TOTAL-FAILED` із `statusMessageDigest=INVALID` навіть коли\nсам підпис валідний — тобто рушій розрізняє «підпис підроблено» та «дані\nзмінено після підписання».\n\n### Онлайн-статус сертифіката (OCSP, повна Art.24)\n\n`check_cert_status_online(cert_der, ...)` запитує OCSP-відповідач надавача й\nповертає актуальний статус відкликання — це повна перевірка за Art.24 (не лише\nстрок дії, а й скасування/блокування в реальному часі):\n\n```python\nfrom dilovod4.infrastructure.uapki import check_cert_status_online\n\nst = check_cert_status_online(\n    cert_der=open(\"signer.cer\", \"rb\").read(),\n    cert_cache_dir=\"certs\",        # має містити сертифікат надавача (CA)\n    crl_cache_dir=\"crls\",\n    ocsp_url=\"http://ca.informjust.ua/services/ocsp/\",\n)\nst.cert_status        # GOOD / REVOKED / UNKNOWN\nst.is_good            # True лише коли SUCCESSFUL + GOOD\nst.is_revoked         # True якщо відкликано\nst.revocation_time    # час відкликання\nst.revocation_reason  # напр. CESSATION_OF_OPERATION\n```\n\nПотребує мережі та сертифіката надавача в `cert_cache_dir`; вмикає онлайн-режим\n(`offline=false`). Перевірено на реальному відповідачі Дія — тестовий сертифікат\nповертає `REVOKED` (CESSATION_OF_OPERATION, 2024-04-05).\n\nОбмеження: нативна libuapki — process-global singleton (один INIT на процес).\nОнлайн-перевірку (`offline=false`) виконуйте в окремому процесі або першою, до\nбудь-якого offline-INIT у тому ж процесі.\n\n### Дотягування сертифіката з КНЕДП (CMP)\n\nКонтейнери з лише приватними ключами (без вбудованого сертифіката) потребують\nдотягування сертифіката підписувача з КНЕДП за public-key-id. UAPKI вбудованого\nCMP-клієнта не має, тож `infrastructure/cmp.py` реалізує проприетарний\nIIT-«transport» формат (сумісний із реалізацією jkurwa):\n\n```python\nfrom dilovod4.infrastructure.cmp import fetch_certificate\n\n# id = subjectKeyIdentifier ключа (UAPKI SELECT_KEY 'id'), hex -\u003e bytes\nr = fetch_certificate(\n    key_id_primary=bytes.fromhex(\"5BC6C06E…\"),\n    cmp_url=\"http://ca.informjust.ua/services/cmp/\",   # з реєстру CAs.json\n)\nr.found          # True якщо сертифікат знайдено на CA\nr.result_code    # 1 = успіх; інакше сертифікат не знайдено\nr.signer_cert    # DER сертифіката підписувача (за успіху)\nr.certificates   # повний дотягнутий ланцюг (DER)\n```\n\nАдреси CMP/OCSP кожного КНЕДП — у реєстрі CAs.json (напр. iit.com.ua). Формат\nзапиту: 120-байтовий payload з id на зміщеннях 0x0c/0x2c у ContentInfo type=data.\nВАЖЛИВО: сервер індексує за **subjectKeyIdentifier** ключа (UAPKI `id`), а НЕ за\nkeyId2. Перевірено на ca.informjust.ua: дотягує повний ланцюг тестового КЕП Дія.\nДалі сертифікат додається у сховище через `client.add_cert(der)` і доступний для\nпідпису CAdES-BES. ОБМЕЖЕННЯ: якщо сертифікат до ключів не випущено/знято, сервер\nповертає ненульовий код — це не помилка клієнта.\n\n### Єдиний потік: підпис із дотягуванням сертифіката\n\n`sign_file_with_remote_cert()` робить весь ланцюг одним викликом для контейнерів\nбез вбудованого сертифіката (лише ключі): OPEN → CMP fetch за SKI → ADD_CERT →\nCAdES-BES SIGN.\n\n```python\nfrom dilovod4.infrastructure.uapki import sign_file_with_remote_cert, verify_signature\n\nres = sign_file_with_remote_cert(\n    file_path=\"document.pdf\",\n    pkcs12_path=\"key.pfx\", password=\"…\",\n    cmp_url=\"http://ca.monobank.ua/services/cmp/\",   # КНЕДП із CAs.json\n    cert_cache_dir=\"certs\", crl_cache_dir=\"crls\",\n)\nopen(\"document.pdf.p7s\", \"wb\").write(res.container)\nres.cert.subject_cn        # підписувач із дотягнутого сертифіката\n\nv = verify_signature(res.container, cert_cache_dir=\"certs\",\n                     crl_cache_dir=\"crls\", content=open(\"document.pdf\",\"rb\").read())\nv.is_valid                 # True -\u003e TOTAL-VALID\n```\n\nПеревірено на реальному КЕП Monobank: контейнер лише з ключами → сертифікат\nдотягнуто з ca.monobank.ua → CAdES-BES → TOTAL-VALID. Для чинного КЕП можна\nвимкнути `ignore_cert_status`. УВАГА безпеки: `.p7s`/`.pfx`/`.p12` (реальні\nпідписи та ключі — персональні дані) у `.gitignore`, у репозиторій не потрапляють.\n\n### Кваліфікована позначка часу (CAdES-T, Art.26.4)\n\nCAdES-BES несе лише недовірений час хоста — czo.gov.ua позначає його як «не\nпідтверджено кваліфікованою позначкою часу». Для постійного зберігання Закон\n2155-VIII (ч.4 ст.26) вимагає **кваліфіковану позначку часу**. Це формат\nCAdES-T: передайте `signature_format='CAdES-T'` і `tsp_url` КНЕДП:\n\n```python\nres = sign_file_with_remote_cert(\n    file_path=\"document.pdf\",\n    pkcs12_path=\"key.pfx\", password=\"…\",\n    cmp_url=\"http://ca.monobank.ua/services/cmp/\",\n    cert_cache_dir=\"certs\", crl_cache_dir=\"crls\",\n    signature_format=\"CAdES-T\",\n    tsp_url=\"http://ca.monobank.ua/services/tsp/dstu/\",   # TSP із CAs.json\n)\n```\n\nПеревірено на реальному КЕП Monobank: підпис містить timeStampToken від TSP\nНадавача, VERIFY повертає `signatureFormat=CAdES-T` і `bestSignatureTime` від\nдовіреної позначки (а не самозаявлений час хоста). TSP/OCSP/CMP-адреси — у\nреєстрі CAs.json.\n\n### Автоматичний підпис за реєстром КНЕДП\n\n`sign_file_auto()` сам визначає CMP/TSP-адреси з реєстру CAs.json за назвою\nнадавача — URL вручну не потрібні:\n\n```python\nfrom dilovod4.infrastructure.uapki import sign_file_auto\n\nres = sign_file_auto(\n    file_path=\"document.pdf\",\n    pkcs12_path=\"key.pfx\", password=\"…\",\n    provider_cn=\"monobank\",        # issuer CN КНЕДП (частковий збіг ок)\n    cert_cache_dir=\"certs\", crl_cache_dir=\"crls\",\n    with_timestamp=True,           # CAdES-T із кваліфікованою позначкою часу\n)\n```\n\nРеєстр (снапшот `infrastructure/data/CAs.json`, ~30 КНЕДП) резолвить cmp/tsp/ocsp\nза issuer CN. Оновити — завантажити свіжий із iit.com.ua або передати\n`registry_source=\u003cшлях|URL\u003e`. Перевірено: `sign_file_auto(..., 'monobank')` →\nCAdES-T 4452 B із timeStampToken, адреси визначено автоматично.\n\n### Підпис кількома КЕП (кілька SignerInfo)\n\nUAPKI за один SIGN створює один SignerInfo. Щоб підписати документ кількома КЕП\n«для факту», кожен підписує окремо, а підписи зводяться в один CMS через\nMODIFY_CMS (один SignedData з кількома SignerInfo):\n\n```python\nfrom dilovod4.infrastructure.uapki import combine_signatures\n\n# cms1, cms2 — detached-підписи ОДНИХ І ТИХ САМИХ даних різними КЕП\nmerged = combine_signatures(\n    [cms1, cms2],\n    cert_cache_dir=\"certs\", crl_cache_dir=\"crls\",\n)\n# merged — один контейнер із двома підписами; VERIFY поверне 2 signatureInfos\n```\n\nПеревірено на реальних ключах: документ підписано КЕП Monobank і тестовим КЕП\nДія → об'єднано в один CMS 3382 B → VERIFY: 2 SignerInfo, обидва TOTAL-VALID.\nУсі підписи мають покривати однакові дані. Підписанти можуть бути рознесені в\nчасі — кожен підписує свій CMS, потім їх зводять.\n\n### Підпис апаратним токеном (E.Key Almaz-1C, headless)\n\nЗахищені носії (ЗНКІ) не віддають приватний ключ — підпис робить сам токен.\nUAPKI працює лише з файловими контейнерами, тож для токена є окремий адаптер\n`infrastructure/token_sign.py` — JSON-RPC до рідного native-messaging host ІІТ\n`euscpnmh` (його ставить «ІІТ Користувач ЦСК»). Протокол і структури звірено\nз референс-клієнтами ІІТ EUSignES6 / SA-SignInfo.\n\n```python\nfrom dilovod4.infrastructure.token_sign import sign_file_with_token\n\nres = sign_file_with_token(\n    file_path=\"document.pdf\",\n    pin=\"…\",                                   # лише у пам'яті, не логувати\n    cmp_url=\"ca.tax.gov.ua/services/cmp/\",     # КНЕДП-емітент (повний шлях!)\n    with_timestamp=True,                        # CAdES-T (Art.26.4)\n    tsp_url=\"ca.tax.gov.ua/services/tsp/\",\n    ocsp_url=\"ca.tax.gov.ua/services/ocsp/\",\n)\nres.container        # CMS (CAdES-BES/T); res.has_timestamp; res.sign_type\n```\n\nПотік: euscp сам дотягує сертифікат підписувача з КНЕДП за ключем носія\n(`GetKeyInfo` → `GetCertificatesByKeyInfo`), у сховище кладеться ланцюг БЕЗ\nKey-Agreement-серта (інакше Sign code 50), підпис — через контекст\n`CtxCreate`→`CtxReadPrivateKey`→`CtxSign`. CAdES-T валідує позначку часу онлайн,\nтож потрібен досяжний OCSP. Перевірено: реальний токен E.Key Almaz-1C, КНЕДП ДПС,\nczo.gov.ua приймає як **Кваліфікований** підпис (CAdES-T з timeStampToken).\nCLI: `TOKEN_PIN='…' TOKEN_CADES_T=1 python3 scripts/token_sign_nmh.py \u003cфайл\u003e`.\n\nУВАГА: апаратний токен має ліміт спроб ПІН (~3) → блокування. Невірний ПІН дає\ncode 0x18 і ВИТРАЧАЄ спробу; помилки формату/носія (code 2/0x11) до автентифікації\nспробу не витрачають. ПІН — лише через пам'ять/env, ніколи в аргументах чи логах.\n\n### Підпис кількома КЕП через ASiC-контейнер\n\nКоли підписи роблять різні стеки (токен euscp + файловий UAPKI), звести їх в один\nCMS не можна (різний eContent → INVALID_DIGEST). Рішення — ASiC-контейнер\n(ETSI EN 319 162-1): detached-підписи лежать ПОРУЧ у ZIP, кожна самодостатня.\n`infrastructure/asic.py` пакує їх (пакування != підписання):\n\n```python\nfrom dilovod4.infrastructure.asic import build_asic_e, AsicSignature\n\nbuild_asic_e(\n    [(\"signed.pdf\", pdf_bytes)],          # файли даних\n    [AsicSignature(token_p7s),            # detached CAdES токена (ДПС)\n     AsicSignature(mono_p7s)],            # detached CAdES monobank\n    \"signed.asice\",\n)\n```\n\nРозкладка ASiC-E: `mimetype` (STORED, перший) + файли даних +\n`META-INF/signatureNNN.p7s` + `META-INF/ASiCManifestNNN.xml`. КРИТИЧНО: кожна\nпідпис має ВЛАСНИЙ ASiCManifestNNN.xml із SHA-256 `DigestValue` кожного\ndata-файла (не лише URI) — без digest czo.gov.ua дає помилку 33.\nАльтернатива — нативний `token_sign.asic_sign_with_token()` (euscp CtxASiCSign\nформує контейнер сам).\n\n### CLI для ASiC: `dilovod4-asic`\n\nЗручний CLI з підкомандами (точка входу `dilovod4-asic` або\n`python3 -m dilovod4.presentation.asic_cli`):\n\n```bash\n# 1) дізнатися, що підписувати (манІфест для 1-го підпису)\ndilovod4-asic manifest doc.pdf -n 1 -o manifest001.xml\n# 2) підписати manifest001.xml detached токеном/UAPKI -\u003e sig1.p7s (зовнішньо)\n# 3) зібрати контейнер з готових підписів\ndilovod4-asic pack doc.pdf -s sig1.p7s -s sig2.p7s -o doc.asice\n# переглянути вміст\ndilovod4-asic inspect doc.asice\n\n# або підпис токеном одразу в ASiC-E (PIN через TOKEN_PIN, -t = CAdES-T):\nTOKEN_PIN=*** dilovod4-asic sign doc.pdf -t \\\n    --cmp ca.tax.gov.ua/services/cmp/ \\\n    --tsp ca.tax.gov.ua/services/tsp/ --ocsp ca.tax.gov.ua/services/ocsp/\n```\n\nПідкоманди: `manifest` (вивести ASiCManifestNNN.xml для зовнішнього підпису),\n`pack` (зібрати з готових detached-підписів над манІфестами), `sign` (підпис\nтокеном напряму в ASiC-E), `inspect` (розібрати наявний контейнер).\n\n## Конфігурація (через оточення)\n\n| Env | Призначення | Типово |\n|---|---|---|\n| `DILOVOD4_OUTPUT_FORMAT` | `text` \\| `json` | `text` |\n| `DILOVOD4_LOG_LEVEL` | рівень логів | `INFO` |\n| `DILOVOD4_DISABLED_RULES` | CSV `rule_id` для профілю | (порожньо) |\n| `DILOVOD4_FONT_REGULAR` / `_BOLD` | TTF для PDF | автопошук |\n| `DILOVOD4_UAPKI_LIB` | шлях до libuapki | автопошук |\n\n## Тести\n\n```bash\npython3 -m pytest -q\n```\n\nДоменні unit-тести (правила + інваріанти, межові значення) та інтеграційні тести\nадаптерів і use-case. Зразки — у `samples/`.\n\n## Межі\n\nКодуються лише обчислювані положення (як і в Catala). Поза межами: координатні\nсхеми (Додаток А), зразки бланків (Додаток Б), приклади документів, бібліографія.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbivex%2Fdstu_4163_2020","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbivex%2Fdstu_4163_2020","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbivex%2Fdstu_4163_2020/lists"}