Offline Verification

One of Blackwalnut's key features is offline license verification. Your software can validate licenses without any network connection using the public RSA key.

How It Works

License tokens are JWTs signed with RSA-SHA256 (RS256). The asymmetric nature of RSA means:

  • Private key (secret) - Signs tokens on your server
  • Public key (distributable) - Verifies tokens in your software

Anyone with the public key can verify a token is authentic, but only the private key holder can create valid tokens.

Getting the Public Key

You have two options for distributing the public key:

Option 1: Bundle in Application

Download the public key and include it in your application binary. Most secure option.

Download key
$ curl https://license.yourapp.com/api/v1/apps/my-app/public-key > public_key.pem

Option 2: Fetch at Runtime

Fetch the key once when your application starts and cache it. Allows key rotation without releasing a new version.

fetch_key.py
import requests
import os

def get_public_key():
    # Check cache first
    cache_path = "/tmp/license_public_key.pem"
    if os.path.exists(cache_path):
        with open(cache_path) as f:
            return f.read()

    # Fetch from server
    resp = requests.get(
        "https://license.yourapp.com/api/v1/apps/my-app/public-key"
    )
    key = resp.text

    # Cache for next time
    with open(cache_path, "w") as f:
        f.write(key)

    return key

Verification Examples

Python

verify.py
import jwt

PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...
-----END PUBLIC KEY-----"""

def verify_license(token):
    try:
        payload = jwt.decode(
            token,
            PUBLIC_KEY,
            algorithms=["RS256"],
            audience="my-app"  # Your app slug
        )
        return {
            "valid": True,
            "tier": payload["tier"],
            "features": payload.get("features", []),
            "installation_id": payload["sub"]
        }
    except jwt.ExpiredSignatureError:
        return {"valid": False, "error": "expired"}
    except jwt.InvalidTokenError as e:
        return {"valid": False, "error": str(e)}

Node.js

verify.js
const jwt = require('jsonwebtoken');

const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...
-----END PUBLIC KEY-----`;

function verifyLicense(token) {
  try {
    const payload = jwt.verify(token, PUBLIC_KEY, {
      algorithms: ['RS256'],
      audience: 'my-app'
    });
    return {
      valid: true,
      tier: payload.tier,
      features: payload.features || []
    };
  } catch (err) {
    return { valid: false, error: err.message };
  }
}

Go

verify.go
package license

import (
    "crypto/rsa"
    "github.com/golang-jwt/jwt/v5"
)

func VerifyLicense(token string, publicKey *rsa.PublicKey) (*Claims, error) {
    parsed, err := jwt.ParseWithClaims(token, &Claims{},
        func(t *jwt.Token) (interface{}, error) {
            return publicKey, nil
        },
        jwt.WithValidMethods([]string{"RS256"}),
        jwt.WithAudience("my-app"),
    )

    if err != nil {
        return nil, err
    }

    return parsed.Claims.(*Claims), nil
}

Checking Tiers & Features

After verifying the token, use the tier and features to control access:

feature_check.py
def can_use_api(license_info):
    if not license_info["valid"]:
        return False

    # Check by tier
    if license_info["tier"] in ["pro", "enterprise"]:
        return True

    # Or check specific feature
    return "api_access" in license_info["features"]

Limitations

Offline verification has some trade-offs to be aware of:

  • No revocation check - Revoked licenses will still validate offline until they expire
  • Key rotation - After rotating keys, old tokens signed with the previous key will fail
  • Clock skew - Ensure system clocks are reasonably accurate for expiration checks

For high-security scenarios, combine offline verification with periodic online checks when a network connection is available.

Hybrid Approach

For best results, use a hybrid approach:

  1. Always verify signature offline (fast, no network)
  2. Periodically check online for revocation status
  3. Cache online verification results with a TTL