Skip to main content

Signature token verification

Context

This document is designed to explain the current pipeline for Signature token verification for myKaarma webhook events.

API partners can take a look at the details about the secret key and the algorithm used for generating the Signature token, a sample cURL for an event received with the signature token in header, and a pseudocode for a Java Spring Boot webhook consumer endpoint with the signature token verification code.

Need for Signature verification

To ensure that your server only processes webhook deliveries that were sent by myKaarma, and to ensure that the delivery was not tampered with, you should validate the webhook signature before processing the delivery further.
This will help you avoid spending server time to process deliveries that are not from myKaarma and will help avoid man-in-the-middle attacks.

Secret key used for signature token generation

All webhook events sent by myKaarma include a myKaarma-signature-token header. We generate this header using a secret key that only you and myKaarma know, and will be shared with you beforehand.
Currently, the secret key used for the signature token generation process is the same as the username shared with you for the basic auth credentials used for myKaarma APIs access. In the meantime, we're working on making this process more robust from a security perspective, which will involve a different strategy for selection of the secret key.

Algorithm used for signature token generation

Currently, we use the HMAC SHA256 algorithm for generating the signature token. The signature token generated is in a hexadecimal format. The algorithm name is also sent pre-pended to the signature token header, so the value for the myKaarma-signature-token received on your end will look something like: sha256=GENERATED_SIGNATURE_TOKEN
You'll also note in the cURL below that another header content-type: text/plain is attached in the event delivered to your API endpoint. The reason for this is to ensure that the payload (whole event, not the payload field) received on your end matches (including whitespaces and delimiters) the one sent by us, so that signatures generated on our end can be verified on your end.
Once signature verification on the raw body has succeeded, you can parse the received payload as a JSON object.

Sample cURL for an event

Below you'll find the cURL for a sample API request received on a webhook consumer endpoint. The signature token in header is generated with HMAC SHA256 algorithm using a sample secret key SampleSecretKey

curl --location 'https://webhook.site/92abbf05-a4b6-4f68-bcd6-01ff71208aca' \
--header 'accept-encoding: gzip,deflate' \
--header 'content-type: text/plain' \
--header 'mykaarma-signature-token: sha256=97c34b6e493e466cab7d37b49750c7109fbb31c82cf15d61bb5f9d953059f007' \
--header 'user-agent: Amazon/EventBridge/ApiDestinations' \
--data-raw '{"id":"756760fe-e5a5-4be9-8e69-eae7c47f24e8","dealeruuid":"cb731d36fd635ddd6ef8dd43500892b0c0249d1c01a46dbcc445a809c0a8e3b2","payload":"{\"customerWithVehicles\":{\"customer\":{\"id\":null,\"customerKey\":null,\"firstName\":\"myKaarma\",\"middleName\":\"\",\"lastName\":\"Testing\",\"company\":null,\"preferredLocale\":\"en-us\",\"emails\":[{\"emailAddress\":\"mykaarma.testing@gmail.com\",\"label\":\"home\",\"okToEmail\":true,\"isPreferred\":true,\"comments\":null}],\"phoneNumbers\":[{\"phoneNumber\":\"+13108170196\",\"label\":\"cell\",\"okToCall\":true,\"okToText\":true,\"isPreferred\":true,\"comments\":null}],\"preferredCommunication\":null,\"bestTimeToContact\":{\"startTime\":null,\"endTime\":null},\"addresses\":[],\"availability\":1,\"isBusiness\":false,\"customerUuid\":\"GFRR8beBGW8agB5kTOIJtgM9XyGJGmZuye7PfCftGh4\",\"customerId\":\"GFRR8beBGW8agB5kTOIJtgM9XyGJGmZuye7PfCftGh4\"},\"vehicles\":[{\"id\":null,\"vin\":\"1GD312CG0BF081737\",\"vehicleKey\":null,\"isValid\":true,\"imageUrl\":null,\"brandId\":null,\"licensePlate\":null,\"color\":null,\"vehicleModel\":\"Sierra 3500HD\",\"vehicleYear\":\"2011\",\"vehicleTrim\":\"Work Truck Long Box 2WD\",\"vehicleUuid\":\"7nFurSTz5Y1L052dxjMuVnhmETXNgUCRM6kQc5iO_6w\",\"vehicleEngine\":\"6.0L V8 OHV 16V FFV\",\"estimatedMileage\":null,\"vehicleMake\":\"GMC\"}]}}","timestamp":1758964622652,"type":"customers"}'
Signature token in header

