• Ru
  • En

Лицензирование: генерация и проверка

В составе UniBPM реализованы функции проверки цифровой подписи и целостности лицензии с использованием стандартного алгоритма (ECDSA-256), входящего в состав Java Security API. Данные функции применяются только для подтверждения подлинности лицензии и не предназначены для защиты информации, являющейся объектом регулирования. Программное обеспечение UniBPM не является СКЗИ, не подлежит обязательной сертификации и не требует лицензии регуляторных ведомств (ФСБ РФ, КНБ РК) на разработку или распространение средств криптографической защиты информации.

1) Общее

Платформа поддерживает два режима:

  • COMMUNITY — ограничения по функционалу (в базовой поставке: разрешён 1 Workflow на весь инстанс).
  • ENTERPRISE — без указанных ограничений. Включается действующей лицензией.

Лицензия — это JWS-токен (подписанный JSON), который хранится на сервере и проверяется на каждом старте и в режиме онлайн.


2) Криптография и форматы

  • Алгоритм подписи: ES256 (ECDSA).
  • Приватный ключ (у лицензиара): PKCS#8 PEM.

Это стандартный контейнер для закрытого ключа (один ключ — один файл), удобен для библиотек и хранения.

  • Публичный ключ (на сервере для проверки):X.509 SubjectPublicKeyInfo PEM.

Это стандартное представление только открытого ключа, без сертификата. Подходит для быстрой верификации подписи.

  • Идентификатор ключа (kid) - это имя файла публичного ключа без расширения, например 2025-08-e1.pem -> kid = 2025-08-e1.

Структура JWS

Header

{
  "alg": "ES256",
  "kid": "2025-08-e1",
  "typ": "JWT"
}

Payload

{
  "edition": "Enterprise",
  "issuedTo": "nazym.kazizmurat@reunico.com",
  "dueDate": "2025-08-20T15:30:59Z",
  "licenseId": "LIC-000123",
  "licenseVersion": 1
}

Поле dueDate — ISO-8601 UTC. По истечении даты лицензия становится недействительной.


3) Размещение публичных ключей

Публичные ключи поставляются вместе с приложением и кладутся в ресурсы:

src/main/resources/licenses.keys/<kid>.pem

При старте компонент PublicKeyProvider загружает все *.pem из:

classpath:/licenses/keys/*.pem

(наш путь маппится на licenses.keys в ресурсах).


4) Генерация ключей (у лицензодателя)

# 1) Сгенерировать EC ключ (secp256r1)
openssl ecparam -name prime256v1 -genkey -noout -out ec-private.pem

# 2) Преобразовать в PKCS#8 (без пароля)
openssl pkcs8 -topk8 -nocrypt -in ec-private.pem -out ec-private-pkcs8.pem

# 3) Достать публичный ключ
openssl ec -in ec-private.pem -pubout -out ec-public.pem

# 4) Содержимое ec-public.pem сохранить в ресурс:
# src/main/resources/licenses.keys/2025-08-e1.pem

5) Выпуск лицензии (CLI)

В проекте есть утилита GenLicenseCli.

Запуск из IDE — аргументы:

--due 2025-08-20T15:30:59Z \
--to "nazym.kazizmurat@reunico.com " \
--kid 2025-08-e1 \
--priv /path/to/ec-private-pkcs8.pem \
--alg ES256

На выходе — строка JWS. Её передают на сервер (см. ниже) или сохраняют в файл.


6) Загрузка лицензии на сервер

HTTP-API

  • PUT /api/admin/license — загрузка новой лицензии.
  • GET /api/admin/license — получить текущее состояние.

Тело запроса (PUT)

{ "license": "JWS" }

Что делает сервер при PUT:

  1. Валидирует JWS (ES256, корректный kid, не истёкший dueDate, edition = ENTERPRISE и т.п.).

  2. Сохраняет сам JWS в БД, таблицу bd_attachment как запись с:

  • entity_type = LICENSE

  • file_name = "license.key"

  • content_type = "text/plain"

  • content = <байты JWS, UTF-8>

  1. Вызывает LicenseManager.reload() — режим переключается без рестарта.

7) Где хранится лицензия

Источник лицензии — компонент LicenseSource, который читает по приоритету:

  1. База данных: берётся последняя запись из bd_attachment с entity_type = LICENSE.
  2. Опциональный config fallback: если в БД нет лицензии, берётся строка из application.yaml:
    app:
      license:
        initial: "${LICENSE_KEY:}" 

Этот fallback только для чтения во время старта и не сохраняется в БД автоматически. Постоянное хранилище — только БД через PUT /api/admin/license.


8) Как идёт проверка на сервере

Компонент LicenseVerifier делает следующее:

  1. Парсит JWS (JJWT).
  2. По kid берёт публичный ключ из PublicKeyProvider.
  3. Проверяет alg == ES256 (другие отклоняются).
  4. Требует edition == "Enterprise".
  5. Сравнивает dueDate с текущим временем (UTC).
  6. Возвращает LicenseClaims (issuedTo, dueDate, kid, и т.п.).

Если верификация не проходит — платформа работает как COMMUNITY.


9) Лицензионная политика (ограничения)

  • Рабочий workflow — всегда самый первый (самый ранний по дате создания).
    • При даунгрейде из ENTERPRISE в COMMUNITY, если в базе уже несколько workflow, система автоматически будет работать только с самым первым, остальные считаются «невидимыми/недоступными» для операций.
  • GET list / GET by id: в COMMUNITY показываем и допускаем операции только для самого первого workflow; прочие id игнорируются (или дают 404/403 — в зависимости от операции).
  • UPDATE / DELETE: разрешены только для самого первого workflow.
  • CREATE:
    • если в системе 0 workflow — можно создать первый;
    • если в системе > 1 (наследие после даунгрейда) — запрещено создавать новые, пока база не приведена к 1 рабочему экземпляру (чтобы не усугублять расхождение).

10) Защита от перезаписи лицензии

    app:
      license:
        immutable: true   # по умолчанию false
  • Если лицензии в БД нет — первый PUT /api/admin/license разрешён (импорт).

  • После импорта и при immutable: true — любые последующие PUT запрещены (403 Forbidden).

  • GET /api/admin/license не зависит от флага.

11) Тест-чеклист

  1. Ключи лежат в src/main/resources/licenses.keys/*.pem, имена = kid.
  2. PUT /api/admin/license с валидным JWS → 200 OK, GET показывает ENTERPRISE + поля.
  3. В БД в bd_attachment появилась запись с entity_type = LICENSE, содержимое — исходный JWS (UTF-8).
  4. Удалили лицензию из БД / истёк dueDate → режим падает в COMMUNITY.
  5. Если в БД нет лицензии, а в application.yaml задан app.license.initial, сервер сможет стартовать в ENTERPRISE в памяти, но не будет создавать запись в БД до явного PUT.