LicenseManager.LicensingClient 1.0.6

LicenseManager.LicensingClient

Клиентская библиотека лицензирования Sentinel HASP (.NET 9). Один NuGet-пакет: LicensingClient + LicensingCore + зависимости и нативы Sentinel.

Жизненный цикл: прогрузка якоря → выпуск .lic в LM → чтение лицензии в продукте.

Требования: .NET 9, физический HASP (Hasp:Runtime = Local). Managed DLL и нативы Sentinel приходят вместе с NuGet-пакетом LicenseManager.LicensingClient — отдельно ничего копировать не нужно. Hasp:NativeDirectory — только опциональный override пути к нативам.


Шаг 1. Прогрузка якоря

На ключ пишется якорь RW v1 (дата старта + AES_C). Параллельно создаются файлы:

Путь Назначение
<out>/license/client_lm_payload.hasp.bin Отдать в LM (License Manager)
<out>/data/public_rsa_der.aesc.bin Положить в продукт (Licensing:WrappedPublicRsaPath)

Способ A — HaspManager

HaspManager                          # список ключей → показать якорь
HaspManager show --id <hasp_id>
HaspManager first-setup --id <hasp_id> [--out <корень>]

Force. Повторная прогрузка по уже записанному якорю допустима только если рядом с HaspManager лежит LicensingServer.dll:

HaspManager first-setup --id <hasp_id> --out <корень> --force --password <секрет>
HaspManager calchash <пароль>   # hex SHA-256 для константы в LicensingServer

Без LicensingServer.dll --force недоступен. Обычный first-setup плагин не требует.


Способ B — своя программа (пакет Client)

Лучше явно записать ключ по его haspid, поэтому рекомендуемый способ это запуск программы с аргументами командной строки.

Список подключённых HASP ID (CLI)

Перед first-setup удобно показать все hasp_id, чтобы пользователь скопировал нужный в --id.
(MyProductSecrets и appsettings.json — см. следующий раздел.)

Пример вызова:

MyApp list-hasp

В Program.cs / Main — ветка по аргументу (имя команды на ваш выбор: list-hasp, list-keys и т.п.):

Без DI:

if (args.Length > 0 && string.Equals(args[0], "list-hasp", StringComparison.OrdinalIgnoreCase))
{
    ILicensingSecrets secrets = new MyProductSecrets();
    IConfiguration configuration = new ConfigurationBuilder()
        .SetBasePath(AppContext.BaseDirectory)
        .AddJsonFile("appsettings.json", optional: false)
        .Build();

    var manager = new LocalHaspKeyManager(secrets, configuration);
    var ids = await manager.ListHaspIdsAsync();

    if (ids.Count == 0)
    {
        Console.WriteLine("No HASP keys found.");
        return 0;
    }

    Console.WriteLine("Connected HASP keys:");
    foreach (var id in ids)
        Console.WriteLine(id);

    return 0;
}

С DI:

if (args.Length > 0 && string.Equals(args[0], "list-hasp", StringComparison.OrdinalIgnoreCase))
{
    var configuration = new ConfigurationBuilder()
        .SetBasePath(AppContext.BaseDirectory)
        .AddJsonFile("appsettings.json", optional: false)
        .Build();

    var services = new ServiceCollection();
    services.AddHaspRuntime(configuration); // или AddHaspFirstRun — тоже регистрирует IHaspKeyManager
    services.AddSingleton<ILicensingSecrets, MyProductSecrets>();

    await using var sp = services.BuildServiceProvider();
    var ids = await sp.GetRequiredService<IHaspKeyManager>().ListHaspIdsAsync();

    if (ids.Count == 0)
    {
        Console.WriteLine("No HASP keys found.");
        return 0;
    }

    Console.WriteLine("Connected HASP keys:");
    foreach (var id in ids)
        Console.WriteLine(id);

    return 0;
}

Дальше пользователь запускает first-setup --id <выбранный_hasp_id>.


Секреты продукта

В хост-приложении объявите ILicensingSecrets: свой ProductCode, vendor code можно взять из вшитого EmbeddedLicensingSecrets.

using LicensingClient.Secrets;

sealed class MyProductSecrets : ILicensingSecrets
{
    private const string ProductCodeValue = "my_product_code";
    private static readonly EmbeddedLicensingSecrets Embedded = new();

