Signature

Betterez signs every webhook before sending it to your endpoint. We do so including the signature in the request header called x-btrz-signature. With this signature you will be able to verify that the webhook was sent by Betterez.

Signature verification

To verify the signature you will need to hash the webhook body payload using a HMAC algorithm with the SHA256 hash function, using your private key of the betterez-webhooks application under admin/integrations/applications.

Step 1: Extract the timestamp and signature from the header

Split the x-btrz-signature request header, using the , character as the separator.

The value for the prefix t corresponds to the timestamp, s corresponds to the deprecated signature and the s2 corresponds to the signature.

Step 2: Prepare the payload string

Concatenate the timestamp (string), the character . and the JSON payload (the body request) encoded as string.

Step 3: Sign the string and get the signature

Compute an HMAC with the SHA256 hash function using your private key as the key and payload string generated in the Step 2 as the message.

Step 4: Compare signatures

Compare both signatures, the one extracted from the x-btrz-signature (Step 1) and the generated by you in the Step 3. If the signature matches, compute the difference between the current timestamp and the received timestamp (Step 1), then decide if the difference is within your tolerance.

To protect against timing attacks, use a constant-time string comparison to compare the expected signature to each of the received signatures.

See below an example of comparing the signatures in node.js (this example is not comparing the timestamp).

Please see that you should use JSON.stringify with your payload object, because they might be some issues if your payload has escaping characters such as \n.

const crypto = require("crypto");

function getTimeStamp(timestampFromHeader) {
  if (timestampFromHeader) {
    return timestampFromHeader.replace("t=", "");
  }
  throw new Error("Invalid timestamp from header");
}

function getSignature(signatureFromHeader) {
  if (signatureFromHeader) {
    return signatureFromHeader.replace("s2=", "");
  }
  throw new Error("Invalid signature from header");
}

function getHeaderData(header) {
  const error = new Error("Invalid header");
  if (header) {
    const [t, s, s2] = header.split(",");
    return {
      timestamp: getTimeStamp(t),
      signature: getSignature(s2)
    };

  }
  throw error;
}

function generateSignature(privateKey, {timestamp}, payload) {
  const payloadToSign = `${timestamp}.${payload}`;
  const hmac = crypto.createHmac("sha256", privateKey);
  hmac.update(payloadToSign);
  return hmac.digest("hex");
}

function isValidSignature({ signature }, signatureToVerify) {
  return signature === signatureToVerify;
}

function verifySignature(privateKey, header, payload) {
  const headerData = getHeaderData(header);
  const signatureToVerify = generateSignature(privateKey, headerData, payload);
  const valid = isValidSignature(headerData, signatureToVerify);
  if (valid) {
    console.log("ALL GOOD");
    return true;
  }
  console.log("INVALID");
  return false;
}

const header = "t=1588080777,s=6e3f4cab186b7cc35d91a80679f01b4a71059669e8fe26e58ea5c1921c51dbc4,s2=6e3f4cab186b7cc35d91a80679f01b4a71059669e8fe26e58ea5c1921c51dbc4";
const payload = JSON.stringify({"attemptId":"46b8b713-e93d-49ad-a3a7-cfd618aa0443","created":1588080777,"data":{"_id":"5e7b6b67549d991c9362ed08","accountId":"52a377ec430c7d4e220001fc","agencyId":"","closed":true,"closedAt":{"offset":0,"value":"2020-04-28T13:32:56.436Z"},"comment":"comment","createdAt":{"offset":0,"value":"2020-03-25T14:32:07.718Z"},"deleted":false,"deposits":[],"internalId":"","location":{"name":"B","stationId":"52a378c8430c7d4e2200020e"},"shiftNumber":"S-STNJ8UD","updatedAt":{"offset":0,"value":"2020-04-28T13:32:56.442Z"},"user":{"email":"susmitha+break@betterez.com","username":"sBreak"},"userId":"54eddf07d4558abf23000004","variance":2870.49,"variances":{}},"event":"shift.closed","id":"99957dd2-56f8-43d2-8b35-15767af1edd0","livemode":false});
const privateKey = "f18dc28f-dd25-4219-86f7-174c0c70dd94";

verifySignature(privateKey, header, payload);

Below you can find another example using C# (.NET 6.0) where the payload has an escaping character.

using System.Text.Json;
using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Unicode;

namespace SignatureVerifier
{
    public class HeaderData
    {
        public string? timestamp { get; set; }
        public string? signature { get; set; }
    }
    public class Signature
    {
        private static string getTimeStamp(string timestampFromHeader) {
            return timestampFromHeader.Replace("t=", "");
        }
        private static string getSignature(string signatureFromHeader) {
            return signatureFromHeader.Replace("s2=", "");
        }
        private static HeaderData getHeaderData(string header) {
            string[] headerData = header.Split(",");
            return new HeaderData {
                timestamp = getTimeStamp(headerData[0]),
                signature = getSignature(headerData[2])
            };
        }
        private static string generateSignature(string privateKey, HeaderData headerData, string payload) {
            var payloadToSign = headerData.timestamp + "." + payload;
            HMACSHA256 hmac = new HMACSHA256(Encoding.UTF8.GetBytes(privateKey));
            var result = hmac.ComputeHash(Encoding.ASCII.GetBytes(payloadToSign));
            return Convert.ToHexString(result).ToLower();
        }
        private static bool isValidSignature(HeaderData headerData, string signatureToVerify) {
            return headerData.signature.Equals(signatureToVerify);
        }

        public static bool verifySignature(string privateKey, string header, string payload) {
            var headerData = getHeaderData(header);
            var signatureToVerify = generateSignature(privateKey, headerData, payload);
            var valid = isValidSignature(headerData, signatureToVerify);
            if (valid) {
                Console.WriteLine("ALL GOOD");
                return true;
            }
            Console.WriteLine("INVALID");
            return false;
        }
    }

    public class Program
    {
        
        public static void Main()
        {
            var header = "t=1647355911,s=a0b1aab7a2d1c869da62286082a31d3a7103018ea94fa7d10b08b5a5f271be71, s2=a0b1aab7a2d1c869da62286082a31d3a7103018ea94fa7d10b08b5a5f271be71";

            var options = new JsonSerializerOptions
            {
                Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
            };

            var payload = JsonSerializer.Serialize(new {
                obj = "Not permitted\nTicket changes"
            }, options);

            var privateKey = "f18dc28f-dd25-4219-86f7-174c0c70dd94";

            Signature.verifySignature(privateKey, header, payload);

        }
    }
}