Протокол лицензирования
Лицензия CertifiEd — самодостаточный подписанный токен. Всё, что нужно приложению для оффлайн-проверки (сроки, feature flags, лимиты), лежит внутри подписанного payload'а. Сервер при проверке не нужен.
Реализация: TokenSerializer, TokenHeader, TokenPayload, HeartbeatMarker (CertifiEd.Application/Licensing); подпись — EncryptedFileSigner (CertifiEd.Infrastructure/Signing); проверка на клиенте — CertifiEdLicenseClient (CertifiEd.Client).
Формат токена
base64url(header) . base64url(payload) . base64url(signature)Три сегмента, разделённые точками. Кодировка сегментов — base64url без padding. JSON внутри сегментов сериализуется в camelCase, поля со значением null опускаются.
Header
{ "alg": "Ed25519", "kid": "019f6b40-0fbb-7a69-adbf-fb5f476ad4bd", "typ": "certified-license", "ver": 1}| Поле | Тип | Назначение |
|---|---|---|
alg | string | Алгоритм подписи, всегда Ed25519 |
kid | GUID | Id подписавшего signing_key — по нему выбирается публичный ключ для проверки |
typ | string | Дискриминатор типа, certified-license |
ver | int | Версия формата токена (сейчас 1) |
Payload
{ "lic": "019f6b40-89c4-7d41-be2a-4a151a3c0063", "key": "CFED-S7FN-RJFA-KAEY-8KZC", "tpl": "019f6b40-0fb8-78f0-9af2-e3589de54069", "tplV": 1, "cfgV": 1, "iss": "certified-api", "sub": "019f6b40-0f9c-7efd-996b-0dadd2ed1077", "iat": 1784210819, "nbf": 1784210819, "exp": 1815746819, "maxOfflineDays": 7, "cfg": "{\"features\":[\"export\",\"api\"],\"limits\":{\"maxSeats\":25}}"}| Поле | Тип | Назначение |
|---|---|---|
lic | GUID | Id лицензии |
key | string | Человекочитаемый ключ CFED-XXXX-XXXX-XXXX-XXXX |
tpl | GUID | Id шаблона |
tplV | int | Номер версии шаблона |
cfgV | int | Версия конфигурации лицензии |
iss | string | Издатель, по умолчанию certified-api |
sub | GUID | Субъект — id компании-лицензиата |
iat | long | Issued at, Unix-секунды |
nbf | long | Not valid before, Unix-секунды |
exp | long | Жёсткое истечение, Unix-секунды (license.expires_at) |
maxOfflineDays | int | Сколько дней клиент живёт без сети — не более 30 |
hwfp | string? | HW fingerprint, если лицензия привязана к железу; иначе поле опущено |
cfg | string | JSON-строка конфигурации (features / limits) по схеме версии шаблона |
Как строится подпись
- Сериализуются
headerиpayload, каждый кодируется в base64url →headerB64,payloadB64. - Формируется сообщение для подписи: ASCII-байты строки
"{headerB64}.{payloadB64}"— всё до последней точки. - Сообщение подписывается приватным ключом Ed25519 шаблона (ключ выбирается по активной
signing_key). Приватные ключи хранятся на диске зашифрованными AES-256-GCM подCERTIFIED_MASTER_KEY. - Подпись кодируется в base64url и приклеивается третьим сегментом.
Проверка на клиенте (CertifiEdLicenseClient.VerifySignature) зеркальна: берётся всё до последней точки как ASCII, из последнего сегмента декодируется подпись, дальше Ed25519.Verify(publicKey, message, signature). Публичный ключ — «сырые» 32 байта; API отдаёт его в hex, на клиенте — Convert.FromHexString.
Файл .ced
Эндпоинт GET /api/v1/panel/licenses/{id}/download отдаёт JSON-конверт:
{ "licenseKey": "CFED-S7FN-RJFA-KAEY-8KZC", "token": "eyJhbGciOiJFZDI1NTE5...<three segments>...RFbKNFNZCiMLL4MlNwOrsDg", "publicKey": "7bac1e3ce8578ad859bfe001a4f4d72702d00e3d1eebdd735ebdb7145d74f35b"}SDK при чтении .ced принимает и такой конверт (берёт поле token), и «голый» токен одной строкой — файл можно сохранять как есть.
Offline-маркер heartbeat
Отдельный, более короткий подписанный объект — две секции вместо трёх:
base64url(payload) . base64url(signature)Payload (typ = certified-heartbeat):
{ "typ": "certified-heartbeat", "lic": "019f6b40-89c4-7d41-be2a-4a151a3c0063", "key": "CFED-S7FN-RJFA-KAEY-8KZC", "iat": 1784210819, "exp": 1784815619, "mid": "64d519a09298303a4ac813c33b663f3e859e6b5122d0210143a6c593819cf2cf"}| Поле | Назначение |
|---|---|
typ | Дискриминатор, certified-heartbeat |
lic | Id лицензии |
key | Ключ лицензии |
iat | Выдан, Unix-секунды |
exp | Действует до = iat + maxOfflineDays, Unix-секунды |
mid | HW fingerprint машины (machine id), если привязка есть |
Маркер выдаётся сервером при активации и при каждом успешном heartbeat (HeartbeatMarker.IssueAsync, подписывается тем же ключом шаблона). SDK кэширует его на диск рядом с файлом лицензии как <license>.ced.hb — атомарная запись через .tmp + File.Move. При рестарте без сети маркер загружается с диска и держит лицензию «живой» до exp.
Grace-период и вычисление Status
CertifiEdLicenseStatus выводится клиентом локально — из exp токена и маркера:
Invalid — токен не загруженExpired — now > token.exp (жёсткое истечение)
если маркер есть: Active — now <= marker.exp GracePeriod — marker.exp < now <= marker.exp + 24 часа Expired — позже
если маркера нет (сети не было с самого старта) —отсчёт от последней локальной валидации (lastValidatedAt): Active — now <= lastValidatedAt + maxOfflineDays GracePeriod — ещё +1 сутки Expired — позжеИтог: пока приложение периодически достаёт свежий маркер (heartbeat раз в heartbeatIntervalSeconds), оно Active. Пропала сеть — маркер держит Active до maxOfflineDays, затем сутки GracePeriod, затем Expired. Жёсткий exp токена перекрывает всё: после него всегда Expired.
HW fingerprint
HwFingerprint.Get() (CertifiEd.Client) считает стабильный SHA-256 hex по имени машины, RuntimeInformation.OSDescription, первому стабильному MAC-адресу и platform machine id: на Windows — MachineGuid из реестра, на Linux — /etc/machine-id, на macOS — IOPlatformSerialNumber. Значение кэшируется на процесс.
- Лицензия несёт режим привязки
hwBinding—none/fixed/firstActivation. В режимеfixedи в закреплённомfirstActivationсервер при активации сверяет фингерпринт и отклоняет чужой (activation.fingerprint_mismatch). firstActivationне имеет отпечатка при выпуске: его записывает первая успешная активация, после чего лицензия ведёт себя какfixed.- Замена железа делается на платформе —
POST /api/v1/panel/licenses/{id}/rebind; лицензия переподписывается, и.cedнужно скачать заново. - Свой фингерпринт передаётся в SDK через
CertifiEdClientOptions.HwFingerprint; по умолчанию берётсяHwFingerprint.Get().
Подробно — в разделе Привязка к железу.
См. также
- Клиентский SDK — интеграция проверки в приложение.
- Быстрый старт — выпуск лицензии и её проверка end-to-end.
- API Reference — client и panel эндпоинты.