Secure Downloads – HMAC demo

Use the generator controls to request a signed URL and open it. Clicking the file tiles performs their native navigation.

Signing snippet (Cloudflare Snippets – path /generate/)

This JavaScript snippet produces tokens compatible with Cloudflare's is_timed_hmac_valid_v0() WAF function. Place this as a Worker/Snippet or endpoint that responds to /generate{path}.

// ============================================
// WAF-COMPATIBLE HMAC URL SIGNING SNIPPET
// This generates tokens compatible with Cloudflare's
// is_timed_hmac_valid_v0() WAF function
// https://developers.cloudflare.com/ruleset-engine/rules-language/functions/#hmac-validation
// ============================================

const CONFIG = {
  SECRET_KEY: 'mysecrettoken',
  EXPIRY_SECONDS: 120, // Match WAF ttl parameter (seconds)
  SEPARATOR: '?verify=', // Must match WAF lengthOfSeparator (8 bytes)
  USE_URL_SAFE_BASE64: false, // Set to true if using 's' flag in WAF
};

const encoder = new TextEncoder();

function arrayBufferToBase64(buffer, urlSafe = false) {
  const bytes = new Uint8Array(buffer);
  let binary = '';
  for (let i = 0; i < bytes.length; i++) {
    binary += String.fromCharCode(bytes[i]);
  }
  const base64 = btoa(binary);
  if (urlSafe) {
    return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
  }
  return encodeURIComponent(base64);
}

async function importKey(secretKey) {
  return crypto.subtle.importKey('raw', encoder.encode(secretKey), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
}

async function generateWafCompatibleToken(url, key) {
  url.pathname = url.pathname.replace('/generate/', '/');
  const message = url.pathname;
  const timestamp = Math.floor(Date.now() / 1000);
  const dataToSign = `${message}${timestamp}`;
  const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(dataToSign));
  const mac = arrayBufferToBase64(signature, CONFIG.USE_URL_SAFE_BASE64);
  const token = `${timestamp}-${mac}`;
  return `${url.pathname}${CONFIG.SEPARATOR}${token}`;
}

export default {
  async fetch(request) {
    const url = new URL(request.url);
    if (url.pathname.startsWith('/generate/')) {
      try {
        const key = await importKey(CONFIG.SECRET_KEY);
        const signedUrl = await generateWafCompatibleToken(url, key);
        return new Response(signedUrl, { status: 200, headers: { 'Content-Type': 'text/plain', 'X-Token-Format': 'waf-compatible' } });
      } catch (err) {
        return new Response('Failed to generate signed URL', { status: 500 });
      }
    }
    return fetch(request);
  },
};

WAF custom rule expression

The WAF expression validates the signed URL on requests to /downloads/. It uses is_timed_hmac_valid_v0() with the same secret and parameters used by the generator. See Cloudflare's documentation for the function here: HMAC validation function.

(http.host eq "hmac.automatic-demo.com" and not is_timed_hmac_valid_v0("mysecrettoken", http.request.uri, 120, http.request.timestamp.sec, 8)) and (starts_with(http.request.uri.path, "/downloads/"))

It is recommended to create a Cache Rule with "Ignore Query String" to allow proper caching of the signed resources under /downloads/. Configure the rule to match the URI path /downloads/. Guidance: Cache levels & caching rules.

Components – brief explanation

Generator endpoint
/generate{path} – Worker/Snippet or endpoint that returns the signed URI string. The UI calls this to obtain a signed URL for a resource under /downloads/.
SECRET_KEY
Shared secret used to compute HMAC-SHA256. Must match the key configured in the WAF rule.
EXPIRY_SECONDS
TTL for tokens. The WAF rule must use the same value (here: 120 seconds).
SEPARATOR
String between message and token in the final URL (here: ?verify=). Its byte length must match the WAF's lengthOfSeparator parameter (8 bytes in the example).
USE_URL_SAFE_BASE64
When true the signature uses URL-safe base64 without padding. When false the snippet emits standard base64 URL-encoded with encodeURIComponent(). The WAF must be configured accordingly (the 's' flag).
WAF function
is_timed_hmac_valid_v0(secret, message, ttl, now, lengthOfSeparator) – verifies the HMAC and timestamp. The message passed to the function is typically http.request.uri, and now is http.request.timestamp.sec.
Cache rule
Recommended: create a Cloudflare Cache Rule that matches /downloads/ and set it to Ignore Query String, so cached responses are served regardless of the appended verification token query. See Cloudflare guidance on caching levels and when to use cache rules: Cache levels & caching rules and when to use snippets.