SHA-256 Signature Recurly Mismatch

Hi there,

I’m attempting to integrate a recurly webhook to a worker, and add the message to a queue.
In order to verify the request is from recurly, recurly will send a signature in the header of the request.
There are a couple of supporting libraries provided by recurly, but they only seem to be in Ruby and Go. I need it in TypeScript. So they suggest in the documentation to do it manually, and that is what I’m attempting to do.

The problem is that verify will never return as true, and I’m not sure what I’m missing.

My code is like this, and yes, I have confirmed my secret is correct in the worker environment variables.

The format of the signature header is “[timestamp],[signature]”

const recurlySignatureHeader = 'recurly-signature'
export interface Env {
	secret: string;
}

// Convert a ByteString (a string whose code units are all in the range
// [0, 255]), to a Uint8Array. If you pass in a string with code units larger
// than 255, their values will overflow.
function byteStringToUint8Array(byteString: string) {
	const ui = new Uint8Array(byteString.length);
	for (let i = 0; i < byteString.length; ++i) {
		ui[i] = byteString.charCodeAt(i);
	}
	return ui;
}

export default {
	async fetch(
		request: Request,
		env: Env,
		ctx: ExecutionContext
	): Promise<Response> {

		// Webhooks are always posts. 
		if(request.method != "POST") return new Response("Not Found",{status:404})

		// Get Signatures from header. 
		// The signature formate is {timestamp}.{signature new|current},{signature old}
		// Note there can be mutiple signatures seperated by a commor ','. As the expired signature will be present for 24 hours
		const signature = request.headers.get(recurlySignatureHeader);
		console.log(`signature is ${signature}`)

		// Handle no header being present. Return 401
		if(!signature) return new Response('Not Authorized', {status:401})

		// Get Signature & Timestamp & configure required objects for next steps
		const elements = signature.split(',');
		const timestamp = elements[0];
		const signatures = elements.slice(1)[0]; // todo - handle multiple signatures getting returned...
		const encoder = new TextEncoder();
		const secret = env.secret;
		const algo = {
			name: 'HMAC',
			hash: { name: 'SHA-256' }
		};

		// Add secret as buffer
		const secretKey = await crypto.subtle.importKey(
			'raw',
			encoder.encode(secret),
			algo,
			true,
			['verify','sign'],
		);

		// generate an expected signed signature 
		const expectedSignature = await crypto.subtle.sign(
			algo,
			secretKey,
			encoder.encode(await request.text()) 
		);

		// Compare expected against received signature(s)
		const isValidSignature = await crypto.subtle.verify (
			algo,
			secretKey,
			byteStringToUint8Array(atob(signatures)),
			expectedSignature
		);

		// todo - Disabled for testing signatures...
		// Compare timestamp from header against current time
		// const currentTimestamp = Date.now() / 1000;
		// const timestampDifference = Math.abs(currentTimestamp - Number(timestamp));
	
		// if (timestampDifference > 24 * 3600) {
		// 	console.warn('Timestamp is too far in the past or future')
		//   	return new Response('Access Denied', { status: 401 });
		// }

		console.log('isValidSignature',isValidSignature)
		if(isValidSignature) {
			// todo add message to queue.
			return new Response('Success', { status: 200 });		
		} else {
			return new Response('Access Denied', { status: 200 }); //todo change to 401, this is to prevent the pausing of request while testing...
		}
	},
};

I’m not sure if I’m missing something with the process or if the library is processing things slightly differently than I expect.

Thanks in advance.

Here is the recurly documentation I’m basing my code off.
https://recurly.com/developers/reference/webhooks/#signature-verification

This topic was automatically closed 15 days after the last reply. New replies are no longer allowed.