LicenseManager.LicensingClient 1.0.5
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
- Передать в License Manager файл
license/client_lm_payload.hasp.binс шага 1. - Выпустить лицензию — получить
.lic. - В продукт положить:
{
"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.
.NET 9.0
- BouncyCastle.Cryptography (>= 2.6.1)
- Microsoft.Extensions.Configuration (>= 9.0.0)
- Microsoft.Extensions.Configuration.Json (>= 9.0.0)
- Microsoft.Extensions.DependencyInjection (>= 9.0.0)
- Microsoft.Extensions.Hosting (>= 9.0.0)
- Microsoft.Extensions.Logging (>= 9.0.0)
- Microsoft.Extensions.Logging.Console (>= 9.0.0)