I need to validate a signature. The code examples for signature validation are only in Python. Here is the Python function:
import hmac
import hashlib
import json
def verify_signature(key, timestamp, provided_signature, payload):
key_bytes = bytes.fromhex(key)
payload_str = json.dumps(payload)
data = timestamp + payload_str
signature = hmac.new(key_bytes, data.encode('utf-8'),
hashlib.sha256).hexdigest()
valid = hmac.compare_digest(provided_signature, signature)
return valid
I need to translate some Python code into TypeScript.
Here is my best attempt:
export function verifyCloseSignature(
request: Request,
key: string,
payload: any,
) {
const headers = request.headers;
const timestamp = headers.get('close-sig-timestamp');
const providedSignature = headers.get('close-sig-hash');
if (!timestamp) {
throw new Error('[verifyCloseSignature] Required timestamp header missing');
}
if (!providedSignature) {
throw new Error('[verifyCloseSignature] Required signature header missing');
}
const payloadString = JSON.stringify(payload);
const hmac = crypto.createHmac('sha256', Buffer.from(key, 'hex'));
hmac.update(timestamp + payloadString);
const calculatedSignature = hmac.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(providedSignature, 'hex'),
Buffer.from(calculatedSignature, 'hex'),
);
}
Why is this code not equivalent? Data that is validated by the Python code, fails the validation in JavaScript, when used like this:
const headers = new Headers();
headers.set('close-sig-hash', signature);
headers.set('close-sig-timestamp', timestamp.toString());
headers.set('Content-Type', 'application/json');
const request = new Request(faker.internet.url(), {
method: 'POST',
headers: headers,
body: JSON.stringify(payload),
});
const actual = verifyCloseSignature(request, key, payload);
const expected = true;
expect(actual).toEqual(expected);
I expect the function to return true
.
@SUTerliakov's SO link helped me to find a good solution. It is indeed caused by the extra white space.
Here is the code that I ended up with:
import { pipe, replace } from Ramda;
export const toJSONWithSpaces = pipe(
(object: unknown) => JSON.stringify(object, null, 1), // stringify with line-breaks and indents
replace(/\n +/gm, ' '), // replace line breaks and following spaces with a single space
replace(/:\s/g, ': '), // ensure a space after colon
replace(/{\s/g, '{'), // remove space after opening brace
replace(/\s}/g, '}'), // remove space before closing brace
replace(/\[\s/g, '['), // remove space after opening bracket
replace(/\s]/g, ']'), // remove space before closing bracket
replace(/,\s/g, ', '), // ensure a space after comma
);
export function verifyCloseSignature(
request: Request,
key: string,
payload: any,
) {
const headers = request.headers;
const timestamp = headers.get('close-sig-timestamp');
const providedSignature = headers.get('close-sig-hash');
if (!timestamp) {
throw new Error('[verifyCloseSignature] Required timestamp header missing');
}
if (!providedSignature) {
throw new Error('[verifyCloseSignature] Required signature header missing');
}
const hmac = crypto.createHmac('sha256', Buffer.from(key, 'hex'));
const cleanedPayload = toJSONWithSpaces(payload);
hmac.update(timestamp + cleanedPayload);
const calculatedSignature = hmac.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(providedSignature, 'hex'),
Buffer.from(calculatedSignature, 'hex'),
);
}