Self-hosted · Open Source

Управляйте лицензиями
вашего программного обеспечения

Licero Server — полностью автономный сервер лицензий.
Разворачивается на вашей инфраструктуре, работает без внешних зависимостей.

licero-server
$ VERSION=$(curl -sSL …/releases/production/latest.txt)
$ curl -sSL "…/licero-server-${VERSION}-linux-amd64" -o licero-server
$ chmod +x licero-server
$ ./licero-server init
config.yaml created successfully!
$ ./licero-server
Admin UI → http://127.0.0.1:8081
Client API → http://0.0.0.0:8080

Всё что нужно для управления лицензиями

Выдача лицензий

Выдавайте лицензии контрагентам на конкретные проекты. Гибкие сроки действия, отзыв в любой момент.

RSA-подпись

Каждый ключ проекта — RSA-2048 пара. Клиенты верифицируют лицензии локально через JWKS без обращения к серверу.

Автопродление

REST API для продления лицензий прямо из вашего приложения. Лог всех операций продления с фильтрацией.

Мультитенантность

Несколько проектов, несколько контрагентов. Изолированные ключевые пары для каждого проекта.

REST API

Два сервера: публичный API для клиентов и закрытый admin-интерфейс. Интеграция с любой платформой.

Веб-интерфейс

Встроенный admin UI. Управление проектами, ключами, контрагентами и лицензиями через браузер.

Скачать

Загрузка информации о версии...

Для кого нужен Licero

Licero — сервер лицензий для компаний, которые разрабатывают собственное программное обеспечение и хотят централизованно управлять лицензированием своих продуктов.

Вы поставляете свой софт клиентам и хотите контролировать срок его использования

Вашему продукту нужна проверка лицензии без постоянной зависимости от внешних облачных сервисов

Вам нужно развернуть систему лицензирования внутри своего периметра, без передачи данных наружу

Вам нужен управляемый механизм активации, продления и прекращения доступа к функциям программы

Как работает Licero

1

Разработчик создаёт проект

Вы разработали программу, которую планируете поставлять клиентам. В веб-интерфейсе Licero вы создаёте новый проект и задаёте ему название — обычно это название вашего программного продукта.

2

Для проекта выпускается ключевая пара

В Licero создаётся пара ключей: приватный ключ хранится на сервере и используется для подписания лицензий; публичный ключ встраивается в ваш продукт и используется для проверки подлинности лицензии.

3

В софт встраивается механизм проверки и обновления лицензии

В вашей программе реализуются два механизма: проверка подписи лицензии публичным ключом и периодическое обращение к серверу Licero за обновлённой лицензией. Это позволяет не только активировать продукт, но и автоматически продлевать его работу в рамках действующего договора.

4

При появлении клиента создаётся договор

В Licero создаётся договор, в котором указываются: клиент, период действия, параметры лицензирования и при необходимости — максимальный срок, на который может выдаваться очередная лицензия.

5

Выпускается первая лицензия

После создания договора выпускается первая лицензия для клиента. Обычно клиент вводит её вручную в интерфейсе вашего продукта при первичной активации.

6

Программа проверяет лицензию и активируется

После ввода лицензии ваш софт проверяет подпись и срок действия, после чего активирует программу. Всё происходит локально — без обращения к серверу.

7

Программа периодически запрашивает новую лицензию

Во время работы программа периодически передаёт текущую лицензию серверу Licero. Если до истечения осталось более 30% срока — сервер вернёт тот же токен. Если меньше — выдаст новый с обновлённым сроком, а старый автоматически отзовёт. Для клиента это происходит прозрачно и незаметно.

8

Если договор подходит к концу — выдаётся последняя лицензия

Сервер никогда не выдаст лицензию с датой окончания позже даты завершения договора. Когда срок договора истекает, программа больше не сможет продолжить работу без продления.

9

Если срок лицензии истёк — доступ блокируется

Ваш софт контролирует срок действия текущей лицензии. При истечении программа останавливает оказание услуг, блокирует доступ к лицензируемым функциям и уведомляет пользователя о необходимости продления.

10

После продления договора работа восстанавливается

Если договор был продлён, программа снова обращается в Licero за новой лицензией — даже если текущая уже истекла. Главное условие: передаваемая лицензия должна быть валидна по подписи. Это позволяет безопасно восстановить работу без ручной перевыдачи.

Коротко по логике работы
создать проект выпустить ключи встроить публичный ключ создать договор выпустить первую лицензию клиент активирует автопродление в рамках договора договор истёк → блокировка

Преимущества

Лицензирование контролируется централизованно — все договоры, лицензии и ключи в одном месте

Публичный ключ можно безопасно встраивать в продукт — даже при его утечке подделать лицензию невозможно

Лицензии обновляются автоматически — без ручных действий со стороны клиента

Прекращение договора автоматически ограничивает работу продукта — никакой ручной блокировки на стороне клиента

Система разворачивается внутри контура компании — данные не покидают вашу инфраструктуру

Документация для разработчиков

