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
  1. Set up trust store with your public keys
  2. Generate .lic files from the Dashboard
  3. Implement verification in your application
  4. 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)

1

Log in to Keymint and go to Products → [Your Product] → Licenses.
2

Locate the license row you wish to issue offline.
3

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.

4. Enhanced .lic File Format (v2)

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:
FieldTypeDescription
licenseTokenstringEd25519-signed JWT (header.alg=EdDSA, header.kid=fingerprint)
signedKeystringLicense key identifier shown to users
signedDatestringISO 8601 timestamp of issuance
keyIdstringSame value as JWT header.kid (fingerprint)

5. JWT Structure

Protected header:
{
  "alg": "EdDSA",
  "kid": "AbC123XyZ"
}
Payload claims:
FieldTypeDescription
sub / keystringLicense key identifier
productIdstringProduct reference
machinestring(Optional) bound hardware fingerprint
typestring”offline”
iatnumberIssued-at timestamp (Unix seconds)
expnumberExpiry timestamp (Unix seconds)
nbfnumberNot-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-----\nMCowBQ...\n-----END PUBLIC KEY-----` },
    { kid: 'DeF456UvW', pem: `-----BEGIN PUBLIC KEY-----\nMCowBQ...\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.