Авторизация
Все запросы рабочего сайта выполняются по HTTPS. Создайте код продукта и ключ приложения в админке. В заголовке X-API-Key передавайте Public API Key. Ключ лицензии передаётся в JSON, а при проверке обновлений — в X-License-Key.
Public API Key идентифицирует приложение и не является секретом клиента. Secret Key нужен только для запросов между доверенными серверами. Не встраивайте его в дистрибутив.
Методы
| Метод | Адрес | Действие |
|---|---|---|
| POST | /api/v1/license/activate | Занять место для устройства |
| POST | /api/v1/license/check | Проверить право запуска на устройстве |
| POST | /api/v1/license/deactivate | Освободить место устройства |
| POST | /api/v1/license/heartbeat | Зафиксировать активность |
| GET | /api/v1/products/{product_code}/update?version=1.0.0 | Проверка версии и временная ссылка скачивания |
| GET | /api/v1/public-key | Публичный ключ Ed25519 |
Тело запроса лицензии
{
"license_key": "IOC-XXXXXXXX-XXXXXXXX-XXXXXXXX-XXXXXXXX",
"product_code": "MY_PRODUCT",
"device_id": "installation-unique-id",
"hardware_hash": "sha256-of-stable-hardware-fingerprint",
"app_version": "1.0.0",
"os": "Windows 11",
"computer_name": "Рабочий компьютер"
}license_key, product_code, device_id, hardware_hash обязательны. Идентификатор и аппаратный отпечаток: 3–190 символов. Device ID должен сохраняться между запусками. При ограничении версий передавайте разрешённую app_version.
Ответ и подпись
В ответе есть success, status, license, server_time, valid_until, device_id, product_code, а также signed_payload и signature.
- Закрепите публичный ключ сервера в клиенте во время сборки. Не доверяйте ключу, полученному вместе с непроверенным ответом.
- Декодируйте
signatureиз Base64 и проверьте Ed25519 по точным UTF-8 байтам строкиsigned_payload. - Только после проверки декодируйте
signed_payloadиз Base64 в JSON. Используйте поля этого проверенного JSON. - Сравните код продукта, Device ID, время сервера и срок действия. Разрешайте работу только при
success=trueиstatus=active.
Обычный ответ действителен максимум 15 минут. Клиент должен регулярно запрашивать проверку. Доверенный источник времени и защита от отката часов реализуются в клиентском приложении.
Статусы лицензии
active | Лицензия действует |
expired | Истёк срок |
blocked | Лицензия, аккаунт или заказ заблокированы |
invalid | Неверный ключ, устройство, отпечаток или версия |
device_limit | Исчерпан лимит устройств или активаций |
product_mismatch | Ключ относится к другой программе |
offline_lease | Место занято автономной активацией |
Неверный ключ приложения: HTTP 401. Неверный запрос: 400. Лимит запросов: 429 с заголовком Retry-After. Ошибки протокола возвращают JSON; бизнес-статусы лицензии возвращаются с HTTP 200.
Автономная активация
Клиент передаёт администратору Machine Code: JSON с кодом продукта, Device ID и hardware_hash. Администратор открывает лицензию и создаёт подписанный ответ на 1–720 часов. Клиент проверяет подпись, продукт, устройство, отпечаток и valid_until. Место устройства остаётся занятым до истечения автономного срока.
Заблокированная на сервере автономная лицензия может продолжать работу до окончания уже выданного срока: мгновенный отзыв без связи с сервером невозможен.
Подпись запросов доверенного сервера
Необязательные заголовки: X-Timestamp (Unix UTC), X-Nonce (16–100 букв, цифр, дефисов или подчёркиваний), X-Signature. Допуск времени — 5 минут. Подпись — hex HMAC-SHA256 с Secret Key от строки:
METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + NONCE + "\n" + SHA256_HEX(RAW_BODY)
PATH — путь без query string. Повторный nonce отклоняется. Для GET тело пустое; Secret Key даёт дополнительную проверку отправителя, но право на платный файл по-прежнему определяется лицензией.
Примеры клиентов
Готовые функции приведены ниже и находятся в каталоге api/examples архива. Передайте адрес вашего сервера, ключ приложения, закреплённый ключ Ed25519 и параметры лицензии. Версию 1.0.0 замените версией своего приложения.
PHP
<?php
// PHP 8.1+, curl, sodium. Используйте HTTPS и закреплённый публичный ключ сервера.
function activateLicense(string $base,string $apiKey,string $publicKey,string $licenseKey,string $productCode,string $deviceId,string $hardwareHash): array {
$request=['license_key'=>$licenseKey,'product_code'=>$productCode,'device_id'=>$deviceId,'hardware_hash'=>$hardwareHash,'app_version'=>'1.0.0','os'=>PHP_OS_FAMILY,'computer_name'=>gethostname()];
$ch=curl_init(rtrim($base,'/').'/api/v1/license/activate');curl_setopt_array($ch,[CURLOPT_POST=>true,CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>20,CURLOPT_SSL_VERIFYPEER=>true,CURLOPT_HTTPHEADER=>['Content-Type: application/json','X-API-Key: '.$apiKey],CURLOPT_POSTFIELDS=>json_encode($request)]);$raw=curl_exec($ch);$status=curl_getinfo($ch,CURLINFO_RESPONSE_CODE);curl_close($ch);if($raw===false||$status!==200)throw new RuntimeException('Ошибка связи с сервером лицензий.');
$envelope=json_decode($raw,true,512,JSON_THROW_ON_ERROR);$payload=$envelope['signed_payload']??'';$signature=base64_decode($envelope['signature']??'',true);$public=base64_decode($publicKey,true);
if(!$signature||strlen($signature)!==SODIUM_CRYPTO_SIGN_BYTES||!$public||strlen($public)!==SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES||!sodium_crypto_sign_verify_detached($signature,$payload,$public))throw new RuntimeException('Подпись лицензии недействительна.');
$data=json_decode(base64_decode($payload,true),true,512,JSON_THROW_ON_ERROR);
if(($data['product_code']??'')!==$productCode||($data['device_id']??'')!==$deviceId||!($data['success']??false)||($data['status']??'')!=='active'||($data['valid_until']??0)<time()||abs(time()-($data['server_time']??0))>300)throw new RuntimeException('Лицензия недействительна.');return $data;
}
Python
"""Python 3.10+, pip install cryptography. Secret Key не нужен клиенту."""
import base64,json,time,urllib.request
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
def activate(base,api_key,public_key,license_key,product_code,device_id,hardware_hash):
body=dict(license_key=license_key,product_code=product_code,device_id=device_id,
hardware_hash=hardware_hash,app_version='1.0.0',os='Windows',computer_name='Рабочий компьютер')
request=urllib.request.Request(base.rstrip('/')+'/api/v1/license/activate',
data=json.dumps(body).encode(),headers={'Content-Type':'application/json','X-API-Key':api_key},method='POST')
with urllib.request.urlopen(request,timeout=20) as response:
envelope=json.load(response)
Ed25519PublicKey.from_public_bytes(base64.b64decode(public_key)).verify(
base64.b64decode(envelope['signature']),envelope['signed_payload'].encode())
data=json.loads(base64.b64decode(envelope['signed_payload']))
if (not data.get('success') or data.get('status')!='active' or data.get('product_code')!=product_code
or data.get('device_id')!=device_id or data.get('valid_until',0)<time()
or abs(time.time()-data.get('server_time',0))>300):
raise ValueError('Лицензия недействительна')
return data
JavaScript
// Современный JavaScript / WebCrypto Ed25519. Используйте на доверенной странице того же origin
// или в приложении, которое поддерживает HTTPS fetch. Секрет приложения не встраивается.
export async function activate(base, apiKey, publicKey, input) {
const response = await fetch(base.replace(/\/$/, '') + '/api/v1/license/activate', {
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey },
body: JSON.stringify(input)
});
if (!response.ok) throw new Error('Ошибка сервера лицензий');
const envelope = await response.json();
const decode = value => Uint8Array.from(atob(value), c => c.charCodeAt(0));
const key = await crypto.subtle.importKey('raw', decode(publicKey), 'Ed25519', false, ['verify']);
if (!await crypto.subtle.verify('Ed25519', key, decode(envelope.signature), new TextEncoder().encode(envelope.signed_payload)))
throw new Error('Подпись лицензии недействительна');
const data = JSON.parse(new TextDecoder().decode(decode(envelope.signed_payload)));
if (!data.success || data.status !== 'active' || data.product_code !== input.product_code ||
data.device_id !== input.device_id || data.valid_until < Date.now()/1000 || Math.abs(Date.now()/1000-data.server_time)>300)
throw new Error('Лицензия недействительна');
return data;
}
C#
// .NET 8+, пакет NSec.Cryptography (Ed25519). Публичный ключ закрепите в клиенте.
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using NSec.Cryptography;
public static class LicenseClient {
public static async Task<JsonElement> Activate(HttpClient http, string baseUrl, string apiKey,
string publicKey, string licenseKey, string productCode, string deviceId, string hardwareHash) {
using var request = new HttpRequestMessage(HttpMethod.Post, baseUrl.TrimEnd('/')+"/api/v1/license/activate");
request.Headers.Add("X-API-Key", apiKey);
request.Content=JsonContent.Create(new { license_key=licenseKey, product_code=productCode,
device_id=deviceId, hardware_hash=hardwareHash, app_version="1.0.0", os="Windows", computer_name=Environment.MachineName });
using var response=await http.SendAsync(request);
response.EnsureSuccessStatusCode();
using var envelope=JsonDocument.Parse(await response.Content.ReadAsStringAsync());
var root=envelope.RootElement; var payload=root.GetProperty("signed_payload").GetString()!;
var algorithm=SignatureAlgorithm.Ed25519;
var key=PublicKey.Import(algorithm,Convert.FromBase64String(publicKey),KeyBlobFormat.RawPublicKey);
if(!algorithm.Verify(key,Encoding.UTF8.GetBytes(payload),Convert.FromBase64String(root.GetProperty("signature").GetString()!)))
throw new InvalidOperationException("Подпись лицензии недействительна");
using var parsed=JsonDocument.Parse(Convert.FromBase64String(payload));var data=parsed.RootElement;
long now=DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if(!data.GetProperty("success").GetBoolean() || data.GetProperty("status").GetString()!="active" ||
data.GetProperty("product_code").GetString()!=productCode || data.GetProperty("device_id").GetString()!=deviceId ||
data.GetProperty("valid_until").GetInt64()<now || Math.Abs(now-data.GetProperty("server_time").GetInt64())>300)
throw new InvalidOperationException("Лицензия недействительна");
return data.Clone();
}
}
C++
// C++17; библиотеки libcurl, libsodium, nlohmann/json. Не отключайте проверку TLS.
#include <curl/curl.h>
#include <sodium.h>
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
#include <stdexcept>
#include <ctime>
#include <cstdlib>
using nlohmann::json;
static size_t receive(char* data,size_t size,size_t count,void* context){
auto& out=*static_cast<std::string*>(context);size_t len=size*count;if(out.size()+len>1048576)return 0;out.append(data,len);return len;
}
static std::vector<unsigned char> decode64(const std::string& value){
std::vector<unsigned char> out(value.size());size_t size=0;
if(sodium_base642bin(out.data(),out.size(),value.data(),value.size(),nullptr,&size,nullptr,sodium_base64_VARIANT_ORIGINAL)!=0)
throw std::runtime_error("Некорректный Base64");out.resize(size);return out;
}
json activate(const std::string& base,const std::string& apiKey,const std::string& publicKey,const json& input){
if(sodium_init()<0)throw std::runtime_error("Криптография недоступна");
CURL* curl=curl_easy_init();if(!curl)throw std::runtime_error("HTTP недоступен");
std::string output,body=input.dump(),url=base+"/api/v1/license/activate",header="X-API-Key: "+apiKey;
auto headers=curl_slist_append(nullptr,"Content-Type: application/json");headers=curl_slist_append(headers,header.c_str());
curl_easy_setopt(curl,CURLOPT_URL,url.c_str());curl_easy_setopt(curl,CURLOPT_POSTFIELDS,body.c_str());
curl_easy_setopt(curl,CURLOPT_HTTPHEADER,headers);curl_easy_setopt(curl,CURLOPT_TIMEOUT,20L);
curl_easy_setopt(curl,CURLOPT_WRITEFUNCTION,receive);curl_easy_setopt(curl,CURLOPT_WRITEDATA,&output);
auto result=curl_easy_perform(curl);long status=0;curl_easy_getinfo(curl,CURLINFO_RESPONSE_CODE,&status);
curl_slist_free_all(headers);curl_easy_cleanup(curl);
if(result!=CURLE_OK||status!=200)throw std::runtime_error("Ошибка сервера лицензий");
auto envelope=json::parse(output);std::string payload=envelope.at("signed_payload");
auto signature=decode64(envelope.at("signature"));auto pub=decode64(publicKey);
if(signature.size()!=crypto_sign_BYTES||pub.size()!=crypto_sign_PUBLICKEYBYTES||
crypto_sign_verify_detached(signature.data(),reinterpret_cast<const unsigned char*>(payload.data()),payload.size(),pub.data())!=0)
throw std::runtime_error("Подпись лицензии недействительна");
auto bytes=decode64(payload);auto data=json::parse(bytes);auto now=std::time(nullptr);
if(!data.value("success",false)||data.value("status","")!="active"||data.at("product_code")!=input.at("product_code")||
data.at("device_id")!=input.at("device_id")||data.value("valid_until",0LL)<now||std::llabs(now-data.value("server_time",0LL))>300)
throw std::runtime_error("Лицензия недействительна");
return data;
}
Скачивание и обновления
Ссылки действуют 15 минут. При каждом скачивании сервер повторно проверяет право доступа. После блокировки или возврата оплаты старые ссылки перестают работать. Бесплатный продукт может требовать вход; тогда скачивание доступно через кабинет.
Ограничения частоты
До 120 API-запросов в минуту с IP и до 90 запросов в минуту на приложение с этого IP. Не выполняйте heartbeat каждую секунду. Обрабатывайте сетевые сбои с увеличивающимся интервалом повторов.