Signature verification
The signature is sent inside x-tidio-signature
header.
Example header:
x-tidio-signature: t=1680652800,s=c64b17322c4519dd324a6014658c518df231ec3e2c6ac8ac19fced7ee4d54014,s=44565a4390252ed0692e1bb55b4ca2c7f581bbf919fec54f797fa8a1647969cd
Where t
is the signing timestamp and s are signatures for each secret generated.
To verify the signature, the request body and signing timestamp should be merged like so:
{request_body}_{signing_timestamp}
And then hash it by HMAC
using SHA256
algorithm with a webhook secret.
Then check if the hash equals one of the s
parameters in the header. If it is, it means that the signature is correct.
Code snippets
class SignatureVerifier
{
public function verify(string $body, string $header, string $secret): bool
{
$timestamp = $this->extractTimestamp($header);
$signatures = $this->extractSignatures($header);
$payload = $body . '_' . $timestamp;
foreach ($signatures as $signature) {
$calculatedSignature = \hash_hmac('sha256', $payload, $secret);
if (\hash_equals($calculatedSignature, $signature)) {
return true;
}
}
return false;
}
private function extractTimestamp(string $header): int
{
$items = \explode(',', $header);
foreach ($items as $item) {
$itemParts = \explode('=', $item, 2);
if ('t' === $itemParts[0]) {
if (!\is_numeric($itemParts[1])) {
throw new \Exception('Invalid timestamp');
}
return (int) ($itemParts[1]);
}
}
throw new \Exception('Invalid header');
}
/**
* @return string[]
*/
private function extractSignatures(string $header): array
{
$items = \explode(',', $header);
$signatures = [];
foreach ($items as $item) {
$itemParts = \explode('=', $item);
if ($itemParts[0] === 's') {
$signatures[] = $itemParts[1];
}
}
if (empty($signatures)) {
throw new \Exception('No signatures found');
}
return $signatures;
}
}
const crypto = require('crypto');
class SignatureVerifier {
verify(body, header, secret) {
const timestamp = this.extractTimestamp(header);
const signatures = this.extractSignatures(header);
const payload = `${body}_${timestamp}`;
for (const signature of signatures) {
const calculatedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
if (crypto.timingSafeEqual(Buffer.from(calculatedSignature), Buffer.from(signature))) {
return true;
}
}
return false;
}
extractTimestamp(header) {
const items = header.split(',');
for (const item of items) {
const [key, value] = item.split('=', 2);
if (key === 't') {
if (!value || !value.match(/^\d+$/)) {
throw new Error('Invalid timestamp');
}
return parseInt(value);
}
}
throw new Error('Invalid header');
}
extractSignatures(header) {
const items = header.split(',');
const signatures = [];
for (const item of items) {
const [key, value] = item.split('=');
if (key === 's') {
signatures.push(value);
}
}
if (signatures.length === 0) {
throw new Error('No signatures found');
}
return signatures;
}
}
const verifier = new SignatureVerifier();
const is_valid = verifier.verify(process.argv[2], process.argv[3], process.argv[4]);
if (is_valid) {
console.log('Signature is valid');
} else {
console.error('Error: signature is invalid');
}
import hmac
import sys
class SignatureVerifier:
def verify(self, body: str, header: str, secret: str) -> bool:
timestamp = self.extract_timestamp(header)
signatures = self.extract_signatures(header)
payload = f"{body}_{timestamp}"
for signature in signatures:
calculated_signature = hmac.new(
bytes(secret, 'utf-8'),
bytes(payload, 'utf-8'),
digestmod='sha256'
).hexdigest()
if hmac.compare_digest(calculated_signature, signature):
return True
return False
def extract_timestamp(self, header: str) -> int:
items = header.split(',')
for item in items:
item_parts = item.split('=', 2)
if item_parts[0] == 't':
if not item_parts[1].isnumeric():
raise Exception('Invalid timestamp')
return int(item_parts[1])
raise Exception('Invalid header')
def extract_signatures(self, header: str) -> list[str]:
items = header.split(',')
signatures = []
for item in items:
item_parts = item.split('=')
if item_parts[0] == 's':
signatures.append(item_parts[1])
if not signatures:
raise Exception('No signatures found')
return signatures
verifier = SignatureVerifier()
is_valid = verifier.verify(sys.argv[1], sys.argv[2], sys.argv[3])
if is_valid:
print('Signature is valid')
sys.exit(0)
else:
print('Error: signature is invalid')
sys.exit(1)
Updated 10 months ago