22. Licensing: key format and trust model
This note documents how GridSim verifies a licence key offline, the on-the-wire key format, and where
enforcement is wired. The user-facing side (tiers, installing a key) is in
docs/manual/23-licensing-and-activation.md.
Trust model: signed, offline, honest-effort
A licence key is signed, not encrypted. It carries its fields in the clear (they are base64-decodable) but they cannot be altered or forged without the private signing key. This is the correct model for offline licensing of source-available software: the fields cannot be hidden in a binary the customer can read anyway, so we protect integrity and authenticity, not secrecy.
- Asymmetric. An ECDSA P-256 keypair. The private key lives only in the out-of-repo forge
(
D:\Work\GridSimLicensing\signing-key.pem) and is never committed. The public key is embedded inGridSim.Core(Licensing/LicencePublicKey.cs, a base64 SubjectPublicKeyInfo constant) and can only verify. - Offline. Verification is a local signature check - no network call, ever. Air-gapped machines activate normally.
- Dependency-free. Everything uses
System.Security.Cryptographyfrom the BCL, soGridSim.Corestays NuGet-free (no BouncyCastle/NSec). P-256 is in-box; Ed25519 is not, which is why P-256 was chosen. - Honest-effort. Because the source is available, a determined user can patch the check out.
LICENSE.md§2 forbids that; we do not build invasive anti-tamper.
Key format (v1)
One line, SSH-authorized-keys style:
GSK1 <base64url-blob>
The blob is:
magic "GSK1" (4 bytes) | version 0x01 (1) | payloadLen u16 big-endian (2) | payload | signature (64)
The payload is canonical, pipe-delimited UTF-8 (fields escape \ and |):
v1 | email | org | type | buslimit | issuedDays | expiresDays | licenceId
typeis the tier wire code:personal | academic | code | deployment | operator.buslimitis per-key;0means unlimited (commercial/operator tiers).issuedDays/expiresDaysare integer days since 1970-01-01 UTC (locale-free, matching the codebase's InvariantCulture discipline).licenceIdis a short opaque id, shown inlicence statusfor support/provenance.
The signature is ECDSA P-256 / SHA-256 in IEEE-P1363 fixed-field form (a fixed 64 bytes), computed over
every byte before it (magic | version | payloadLen | payload). Base64url (unpadded, URL-safe) keeps the
token to one clean line - typically ~180-260 characters, SSH-key length.
Code map (src/GridSim.Core/Licensing/)
| File | Role |
|---|---|
LicenceTier.cs |
The tier enum + wire-code/display helpers. Named to avoid the DNO Licence collision. |
Entitlement.cs |
A decoded, verified licence (fields + IsExpired, IsUnlimited, FreeDefault). |
LicenceKey.cs |
Encode (forge) and TryParse (verify) - the format above; never throws on bad input. |
LicencePublicKey.cs |
The embedded public SPKI. Replace on key rotation. |
Base64Url.cs |
URL-safe unpadded base64. |
LicenceGate.cs |
Resolve (precedence), Check/Enforce (bus limit + expiry), Describe, Install. |
LicenceException.cs |
User-facing failure (rejected key; a front-end hard-block). |
Enforcement seams
Bus count is the only licensed quantity (GridModel.BusCount). Enforcement happens at each front-end's
model-load choke point; Bundled Networks are passed bundled: true and bypass the check.
- CLI (
GridSim.Cli): aGatedhelper wraps every user model load (Gated.Dir/Gated.Matpower); bundled-ness is decided by path (anything under adata/tree is a bundled case). Thelicenceverb (LicenceCli.cs) does status/import/path, and the--importLicence <file.gridsim>global switch installs a key (consumed before flag validation). - Web (
GridSim.Web): theLicenceGateis a DI singleton;CaseCatalog.LoadModelenforces usingCaseKind(built-ins and the MATPOWER library are bundled; onlygda-*JSON models are licensed). The REST API surfaces a denial as HTTP 403; the dashboard shows the tier in a footer badge. - WPF (
GridSim.Wpf):App.Licenceis resolved inOnStartup;MainWindow.SwitchTochecks after load, and the tier is shown in the title bar.
The forge
D:\Work\GridSimLicensing (out of this repo) is a small console tool that links these same
Licensing/*.cs files - so the issuer and the verifier can never drift on the format. keygen mints the
keypair; issue signs a key; verify round-trips one. See that folder's README.md.
Rotating the signing key
Run the forge's keygen --force, paste the new public SPKI into LicencePublicKey.cs, and rebuild. Keys
signed by the old private key stop verifying, so rotate deliberately.