CertifiEd

Клиентский SDK (CertifiEd.Client)

Лёгкий .NET-SDK, который встраивается в ваше приложение и берёт на себя весь клиентский протокол: загрузку файла лицензии, оффлайн-проверку подписи Ed25519, активацию, фоновые heartbeat'ы с кэшированием offline-маркера и деактивацию при завершении.

Тарҷумаи тоҷикии ҳуҷҷатҳо дар ҳоли омодашавӣ аст. Матни зерин ба забони русӣ оварда шудааст.

SDK зависит только от формата токена и NSec.Cryptography — он не тянет за собой ни Domain, ни Infrastructure сервера.

Установка

SDK поставляется NuGet-пакетом `CertifiEd.Client` 1.0.0 (лицензия MIT, автор Ofarandagon). Пакет несёт XML-документацию для IntelliSense и symbols-пакет .snupkg; единственная зависимость — NSec.Cryptography.

bash
dotnet add package CertifiEd.Client

ProjectReference

Вариант для моно-репозитория или сборки из исходников:

XML
<ItemGroup>  <ProjectReference Include="path/to/src/CertifiEd.Client/CertifiEd.Client.csproj" /></ItemGroup>

Локальный NuGet-фид

Вариант для поставки SDK клиентам отдельно от кода сервера:

bash
# build the packagedotnet pack src/CertifiEd.Client -c Release -o ./nupkg
# register the local source and installdotnet nuget add source ./nupkg --name certified-localdotnet add package CertifiEd.Client --source certified-local

Публичный API

ЧленНазначение
new CertifiEdLicenseClient(CertifiEdClientOptions options, HttpClient? http = null)Конструктор. Если http не передан, SDK создаёт свой HttpClient с BaseAddress = ServerUrl и владеет им
Task<bool> InitializeAsync(ct)Грузит .ced, оффлайн-проверяет подпись по PublicKey, активируется на сервере (best-effort) и запускает heartbeat-петлю. false — файла нет / подпись невалидна / токен истёк
CertifiEdLicenseStatus Status { get; }Invalid / Active / GracePeriod / Expired / Revoked — выводится из exp токена и маркера
bool HasFeature(string name)Есть ли name в массиве features подписанного cfg
T? GetConfig<T>(string path) where T : structЗначение по dotted-path ("limits.maxSeats") из cfg; null, если пути нет
ValueTask DisposeAsync()Best-effort deactivate и остановка таймера heartbeat

CertifiEdClientOptions

СвойствоОбязательноеНазначение
ServerUrlдаБазовый URL сервера, например https://api.certified.example
LicenseFilePathдаПуть к локальному .ced — конверт или «голый» токен
PublicKeyда«Сырые» 32 байта Ed25519 (Convert.FromHexString(hex))
HwFingerprintнетПереопределяет фингерпринт; по умолчанию HwFingerprint.Get()

Полный пример интеграции

