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.
$ 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.
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
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
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
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:
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:
- Always verify signature offline (fast, no network)
- Periodically check online for revocation status
- Cache online verification results with a TTL