Verification process for received signature token

To verify that the event received on your webhook endpoint is a genuine event from myKaarma, you can use the secret key to generate your own signature for each webhook.
Since only you and myKaarma know the secret key, if both signatures match, you can be sure that a received event came from myKaarma.

Code samples

const crypto = require('crypto');

// Use express.raw() — NOT express.json() — so the body is the unmodified bytes
// that were signed. Parsing the JSON first changes whitespace and will break
// signature verification.
app.post('/webhook', express.raw({ type: '*/*' }), (req, res) => {
  const signatureHeader = req.headers['mykaarma-signature-token'];

  if (!signatureHeader) {
    return res.sendStatus(400);
  }

  const secret = process.env.MYKAARMA_WEBHOOK_SECRET; // your API username

  // The header may carry multiple tokens separated by semicolons:
  //   sha256=abc123[;sha512=def456;...]
  // This lets myKaarma add new algorithms or rotate keys without a hard cutover.
  // Verify against the first token whose algorithm we support.
  const SUPPORTED = new Set(['sha256']);
  let foundSupportedAlgo = false;
  let verified = false;

  for (const token of signatureHeader.split(';')) {
    const eqIdx = token.indexOf('=');
    if (eqIdx === -1) continue;
    const algo     = token.slice(0, eqIdx).trim();
    const received = token.slice(eqIdx + 1).trim();

    if (!SUPPORTED.has(algo)) continue;
    foundSupportedAlgo = true;

    const computed = crypto.createHmac(algo, secret)
      .update(req.body) // req.body is a Buffer when using express.raw()
      .digest('hex');

    // Constant-time comparison to prevent timing attacks
    const expectedBuf = Buffer.from(computed);
    const receivedBuf = Buffer.from(received);
    if (expectedBuf.length === receivedBuf.length && crypto.timingSafeEqual(expectedBuf, receivedBuf)) {
      verified = true;
      break;
    }
  }

  if (!foundSupportedAlgo) {
    return res.sendStatus(400); // no recognised algorithm in header
  }
  if (!verified) {
    return res.sendStatus(401); // signature mismatch
  }

  // Signature verified — now safe to parse the JSON body
  const event = JSON.parse(req.body.toString());
  // process event ...

  res.sendStatus(200);
});

Important Notes

  • Signature token generated in myKaarma is stripped of padding. Make sure the expected signature token you generate on your end is also generated in a similar manner.
  • Don't transform or process the raw body of the request, including adding whitespace or applying other formatting, before verifying the signature token. This results in a different signed payload, meaning signatures won't match when you compare.
  • It is recommended to use a secure constant-time comparison method for comparing the expected and the received signature tokens, rather than String.equals or other similar alternatives, to counter a possible DDoS from a malicious entity. For eg. the above sample consumer compares the MessageDigest for the signatures.
  • Currently, we support only 1 signature token in header sent in the webhook event. However, keeping in mind the possibilities in the future - of (a) secret key rotation, and (b) signature token generation algorithm rotation - we might need to send multiple signature tokens in the event. In such a case, the format of the event/header might change somewhat. For a breaking change, we'll provide all webhook consumers a 2-week buffer to ensure they are updated to the newer format, before rolling it out.

Questions

What do I need to do to start receiving the signature token in header?

Nothing. All events received from myKaarma WILL contain the signature token in the header.

Can I get some custom headers added specific to my webhook?

Third-party partners can request additional static headers to be added to the webhook API request, as long as these do not conflict with any of the existing header names already being passed in the request.