    public string ProductCode
    {
        get
        {
            var code = ProductCodeValue.Trim();
            if (string.IsNullOrEmpty(code))
                throw new InvalidOperationException("ProductCode is not set.");
            return code;
        }
    }

    // Vendor code уже внутри LicensingClient (VendorCodeFragments).
    public string VendorCode => Embedded.VendorCode;
}

В лицензируемом приложении force / LicensingServer не используются (это только для HaspManager). При ручной сборке без DI передайте deny-авторизатор:

using LicensingCore.FirstRun;

sealed class DenyForceFirstRunAuthorizer : IForceFirstRunAuthorizer
{
    public bool CanUseForceMode => false;
    public void Authorize(string? password) =>
        throw new InvalidOperationException("Force is not available in the product application.");
}

appsettings.json рядом с exe:

{ "Hasp": { "Runtime": "Local" } }

B1. Рекомендуемый, без DI

CLI: MyApp first-setup --id <hasp_id> [--out <корень>]. Объекты создаёте вручную.

using LicensingClient.Hasp;
using LicensingClient.Secrets;
using LicensingClient.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

// haspId, outRoot — из argv
ILicensingSecrets secrets = new MyProductSecrets();
IConfiguration configuration = new ConfigurationBuilder()
    .SetBasePath(AppContext.BaseDirectory)
    .AddJsonFile("appsettings.json", optional: false)
    .Build();

using var loggerFactory = LoggerFactory.Create(b => b.AddConsole());
using var hasp = new LocalSentinelHaspRuntime(
    secrets,
    configuration,
    loggerFactory.CreateLogger<LocalSentinelHaspRuntime>());

var firstLaunch = new LocalHaspFirstLaunchService(hasp, new DenyForceFirstRunAuthorizer());

await firstLaunch.ExecuteAsync(
    outRoot,
    new FirstRunExecutionOptions(ExpectedHaspId: haspId));

Если --id не найден среди подключённых ключей — ошибка с перечислением доступных id.


B2. Рекомендуемый, с DI

Тот же CLI (--id / --out), но регистрация через AddHaspFirstRun.

using LicensingClient.Extensions;
using LicensingClient.Secrets;
using LicensingClient.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

// haspId, outRoot — из argv
var configuration = new ConfigurationBuilder()
    .SetBasePath(AppContext.BaseDirectory)
    .AddJsonFile("appsettings.json", optional: false)
    .Build();

var services = new ServiceCollection();
services.AddLogging(b => b.AddConsole());
services.AddHaspFirstRun(configuration);
services.AddSingleton<ILicensingSecrets, MyProductSecrets>();
// Force в лицензируемом приложении не подключается — только в HaspManager.

await using var sp = services.BuildServiceProvider();
await sp.GetRequiredService<IFirstLaunchService>().ExecuteAsync(
    outRoot,
    new FirstRunExecutionOptions(ExpectedHaspId: haspId));

B3. Резервный, без DI

Когда CLI недоступен: при старте читаем якорь; если его нет — пишем. Без force.

ILicensingSecrets secrets = new MyProductSecrets();
IConfiguration configuration = new ConfigurationBuilder()
    .SetBasePath(AppContext.BaseDirectory)
    .AddJsonFile("appsettings.json", optional: false)
    .Build();

using var loggerFactory = LoggerFactory.Create(b => b.AddConsole());
using var hasp = new LocalSentinelHaspRuntime(
    secrets,
    configuration,
    loggerFactory.CreateLogger<LocalSentinelHaspRuntime>());

var firstLaunch = new LocalHaspFirstLaunchService(hasp, new DenyForceFirstRunAuthorizer());

var hasAnchor = false;
try
{
    _ = await hasp.ReadRwV1AnchorAsync();
    hasAnchor = true;
}
catch (InvalidOperationException)
{
    // якоря v1 нет
}

if (!hasAnchor)
{
    await firstLaunch.ExecuteAsync(
        AppContext.BaseDirectory,
        new FirstRunExecutionOptions()); // без ForceReinitialize, без ExpectedHaspId
}

B4. Резервный, с DI

Тот же сценарий «проверить и при необходимости записать», через контейнер.

