Unable to serve private CF images

Got a SvelteKit app, and I'm trying out CF images. Upload was nice and easy, but actually serving the images is proving to be a bit of a nightmare I've follow the docs here: https://developers.cloudflare.com/images/cloudflare-images/serve-images/serve-private-images-using-signed-url-tokens/ It produces a good looking signature... https://imagedelivery.net/p78SV55WKTCHZ1_ICRgIKg/43b26a16-265b-4dca-8516-f74e83572501/public?exp=1695126191&sig=875063554c4495f3fc173d87b9483e67e11af8df78a3fe57e52b01dd37eb0c5a But visiting the link errors with:
ERROR 9425: Image access denied: check failed: Malformed data found: The URL signature does not match. Expecting SHA-256 HMAC of the path without domain nor query string
So far I've tried troubleshooting: * Checked API token permissions (currently Cloudflare Images:Read, Cloudflare Images:Edit, Account Analytics:Read, Stream:Edit, Stream:Read) * Have tried rolling new token, didn't make a difference * Have verified that the HMAC signature is "correct" * Using CLOUDFLARE_ACCOUNT_ID instead of CLOUDFLARE_ACCOUNT_HASH * Tried without exp query param so it's just signing url.pathname ...but I've run out of ideas... Any advice would be much appreciated
Serve private images using signed URL tokens · Cloudflare Image Opt...
If an image is marked to require a signed URL, it cannot be accessed without a token unless it is being requested for a variant that is set to always …
1 Reply
Oscar
Oscar9mo ago
Here's the code, 95% copied from the sample code linked above
const EXPIRATION = 60 * 60 * 24; // 1 day

const bufferToHex = (buffer: ArrayBufferLike) =>
[...new Uint8Array(buffer)].map((x) => x.toString(16).padStart(2, '0')).join('');

export async function generateSignedUrl(imageId: string) {
const url = new URL(`https://imagedelivery.net/${env.CF_ACCOUNT_HASH}/${imageId}/public`);
const encoder = new TextEncoder();
const secretKeyData = encoder.encode(env.CF_IMAGES_SECRET_KEY);
const key = await crypto.subtle.importKey(
'raw',
secretKeyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign'],
);

const expiry = Math.floor(Date.now() / 1000) + EXPIRATION;
url.searchParams.set('exp', String(expiry));

const stringToSign = url.pathname + '?' + url.searchParams.toString();

const mac = await crypto.subtle.sign('HMAC', key, encoder.encode(stringToSign));
const sig = bufferToHex(new Uint8Array(mac).buffer);
url.searchParams.set('sig', sig);
return url;
}
const EXPIRATION = 60 * 60 * 24; // 1 day

const bufferToHex = (buffer: ArrayBufferLike) =>
[...new Uint8Array(buffer)].map((x) => x.toString(16).padStart(2, '0')).join('');

export async function generateSignedUrl(imageId: string) {
const url = new URL(`https://imagedelivery.net/${env.CF_ACCOUNT_HASH}/${imageId}/public`);
const encoder = new TextEncoder();
const secretKeyData = encoder.encode(env.CF_IMAGES_SECRET_KEY);
const key = await crypto.subtle.importKey(
'raw',
secretKeyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign'],
);

const expiry = Math.floor(Date.now() / 1000) + EXPIRATION;
url.searchParams.set('exp', String(expiry));

const stringToSign = url.pathname + '?' + url.searchParams.toString();

const mac = await crypto.subtle.sign('HMAC', key, encoder.encode(stringToSign));
const sig = bufferToHex(new Uint8Array(mac).buffer);
url.searchParams.set('sig', sig);
return url;
}