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)