var configuration = new ConfigurationBuilder()
    .SetBasePath(AppContext.BaseDirectory)
    .AddJsonFile("appsettings.json", optional: false)
    .Build();

var services = new ServiceCollection();
services.AddLogging(b => b.AddConsole());
services.AddHaspFirstRun(configuration);
services.AddSingleton<ILicensingSecrets, MyProductSecrets>();

await using var sp = services.BuildServiceProvider();
var hasp = sp.GetRequiredService<ISentinelHaspRuntime>();
var firstLaunch = sp.GetRequiredService<IFirstLaunchService>();

var hasAnchor = false;
try
{
    _ = await hasp.ReadRwV1AnchorAsync();
    hasAnchor = true;
}
catch (InvalidOperationException)
{
    // якоря v1 нет
}

if (!hasAnchor)
{
    await firstLaunch.ExecuteAsync(
        AppContext.BaseDirectory,
        new FirstRunExecutionOptions());
}

Шаг 2. Формирование файла лицензии через LM

  1. Передать в License Manager файл license/client_lm_payload.hasp.bin с шага 1.
  2. Выпустить лицензию — получить .lic.
  3. В продукт положить:
{
  "Hasp": { "Runtime": "Local" },
  "Licensing": {
    "LicenseFilePath": "licenses/product.lic",
    "WrappedPublicRsaPath": "data/public_rsa_der.aesc.bin",
    "ClientCode": "<client_id из лицензии>"
  }
}

ProductCode из MyProductSecrets должен совпасть с полем в .lic.

Один HASP — несколько продуктов

Если у клиента уже используется HASP (якорь уже прогружен, hasp_id известен), на тот же ключ можно выпускать лицензии для других продуктов и пользоваться одним HASP одновременно в нескольких приложениях.

  • Якорь на ключе один (один first-setup). Для нового продукта не делайте повторный first-setup / --force — это перезапишет AES_C и сломает уже выданные лицензии.
  • В LM на тот же hasp_id формируете новую .lic с другим product_code (и своим client_id / параметрами).
  • В каждом продукте: свой ProductCode в ILicensingSecrets, свой файл .lic и свой ClientCode; файл public_rsa_der.aesc.bin и физический ключ — общие (из исходного first-setup).

Проверка в каждом приложении смотрит свой .lic и свой ProductCode — продукты не мешают друг другу на одном HASP.


Шаг 3. Программное чтение лицензии

Те же MyProductSecrets и секция Licensing из шага 2.

Разделение простое:

Где Что делать
**Program.cs (старт приложения)** Собрать зависимости, зарегистрировать сервисы, сохранить ссылку на verifier / snapshot
Позже (кнопка, меню, перед фичей) Вызвать проверку и прочитать параметры

Срок действия при наличии datetime_end_utc проверяется внутри вызова verify автоматически. Отдельно дату окончания в продукте считать не нужно.


Вариант A1 — DI, проверка вручную (по кнопке)

В Program.cs — только подготовка

var configuration = new ConfigurationBuilder()
    .SetBasePath(AppContext.BaseDirectory)
    .AddJsonFile("appsettings.json", optional: false)
    .Build();

var services = new ServiceCollection();
services.AddLogging(b => b.AddConsole());
services.AddLicenseVerification(configuration);
services.AddSingleton<ILicensingSecrets, MyProductSecrets>();

// сохраните provider на жизнь приложения (поле/статик/DI-контейнер хоста)
_serviceProvider = services.BuildServiceProvider();

// Здесь Verify вызывать НЕ обязательно — можно отложить до кнопки.

Позже — например обработчик кнопки «Проверить лицензию»

async void OnCheckLicenseClick(object sender, EventArgs e)
{
    var verifier = _serviceProvider.GetRequiredService<IManualLicenseVerificationService>();
    var report = await verifier.VerifyWithReportAsync();

    if (!report.IsValid)
    {
        // DEMO / отказано
        return;
    }

    // параметры из LM (поле params в лицензии)
    if (report.Parameters.TryGetValue("max_users", out var maxUsersText)
        && int.TryParse(maxUsersText, out var maxUsers))
    {
        // применить к продукту
    }

    foreach (var (key, value) in report.Parameters)
    {
        // все параметры
    }
}

Вариант A2 — DI, фоновый сервис (hosted)

