Validating a licence
You downloaded DynDyn.Licensing.dll alongside the product. This page is everything you need to
check a licence at startup, react to its state, bind it to a machine, and handle revocation — without
reading any other file in this repo. No knowledge of the portal's internals is assumed.
1. What a licence key is
A licence key is an opaque string your customer account received from the Licences page of the
portal (/portal/licences). It is shown to you exactly once, at generation or re-issue time —
the portal only stores its hash, never the key itself, so if it's lost the only fix is to re-issue
(which invalidates the old key).
Structurally, the key is two base64url segments joined by a .: a JSON payload and an ECDSA
signature over it. You never need to parse this by hand — DynDyn.Licensing does it for you. Treat
the key as a secret (an environment variable or a protected config value, not source control).
Alongside the key, the portal also shows a licence id (a GUID) — a stable identifier for the licence that stays the same across re-issues. You don't need it for the checks in this doc, but you'll see it in support conversations and in the activation flow (§4).
2. Minimal startup check
The whole point of DynDyn.Licensing is that this check runs entirely offline, in-process. No
network call is required to validate a licence — the signature is checked against a public key you
embed in your app, and the private key that produced it never leaves the portal.
using DynDyn.Licensing;
// The public key(s) the portal told you to trust — ship this in your app's config or embed it as a
// constant. It is a PUBLIC key: there is nothing to protect by keeping it secret.
using var trustedKeys = TrustedKeys.FromPem(new Dictionary<string, string>
{
["portal-v1"] = "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
});
var verifier = new LicenseVerifier(trustedKeys);
VerificationResult verification = verifier.Verify(storedLicenseKey);
if (verification.Status != VerificationStatus.Valid)
{
// Malformed / UnknownKey / BadSignature — treat exactly like Community below (see §6).
RunAsCommunityEdition();
return;
}
PolicyResult policy = LicensePolicy.Evaluate(
verification.Payload!,
now: DateTimeOffset.UtcNow,
productVersion: ThisAppsVersion, // e.g. "2.3.0" — checked against the licence's min/max
fingerprint: null, // see §4 — pass the collected fingerprint once you have one
revokedIds: null); // see §5 — pass the parsed revocation list once you have one
switch (policy.State)
{
case LicenseState.Licensed:
RunAsLicensed(verification.Payload!.Edition, verification.Payload.Features);
break;
case LicenseState.Grace:
RunAsLicensed(verification.Payload!.Edition, verification.Payload.Features); // full features — see §3
WarnUser(policy.Reason);
break;
case LicenseState.Community:
RunAsCommunityEdition();
break;
}
Verify never throws — a corrupt or tampered key comes back as a non-Valid status, not an
exception. LicensePolicy.Evaluate is a pure function: same inputs, same output, no I/O. Run this
check once at startup and cache the result for the process lifetime (or re-run it periodically if
your app is long-lived and you want expiry/grace to update without a restart).
3. What each LicenseState means
| State | What your app should do |
|---|---|
Licensed |
Full features, no warnings. |
Grace |
Full features. This is not a degraded mode — it exists specifically so a customer whose licence just expired, or whose machine fingerprint drifted (new NIC, OS reinstall, VM migration), is not locked out while they renew or re-activate. Surface a non-blocking warning (see policy.Reason) but do not restrict functionality. |
Community |
Whatever your product defines as its free/community feature set. This is the fallback for every other case: no licence, malformed licence, unknown signing key, wrong product, expired beyond the grace window, or revoked. |
There is no state that means "stop working immediately" — the worst case a valid-but-unusable
licence produces is Community, never an exception or a crash. Design your feature gating around
that: check policy.State (and Edition/Features from the payload when Licensed/Grace), and
never treat licence validation itself as something that can fail your app's startup.
4. Binding a licence to a machine (activation)
A licence's Fingerprint field starts empty. The first time you check it, there is nothing to
compare against, so LicensePolicy.Evaluate cannot reject on fingerprint mismatch — the licence
works everywhere until it is activated. Activation is what binds it to one (or MaxInstances)
specific machines, and it produces an activation code: a second, short-lived signed payload
(Kind = "activation") that carries the fingerprint the licence is now expected to run on.
Step 1 — collect a fingerprint:
using DynDyn.Licensing;
var fingerprint = MachineFingerprint.Collect(new FingerprintOptions
{
InstallIdPath = "/var/lib/yourapp/install-id", // or an app-data path on Windows
});
This hashes five components (machine name, OS-level machine id, primary MAC address, CPU
architecture + core count, and a locally persisted install id) into five independent, non-reversible
hash strings. Nothing here is transmitted in the clear, and any single component that can't be read
degrades to a literal "unavailable" rather than throwing — the fingerprint always comes back with
exactly five entries.
Step 2 — build and present a request code:
var request = new ActivationRequest
{
LicenseId = verification.Payload!.LicenseId,
Fingerprint = fingerprint,
ProductVersion = ThisAppsVersion,
Nonce = RandomNumberGenerator.GetBytes(8),
};
string requestCode = request.Encode(); // "DDR-XXXXX-XXXXX-...": show/copy this to the user
The customer takes that DDR-… code to the portal (either through POST /api/v1/activations if
your app can reach the internet, giving it the licence key + this code, or by pasting it into the
portal's Activations page for a fully offline/manual flow) and gets back an activation code —
another opaque token, structurally identical to a licence key.
Step 3 — verify the activation code the same way as the licence, and use ITS fingerprint:
VerificationResult activationVerification = verifier.Verify(activationCode);
// activationVerification.Payload!.Fingerprint is now non-empty — it's what the portal echoed back.
PolicyResult policy = LicensePolicy.Evaluate(
verification.Payload!, // the original licence payload
DateTimeOffset.UtcNow,
ThisAppsVersion,
fingerprint: MachineFingerprint.Collect(fingerprintOptions), // THIS machine's current fingerprint
revokedIds: revokedIds);
Store the activation code alongside the licence key; re-check both at startup. An activation code has
its own ExpiresAt (shorter-lived than the licence itself, by design — it forces a periodic
check-in), so your app should re-activate before it lapses if it expects to keep running unattended
for a long time.
Fingerprint drift is expected and tolerated. Hardware and OS details change — a NIC gets
replaced, a VM gets migrated, a disk gets re-imaged. MachineFingerprint.Matches requires only 3 of
the 5 components to still agree, and even a full mismatch does not produce a hard failure: it
degrades LicensePolicy.Evaluate's result to Grace with LicensePolicyReason.FingerprintMismatch,
not to Community and not to an exception. Re-activating (steps 1–3 again) clears it.
5. Revocation — the one part that needs the network
Everything above works fully offline. Revocation is the exception: if a licence is revoked (customer stopped paying, key leaked, chargeback), the licence itself doesn't know that — the fact only lives on the portal, so your app has to ask.
GET /api/v1/revocations (anonymous, no auth)
→ { "token": "<signed payload, Kind = \"revocations\">" }
VerificationResult revocationVerification = verifier.Verify(revocationsResponse.Token);
IReadOnlyList<Guid> revokedIds = revocationVerification.Status == VerificationStatus.Valid
? RevocationList.ParseRevokedIds(revocationVerification.Payload!)
: cachedRevokedIds; // see below
var policy = LicensePolicy.Evaluate(licensePayload, DateTimeOffset.UtcNow, ThisAppsVersion,
fingerprint, revokedIds);
// policy.State == LicenseState.Community, Reason == LicensePolicyReason.Revoked, if listed.
The response is itself a signed payload (verify it exactly like a licence, with the same
TrustedKeys) rather than a bare list — that's what makes it safe to cache and replay. Practical
guidance:
- Cache the token on disk, not just in memory, together with when you fetched it.
- Fetch it periodically (daily is reasonable for most products) and whenever your app starts with network access.
- If the network is unreachable, keep using the last cached token. Do not treat "can't reach the portal" as "revoked" — that would turn a network blip into an outage for every offline customer. Revocation is deliberately eventually consistent: a revoked licence keeps working, in the worst case, until the next successful fetch.
- A licence that is re-issued (new key, same
LicenseId) is not revoked — the portal only lists aLicenseIdhere when every row for it is revoked and none is active, so you don't need to special case re-issues on your end. - If you have never successfully fetched the list (first run, firewalled environment), pass an empty
revokedIds— that's the same as omitting the parameter, and every other check still applies.
6. Troubleshooting
| Symptom | VerificationStatus / LicensePolicyReason |
Likely cause | Fix |
|---|---|---|---|
| App falls back to Community immediately, no warning | VerificationStatus.Malformed |
The stored string isn't a licence key at all (truncated, wrong env var, pasted with extra whitespace/quotes) | Re-copy the key from the portal's Licences page |
| App falls back to Community immediately | VerificationStatus.UnknownKey |
Your TrustedKeys don't include the KeyId this licence was signed with — usually a stale embedded public key after the portal rotated its signing key |
Update the embedded public key(s); the portal's docs/announcements list the current KeyId |
| App falls back to Community immediately | VerificationStatus.BadSignature |
The key was edited/corrupted in storage, or truncated in transit (e.g. an env var with a length limit) | Re-copy the key; store it as a single unmodified string |
Community, LicensePolicyReason.WrongProduct |
The licence's Product field doesn't match this application |
You have a licence key for a different DynDyn product | Confirm you're using the key issued for this product |
Community, LicensePolicyReason.VersionBelowMin / VersionAboveMax |
Your app's version is outside the licence's supported range | An old or too-new build is running against a version-pinned licence | Upgrade/downgrade the app, or ask for a licence covering this version range |
Community, LicensePolicyReason.Revoked |
The licence id appears in the fetched revocation list | Subscription lapsed, key compromised, or an explicit revoke by the customer's admin | Get a re-issued key from the portal |
Community, LicensePolicyReason.ExpiredBeyondGrace |
Licence expired more than 30 days ago | Renewal never happened | Renew; a fresh licence (or re-issue) clears this immediately |
Grace, LicensePolicyReason.Expired |
Licence expired ≤ 30 days ago | Renewal is due but not yet processed | Full features still work — just renew before the 30-day window closes |
Grace, LicensePolicyReason.FingerprintMismatch |
This machine's current fingerprint doesn't 3-of-5 match the licence's activated fingerprint | Hardware/VM change, OS reinstall, or the licence was never activated on this exact machine | Full features still work — re-run activation (§4) to bind the new fingerprint |
Related: portal.md §2 documents the payload/signing internals and the full API
surface this doc draws from; licences-and-seats.md covers the
customer-facing (non-developer) view of licences, seats, and renewal from the portal UI.