Client SDK (CertifiEd.Client)
A lightweight .NET SDK you embed into your application. It owns the entire client-side protocol: loading the license file, verifying the Ed25519 signature offline, activating on the server, running background heartbeats with a cached offline marker, and deactivating on shutdown.
The SDK depends only on the token format and NSec.Cryptography — it drags in neither the server's Domain nor its Infrastructure.
Installation
The SDK ships as the NuGet package `CertifiEd.Client` 1.0.0 (MIT licence, authored by Ofarandagon). It carries XML documentation for IntelliSense and a .snupkg symbols package; the only dependency is NSec.Cryptography.
dotnet add package CertifiEd.ClientProjectReference
For a mono-repository or a source build:
<ItemGroup> <ProjectReference Include="path/to/src/CertifiEd.Client/CertifiEd.Client.csproj" /></ItemGroup>Local NuGet feed
For shipping the SDK to customers separately from the server code:
# build the packagedotnet pack src/CertifiEd.Client -c Release -o ./nupkg
# register the local source and installdotnet nuget add source ./nupkg --name certified-localdotnet add package CertifiEd.Client --source certified-localPublic API
| Member | Purpose |
|---|---|
new CertifiEdLicenseClient(CertifiEdClientOptions options, HttpClient? http = null) | Constructor. When http is omitted the SDK creates its own HttpClient with BaseAddress = ServerUrl and owns it |
Task<bool> InitializeAsync(ct) | Loads the .ced file, verifies the signature offline against PublicKey, activates on the server (best-effort) and starts the heartbeat loop. false — file missing / signature invalid / token expired |
CertifiEdLicenseStatus Status { get; } | Invalid / Active / GracePeriod / Expired / Revoked — derived from the token exp and the marker |
bool HasFeature(string name) | Whether name is present in the features array of the signed cfg |
T? GetConfig<T>(string path) where T : struct | The value at a dotted path ("limits.maxSeats") inside cfg; null when the path is absent |
ValueTask DisposeAsync() | Best-effort deactivate plus stopping the heartbeat timer |
CertifiEdClientOptions
| Property | Required | Purpose |
|---|---|---|
ServerUrl | yes | Server base URL, e.g. https://api.certified.example |
LicenseFilePath | yes | Path to the local .ced file — envelope or bare token |
PublicKey | yes | Raw 32 Ed25519 bytes (Convert.FromHexString(hex)) |
HwFingerprint | no | Overrides the fingerprint; defaults to HwFingerprint.Get() |
Full integration example
using CertifiEd.Client;
// The public key ships with your product (from the .ced file or GET .../public-key).// The API returns it as hex — unpack it into raw bytes on the client.byte[] publicKey = Convert.FromHexString( "7bac1e3ce8578ad859bfe001a4f4d72702d00e3d1eebdd735ebdb7145d74f35b");
var options = new CertifiEdClientOptions{ ServerUrl = "https://api.certified.example", LicenseFilePath = "/etc/myapp/license.ced", PublicKey = publicKey, // HwFingerprint = "..." // optional; defaults to HwFingerprint.Get()};
await using var license = new CertifiEdLicenseClient(options);
if (!await license.InitializeAsync()){ Console.Error.WriteLine("License missing, corrupted or expired."); return 1;}
// Gate on statusswitch (license.Status){ case CertifiEdLicenseStatus.Active: break; // all good
case CertifiEdLicenseStatus.GracePeriod: Console.WriteLine("License is in the grace period — restore connectivity."); break; // keep running, but warn
case CertifiEdLicenseStatus.Expired: case CertifiEdLicenseStatus.Revoked: case CertifiEdLicenseStatus.Invalid: Console.Error.WriteLine($"Access denied: {license.Status}."); return 1; // block}
// Gate on features and limitsif (license.HasFeature("export")) EnableExport();
int maxSeats = license.GetConfig<int>("limits.maxSeats") ?? 1; // fallback defaultEnforceSeatLimit(maxSeats);
RunApplication();return 0;// await using calls DisposeAsync -> deactivate + stop the heartbeat timer.GetConfig<T> is constrained to where T : struct — int, long, bool, double, DateTimeOffset and friends. For strings and arrays read cfg directly or use HasFeature.
What InitializeAsync does
- Reads the file (a
.cedenvelope or a bare token) and parses the three segments. - Verifies the Ed25519 signature offline against
PublicKey. On failure the token is dropped and the method returnsfalse. - Checks
exp: an expired token →false. - Loads the cached
<license>.ced.hbmarker if there is one. - Best-effort activation via
POST /api/v1/client/activate: on success it storesactivationId, applies the fresh marker and starts the heartbeat timer.
Offline behaviour
- While the app keeps fetching a fresh marker, the status stays
Active. - Connectivity lost: the marker holds
Activeuntil its ownexp(issue time +maxOfflineDays), then 24 hours ofGracePeriod, thenExpired. - Restart without network: the marker is read from disk and the license stays valid until the marker's
exp. - The token's hard
expoverrides everything: past it the status is alwaysExpired, even online.
The exact status computation lives in the License protocol section.
Handling Expired and Revoked
- Expired — the term has run out (
token.exppassed) or the marker plus grace expired without connectivity. Usually this requires the operator to re-issue the license. - Revoked — the license was revoked on the server. The SDK learns about it indirectly: the server rejects activation and heartbeat (
license.not_active), fresh markers stop arriving, and once the offline window ends the status degrades toExpired.
In practice: gate functionality on Status == Active || Status == GracePeriod, show a warning during GracePeriod, and block protected functionality on Expired / Revoked / Invalid.
Hardware binding
The SDK always sends hwFingerprint to POST /api/v1/client/activate. By default that is HwFingerprint.Get() — a stable SHA-256 over the machine name, the OS description, the first stable MAC address and the platform machine id, computed once per process. Your own identifier (a stable per-tenant one, say) goes into CertifiEdClientOptions.HwFingerprint.
| License mode | What the integrator sees |
|---|---|
none | The fingerprint is recorded on the activation but constrains nothing |
fixed | Activation only succeeds on the machine with the pinned fingerprint |
firstActivation | The first run pins the machine; from then on it behaves like fixed |
The modes and rebinding are covered in full in Hardware binding.
What the client sees on a mismatch
Activation answers 400:
{ "code": "activation.fingerprint_mismatch", "message": "License is bound to different hardware.", "status": 400}This does not fail InitializeAsync: activation is best-effort and the access decision comes from Status, derived from the local token and marker. In practice:
- Fresh offline markers stop arriving — a heartbeat is impossible without an
activationId. - While the last marker is alive the status stays
Active, thenGracePeriod, thenExpired. - On a clean machine that never had a marker the countdown starts from the last local validation, and the license degrades over the same
maxOfflineDays.
Replacing the machine
The fingerprint cannot be “fixed” on the client side — rebinding happens on the platform:
- An operator or a company Admin calls
POST /api/v1/panel/licenses/{id}/rebindwith the newhwFingerprint(ornull, so the license pins itself to the next machine). - The platform deactivates the old activations and re-signs the license as a new version.
- The customer downloads the `.ced` again (
GET /api/v1/panel/licenses/{id}/downloador the portal) and places it atLicenseFilePath. - The stale marker cache
<license>.ced.hbcan be deleted — a new one arrives with the first successful activation.
The public key does not change (unless the template key was rotated), so the PublicKey compiled into your application stays valid.
Lifecycle and Dispose
- Call
DisposeAsyncexactly once — viaawait usingor explicitly. The SDK owns theHttpClientit created itself and closes it; a second call would touch an already-disposed client. - If you passed your own
HttpClientinto the constructor, the SDK does not dispose it — that is your responsibility. DisposeAsyncsends a best-effortdeactivate; an unreachable server does not prevent a clean shutdown.