Security Enhancement in v2 : Offline verification no longer trusts embedded public keys in .lic files. Applications now maintain a local trust store that maps key IDs to trusted public keys, preventing attackers from crafting malicious licenses with their own keys.
What’s Covered in This Guide
Trust Store Architecture Security model and key management concepts
License Generation Creating offline licenses in the Dashboard
Client-Side Verification Implementing secure offline validation
Key Management Key rotation and migration strategies
Set up trust store with your public keys
Generate .lic files from the Dashboard
Implement verification in your application
Handle key rotation for long-term security
Trust Store Architecture
The trust store is a client-side security mechanism that maps Key IDs (kid) to trusted public keys, preventing malicious license crafting.
The Key ID (kid) is a stable fingerprint of the public key computed as: kid = base64url(SHA-256(SPKI DER))
Computing Key ID
From PEM (Browser/Node.js):
async function computeKidFromPem ( pem ) {
const b64 = pem . replace ( /-----BEGIN PUBLIC KEY----- | -----END PUBLIC KEY----- | \s + / g , '' );
const der = Uint8Array . from ( atob ( b64 ), c => c . charCodeAt ( 0 ));
const hash = await crypto . subtle . digest ( 'SHA-256' , der );
const b = String . fromCharCode ( ... new Uint8Array ( hash ));
return btoa ( b ). replace ( /=/ g , '' ). replace ( / \+ / g , '-' ). replace ( / \/ / g , '_' );
}
Trust Store Implementation
import { importSPKI } from 'jose' ;
// Build trust store during application initialization
const trustStore = new Map ();
// Add trusted keys (ship with your application)
const publicKeyPem = `-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAr7Z8s2Z...
-----END PUBLIC KEY-----` ;
const kid = 'AbC123XyZ' ; // Computed fingerprint
trustStore . set ( kid , await importSPKI ( publicKeyPem , 'EdDSA' ));
2. Generate an Offline License (Dashboard)
Log in to Keymint and go to Products → [Your Product] → Licenses .
Locate the license row you wish to issue offline.
In the Activations column, click the Eye icon (👁️) to open the Offline License modal.
3. Offline License Modal
The modal has two panels:
A. Offline License Generator
Machine Code (optional) — A hardware fingerprint (e.g., host-ABC123) locks this file to one device.
Custom TTL (seconds) — Required if the license has no built-in expiration. Example: 2592000 for 30 days.
Generate Signed License — Signs a fresh JWT and immediately triggers download.
B. Device Activations
Below the generator, a table lists all machines that have activated this key. Useful for auditing.
When you click Generate, the browser downloads a file named:
license-<KEY>-<machineCode?>-<YYYY-MM-DD>.lic
Contents (JSON) — No embedded public key:
{
"licenseToken" : "eyJhbGciOiJFZERTQSIsImtpZCI6IkFiQzEyM1h5WiJ9..." ,
"signedKey" : "ABCD-EFGH-1234-5678" ,
"signedDate" : "2025-08-09T14:12:00.000Z" ,
"keyId" : "AbC123XyZ"
}
Field descriptions:
Field Type Description licenseToken string Ed25519-signed JWT (header.alg=EdDSA, header.kid=fingerprint) signedKey string License key identifier shown to users signedDate string ISO 8601 timestamp of issuance keyId string Same value as JWT header.kid (fingerprint)
5. JWT Structure
Protected header:
{
"alg" : "EdDSA" ,
"kid" : "AbC123XyZ"
}
Payload claims:
Field Type Description sub / key string License key identifier productId string Product reference machine string (Optional) bound hardware fingerprint type string ”offline” iat number Issued-at timestamp (Unix seconds) exp number Expiry timestamp (Unix seconds) nbf number Not-before timestamp (Unix seconds)
6. Distribution Methods
Web download
Email attachment
Physical media (USB, DVD, SD card)
Embed in installer
7. Client-Side Verification with Trust Store
Complete verification implementation:
import { importSPKI , jwtVerify , decodeProtectedHeader } from "jose" ;
import fs from "fs" ;
// Error codes for granular failure handling
type VerificationResult =
| { ok : true ; code : 'ok' ; payload : any }
| { ok : false ; code : 'unknown_kid' | 'token_malformed' | 'signature_invalid'
| 'token_expired' | 'not_yet_valid' | 'machine_mismatch'
| 'product_mismatch' };
// Initialize trust store (ship with your application)
const trustStore = new Map < string , any >();
// Add your trusted public keys
const trustedKeys = [
{
kid: 'AbC123XyZ' , // Fingerprint from Keymint dashboard
pem: `-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAr7Z8s2Z...
-----END PUBLIC KEY-----`
},
// Add more keys for rotation
];
// Build trust store
for ( const { kid , pem } of trustedKeys ) {
trustStore . set ( kid , await importSPKI ( pem , 'EdDSA' ));
}
export async function verifyLicense (
licenseFilePath : string ,
options : {
product ?: string ;
machine ?: string ;
now ?: number ;
} = {}
) : Promise < VerificationResult > {
try {
// 1. Load the .lic file
const licenseData = JSON . parse ( fs . readFileSync ( licenseFilePath , "utf8" ));
const { licenseToken } = licenseData ;
if ( ! licenseToken ) {
return { ok: false , code: 'token_malformed' };
}
// 2. Decode JWT header (don't trust payload until signature verified)
const header = decodeProtectedHeader ( licenseToken );
// 3. Resolve verification key
let verificationKey ;
if ( header . kid ) {
// Use kid-based lookup
verificationKey = trustStore . get ( header . kid );
if ( ! verificationKey ) {
return { ok: false , code: 'unknown_kid' };
}
try {
const { payload } = await jwtVerify ( licenseToken , verificationKey , {
algorithms: [ 'EdDSA' ]
});
return validateClaims ( payload , options );
} catch {
return { ok: false , code: 'signature_invalid' };
}
} else {
// Legacy fallback: try all trusted keys
for ( const key of trustStore . values ()) {
try {
const { payload } = await jwtVerify ( licenseToken , key , {
algorithms: [ 'EdDSA' ]
});
return validateClaims ( payload , options );
} catch {
// Continue to next key
}
}
return { ok: false , code: 'signature_invalid' };
}
} catch {
return { ok: false , code: 'token_malformed' };
}
}
function validateClaims (
payload : any ,
{ now = Math . floor ( Date . now () / 1000 ), product , machine } : any
) : VerificationResult {
// Basic payload structure check
if ( typeof payload !== 'object' || ! payload ) {
return { ok: false , code: 'token_malformed' };
}
// Check expiration (with small clock skew tolerance)
const clockSkew = 300 ; // 5 minutes
if ( payload . exp && now > ( payload . exp + clockSkew )) {
return { ok: false , code: 'token_expired' };
}
// Check not-before
if ( payload . nbf && now < ( payload . nbf - clockSkew )) {
return { ok: false , code: 'not_yet_valid' };
}
// Check product match
if ( product && payload . productId !== product ) {
return { ok: false , code: 'product_mismatch' };
}
// Check machine binding
if ( machine && payload . machine && payload . machine !== machine ) {
return { ok: false , code: 'machine_mismatch' };
}
return { ok: true , code: 'ok' , payload };
}
Quick implementation:
import { importSPKI , jwtVerify , decodeProtectedHeader } from 'jose' ;
import fs from 'fs' ;
// Trust store helper - ship with your app
async function buildTrustStore () {
const trustedKeys = [
// Copy from Keymint "Manage Public Keys"
{ kid: 'AbC123XyZ' , pem: `-----BEGIN PUBLIC KEY----- \n MCowBQ... \n -----END PUBLIC KEY-----` },
{ kid: 'DeF456UvW' , pem: `-----BEGIN PUBLIC KEY----- \n MCowBQ... \n -----END PUBLIC KEY-----` }, // Next key for rotation
];
const store = new Map ();
for ( const { kid , pem } of trustedKeys ) {
store . set ( kid , await importSPKI ( pem , 'EdDSA' ));
}
return store ;
}
// Verify license
async function verifyOfflineLicense ( licFilePath : string , options : { productId ?: string , machineCode ?: string } = {}) {
const trustStore = await buildTrustStore ();
// 1. Read .lic file
const { licenseToken } = JSON . parse ( fs . readFileSync ( licFilePath , 'utf8' ));
// 2. Decode header, get kid
const header = decodeProtectedHeader ( licenseToken );
// 3. Resolve key from trust store
let key = header . kid ? trustStore . get ( header . kid ) : null ;
if ( ! key ) {
// Fallback: try all trusted keys
for ( const k of trustStore . values ()) {
try {
await jwtVerify ( licenseToken , k , { algorithms: [ 'EdDSA' ] });
key = k ;
break ;
} catch { /* continue */ }
}
}
if ( ! key ) throw new Error ( 'Unknown key ID or invalid signature' );
// 4. Verify JWT with Ed25519 only
const { payload } = await jwtVerify ( licenseToken , key , { algorithms: [ 'EdDSA' ] });
// 5. Validate claims
const now = Math . floor ( Date . now () / 1000 );
if ( payload . exp && now > payload . exp ) throw new Error ( 'License expired' );
if ( payload . nbf && now < payload . nbf ) throw new Error ( 'License not yet valid' );
if ( payload . type && payload . type !== 'offline' ) throw new Error ( 'Invalid license type' );
if ( options . productId && payload . productId !== options . productId ) throw new Error ( 'Product mismatch' );
if ( options . machineCode && payload . machineCode && payload . machineCode !== options . machineCode ) throw new Error ( 'Machine mismatch' );
return payload ; // License valid!
}
8. Best Practices
Ship multiple trusted public keys for seamless key rotation.
Enforce Ed25519 (EdDSA) only; do not accept other algorithms.
Keep clock skew tolerance small (e.g., 5 minutes).
Bind to machine code where possible for higher assurance.
Provide user-friendly messages for common failure codes.