Фон сам перепроверяет лицензию (старт / периодически / shutdown). В UI вы только читаете уже готовый снимок.

В Program.cs — регистрация + старт host

var builder = Host.CreateApplicationBuilder(args);
// подтянуть appsettings с секцией Licensing...

builder.Services.AddLicenseVerification(builder.Configuration);
builder.Services.AddSingleton<ILicensingSecrets, MyProductSecrets>();

var policy = ProductLicenseVerificationPolicy.CreateDefaultFromEmbedded();
builder.Services.Replace(ServiceDescriptor.Singleton<ILicenseVerificationPolicy>(policy));

_host = builder.Build();
await _host.StartAsync(); // тут же стартует LicenseReverificationHostedService

// ссылку на snapshot можно сохранить один раз
_snapshot = _host.Services.GetRequiredService<ILicenseRuntimeSnapshot>();

Позже — на кнопке / перед фичей (verify вручную не нужен)

void OnUseLicensedFeatureClick(object sender, EventArgs e)
{
    if (!_snapshot.IsLicenseValid || _snapshot.Payload is null)
    {
        // DEMO / отказано
        return;
    }

    var parameters = _snapshot.Payload.parameters;
    if (parameters.TryGetValue("feature_x", out var featureX))
    {
        // применить к продукту
    }
}

Если нужна принудительная перепроверка по кнопке — дополнительно:

await _host.Services.GetRequiredService<ILicenseVerificationService>().VerifyAsync();
// после этого снова читайте _snapshot

Вариант B — без DI

В Program.cs — создать verifier один раз и держать ссылку

ILicensingSecrets secrets = new MyProductSecrets();
IConfiguration configuration = new ConfigurationBuilder()
    .SetBasePath(AppContext.BaseDirectory)
    .AddJsonFile("appsettings.json", optional: false)
    .Build();

var licensingOptions = Options.Create(new LicensingOptions
{
    LicenseFilePath = "licenses/product.lic",
    WrappedPublicRsaPath = "data/public_rsa_der.aesc.bin",
    ClientCode = "<client_id>"
});

ILicenseVerificationPolicy policy = ProductLicenseVerificationPolicy.CreateDefaultFromEmbedded();
var snapshot = new LicenseRuntimeSnapshot();

_loggerFactory = LoggerFactory.Create(b => b.AddConsole());
_hasp = new LocalSentinelHaspRuntime(
    secrets,
    configuration,
    _loggerFactory.CreateLogger<LocalSentinelHaspRuntime>());

_verifier = new LicenseVerificationService(
    _hasp,
    secrets,
    licensingOptions,
    policy,
    snapshot,
    _loggerFactory.CreateLogger<LicenseVerificationService>());

// Verify здесь вызывать НЕ обязательно — можно отложить до кнопки.

Позже — на кнопке

async void OnCheckLicenseClick(object sender, EventArgs e)
{
    var report = await ((IManualLicenseVerificationService)_verifier)
        .VerifyWithReportAsync();

    if (!report.IsValid)
    {
        // DEMO / отказано
        return;
    }

    if (report.Parameters.TryGetValue("max_users", out var maxUsersText)
        && int.TryParse(maxUsersText, out var maxUsers))
    {
        // применить к продукту
    }
}

При выходе из приложения освободите _hasp и _loggerFactory (Dispose).


Срок действия (напоминание)

Внутри каждого Verify / VerifyWithReportAsync / фоновой перепроверки:

  • есть datetime_end_utc — срок сверяется сам; при истечении IsValid = false;
  • нет datetime_end_utc — бессрочная лицензия;
  • также проверяются старт и anti-rollback по якорю HASP.

Краткая схема

1. Якорь:
   A) HaspManager
   B) своя программа:
      list-hasp → список hasp_id
      B1/B2 first-setup --id …
      B3/B4 резерв при старте
2. LM: client_lm_payload.hasp.bin → .lic
3. Продукт:
   Program.cs → собрать verifier/host/snapshot
   кнопка/фича → Verify (A1/B) или читать snapshot (A2) → parameters

No packages depend on LicenseManager.LicensingClient.

Version Downloads Last updated
1.0.6 3 16.07.2026
1.0.5 3 16.07.2026
1.0.4 4 15.07.2026
1.0.3 0 15.07.2026
1.0.2 0 15.07.2026