Keygen.Net 0.3.0

dotnet add package Keygen.Net --version 0.3.0
                    
NuGet\Install-Package Keygen.Net -Version 0.3.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Keygen.Net" Version="0.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Keygen.Net" Version="0.3.0" />
                    
Directory.Packages.props
<PackageReference Include="Keygen.Net" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Keygen.Net --version 0.3.0
                    
#r "nuget: Keygen.Net, 0.3.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Keygen.Net@0.3.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Keygen.Net&version=0.3.0
                    
Install as a Cake Addin
#tool nuget:?package=Keygen.Net&version=0.3.0
                    
Install as a Cake Tool

Keygen.Net

build

dotnet add package Keygen.Net

A .NET client for the Keygen software licensing API: license validation and lifecycle, machine activation and heartbeats, certificate checkout, the Distribution API for releases and artifacts, CRUD and paging over the rest of the account, offline verification of cryptographic license and machine files, and a client for Keygen Relay.

MIT licensed, by Santiago Saavedra info@ssaavedra.eu.

This is a community-supported SDK. It is not affiliated with, endorsed by, or supported by Keygen LLC. Keygen's only official SDK is keygen-go; everything else, including this, is community work. There is no .NET client at all — this exists to fill that gap.

We would be glad to see it become the official one. If Keygen would like to adopt, fork or take over maintenance of this package, that is welcome and no strings are attached: open a PR or an issue here and we will work out the handover, including transferring the NuGet package owner and this repository. Until then it is maintained on a best-effort basis by its contributors.

No dependencies on any target. The cryptography is entirely System.Security.Cryptography.

Two assets ship:

Target Contents
net8.0 Portable. No Windows APIs, nothing platform-specific.
net8.0-windows Adds Fingerprint.WindowsMachineGuid(), which reads the registry.

Registry types come from the Windows targeting pack, so the Windows asset needs no package reference either. Target net8.0 and you download nothing extra and reference nothing Windows-specific; target net8.0-windows and you get the machine GUID as well.

Verifying a license file offline

This is the part that matters: it needs no network, so a licensing outage can never reach your users.

using Keygen;

var verifier = new KeygenVerifier(
    publicKey: MyAccountPublicKeyPem,
    expectedAlgorithm: new KeygenAlgorithm(PayloadEncoding.Base64, SignatureScheme.EcdsaP256));

var file = verifier.VerifyLicenseFile(certificate);   // throws on anything untrustworthy

using var payload = file.ParsePayload();
var status = payload.RootElement
    .GetProperty("data").GetProperty("attributes").GetProperty("status").GetString();

Encrypted files need the license key, and machine files additionally need the fingerprint:

var file    = verifier.VerifyLicenseFile(certificate, licenseKey);
var machine = verifier.VerifyMachineFile(certificate, licenseKey, fingerprint);

Choose your algorithm, and say so

expectedAlgorithm is asserted against every certificate. This is deliberate: trusting the alg field inside the file would let an attacker downgrade you to whichever scheme they can forge. Set it to whatever your Keygen policy's scheme is, and never derive it from input.

Ed25519

Keygen signs with Ed25519 when a policy sets no scheme, and .NET has no Ed25519 primitive. Either:

  • set the policy's scheme to ECDSA P-256, which this library verifies with no extra dependency — recommended; or
  • plug in an implementation:
KeygenVerifier.Ed25519Verifier = (publicKey, data, signature) =>
    /* libsodium, BouncyCastle, … */;

What the exceptions mean

Exception Meaning Reasonable response
KeygenSignatureException Bad signature, or the algorithm was not the one demanded Treat as hostile. Alarm.
KeygenFileExpiredException The certificate's TTL has elapsed Routine. Check out a fresh one.
KeygenDecryptionException Wrong license key or machine fingerprint Ask the user to re-check
KeygenFormatException Not a well-formed certificate Reject the input

They are distinct so an expired snapshot never looks like an attack, and an attack never looks routine.

Talking to the API

KeygenClient covers the common licensing calls directly, and every collection hangs off it: client.Licenses, client.Machines, client.Releases, client.Users, and so on.

using var http = new HttpClient();

var client = new KeygenClient(http, accountId, KeygenCredentials.Token(token))
{
    ApiVersion = "1.8",   // optional: pin the API version, as keygen-go does
};

