Security

Cryptographic Signing at the Edge: Ed25519, HMAC-SHA256, and Offline Verification

TOT
Traffic Orchestrator Team
Engineering
April 19, 2026 13 min read 896 words
Share

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.

ApproachSigning LatencyKey DistributionOffline Capable
Centralized HSM20-100ms (network)Single locationNo
Cloud KMS API10-50ms (API call)Single regionNo
Edge signing (Ed25519)<1ms (local compute)All edge locationsYes (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:

  1. Small keys — 32-byte public key, 64-byte private key (vs RSA's 256+ bytes)
  2. Fast signing — ~70μs per signature on modern CPUs (vs RSA-2048's ~1ms)
  3. 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

  1. Generate new key pair — Create a new Ed25519 key pair
  2. Deploy new private key — Push to all edge locations as an encrypted secret
  3. Grace period — Accept both old and new signatures for 7 days
  4. Publish new public key — Update the SDK and documentation
  5. 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.

TOT
Traffic Orchestrator Team
Engineering

The engineering team behind Traffic Orchestrator, building enterprise-grade software licensing infrastructure used by developers worldwide.

Was this article helpful?
Get licensing insights delivered

Engineering deep-dives, security advisories, and product updates. Unsubscribe anytime.

Share this article
Free Plan Available

Ship licensing in your next release

5 licenses, 500 validations/month, full API access. Set up in under 5 minutes — no credit card required.

2-minute setup No credit card Cancel anytime