Инструменты выпуска (tessera_issuer)
Tessera проверяет сертификаты на устройстве, но сами сертификаты нужно чем-то
выпускать. Инструменты выпуска закрывают эту сторону: одно Rust-ядро собирает
TBSCertificate с расширениями Tessera, проверяет монотонное сужение рамок
делегирования до подписи и подписывает результат ключом выбранного бэкенда —
токена/HSM (PKCS#11), Vault Transit или локального PKCS#8-файла. С бэкендами
PKCS#11 и Vault инструмент — не кастодиан: приватные ключи через код выпуска
не проходят; файловый бэкенд — осознанный компромисс, ключ живёт в памяти
процесса выпуска (см. threat-model.md §11).
Компоненты:
- Ядро (
tessera_issuer, библиотека) — сборка TBS листа смены и CA организации, проверки рамок, случайные 128-битные серийники, выпуск CRL, журнал выпусков. Ядро pure-Rust и собирается в том числе подwasm32; адаптеры подписи — за feature-флагами. - CLI
issuer— код выпуска для автоматизации (тикет-системы, скрипты) и ручной работы. Ни одна проверка в CLI не переопределяется: запрос, который отверг бы ядро, CLI отвергает точно так же.
Выпуск из браузера — локальный агент подписи и веб-кабинет, работающие с тем же ядром, — поставляется отдельно, в составе коммерческих инструментов (см. «Выпуск из браузера»). Открытый репозиторий поставляет CLI.
Модель ролей — из предъявленного родительского сертификата: любой CA с рамками делегирования (корень парка или CA организации) выпускает и подчинённые CA организаций, и листы смен инженерам — строго в своих рамках делегирования, которые на каждом шаге могут только сужаться. Отдельного «режима по должности» нет.
Семантику самих расширений (host_binding, allowed_roles,
max_integrity, profile_version, delegation_constraints) и их OID см. в
cert-issuance.md — здесь описан инструмент, а не формат.
Поверхность атаки инструментов выпуска разобрана в
threat-model.md §11; дублировать её тут не будем.
Быстрый старт CLI
Заголовок раздела «Быстрый старт CLI»Все выпускающие подкоманды выбирают бэкенд подписи флагами --backend
(pkcs11 — по умолчанию, vault, file), --key (метка ключа CA;
для file — опционален, по умолчанию имя файла ключа), --algorithm
(ecdsa-p256 — по умолчанию, ecdsa-p384, rsa-sha256; для file
алгоритм выводится из самого ключа, а флаг работает как сверка). Времена (--not-before,
--not-after, --this-update, …) задаются в Unix-секундах. Вход (--parent,
--spki, --csr, --issuer) принимается в PEM или DER — формат определяется по
содержимому. Выход — PEM, либо DER при --der.
PIN токена никогда не передаётся аргументом командной строки: PKCS#11-бэкенд
запрашивает его через pinentry на время операции, а при отсутствии pinentry —
из переменной TESSERA_ISSUER_PIN (см. Бэкенды подписи).
Выпуск CA организации
Заголовок раздела «Выпуск CA организации»Под корневым сертификатом парка выпускается CA организации с назначением рамок делегирования (роли, потолок уровня МКЦ, потолок TTL, требуемые метки):
issuer issue-ca \ --backend pkcs11 --module /usr/lib/x86_64-linux-gnu/opensc-pkcs11.so \ --key tessera-root --algorithm ecdsa-p256 \ --parent root.pem \ --spki org-ca.spki.der \ --subject "CN=Org North CA,O=Org" \ --not-before 1750000000 --not-after 1900000000 \ --allow-role oper --allow-role serv \ --max-level 5 --max-ttl 14400 \ --require-tag region=north \ --journal issuance.ndjson \ --out org-ca.pemФлаги --allow-role, --require-tag повторяются для нескольких значений.
Рамки выпускаемого CA обязаны быть ⊆ рамок родителя — иначе ядро отказывает
до подписи с указанием измерения (см. монотонное сужение в
cert-issuance.md).
Обязательные рамки: роли и потолок TTL
Заголовок раздела «Обязательные рамки: роли и потолок TTL»--allow-role обязателен и у issue-ca, и у issue-root: список ролей в
рамках делегирования — это закрытый белый список, и пустой список разрешает не
«любую роль», а ни одной. Умолчания здесь быть не может: имена ролей
принадлежат конкретному внедрению, поэтому любое подставленное значение либо
повторяет тот же тупик, либо молча расширяет рамки сверх названного оператором.
--max-ttl ограничивает срок жизни дочернего звена, поэтому у двух операций
разный смысл и разные умолчания:
| Операция | Что ограничивает --max-ttl | Умолчание |
|---|---|---|
issue-root | срок CA организации под корнем парка | 31536000 (год) |
issue-ca | срок листа смены под CA организации | 14400 (4 часа) |
Явный --max-ttl 0 отвергается при разборе аргументов: нулевой потолок требует
от дочернего звена нулевого срока действия, то есть под таким CA не проходит
ни один выпускаемый сертификат.
Оба ограничения снимают один и тот же класс ошибки: сертификат с пустым или нулевым измерением рамок выглядит валидным, попадает в журнал выдачи и не вызывает предупреждений, но вход по нему отказывает всегда, и обнаруживается это уже на устройстве.
Выпуск листа смены
Заголовок раздела «Выпуск листа смены»Публичный ключ листа берётся из явного --spki (тогда --subject обязателен)
или из --csr (тогда субъект и ключ берутся из запроса). Флаги взаимно
исключающие.
Прямой путь (SPKI):
issuer issue-leaf \ --backend pkcs11 --module /usr/lib/.../opensc-pkcs11.so \ --key org-north-ca \ --parent org-ca.pem \ --spki ivanov.spki.der \ --subject "CN=ivanov,O=Org" \ --host "sha256:<host_id_hash>" \ --role oper \ --not-before 1750000000 --not-after 1750086400 \ --max-integrity-level 2 --max-integrity-categories 0x1 \ --journal issuance.ndjson \ --out ivanov.pemПуть по CSR (см. CSR-поток):
issuer issue-leaf \ --backend pkcs11 --module /usr/lib/.../opensc-pkcs11.so \ --key org-north-ca \ --parent org-ca.pem \ --csr ivanov.csr.pem \ --host "sha256:<host_id_hash>" --role oper \ --not-before 1750000000 --not-after 1750086400 \ --journal issuance.ndjson \ --out ivanov.pem--host и --role повторяются. --max-integrity-level опционален
(без него потолок целостности не задаётся); --max-integrity-categories
(битовая маска) учитывается только вместе с уровнем.
Допуск задают два расширения выпускаемого листа, и оба собираются из этих
флагов: pam_cert_host_binding (--host) — на каких устройствах удостоверение
принимается, pam_cert_allowed_roles (--role) — какие роли предъявитель может
активировать. Имя учётной записи входа и есть роль, поэтому второй список
одновременно определяет, в какие ролевые учётные записи пущен предъявитель:
отдельного списка допуска по учётным записям нет.
Выпуск CRL
Заголовок раздела «Выпуск CRL»issuer issue-crl \ --backend pkcs11 --module /usr/lib/.../opensc-pkcs11.so \ --key org-north-ca \ --issuer org-ca.pem \ --this-update 1750000000 --next-update 1750604800 \ --crl-number 7 --last-crl-number 6 \ --revoke 2a:1750000500:1 \ --revoke 3b:1750000600 \ --journal issuance.ndjson \ --out org-ca.crl--crl-number обязан быть строго больше --last-crl-number (монотонность
crlNumber в стейте CA) — иначе отказ. Каждый --revoke — это
serial_hex:unix_date[:reason_code], где reason_code — код причины RFC 5280
(0–6), опционален; флаг повторяется.
Верификация журнала
Заголовок раздела «Верификация журнала»issuer verify-journal --journal issuance.ndjsonПечатает одно из трёх состояний: цепочка цела и полностью подписана; цела, но
хвост не подписан (с номером seq, с которого); нарушена (с позицией первой
невалидной записи — тогда ненулевой код возврата). См.
Журнал выпусков.
Язык сообщений
Заголовок раздела «Язык сообщений»Результатные сообщения оператору локализованы (RU/EN). Локаль: флаг --lang
(ru/en) → TESSERA_ISSUER_LANG → LANG → английский по умолчанию.
Совпадение по префиксу: любое значение, начинающееся на ru, выбирает русский.
Технические идентификаторы (субъект RFC 4514, OID, crlNumber, серийники) не
переводятся. См. Локализация.
CSR-поток
Заголовок раздела «CSR-поток»CSR (PKCS#10) — равноправный с прямым SPKI источник ключа листа. Он снимает необходимость передавать инструменту публичный ключ отдельно и даёт proof-of-possession: инженер генерирует ключ на своём токене и подписывает им запрос.
Сторона инженера — сформировать CSR ключом на токене:
issuer csr \ --backend pkcs11 --module /usr/lib/.../opensc-pkcs11.so \ --key ivanov-token-key --algorithm ecdsa-p256 \ --subject "CN=ivanov,O=Org" \ --spki ivanov.spki.der \ --out ivanov.csr.pemИнструмент только подписывает: публичный ключ инженера (--spki) подаётся
явно, запрос подписывается тем ключом токена, что адресует --key.
Proof-of-possession действителен, только если ключ токена соответствует
--spki — это ответственность инженера, ключи инструмент не генерирует.
Сторона оператора — issue-leaf --csr (см. выше). Что важно:
- Ядро проверяет самоподпись CSR (P-256/RSA, pure-Rust) до выпуска; битая самоподпись → отказ до подписи. CLI дополнительно печатает субъект CSR и статус самоподписи перед выпуском.
- Субъект и публичный ключ берутся из CSR. Скоуп (рамки, привязки, роли) задаёт исключительно оператор флагами — атрибуты CSR на состав расширений не влияют. Иначе CSR стал бы каналом «инженер сам запросил себе шире».
Выпуск из браузера
Заголовок раздела «Выпуск из браузера»Выпуск из браузера — локальный агент подписи на 127.0.0.1, который мостит
браузер к токену/HSM, плюс веб-кабинет (SPA поверх того же WASM-ядра), который
собирает TBS на клиенте и показывает оператору сводку для подтверждения, —
поставляется отдельно, в составе коммерческих инструментов (tessera-enterprise).
Из этого репозитория он не собирается. Контакт — см.
LICENSE.commercial.
Открытый репозиторий поставляет CLI issuer, который работает с тем же ядром и
теми же бэкендами подписи и выполняет те же проверки до подписи. Всё ниже —
бэкенды подписи и журнал выпусков — относится к CLI.
Бэкенды подписи
Заголовок раздела «Бэкенды подписи»Ядро не знает, где ключ: подпись готового TBS уходит за единый интерфейс,
никакой ключевой материал через него не проходит. Бэкенд выбирается --backend.
PKCS#11 (токен и HSM)
Заголовок раздела «PKCS#11 (токен и HSM)»Бэкенд по умолчанию, один код для аппаратных токенов и HSM. Флаги: --module
(путь к .so/.dylib/.dll — обязателен), --token-label (выбор токена,
если их несколько), --key (метка CKA_LABEL ключа CA), --pinentry
(программа pinentry явно).
PIN запрашивается через pinentry на время операции (Secret + zeroize,
не в логах и не в argv); при отсутствии pinentry — из TESSERA_ISSUER_PIN.
Для проб и CI подойдёт SoftHSM как программный PKCS#11-модуль. ГОСТ-токены работают через тот же адаптер, если токен отдаёт нужный PKCS#11-механизм.
Vault / OpenBao Transit
Заголовок раздела «Vault / OpenBao Transit»Подпись готового TBS через HTTP-API Transit. Флаги: --vault-addr
(например https://vault.example:8200 — обязателен), --mount (mount Transit,
по умолчанию transit), --vault-key (имя ключа Transit; по умолчанию равно
--key), --ca-bundle (PEM-бандл доверенных CA вместо системного стора — для
приватных Vault-CA), --prehashed (слать локально вычисленный дайджест с
prehashed=true — для ключей, настроенных на pre-hashed вход).
Токен Vault читается из переменной окружения VAULT_TOKEN (пустой/незаданный →
отказ), передаётся в заголовке X-Vault-Token и не логируется. Для ECDSA
адаптер запрашивает marshaling_algorithm=asn1 (Vault возвращает DER-подпись).
Только Transit, не Vault PKI. Движок Vault PKI непригоден для Tessera:
encoding/asn1в Go не разбирает OID-дуги большеint64, а наши расширения сидят в арке2.25.<UUID>— через Vault PKI такой сертификат не выпустить. Поэтому Transit подписывает уже собранный нами TBS, а не строит сертификат.
Transit не проверяет, что подписывает, — все проверки выпуска выполняются до
подписи в ядре, на клиенте; кто может звать sign, ограничивает политика Vault.
Ключ в файле
Заголовок раздела «Ключ в файле»Бэкенд file подписывает ключом CA из локального файла: --key-file <path>.
Формат — PKCS#8 (PEM или DER), включая зашифрованный (ENCRYPTED PRIVATE KEY, PBES2); типы ключей — ECDSA P-256/P-384 и RSA. ГОСТ-ключи файловым
бэкендом не поддерживаются — для ГОСТ остаётся PKCS#11. Другие форматы
конвертируются штатно:
# новый зашифрованный ключ P-256openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 \ | openssl pkcs8 -topk8 -v2 aes-256-cbc -out ca-key.p8chmod 600 ca-key.p8
# конвертация существующего ключа (SEC1/PKCS#1 → PKCS#8)openssl pkcs8 -topk8 -v2 aes-256-cbc -in old-key.pem -out ca-key.p8Правила бэкенда:
- Файл ключа обязан быть недоступен группе и остальным (
chmod 600) — иначе отказ до чтения содержимого. Владение файлом и права каталога бэкенд не проверяет — держите ключ в своём каталоге с правами700. - Пароль зашифрованного ключа запрашивается через pinentry, при его отсутствии
берётся из
TESSERA_ISSUER_KEY_PASSPHRASE; в аргументы командной строки и логи пароль не попадает, память затирается. - Незашифрованный ключ принимается, но с предупреждением при каждом старте; рекомендация — зашифрованный PKCS#8.
- Алгоритм подписи выводится из самого ключа;
--algorithm, не совпадающий с ключом, — ошибка.--keyопционален (по умолчанию — имя файла) и служит идентификатором ключа в журнале выпусков.
Ключ в файле — осознанный компромисс для стендов, CI и малых инсталляций: при компрометации хоста он, в отличие от токена/HSM/Vault, извлекаем. Для прода рекомендованы PKCS#11 или Vault Transit (см. threat-model.md §11).
Журнал выпусков
Заголовок раздела «Журнал выпусков»Каждая операция (выпуск листа, CA, CRL) — запись в NDJSON-журнале, связанная в
hash-chain: монотонный seq, хэш предыдущей записи, фиксированный genesis.
Журнал fail-closed: запись делается до выдачи артефакта, и если журнал
недоступен, операция отклоняется (сертификат без записи не выпускается). Путь
задаётся флагом --journal каждой выпускающей подкоманды.
Голова цепочки периодически подписывается через тот же интерфейс подписи (по
завершении сессии и по команде). issuer verify-journal различает три
состояния:
- цела, хвост полностью подписан — всё в порядке;
- цела, неподписанный хвост с seq N — цепочка не нарушена, но записи с
Nещё не покрыты подписью головы; - нарушена в позиции N — разрыв/подмена/переупорядочивание на записи
N(ненулевой код возврата).
Журнал вторичен: первичная правда — аудит входов на самих устройствах; журнал служит инвентаризации выпуска и разбору инцидентов.
Локализация
Заголовок раздела «Локализация»Операторские поверхности инструмента (сводка операции, построенная из TBS, и вывод CLI) локализованы на русский и английский без i18n-фреймворка (компактная таблица строк). Для CLI локаль разрешается один раз при старте:
- явная настройка — флаг
--lang(ru/en); - переменная
TESSERA_ISSUER_LANG; - переменная
LANG; - fallback — английский.
Совпадение по префиксу языка, регистронезависимо: ru_RU.UTF-8 и RU дают
русский, en_GB — английский; нераспознанное значение просто проваливается к
следующему источнику. Переводятся только подписи полей — технические данные
(субъект RFC 4514, OID, role_id, серийники, crlNumber, таймстемпы)
воспроизводятся байт-в-байт в любой локали.
См. также
Заголовок раздела «См. также»- cert-issuance.md — расширения Tessera, их OID и семантика, монотонное сужение рамок делегирования.
- threat-model.md §11 — поверхность атаки инструментов выпуска, ограничение ущерба, остаточные риски.