Интегрируйте лицензирование в ваше приложение за несколько шагов

Обзор

Licero предоставляет два HTTP-сервера. Клиентский API предназначен для интеграции в сторонние приложения: проверка и продление лицензий. Он должен быть доступен клиентским машинам извне. Админ-интерфейс используется только для управления и не должен быть открыт наружу.

Клиентский API Порт 8080 · Публичный · Для интеграции в приложение
Админ UI Порт 8081 · Только внутри сети · Управление проектами и лицензиями
Базовый URL http://your-server:8080/api/v1
Формат ответов application/json
Алгоритм подписи RS256 (JWT)
Структура лицензионного токена (JWT)

Лицензия — это подписанный JWT (RS256). Токен содержит заголовок и набор claims:

Заголовок (header)

{ "alg": "RS256", "kid": "Wjg4KVtRlPVyu1wwaJNup7cjHBAtrStX", // ID ключа подписи "prd": "a3f2c1d0-..." // ID проекта }

Полезная нагрузка (payload)

{ "lid": "550e8400-e29b-...", // ID лицензии "aud": "ООО Ромашка", // Название контрагента "tin": "7814505170", // ИНН "psrn": "1117847263940", // ОГРН "iat": 1713000000, // Выдан (Unix) "nbf": 1713000000, // Действует с (Unix) "exp": 1744536000, // Истекает (Unix) // ... кастомные поля проекта }
Сервер принимает истёкшие токены на эндпоинте /renew — это намеренно. Клиент может хранить токен и обновить его даже после истечения срока.
Эндпоинты
POST /api/v1/license/renew

Продлевает лицензию. Принимает текущий JWT (в том числе истёкший). Поведение зависит от оставшегося срока:

  • Осталось более 30% срока действия — возвращает тот же токен, новая запись не создаётся.
  • Осталось 30% или менее — выдаёт новый токен, старая лицензия автоматически отзывается.

Клиент должен всегда сохранять возвращённый токен — он может быть как прежним, так и новым.

Заголовки запроса

Authorization: Bearer <текущий JWT>

Успешный ответ (200)

{ "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." }

Коды ответов

КодЗначениеДействие клиента
200Новый токен выданСохранить токен, проверить exp нового токена
400Отсутствует или некорректен заголовок AuthorizationПроверить формат запроса
401Подпись JWT невалиднаЛицензия повреждена или подделана
402Договор с контрагентом истёкУведомить пользователя об окончании договора
404Лицензия, проект или контрагент не найденПроверить целостность токена
500Внутренняя ошибка сервераПовторить позже
Скрытый отзыв лицензии: если лицензия отозвана, сервер возвращает 200, но токен уже истёк (exp в прошлом). Проверяйте exp возвращённого токена после каждого renew. При выдаче новой лицензии старая отзывается автоматически — не используйте старый токен повторно.

GET /api/v1/projects/{prd}/public.jwk

Возвращает набор публичных ключей проекта в формате JWKS (RFC 7517). Используйте для верификации подписи JWT на стороне клиента. Ответ кэшируется 1 час. Авторизация не требуется.

Параметры пути

prd ID проекта из JWT-заголовка

GET /api/v1/projects/{prd}/keys/{kid}/public.pem

Возвращает публичный ключ в формате PEM. Альтернатива JWKS для библиотек, которые предпочитают PEM. Авторизация не требуется.

Параметры пути

prd ID проекта из JWT-заголовка
kid ID ключа из JWT-заголовка
Рекомендуемый сценарий интеграции
  • 1 При первом запуске — загрузите публичный ключ проекта через /public.jwk или /public.pem и закэшируйте его.
  • 2 Проверяйте лицензию локально (без обращения к серверу): верифицируйте подпись RS256, проверьте exp. Это быстро и не требует сети.
  • 3 Периодически вызывайте POST /license/renew с текущим токеном. Сервер сам решает: вернёт тот же токен (если до истечения > 30% срока) или выдаст новый. Всегда сохраняйте возвращённый токен.
  • 4 После каждого renew проверьте exp возвращённого токена. Если он в прошлом — лицензия отозвана. Заблокируйте доступ к функциям.
  • 5 При ответе 402 — договор с контрагентом истёк. Покажите пользователю соответствующее сообщение.
  • 6 Обновляйте публичный ключ при смене ключа на сервере. Если верификация падает с ошибкой подписи, повторно загрузите JWKS.
Примеры кода
package license import ( "encoding/json" "fmt" "net/http" "time" "github.com/golang-jwt/jwt/v5" ) const apiBase = "http://your-server:8080/api/v1" // CheckAndRenew проверяет токен и при необходимости обновляет его. func CheckAndRenew(token string) (string, error) { // 1. Парсим без верификации чтобы достать kid и prd parser := jwt.NewParser() unverified, _, _ := parser.ParseUnverified(token, jwt.MapClaims{}) kid := unverified.Header["kid"].(string) prd := unverified.Header["prd"].(string) // 2. Загружаем публичный ключ pemURL := fmt.Sprintf("%s/projects/%s/keys/%s/public.pem", apiBase, prd, kid) resp, _ := http.Get(pemURL) defer resp.Body.Close() var pemBytes []byte json.NewDecoder(resp.Body).Decode(&pemBytes) pubKey, _ := jwt.ParseRSAPublicKeyFromPEM(pemBytes) // 3. Верифицируем подпись (разрешаем истёкшие) parsed, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) { return pubKey, nil }, jwt.WithValidMethods([]string{"RS256"}), jwt.WithLeeway(100*365*24*time.Hour)) if err != nil { return "", fmt.Errorf("invalid token: %w", err) } claims := parsed.Claims.(jwt.MapClaims) exp := int64(claims["exp"].(float64)) // 4. Если не истёк — токен валиден if time.Now().Unix() < exp { return token, nil } // 5. Токен истёк — вызываем renew req, _ := http.NewRequest("POST", apiBase+"/license/renew", nil) req.Header.Set("Authorization", "Bearer "+token) renewResp, err := http.DefaultClient.Do(req) if err != nil { return "", fmt.Errorf("renew failed: %w", err) } defer renewResp.Body.Close() if renewResp.StatusCode == 402 { return "", fmt.Errorf("contract expired") } if renewResp.StatusCode != 200 { return "", fmt.Errorf("renew failed: HTTP %d", renewResp.StatusCode) } var result struct{ Token string `json:"token"` } json.NewDecoder(renewResp.Body).Decode(&result) // 6. Проверяем silent revocation newParsed, _ := jwt.Parse(result.Token, func(t *jwt.Token) (interface{}, error) { return pubKey, nil }, jwt.WithLeeway(100*365*24*time.Hour)) newClaims := newParsed.Claims.(jwt.MapClaims) if time.Now().Unix() >= int64(newClaims["exp"].(float64)) { return "", fmt.Errorf("license revoked") } return result.Token, nil }
import time import requests import jwt # pip install PyJWT cryptography API_BASE = "http://your-server:8080/api/v1" def check_and_renew(token: str) -> str: """Проверяет токен и при необходимости обновляет его.""" # 1. Парсим заголовок без верификации header = jwt.get_unverified_header(token) kid = header["kid"] prd = header["prd"] # 2. Загружаем публичный ключ pem_url = f"{API_BASE}/projects/{prd}/keys/{kid}/public.pem" pub_key_pem = requests.get(pem_url).content # 3. Верифицируем подпись (проверяем exp вручную) try: claims = jwt.decode( token, pub_key_pem, algorithms=["RS256"], options={"verify_exp": False}, audience=jwt.decode(token, options={"verify_signature": False})["aud"], ) except jwt.InvalidSignatureError: raise Exception("Invalid token signature") # 4. Если не истёк — возвращаем как есть if claims["exp"] > time.time(): return token # 5. Истёк — обновляем resp = requests.post( f"{API_BASE}/license/renew", headers={"Authorization": f"Bearer {token}"} ) if resp.status_code == 402: raise Exception("Contract expired") if resp.status_code != 200: raise Exception(f"Renew failed: HTTP {resp.status_code}") new_token = resp.json()["token"] # 6. Проверяем silent revocation new_claims = jwt.decode( new_token, pub_key_pem, algorithms=["RS256"], options={"verify_exp": False}, audience=claims["aud"], ) if new_claims["exp"] <= time.time(): raise Exception("License revoked") return new_token
// npm install jose import { importSPKI, jwtVerify } from 'jose' const API_BASE = 'http://your-server:8080/api/v1' async function checkAndRenew(token) { // 1. Парсим заголовок без верификации const [headerB64] = token.split('.') const header = JSON.parse(atob(headerB64.replace(/-/g,'+').replace(/_/g,'/'))) const { kid, prd } = header // 2. Загружаем публичный ключ const pem = await fetch(`${API_BASE}/projects/${prd}/keys/${kid}/public.pem`).then(r => r.text()) const pubKey = await importSPKI(pem, 'RS256') // 3. Верифицируем подпись (разрешаем истёкшие) let claims try { const result = await jwtVerify(token, pubKey, { clockTolerance: '100y' }) claims = result.payload } catch { const [, payloadB64] = token.split('.') claims = JSON.parse(atob(payloadB64.replace(/-/g,'+').replace(/_/g,'/'))) } // 4. Если не истёк — токен валиден if (claims.exp > Date.now() / 1000) return token // 5. Истёк — обновляем const resp = await fetch(`${API_BASE}/license/renew`, { method: 'POST', headers: { Authorization: `Bearer ${token}` }, }) if (resp.status === 402) throw new Error('Contract expired') if (!resp.ok) throw new Error(`Renew failed: HTTP ${resp.status}`) const { token: newToken } = await resp.json() // 6. Проверяем silent revocation const [, newPayloadB64] = newToken.split('.') const newClaims = JSON.parse(atob(newPayloadB64.replace(/-/g,'+').replace(/_/g,'/'))) if (newClaims.exp <= Date.now() / 1000) throw new Error('License revoked') return newToken }