C#
using CertifiEd.Client;
// The public key ships with your product (from the .ced file or GET .../public-key).// The API returns it as hex — unpack it into raw bytes on the client.byte[] publicKey = Convert.FromHexString(    "7bac1e3ce8578ad859bfe001a4f4d72702d00e3d1eebdd735ebdb7145d74f35b");
var options = new CertifiEdClientOptions{    ServerUrl       = "https://api.certified.example",    LicenseFilePath = "/etc/myapp/license.ced",    PublicKey       = publicKey,    // HwFingerprint = "..."   // optional; defaults to HwFingerprint.Get()};
await using var license = new CertifiEdLicenseClient(options);
if (!await license.InitializeAsync()){    Console.Error.WriteLine("License missing, corrupted or expired.");    return 1;}
// Gate on statusswitch (license.Status){    case CertifiEdLicenseStatus.Active:        break;                                   // all good
    case CertifiEdLicenseStatus.GracePeriod:        Console.WriteLine("License is in the grace period — restore connectivity.");        break;                                   // keep running, but warn
    case CertifiEdLicenseStatus.Expired:    case CertifiEdLicenseStatus.Revoked:    case CertifiEdLicenseStatus.Invalid:        Console.Error.WriteLine($"Access denied: {license.Status}.");        return 1;                                // block}
// Gate on features and limitsif (license.HasFeature("export"))    EnableExport();
int maxSeats = license.GetConfig<int>("limits.maxSeats") ?? 1;   // fallback defaultEnforceSeatLimit(maxSeats);
RunApplication();return 0;// await using calls DisposeAsync -> deactivate + stop the heartbeat timer.

GetConfig<T> работает с where T : structint, long, bool, double, DateTimeOffset и т. п. Для строк и массивов читайте cfg напрямую либо используйте HasFeature.

Что делает InitializeAsync

  1. Читает файл (.ced-конверт или «голый» токен) и парсит три сегмента.
  2. Оффлайн проверяет подпись Ed25519 по PublicKey. Провал → токен сбрасывается, метод возвращает false.
  3. Проверяет exp: истёкший токен → false.
  4. Загружает закэшированный маркер <license>.ced.hb, если он есть.
  5. Best-effort активация POST /api/v1/client/activate: при успехе сохраняет activationId, применяет свежий маркер и запускает таймер heartbeat.

Оффлайн-поведение

  • Пока приложение периодически берёт свежий маркер — статус Active.
  • Пропала сеть: маркер держит Active до своего exp (= момент выдачи + maxOfflineDays), затем 24 часа GracePeriod, затем Expired.
  • Рестарт без сети: маркер читается с диска, лицензия остаётся валидной до exp маркера.
  • Жёсткий exp токена перекрывает всё: после него статус всегда Expired, даже онлайн.

Подробности вычисления статуса — в разделе Протокол лицензий.

Обработка Expired и Revoked

  • Expired — срок вышел (token.exp прошёл) либо маркер и grace истекли без связи. Обычно требует перевыпуска лицензии оператором.
  • Revoked — лицензия отозвана на сервере. SDK узнаёт об этом косвенно: сервер отклоняет активацию и heartbeat (license.not_active), свежие маркеры перестают приходить, и по истечении окна оффлайна статус деградирует до Expired.

Практика: гейтите функциональность по Status == Active || Status == GracePeriod, в GracePeriod показывайте предупреждение, на Expired / Revoked / Invalid — блокируйте защищённые возможности.

Привязка к оборудованию

SDK всегда отправляет hwFingerprint в POST /api/v1/client/activate. По умолчанию это HwFingerprint.Get() — стабильный SHA-256 по имени машины, описанию ОС, первому стабильному MAC-адресу и platform machine id, вычисляется один раз за процесс. Своё значение (например, стабильный per-tenant идентификатор) задаётся через CertifiEdClientOptions.HwFingerprint.

Режим лицензииЧто видит интегратор
noneОтпечаток пишется в активацию, но ничего не ограничивает
fixedАктивация проходит только на машине с заданным отпечатком
firstActivationПервый запуск закрепляет машину; дальше — как fixed

Полное описание режимов и перепривязки — в разделе Привязка к железу.

Что видит клиент при mismatch

Активация возвращает 400:

JSON
{  "code": "activation.fingerprint_mismatch",  "message": "License is bound to different hardware.",  "status": 400}

SDK не роняет InitializeAsync из-за этого: активация — best-effort, а решение о доступе принимается по Status, который выводится из локального токена и маркера. Практический эффект такой:

  • Новые offline-маркеры перестают приходить — heartbeat невозможен без activationId.
  • Пока жив последний маркер, статус остаётся Active, затем GracePeriod, затем Expired.
  • На «чистой» машине, где маркера не было, отсчёт идёт от последней локальной валидации и лицензия деградирует через тот же maxOfflineDays.

Замена машины

Отпечаток на стороне клиента не «чинится» — перепривязка делается на платформе:

  1. Оператор или Admin компании вызывает POST /api/v1/panel/licenses/{id}/rebind с новым hwFingerprint (или null, чтобы лицензия закрепилась на следующей машине сама).
  2. Платформа деактивирует старые активации и переподписывает лицензию новой версией.
  3. Заказчик заново скачивает `.ced` (GET /api/v1/panel/licenses/{id}/download или портал) и кладёт файл по пути LicenseFilePath.
  4. Старый кэш маркера <license>.ced.hb можно удалить — новый придёт с первой успешной активацией.

Публичный ключ при этом не меняется (если не было ротации ключа шаблона), так что зашитый в приложение PublicKey остаётся валидным.

Жизненный цикл и Dispose

  • Вызывайте DisposeAsync ровно один раз — через await using или явно. SDK владеет HttpClient, который создал сам, и закрывает его; повторный вызов обратится к уже закрытому клиенту.
  • Если вы передали свой HttpClient в конструктор — SDK его не закрывает, управляйте им сами.
  • DisposeAsync шлёт best-effort deactivate; недоступность сервера не мешает корректному завершению.