Keygen.Net.Ed25519
0.3.0
dotnet add package Keygen.Net.Ed25519 --version 0.3.0
NuGet\Install-Package Keygen.Net.Ed25519 -Version 0.3.0
<PackageReference Include="Keygen.Net.Ed25519" Version="0.3.0" />
<PackageVersion Include="Keygen.Net.Ed25519" Version="0.3.0" />
<PackageReference Include="Keygen.Net.Ed25519" />
paket add Keygen.Net.Ed25519 --version 0.3.0
#r "nuget: Keygen.Net.Ed25519, 0.3.0"
#:package Keygen.Net.Ed25519@0.3.0
#addin nuget:?package=Keygen.Net.Ed25519&version=0.3.0
#tool nuget:?package=Keygen.Net.Ed25519&version=0.3.0
Keygen.Net
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 | Versions 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. 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. |
-
net8.0
- BouncyCastle.Cryptography (>= 2.4.0)
- Keygen.Net (>= 0.3.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.