var validation = await client.ValidateKeyAsync(licenseKey, fingerprint);
if (!validation.Valid) logger.LogWarning("Licence refused: {Code}", validation.Code);

A refused operation that is a normal outcome comes back as a value, not an exception — a validation that fails, a machine that was already gone, an upgrade that is not available. Only faults throw, and KeygenApiException.Code carries Keygen's reason code.

Leaving ApiVersion unset keeps 0.2.0's behaviour: Keygen then applies whichever API version your account is pinned to. Set it and you find out about a Keygen version change by reading a changelog rather than by watching a response shape move.

Licences

await client.Licenses.SuspendAsync(licenseId);      // fails every validation until reinstated
await client.Licenses.ReinstateAsync(licenseId);
await client.Licenses.RenewAsync(licenseId);        // extends by the policy's duration
await client.Licenses.CheckInAsync(licenseId);      // resets the requireCheckIn clock
await client.Licenses.RevokeAsync(licenseId);       // deletes it, and its machines. Not undoable.

await client.Licenses.IncrementUsageAsync(licenseId, increment: 25);
await client.Licenses.ResetUsageAsync(licenseId);

Suspension is the reversible one. Revoke when the licence should cease to exist, and suspend for anything you might want back.

Validation is worth scoping. An unscoped validation only asks whether the key exists and is live, and will answer yes on a thousand machines at once:

var validation = await client.Licenses.ValidateKeyAsync(
    licenseKey,
    new KeygenScope
    {
        Fingerprint  = fingerprint,
        Product      = productId,
        Entitlements = ["REPORTING"],
        Version      = "1.4.2",
    });

Asserting an entitlement in the scope is better than reading the codes back and checking them yourself: the check happens server-side, inside the signed answer.

Distribution

var upgrade = await client.Releases.CheckUpgradeAsync(currentVersion, channel: "stable");

if (upgrade.Available)
{
    var download = await client.Artifacts.GetDownloadAsync($"app-{upgrade.NextVersion}.tar.gz");

    using var stream = await client.Artifacts.OpenReadStreamAsync($"app-{upgrade.NextVersion}.tar.gz");
    // ...verify download.Artifact.String("checksum") over the bytes before running them
}

Being on the latest version is the steady state of every deployed copy of an application, so it is upgrade.Available == false rather than an exception, even though Keygen answers it with a 404.

Downloads are the part where a plausible implementation is wrong. Keygen answers an artifact request with a 303 to pre-signed storage, and an HttpClient following that redirect replays your Authorization header to a third-party origin — which both leaks the credential and gets rejected. This client sends Prefer: no-redirect and resolves the URL from the response, so it is correct whatever your HttpClient is configured to do, and it fetches the bytes with no Keygen credential attached.

An artifact whose file has not been uploaded, or whose release is not published, comes back with IsAvailable false rather than as an error. Keygen signals that with a 200 and no redirect, which is the opposite of the intuitive reading.

Publishing has the same hazard the other way round. Artifacts.CreateForUploadAsync returns the URL to PUT the bytes to, again without letting Keygen redirect — an upload answers with a 307, and a 307 is the redirect an HTTP client replays verbatim, credential and all.

Everything else

Machines, Processes, Components, Releases, Artifacts, Packages, Channels, Engines, Platforms, Arches, Users, Groups, Entitlements, Policies, Products and Tokens all expose ListAsync, ListPageAsync, GetAsync and FindAsync, and the writable ones add CreateAsync, UpdateAsync and DeleteAsync over Keygen's own attribute names:

var entitlement = await client.Entitlements.CreateAsync(
    new JsonObject { ["name"] = "Reporting", ["code"] = "REPORTING" });

GetAsync throws on a missing resource and FindAsync returns null for one, so absence is whichever of the two the calling code actually means. DeleteAsync returns false rather than throwing when the resource was already gone.

Listing and paging

ListAsync streams the whole collection, following the links Keygen returns until they run out:

await foreach (var license in client.Licenses.ListAsync(
    new KeygenQuery { { "status", "ACTIVE" } }.WithPageSize(100)))
{
    // ...
}

It is lazy: abandoning the enumeration stops the requests. Because the walk follows links.next rather than counting pages itself, it works under cursor paging and offset paging alike.

Keygen's page size defaults to 10, so set one for anything large. WithPageSize starts a cursor walk, which is Keygen's recommended strategy — offset paging is deprecated there, capped at 100 pages, and can duplicate or skip records written while the walk is in progress. WithPageNumber is available if you need it anyway.

