Every license validation request trusts that the response hasn't been tampered with. Without cryptographic signing, a man-in-the-middle can intercept the validation response and return `{ "valid": true }` regardless of the actual license status. Signing at the edge solves this — and does it with zero additional latency.
Why Sign at the Edge?
Traditional signing architectures keep private keys locked in a centralized HSM (Hardware Security Module) or KMS (Key Management Service). Every signature request must round-trip to that single location, adding 20-100ms of latency. Edge signing distributes the signing capability to every edge location while maintaining security through asymmetric cryptography.
| Approach | Signing Latency | Key Distribution | Offline Capable |
|---|---|---|---|
| Centralized HSM | 20-100ms (network) | Single location | No |
| Cloud KMS API | 10-50ms (API call) | Single region | No |
| Edge signing (Ed25519) | <1ms (local compute) | All edge locations | Yes (public key only) |
Ed25519: The Optimal Choice for Edge Signing
Ed25519 (Edwards-curve Digital Signature Algorithm using Curve25519) offers three properties that make it ideal for edge environments:
- Small keys — 32-byte public key, 64-byte private key (vs RSA's 256+ bytes)
- Fast signing — ~70μs per signature on modern CPUs (vs RSA-2048's ~1ms)
- Deterministic — Same input always produces the same signature (no random nonce needed, eliminating a class of implementation bugs)
// Ed25519 license signing at the edge
import { sign, verify, generateKeyPair } from 'ed25519'
// At deployment: generate key pair
const { publicKey, privateKey } = generateKeyPair()
// publicKey: 32 bytes (distribute to all clients for offline verification)
// privateKey: 64 bytes (stored as encrypted secret at edge)
// At validation time: sign the response
const signLicenseResponse = (license) => {
const payload = JSON.stringify({
key: license.key,
plan: license.plan,
domains: license.domains,
expiresAt: license.expiresAt,
timestamp: Date.now(),
nonce: crypto.randomUUID()
})
const signature = sign(Buffer.from(payload), privateKey)
return {
payload,
signature: Buffer.from(signature).toString('base64'),
publicKey: Buffer.from(publicKey).toString('base64')
}
}
Offline Verification
The killer feature of asymmetric signing is offline verification. Your customers' applications can verify license authenticity without any network request — they only need the public key.
// Client-side offline verification (runs in customer's application)
const verifyLicense = (signedResponse, trustedPublicKey) => {
const { payload, signature } = signedResponse
// Verify signature using the public key
// This runs entirely locally — no network required
const isValid = verify(
Buffer.from(signature, 'base64'),
Buffer.from(payload),
Buffer.from(trustedPublicKey, 'base64')
)
if (!isValid) {
throw new Error('License signature verification failed — possible tampering')
}
const license = JSON.parse(payload)
// Check expiration
if (license.expiresAt && Date.now() > license.expiresAt) {
return { valid: false, error: 'expired' }
}
// Check domain (if applicable)
if (license.domains && !license.domains.includes(currentDomain)) {
return { valid: false, error: 'domain_mismatch' }
}
return { valid: true, plan: license.plan }
}
HMAC-SHA256: When Symmetric Signing Suffices
Not every use case needs asymmetric cryptography. HMAC-SHA256 is faster (~10μs vs ~70μs) and simpler, but requires shared secrets — meaning both signer and verifier must have the same key.
Use HMAC-SHA256 for:
- Webhook signatures — Your server and the recipient share a secret
- API request authentication — Both parties are servers you control
- Internal service-to-service auth — Trusted network, shared secrets are manageable
Use Ed25519 for:
- License responses — Clients need offline verification without holding secrets
- Public API responses — Any party can verify without shared secrets
- Audit trails — Non-repudiation (the signer can't deny they signed)
// HMAC-SHA256 webhook signing
const signWebhook = async (payload, secret) => {
const encoder = new TextEncoder()
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
)
const signature = await crypto.subtle.sign(
'HMAC',
key,
encoder.encode(payload)
)
return Buffer.from(signature).toString('hex')
}
// Verification (recipient side)
const verifyWebhook = async (payload, signature, secret) => {
const expected = await signWebhook(payload, secret)
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expected, 'hex')
)
}
Key Management at Scale
Distributing private keys to 300+ edge locations requires careful key management:
Key Rotation Strategy
- Generate new key pair — Create a new Ed25519 key pair
- Deploy new private key — Push to all edge locations as an encrypted secret
- Grace period — Accept both old and new signatures for 7 days
- Publish new public key — Update the SDK and documentation
- Deprecate old key — Stop signing with the old key after the grace period
// Key rotation with grace period
const KEYS = {
current: { id: 'k2', privateKey: env.SIGNING_KEY_V2, publicKey: '...' },
previous: { id: 'k1', privateKey: null, publicKey: '...' } // Read-only
}
const signResponse = (payload) => {
const sig = sign(payload, KEYS.current.privateKey)
return {
payload,
signature: sig,
keyId: KEYS.current.id // Client knows which public key to use
}
}
// Client verification supports multiple keys
const verifyResponse = (signed) => {
const key = TRUSTED_KEYS[signed.keyId]
if (!key) throw new Error(`Unknown key: ${signed.keyId}`)
return verify(signed.signature, signed.payload, key.publicKey)
}
Anti-Tampering Architectures
Signing alone doesn't prevent replay attacks. A complete anti-tampering architecture combines:
- Timestamp binding — Include a timestamp in the signed payload; reject signatures older than 5 minutes
- Nonce generation — Include a unique nonce; clients track seen nonces to detect replays
- Domain binding — Include the requesting domain in the signed payload; reject validation for domains not in the signed list
- Certificate pinning — SDK pins the TLS certificate of the validation API to prevent MITM interception
Edge-based cryptographic signing transforms license validation from a trust-the-network model to a trust-the-math model. With Ed25519 signatures computed in under 70 microseconds, the security overhead is effectively zero — and your customers gain the ability to verify licenses completely offline.
Ship licensing in your next release
5 licenses, 500 validations/month, full API access. Set up in under 5 minutes — no credit card required.