When the page boundary itself matters — a paged table, or resuming from a stored cursor — ListPageAsync returns one page with its links and counts, and GetPageAsync follows a NextLink you saved.

Fingerprinting

var fingerprint = await Fingerprint.KubernetesClusterUidAsync(httpClient)   // clustered installs
                  ?? Fingerprint.MachineId();

string?[] components =
[
    Fingerprint.MachineId(),
    Fingerprint.PrimaryMacAddress(),
    Fingerprint.BoardSerial(),
];

Every collector returns null rather than throwing when an identifier cannot be read — a container with no DMI access, a host with no active NIC. Absent components are expected and fine: pair this with Keygen's MATCH_MOST component matching strategy so that replacing a NIC or migrating a VM does not invalidate a license, while cloning a whole install still fails to match.

On the portable net8.0 asset, MachineId() returns null when running on Windows — the machine GUID needs registry access. If your application runs on Windows and wants that component, target net8.0-windows:

var guid = Fingerprint.WindowsMachineGuid();   // net8.0-windows asset only

Hash before sending. A fingerprint should identify a host, not inventory it:

components.Where(c => c is not null).Select(Fingerprint.Hash!);

KubernetesClusterUidAsync needs the pod's service account to hold get on namespaces; without that RBAC it returns null.

Keygen Relay

Relay is a separate API, not a variant of the main one: no JSON:API envelope, no account in the path, and a lease model instead of machine activation. It is what makes air-gapped sites work.

It stays a separate client here because that is how Keygen structures it. keygen-go, the reference SDK, contains no reference to Relay anywhere in its history, and the dependency runs the other way: keygen-relay depends on keygen-go for license-file decryption, and its README shows applications talking to it with plain curl. Folding Relay into KeygenClient would mean inventing a shape Keygen does not have.

var relay = new RelayClient(httpClient, new Uri("http://relay.plant.local:6349"));

var lease = await relay.ClaimAsync(fingerprint);   // PUT /v1/nodes/{fingerprint}
// lease.Certificate, lease.LicenseKey, lease.ExpiresAt, lease.Extended

await relay.ReleaseAsync(fingerprint);             // DELETE, returns the licence to the pool

Re-claiming extends the lease when the server has heartbeats enabled; lease.Extended reports whether Relay renewed an existing lease (202) or granted a new one (201). Two refusals are worth telling apart, and have their own exception types: RelayPoolExhaustedException (410, no licence left) and RelayLeaseHeldException (409, this node holds one and heartbeats are off).

Verify the leased certificate exactly as any other — Relay distributes certificates, it does not vouch for them.

keygen-cli

A small tool for support calls and for trying the library without a Keygen instance.

dotnet run --project tools/keygen-cli -- fingerprint
dotnet run --project tools/keygen-cli -- demo-cert --out /tmp/demo
dotnet run --project tools/keygen-cli -- verify --cert /tmp/demo/demo.lic --pubkey /tmp/demo/demo-public-key.pem

fingerprint prints this host's components, raw and hashed, and says how many are readable — useful for judging whether MATCH_MOST has enough to work with before issuing anything.

verify works against real Keygen certificates and exits 0 valid, 2 stale, 3 bad signature, 4 cannot decrypt, 5 malformed — so it drops into a health check or a support script without parsing output.

demo-cert writes a throwaway certificate signed by an ephemeral key. It is not an issuer: minting real licences belongs in Keygen, and a second signing implementation is the thing that drifts.

Licence

MIT. See LICENSE.

Contributing

Issues and pull requests are welcome, from users and from Keygen alike — see the note at the top about adoption.

Releases publish to NuGet through GitHub Actions using NuGet Trusted Publishing (OIDC), so no long-lived API key is stored in this repository. Publishing runs on a published GitHub release.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net8.0-windows7.0 is compatible.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net8.0

    • No dependencies.
  • net8.0-windows7.0

    • No dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Keygen.Net:

Package Downloads
Keygen.Net.Ed25519

Ed25519 signature verification for Keygen.Net. .NET has no Ed25519 primitive, and Ed25519 is Keygen's default signing scheme, so this adds it through BouncyCastle. Kept in a separate package so applications using ECDSA P-256 or RSA policies take no dependency at all.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.3.0 126 8/19/2026
0.2.0 116 8/19/2026